mirror of
https://github.com/DM67-Developer/opensim-ai-npc-framework.git
synced 2026-08-14 00:47:59 +00:00
858 lines
31 KiB
Plaintext
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();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} |