Files
DM67-Developer 4a850d3a24 Add files via upload
Add initial NPC Framework files
2025-12-08 13:43:33 -06:00

858 lines
31 KiB
Plaintext

// Actions.lsl — ENHANCED with VELOCITY PREDICTION and PATHFINDING INTEGRATION (DIVISION-BY-ZERO SAFE)
// Universal seating awareness with occupancy detection + predictive movement
// ---- Opcodes (match Main and new Pathfinding module)
integer LM_INIT = 1;
integer LM_CONFIG = 300;
integer LM_FOLLOW = 210; // str: mode|key , id: target key (avatar/object)
integer LM_UNFOLLOW = 211; // stop follow
integer LM_SIT = 212; // make NPC sit
integer LM_IGNORE = 213; // ignore current conversation
integer LM_STAND = 214; // make NPC stand
integer LM_TIMER_TICK = 250; // Master timer tick broadcast
integer LM_TIMER_CONFIG = 251; // Timer configuration update
// ---- Senses and Universal Seating Awareness integration opcodes
integer LM_SENSES_UPDATE = 401; // str: senses data from Senses.lsl
integer LM_SEATING_UPDATE = 402; // Tell Chat.lsl what object was selected for seating
integer LM_AVATAR_UPDATE = 403; // Receive avatar position data for occupancy detection
// ---- NEW: Pathfinding integration opcodes
integer LM_PATH_REQUEST = 500; // Request pathfinding to target
integer LM_PATH_RESULT = 501; // Receive pathfinding result
integer LM_PATH_WAYPOINT = 502; // Move to waypoint along path
integer LM_PATH_COMPLETE = 503; // Path following complete
integer LM_PATH_FAILED = 504; // Pathfinding failed
// ---- Config and runtime
key NPC = NULL_KEY;
integer follow_on = FALSE;
key follow_target = NULL_KEY;
float follow_radius = 2.5;
integer follow_opts = OS_NPC_NO_FLY | OS_NPC_RUNNING;
float follow_thresh_offset = 0.5;
integer sitting = FALSE;
float ACTIONS_TIMER_INTERVAL = 0.5;
float MASTER_TIMER_INTERVAL = 1.0; // Will be overridden by config
integer ACTION_CYCLE_OFFSET = 0; // Act immediately on cycle 0
integer ACTION_SUB_CYCLE = 0; // Sub-cycle counter for frequent updates
// ---- NEW: Velocity Prediction System
integer ENABLE_VELOCITY_PREDICTION = TRUE;
float PREDICTION_HORIZON = 2.0; // Predict 2 seconds ahead
float PREDICTION_MIN_VELOCITY = 1.0; // Only predict if target moving fast enough
float PREDICTION_UPDATE_INTERVAL = 0.2; // How often to update velocity
float PREDICTION_ACCURACY_THRESHOLD = 0.1; // Minimum accuracy for prediction use
// Velocity tracking variables
vector last_target_pos = ZERO_VECTOR;
float last_pos_time = 0.0;
vector target_velocity = ZERO_VECTOR;
vector predicted_target_pos = ZERO_VECTOR;
float prediction_accuracy = 0.0;
integer velocity_samples = 0;
// ---- NEW: Basic Pathfinding System
integer ENABLE_PATHFINDING = TRUE;
float PATHFINDING_TRIGGER_DISTANCE = 8.0; // Use pathfinding if target is far
integer pathfinding_active = FALSE;
list current_path = []; // List of waypoints
integer current_waypoint_index = 0;
float WAYPOINT_REACHED_DISTANCE = 1.5;
integer path_request_pending = FALSE;
// ---- Universal Seating Configuration
integer ENABLE_SEATING_AWARENESS = 1;
float SEATING_MEMORY_DURATION = 300.0;
float SEATING_SCAN_RADIUS = 10.0;
float MAX_SEATING_HEIGHT_DIFF = 2.0;
float MIN_SEATING_SCORE = 3.0;
list SEATING_BLACKLIST = [];
integer SEATING_DEBUG = FALSE; // Enable detailed seating scoring debug
// ---- Occupancy Detection Configuration
integer ENABLE_OCCUPANCY_DETECTION = 1;
float OCCUPANCY_PROXIMITY_RADIUS = 1.5;
float OCCUPANCY_DETECTION_RADIUS = 1.5;
float OCCUPIED_SEAT_PENALTY = -20.0;
// ---- Seating Configuration Notecard
string SEATING_CONFIG_CARD = "seating.cfg";
list SEATING_KEYWORDS = []; // Dynamic keyword list from seating.cfg
list SEATING_SCORES = []; // Corresponding scores for each keyword
// ---- Seating Scoring Parameters (loaded from seating.cfg)
float BASE_SCORE = 5.0;
float FURNITURE_TYPE_BONUS = 5.0;
float SCRIPTED_TYPE_BONUS = 3.0;
float DISTANCE_WEIGHT = 0.5;
float HEIGHT_BONUS = 2.0;
float HEIGHT_THRESHOLD = 0.5;
// ---- Universal Seating Awareness
string current_seating_name = "";
key current_seating_key = NULL_KEY;
float seating_selection_time = 0.0;
// ---- Cache of detected objects from Senses.lsl
list detected_objects = []; // Format: [name, desc, pos, distance, key, type]
list detected_avatars = []; // Format: [key, name, pos] from Senses.lsl
// ---- Config loading support
string CONFIG_CARD = "general.cfg";
// ---- SAFE Math Functions (Division-by-zero protection)
float safeDivide(float numerator, float denominator) {
if (llFabs(denominator) < 0.001) return 0.0;
return numerator / denominator;
}
vector safeVecNorm(vector v) {
float magnitude = llVecMag(v);
if (magnitude < 0.001) return <1,0,0>; // Return safe default
return v / magnitude;
}
// ---- Helpers
vector get_target_pos(key k) {
if (k == NULL_KEY) return ZERO_VECTOR;
list od = llGetObjectDetails(k, [OBJECT_POS]);
if (llGetListLength(od) == 0) return ZERO_VECTOR;
return (vector)llList2String(od, 0);
}
string get_card(string name) {
if (llGetInventoryType(name) != INVENTORY_NOTECARD) return "";
return osGetNotecard(name);
}
list split_pipe(string s)
{
return llParseString2List(s, ["|"], []);
}
// ---- NEW: SAFE Velocity Prediction Functions
updateTargetVelocityTracking(vector currentTargetPos) {
if (!ENABLE_VELOCITY_PREDICTION) return;
if (currentTargetPos == ZERO_VECTOR) return; // Safety check
float currentTime = llGetTime();
float deltaTime = currentTime - last_pos_time;
if (deltaTime >= PREDICTION_UPDATE_INTERVAL && last_target_pos != ZERO_VECTOR) {
// Calculate instantaneous velocity with safety checks
vector posDiff = currentTargetPos - last_target_pos;
if (llVecMag(posDiff) < 0.001 || deltaTime < 0.001) {
// Target hasn't moved significantly or time delta too small
last_pos_time = currentTime;
return;
}
vector new_velocity = <safeDivide(posDiff.x, deltaTime),
safeDivide(posDiff.y, deltaTime),
safeDivide(posDiff.z, deltaTime)>;
// Smooth velocity with previous readings (simple exponential smoothing)
if (velocity_samples > 0) {
float alpha = 0.3; // Smoothing factor
target_velocity = (target_velocity * (1.0 - alpha)) + (new_velocity * alpha);
} else {
target_velocity = new_velocity;
}
velocity_samples++;
// Predict where target will be with safety checks
if (PREDICTION_HORIZON > 0.0) {
predicted_target_pos = currentTargetPos + (target_velocity * PREDICTION_HORIZON);
} else {
predicted_target_pos = currentTargetPos;
}
// Calculate prediction accuracy based on velocity consistency
float velocity_magnitude = llVecMag(target_velocity);
// SAFETY CHECK: Exit early if target has no meaningful velocity
if (velocity_magnitude < 0.001) {
prediction_accuracy = 0.0;
return; // Exit the function early - nothing more to calculate
}
if (velocity_magnitude > PREDICTION_MIN_VELOCITY) {
float new_vel_mag = llVecMag(new_velocity);
prediction_accuracy = 1.0 - safeDivide(llFabs(new_vel_mag - velocity_magnitude), velocity_magnitude);
if (prediction_accuracy < 0.0) prediction_accuracy = 0.0;
if (prediction_accuracy > 1.0) prediction_accuracy = 1.0;
} else {
prediction_accuracy = 0.0;
}
last_pos_time = currentTime;
}
last_target_pos = currentTargetPos;
}
vector getOptimalTargetPosition(vector currentTargetPos) {
if (!ENABLE_VELOCITY_PREDICTION) return currentTargetPos;
if (currentTargetPos == ZERO_VECTOR) return ZERO_VECTOR;
if (NPC == NULL_KEY) return currentTargetPos;
updateTargetVelocityTracking(currentTargetPos);
// Use prediction if target is moving fast enough and prediction is accurate
float velocity_magnitude = llVecMag(target_velocity);
if (velocity_magnitude > PREDICTION_MIN_VELOCITY &&
prediction_accuracy > PREDICTION_ACCURACY_THRESHOLD &&
velocity_samples >= 3) {
vector npcPos = osNpcGetPos(NPC);
if (npcPos == ZERO_VECTOR) return currentTargetPos; // Safety check
float distToTarget = llVecDist(npcPos, currentTargetPos);
// Use predicted position if target is moving and far enough away
if (distToTarget > 5.0 && predicted_target_pos != ZERO_VECTOR) {
return predicted_target_pos;
}
}
return currentTargetPos;
}
// ---- NEW: SAFE Pathfinding Integration Functions
requestPathfinding(vector targetPos) {
if (!ENABLE_PATHFINDING || path_request_pending) return;
if (targetPos == ZERO_VECTOR || NPC == NULL_KEY) return; // Safety checks
vector npcPos = osNpcGetPos(NPC);
if (npcPos == ZERO_VECTOR) return; // Safety check
float distance = llVecDist(npcPos, targetPos);
// Only use pathfinding for distant targets or complex scenarios
if (distance > PATHFINDING_TRIGGER_DISTANCE) {
path_request_pending = TRUE;
string request = (string)npcPos + "|" + (string)targetPos;
llMessageLinked(LINK_SET, LM_PATH_REQUEST, request, NPC);
}
}
handlePathfindingResult(string pathData) {
path_request_pending = FALSE;
if (pathData == "" || pathData == "FAILED") {
pathfinding_active = FALSE;
current_path = [];
return;
}
// Parse waypoints from pathfinding result
current_path = llParseString2List(pathData, ["|"], []);
current_waypoint_index = 0;
pathfinding_active = TRUE;
}
integer follow_pathfinding_waypoints() {
if (!pathfinding_active || llGetListLength(current_path) == 0) return FALSE;
if (NPC == NULL_KEY) return FALSE;
if (current_waypoint_index >= llGetListLength(current_path)) {
// Path complete
pathfinding_active = FALSE;
current_path = [];
llMessageLinked(LINK_SET, LM_PATH_COMPLETE, "", NPC);
return FALSE;
}
vector waypoint = (vector)llList2String(current_path, current_waypoint_index);
if (waypoint == ZERO_VECTOR) {
// Skip invalid waypoint
current_waypoint_index++;
return TRUE;
}
vector npcPos = osNpcGetPos(NPC);
if (npcPos == ZERO_VECTOR) return FALSE; // Safety check
float distance = llVecDist(npcPos, waypoint);
if (distance <= WAYPOINT_REACHED_DISTANCE) {
// Move to next waypoint
current_waypoint_index++;
if (current_waypoint_index >= llGetListLength(current_path)) {
pathfinding_active = FALSE;
current_path = [];
llMessageLinked(LINK_SET, LM_PATH_COMPLETE, "", NPC);
return FALSE;
}
waypoint = (vector)llList2String(current_path, current_waypoint_index);
if (waypoint == ZERO_VECTOR) return TRUE; // Skip invalid waypoint
}
osNpcMoveToTarget(NPC, waypoint, follow_opts);
return TRUE;
}
// ---- Enhanced Following Logic with Velocity Prediction (SAFE)
handle_enhanced_following() {
if (!follow_on || NPC == NULL_KEY || sitting || follow_target == NULL_KEY) return;
vector targetPos = get_target_pos(follow_target);
if (targetPos == ZERO_VECTOR) {
follow_on = FALSE;
osNpcStopMoveToTarget(NPC);
return;
}
vector npcPos = osNpcGetPos(NPC);
if (npcPos == ZERO_VECTOR) return; // Safety check
float distToTarget = llVecDist(npcPos, targetPos);
// Use pathfinding for distant targets if enabled
if (ENABLE_PATHFINDING && distToTarget > PATHFINDING_TRIGGER_DISTANCE && !pathfinding_active) {
requestPathfinding(targetPos);
return;
}
// If pathfinding is active, follow waypoints
if (pathfinding_active && follow_pathfinding_waypoints()) {
return;
}
// Standard following with velocity prediction
vector optimalTargetPos = getOptimalTargetPosition(targetPos);
if (optimalTargetPos == ZERO_VECTOR) optimalTargetPos = targetPos; // Fallback
float distToOptimal = llVecDist(npcPos, optimalTargetPos);
if (distToOptimal > (follow_radius + follow_thresh_offset)) {
osNpcMoveToTarget(NPC, optimalTargetPos, follow_opts);
} else {
osNpcStopMoveToTarget(NPC);
}
}
// ---- Data parsing functions (SAFE versions)
list parseObjectData(string objStr) {
if (objStr == "") return [];
list parts = llParseString2List(objStr, ["~"], []);
if (llGetListLength(parts) < 6) return [];
string name = llList2String(parts, 0);
string desc = llList2String(parts, 1);
string posStr = llList2String(parts, 2);
float dist = llList2Float(parts, 3);
key objKey = (key)llList2String(parts, 4);
string objType = llList2String(parts, 5);
// Parse position from "x:y:z" format with safety checks
list posParts = llParseString2List(posStr, [":"], []);
vector pos = ZERO_VECTOR;
if (llGetListLength(posParts) >= 3) {
pos.x = llList2Float(posParts, 0);
pos.y = llList2Float(posParts, 1);
pos.z = llList2Float(posParts, 2);
}
return [name, desc, pos, dist, objKey, objType];
}
list parseAvatarData(string avatarStr) {
if (avatarStr == "") return [];
list parts = llParseString2List(avatarStr, ["~"], []);
if (llGetListLength(parts) < 3) return [];
key avatarKey = (key)llList2String(parts, 0);
string avatarName = llList2String(parts, 1);
string posStr = llList2String(parts, 2);
list posParts = llParseString2List(posStr, [":"], []);
vector pos = ZERO_VECTOR;
if (llGetListLength(posParts) >= 3) {
pos.x = llList2Float(posParts, 0);
pos.y = llList2Float(posParts, 1);
pos.z = llList2Float(posParts, 2);
}
return [avatarKey, avatarName, pos];
}
// ---- Existing seating functions (unchanged but called from enhanced movement)
integer isBlacklisted(string name, string desc) {
string searchText = llToLower(name + " " + desc);
integer i;
for (i = 0; i < llGetListLength(SEATING_BLACKLIST); i++) {
string blacklisted = llToLower(llList2String(SEATING_BLACKLIST, i));
if (llSubStringIndex(searchText, blacklisted) >= 0) {
return TRUE;
}
}
return FALSE;
}
string checkSeatOccupancy(vector seatPos, key seatKey) {
if (!ENABLE_OCCUPANCY_DETECTION) return "";
if (seatPos == ZERO_VECTOR) return ""; // Safety check
integer avatarCount = llGetListLength(detected_avatars) / 3;
integer i;
for (i = 0; i < avatarCount; i++) {
integer idx = i * 3;
key avatarKey = llList2Key(detected_avatars, idx);
string avatarName = llList2String(detected_avatars, idx + 1);
vector avatarPos = llList2Vector(detected_avatars, idx + 2);
if (avatarKey == NULL_KEY || avatarPos == ZERO_VECTOR) jump next_avatar;
if (avatarKey == NPC) jump next_avatar;
float distance = llVecDist(avatarPos, seatPos);
if (distance <= OCCUPANCY_PROXIMITY_RADIUS) {
return avatarName;
}
@next_avatar;
}
return "";
}
key findBestSeatingFromSenses() {
if (NPC == NULL_KEY) return NULL_KEY;
key brainKey = llGetKey();
vector npcPos = osNpcGetPos(NPC);
if (npcPos == ZERO_VECTOR) return NULL_KEY; // Safety check
if (llGetListLength(detected_objects) == 0) return NULL_KEY;
key bestTarget = NULL_KEY;
float bestScore = -1.0;
string bestName = "";
string bestDesc = "";
string occupancyInfo = "";
integer count = llGetListLength(detected_objects) / 6;
integer i;
for (i = 0; i < count; i++) {
integer idx = i * 6;
string name = llList2String(detected_objects, idx);
string desc = llList2String(detected_objects, idx + 1);
vector pos = llList2Vector(detected_objects, idx + 2);
float dist = llList2Float(detected_objects, idx + 3);
key objKey = llList2Key(detected_objects, idx + 4);
string objType = llList2String(detected_objects, idx + 5);
if (objKey == brainKey) jump next_obj;
if (isBlacklisted(name, desc)) jump next_obj;
if (pos.z > npcPos.z + MAX_SEATING_HEIGHT_DIFF) jump next_obj;
if (dist > SEATING_SCAN_RADIUS) jump next_obj;
string occupant = checkSeatOccupancy(pos, objKey);
float score = BASE_SCORE;
// Type-based scoring
if (objType == "furniture") score += FURNITURE_TYPE_BONUS;
if (objType == "scripted") score += SCRIPTED_TYPE_BONUS;
// Keyword-based scoring (loaded from seating.cfg)
string searchText = llToLower(name + " " + desc);
// Dynamic keyword-based scoring
integer k;
for (k = 0; k < llGetListLength(SEATING_KEYWORDS); k++) {
string keyword = llList2String(SEATING_KEYWORDS, k);
if (llSubStringIndex(searchText, keyword) >= 0) {
float keyword_score = llList2Float(SEATING_SCORES, k);
score += keyword_score;
if (SEATING_DEBUG) {
llOwnerSay(" Keyword '" + keyword + "' matched, +" + (string)keyword_score + " points");
}
}
}
// Occupancy penalty
if (occupant != "") {
score += OCCUPIED_SEAT_PENALTY;
}
// Distance-based scoring
if (SEATING_SCAN_RADIUS > 0.0) {
score += safeDivide((SEATING_SCAN_RADIUS - dist), SEATING_SCAN_RADIUS) * DISTANCE_WEIGHT;
}
// Height-based bonus
if (pos.z <= npcPos.z + HEIGHT_THRESHOLD) score += HEIGHT_BONUS;
if (score < MIN_SEATING_SCORE) jump next_obj;
if (score > bestScore) {
bestScore = score;
bestTarget = objKey;
bestName = name;
bestDesc = desc;
if (occupant != "") {
occupancyInfo = " (avoided " + occupant + "'s seat)";
} else {
occupancyInfo = "";
}
}
@next_obj;
}
if (bestTarget != NULL_KEY && ENABLE_SEATING_AWARENESS) {
current_seating_name = bestName;
current_seating_key = bestTarget;
seating_selection_time = llGetTime();
string seatingInfo = bestName;
if (bestDesc != "" && bestDesc != bestName) {
seatingInfo += " (" + bestDesc + ")";
}
seatingInfo += occupancyInfo;
llMessageLinked(LINK_SET, LM_SEATING_UPDATE, "sitting_on|" + seatingInfo, bestTarget);
}
return bestTarget;
}
// ---- Load enhanced configuration with safety checks
integer load_enhanced_config() {
string txt = get_card(CONFIG_CARD);
if (txt == "") return FALSE;
list lines = llParseString2List(txt, ["\n"], []);
integer i;
for (i = 0; i < llGetListLength(lines); i++) {
string line = llStringTrim(llList2String(lines, i), STRING_TRIM);
if (line == "") jump cont;
if (llGetSubString(line,0,0) == "#") jump cont;
if (llGetSubString(line,0,1) == "//") jump cont;
integer eq = llSubStringIndex(line, "=");
if (eq <= 0) jump cont;
string k = llStringTrim(llGetSubString(line, 0, eq-1), STRING_TRIM);
string v = llStringTrim(llGetSubString(line, eq+1, -1), STRING_TRIM);
// Existing config options with safety checks
if (k == "SEATING_SCAN_RADIUS") {
float val = (float)v;
if (val > 0.0) SEATING_SCAN_RADIUS = val;
}
else if (k == "MAX_SEATING_HEIGHT_DIFF") MAX_SEATING_HEIGHT_DIFF = (float)v;
else if (k == "MIN_SEATING_SCORE") MIN_SEATING_SCORE = (float)v;
else if (k == "SEATING_BLACKLIST") {
SEATING_BLACKLIST = llParseString2List(v, ["|"], []);
}
else if (k == "ENABLE_SEATING_AWARENESS") ENABLE_SEATING_AWARENESS = (integer)v;
else if (k == "SEATING_MEMORY_DURATION") SEATING_MEMORY_DURATION = (float)v;
else if (k == "ENABLE_OCCUPANCY_DETECTION") ENABLE_OCCUPANCY_DETECTION = (integer)v;
else if (k == "OCCUPANCY_PROXIMITY_RADIUS") OCCUPANCY_PROXIMITY_RADIUS = (float)v;
else if (k == "OCCUPANCY_DETECTION_RADIUS") OCCUPANCY_DETECTION_RADIUS = (float)v;
else if (k == "OCCUPIED_SEAT_PENALTY") OCCUPIED_SEAT_PENALTY = (float)v;
else if (k == "FOLLOW_RADIUS") {
float val = (float)v;
if (val > 0.0) follow_radius = val;
}
else if (k == "FOLLOW_THRESHOLD_OFFSET") follow_thresh_offset = (float)v;
else if (k == "ACTIONS_TIMER_INTERVAL") {
float val = (float)v;
if (val > 0.0) ACTIONS_TIMER_INTERVAL = val;
}
// NEW: Velocity prediction config with safety checks
else if (k == "ENABLE_VELOCITY_PREDICTION") ENABLE_VELOCITY_PREDICTION = (integer)v;
else if (k == "PREDICTION_HORIZON") {
float val = (float)v;
if (val > 0.0) PREDICTION_HORIZON = val;
}
else if (k == "PREDICTION_MIN_VELOCITY") {
float val = (float)v;
if (val >= 0.0) PREDICTION_MIN_VELOCITY = val;
}
else if (k == "PREDICTION_UPDATE_INTERVAL") {
float val = (float)v;
if (val > 0.0) PREDICTION_UPDATE_INTERVAL = val;
}
else if (k == "PREDICTION_ACCURACY_THRESHOLD") {
float val = (float)v;
if (val >= 0.0 && val <= 1.0) PREDICTION_ACCURACY_THRESHOLD = val;
}
// NEW: Pathfinding config with safety checks
else if (k == "ENABLE_PATHFINDING") ENABLE_PATHFINDING = (integer)v;
else if (k == "PATHFINDING_TRIGGER_DISTANCE") {
float val = (float)v;
if (val > 0.0) PATHFINDING_TRIGGER_DISTANCE = val;
}
else if (k == "WAYPOINT_REACHED_DISTANCE") {
float val = (float)v;
if (val > 0.0) WAYPOINT_REACHED_DISTANCE = val;
}
@cont;
}
return TRUE;
}
// ---- Enhanced sit handler
handleSitCommand() {
if (NPC == NULL_KEY) return;
if (follow_on) {
follow_on = FALSE;
osNpcStopMoveToTarget(NPC);
pathfinding_active = FALSE; // Stop pathfinding when sitting
current_path = [];
}
key sitTarget = findBestSeatingFromSenses();
if (sitTarget != NULL_KEY) {
osNpcSit(NPC, sitTarget, OS_NPC_SIT_NOW);
sitting = TRUE;
llOwnerSay("Delphina sitting on: " + current_seating_name);
} else {
llOwnerSay("No suitable seating found (all may be occupied or inappropriate)");
sitting = FALSE;
}
}
// ---- Enhanced stand handler
handleStandCommand() {
if (NPC != NULL_KEY) {
osNpcStand(NPC);
sitting = FALSE;
if (ENABLE_SEATING_AWARENESS) {
llMessageLinked(LINK_SET, LM_SEATING_UPDATE, "standing|", NULL_KEY);
current_seating_name = "";
current_seating_key = NULL_KEY;
seating_selection_time = 0.0;
}
}
}
// ---- Load seating configuration from seating.cfg
integer load_seating_config() {
string txt = get_card(SEATING_CONFIG_CARD);
if (txt == "") {
llOwnerSay("Actions: seating.cfg not found, using defaults");
return FALSE;
}
// Reset dynamic keyword lists
SEATING_KEYWORDS = [];
SEATING_SCORES = [];
integer keyword_count = 0;
list lines = llParseString2List(txt, ["\n"], []);
integer i;
for (i = 0; i < llGetListLength(lines); i++) {
string line = llStringTrim(llList2String(lines, i), STRING_TRIM);
if (line == "") jump cont;
if (llGetSubString(line,0,0) == "#") jump cont;
if (llGetSubString(line,0,1) == "//") jump cont;
integer eq = llSubStringIndex(line, "=");
if (eq <= 0) jump cont;
string k = llStringTrim(llGetSubString(line, 0, eq-1), STRING_TRIM);
string v = llStringTrim(llGetSubString(line, eq+1, -1), STRING_TRIM);
// Blacklist
if (k == "BLACKLIST") {
SEATING_BLACKLIST = llParseString2List(v, ["|"], []);
}
// System parameters (with underscores or specific known names)
else if (k == "base_score") BASE_SCORE = (float)v;
else if (k == "height_bonus") HEIGHT_BONUS = (float)v;
else if (k == "height_threshold") HEIGHT_THRESHOLD = (float)v;
else if (k == "distance_weight") DISTANCE_WEIGHT = (float)v;
else if (k == "furniture_type_bonus") FURNITURE_TYPE_BONUS = (float)v;
else if (k == "scripted_type_bonus") SCRIPTED_TYPE_BONUS = (float)v;
else if (k == "occupied_seat_penalty") OCCUPIED_SEAT_PENALTY = (float)v;
else if (k == "occupancy_detection_radius") OCCUPANCY_DETECTION_RADIUS = (float)v;
else if (k == "occupancy_proximity_radius") OCCUPANCY_PROXIMITY_RADIUS = (float)v;
else if (k == "enable_occupancy_detection") ENABLE_OCCUPANCY_DETECTION = (integer)v;
else if (k == "enable_seating_awareness") ENABLE_SEATING_AWARENESS = (integer)v;
else if (k == "seating_memory_duration") SEATING_MEMORY_DURATION = (float)v;
else if (k == "seating_scan_radius") SEATING_SCAN_RADIUS = (float)v;
else if (k == "max_seating_height_diff") MAX_SEATING_HEIGHT_DIFF = (float)v;
else if (k == "min_seating_score") MIN_SEATING_SCORE = (float)v;
else if (k == "debug") SEATING_DEBUG = (integer)v;
// Everything else is a keyword score (dynamic)
else {
float score_val = (float)v;
// Optional: Log suspicious values
if (score_val == 0.0 && v != "0.0" && v != "0" && SEATING_DEBUG) {
llOwnerSay("WARNING: Keyword '" + k + "' has invalid score value: " + v + " (treating as 0.0)");
}
SEATING_KEYWORDS += [llToLower(k)];
SEATING_SCORES += [score_val];
keyword_count++;
}
@cont;
}
if (SEATING_DEBUG) {
llOwnerSay("Actions: Loaded " + (string)keyword_count + " seating keywords from " + SEATING_CONFIG_CARD);
llOwnerSay("Blacklist items: " + (string)llGetListLength(SEATING_BLACKLIST));
}
return TRUE;
}
// ---- DEFAULT State ----
default {
state_entry() {
// Initialize with safe defaults
if (ACTIONS_TIMER_INTERVAL <= 0.0) ACTIONS_TIMER_INTERVAL = 0.5;
if (follow_radius <= 0.0) follow_radius = 2.5;
if (PREDICTION_HORIZON <= 0.0) PREDICTION_HORIZON = 2.0;
if (PREDICTION_UPDATE_INTERVAL <= 0.0) PREDICTION_UPDATE_INTERVAL = 0.2;
if (WAYPOINT_REACHED_DISTANCE <= 0.0) WAYPOINT_REACHED_DISTANCE = 1.5;
load_enhanced_config();
load_seating_config(); // Load seating keywords and scores
llOwnerSay("Actions: Enhanced with Safe Velocity Prediction + Pathfinding");
llOwnerSay("Actions: Enhanced with Master Timer Coordination");
llSetTimerEvent(0.5); // Fast internal updates
}
link_message(integer sender, integer num, string str, key id) {
if (num == LM_CONFIG) {
NPC = id;
}
else if (num == LM_TIMER_CONFIG) {
list L = split_pipe(str);
integer i;
for (i = 0; i < llGetListLength(L); i++) {
string kv = llList2String(L, i);
integer eq = llSubStringIndex(kv, "=");
if (eq >= 0) {
string k = llStringTrim(llGetSubString(kv, 0, eq-1), STRING_TRIM);
string v = llStringTrim(llGetSubString(kv, eq+1, -1), STRING_TRIM);
if (k == "MASTER_INTERVAL") MASTER_TIMER_INTERVAL = (float)v;
}
}
}
else if (num == LM_TIMER_TICK) {
// Parse tick data if needed
list tick_data = split_pipe(str);
// Handle master timer tick coordination here if needed
}
else if (num == LM_SENSES_UPDATE) {
detected_objects = [];
if (str != "") {
if (llSubStringIndex(str, "~") >= 0) {
list objectStrings = llParseString2List(str, ["|"], []);
integer i;
for (i = 0; i < llGetListLength(objectStrings); i++) {
string objStr = llList2String(objectStrings, i);
if (objStr != "") { // Safety check
list objData = parseObjectData(objStr);
if (llGetListLength(objData) >= 6) {
detected_objects += objData;
}
}
}
}
}
}
else if (num == LM_AVATAR_UPDATE) {
detected_avatars = [];
if (str != "") {
list avatarStrings = llParseString2List(str, ["|"], []);
integer i;
for (i = 0; i < llGetListLength(avatarStrings); i++) {
string avatarStr = llList2String(avatarStrings, i);
if (avatarStr != "") { // Safety check
list avatarData = parseAvatarData(avatarStr);
if (llGetListLength(avatarData) >= 3) {
detected_avatars += avatarData;
}
}
}
}
}
else if (num == LM_FOLLOW) {
if (sitting) {
handleStandCommand();
}
follow_target = id;
if (follow_target != NULL_KEY && NPC != NULL_KEY) {
follow_on = TRUE;
// Reset velocity tracking for new target
last_target_pos = ZERO_VECTOR;
last_pos_time = 0.0;
target_velocity = ZERO_VECTOR;
velocity_samples = 0;
pathfinding_active = FALSE;
current_path = [];
} else {
follow_on = FALSE;
}
}
else if (num == LM_UNFOLLOW) {
follow_on = FALSE;
pathfinding_active = FALSE;
current_path = [];
if (NPC != NULL_KEY) {
osNpcStopMoveToTarget(NPC);
}
}
else if (num == LM_SIT) {
handleSitCommand();
}
else if (num == LM_STAND) {
handleStandCommand();
}
else if (num == LM_IGNORE) {
follow_on = FALSE;
pathfinding_active = FALSE;
current_path = [];
if (NPC != NULL_KEY) {
osNpcStopMoveToTarget(NPC);
}
}
// NEW: Handle pathfinding results
else if (num == LM_PATH_RESULT) {
handlePathfindingResult(str);
}
}
timer()
{
// Get current master cycle
integer current_cycle = (integer)(llGetTime() / MASTER_TIMER_INTERVAL) % 4;
// Act more frequently than the master cycle
ACTION_SUB_CYCLE++;
// Enhanced following needs frequent updates - act every sub-cycle
if (ACTION_SUB_CYCLE % 2 == 0) {
handle_enhanced_following();
}
// Less critical updates only on our designated cycle
if (current_cycle == ACTION_CYCLE_OFFSET) {
if (ACTION_SUB_CYCLE % 4 == 0) {
// Process pathfinding waypoints less frequently
if (pathfinding_active) {
follow_pathfinding_waypoints();
}
}
}
}
}