mirror of
https://github.com/DM67-Developer/opensim-ai-npc-framework.git
synced 2026-08-14 00:47:59 +00:00
848 lines
28 KiB
Plaintext
848 lines
28 KiB
Plaintext
// Senses.lsl — ENHANCED with Universal Seating + Occupancy Detection Integration
|
||
// Now provides avatar position data for occupancy checking
|
||
|
||
// ---- Opcodes align with existing project
|
||
integer LM_INIT = 1;
|
||
integer LM_CONFIG = 300;
|
||
integer LM_FOLLOW = 210;
|
||
integer LM_UNFOLLOW = 211;
|
||
integer LM_SIT = 212;
|
||
integer LM_STAND = 214;
|
||
integer LM_TIMER_TICK = 250; // Master timer tick broadcast
|
||
integer LM_TIMER_CONFIG = 251; // Timer configuration update
|
||
integer LM_SENSE = 400; // Senses -> others
|
||
integer LM_SENSES_UPDATE = 401; // Send object data to Actions.lsl
|
||
integer LM_SEATING_UPDATE = 402; // NEW: Receive seating awareness from Actions.lsl
|
||
integer LM_AVATAR_UPDATE = 403; // NEW: Send avatar position data for occupancy detection
|
||
|
||
// ---- Notecard references
|
||
string CONFIG_CARD = "general.cfg";
|
||
|
||
// ---- Config defaults (overridden by config)
|
||
integer SENSES_ENABLED = TRUE;
|
||
float SENSES_TIMER_INTERVAL = 1.5; // seconds
|
||
float SENSES_RADIUS = 20.0; // meters
|
||
float SENSES_ARC_DEG = 90.0; // degrees
|
||
integer SENSES_TOPK_AVATARS = 3;
|
||
|
||
// Enhanced object detection configuration
|
||
integer SENSES_MAX_OBJECTS = 5; // Top objects to report
|
||
integer SENSES_INCLUDE_ATTACHMENTS = FALSE; // Skip worn items
|
||
integer SENSES_OBJECT_DETAILS = TRUE; // Enable detailed object info
|
||
integer SENSES_DEBUG = FALSE; // Debug mode for testing
|
||
|
||
// ---- Runtime
|
||
key NPC = NULL_KEY;
|
||
integer following = FALSE;
|
||
key followtarget = NULL_KEY;
|
||
integer sitting = FALSE;
|
||
|
||
// ---- Master Timer
|
||
float MASTER_TIMER_INTERVAL = 1.5; // Will be overridden by config
|
||
integer SENSES_CYCLE_OFFSET = 1; // Act on cycle 1
|
||
|
||
// NEW: Universal Seating Awareness variables
|
||
string current_seating_info = ""; // What the NPC is sitting on
|
||
integer seating_awareness_active = FALSE;
|
||
|
||
// FOV running tallies from last sensor event
|
||
integer fovscripted = 0;
|
||
integer fovpassive = 0;
|
||
|
||
// Enhanced object detail storage
|
||
list detected_objects = []; // Format: [name, desc, pos, distance, key, type]
|
||
|
||
// NEW: Avatar position storage for occupancy detection
|
||
list detected_avatars = []; // Format: [key, name, pos]
|
||
|
||
// ---- Helpers
|
||
list split_pipe(string s)
|
||
{
|
||
return llParseString2List(s, ["|"], []);
|
||
}
|
||
|
||
string join_pipe(list L)
|
||
{
|
||
return llDumpList2String(L, "|");
|
||
}
|
||
|
||
string getcard(string name)
|
||
{
|
||
if (llGetInventoryType(name) != INVENTORY_NOTECARD) return "";
|
||
return osGetNotecard(name);
|
||
}
|
||
|
||
vector getSelfPos()
|
||
{
|
||
if (NPC != NULL_KEY) return osNpcGetPos(NPC);
|
||
return llGetPos();
|
||
}
|
||
|
||
vector getAnchorPos()
|
||
{
|
||
return llGetRootPosition(); // anchor for FOV/range
|
||
}
|
||
|
||
rotation getAnchorRot()
|
||
{
|
||
// If we have an NPC, use their actual rotation
|
||
// Otherwise fall back to controller rotation
|
||
if (NPC != NULL_KEY)
|
||
{
|
||
rotation npc_rot = osNpcGetRot(NPC);
|
||
if (npc_rot != ZERO_ROTATION)
|
||
return npc_rot;
|
||
}
|
||
|
||
// Fallback to controller rotation if NPC not available
|
||
return llGetRootRotation();
|
||
}
|
||
|
||
string fmtFloat(float v, integer places)
|
||
{
|
||
float m = llPow(10.0, (float)places);
|
||
integer rounded = llRound(v * m);
|
||
|
||
if (places == 0)
|
||
return (string)rounded;
|
||
|
||
// Build string manually to avoid LSL's 6-decimal expansion
|
||
integer whole = rounded / llRound(m);
|
||
integer frac = rounded % llRound(m);
|
||
|
||
// Pad fractional part with leading zeros if needed
|
||
string frac_str = (string)frac;
|
||
while (llStringLength(frac_str) < places)
|
||
frac_str = "0" + frac_str;
|
||
|
||
return (string)whole + "." + frac_str;
|
||
}
|
||
|
||
string getRelativeDirection(vector npc_pos, rotation npc_rot, vector obj_pos)
|
||
{
|
||
// Get vector from NPC to object
|
||
vector to_obj = obj_pos - npc_pos;
|
||
|
||
// Transform to NPC's local coordinate system
|
||
vector local = to_obj / npc_rot;
|
||
|
||
// Calculate angle in horizontal plane
|
||
// After rotation division in LSL:
|
||
// local.x = left/right (positive = left, negative = right)
|
||
// local.y = forward/backward (positive = forward, negative = backward)
|
||
// For llAtan2(y, x): use forward as X-axis, left as Y-axis
|
||
|
||
float angle = llAtan2(local.y, local.x) * RAD_TO_DEG;
|
||
|
||
// TEMPORARY DEBUG to see where exactly the npc is seeing
|
||
if (SENSES_DEBUG)
|
||
{
|
||
llOwnerSay("DEBUG Direction: local.x=" + (string)local.x +
|
||
", local.y=" + (string)local.y +
|
||
", angle=" + (string)angle + " degrees");
|
||
}
|
||
|
||
// 8-direction classification (45-degree segments)
|
||
// Front: -22.5 to 22.5 degrees
|
||
// Front-Left: 22.5 to 67.5 degrees
|
||
// Left: 67.5 to 112.5 degrees
|
||
// Behind-Left: 112.5 to 157.5 degrees
|
||
// Behind: 157.5 to 180 and -180 to -157.5 degrees
|
||
// Behind-Right: -157.5 to -112.5 degrees
|
||
// Right: -112.5 to -67.5 degrees
|
||
// Front-Right: -67.5 to -22.5 degrees
|
||
|
||
if (angle >= -22.5 && angle < 22.5)
|
||
return "in front of me";
|
||
else if (angle >= 22.5 && angle < 67.5)
|
||
return "in front of me to my left";
|
||
else if (angle >= 67.5 && angle < 112.5)
|
||
return "to my left";
|
||
else if (angle >= 112.5 && angle < 157.5)
|
||
return "behind me to my left";
|
||
else if (angle >= 157.5 || angle < -157.5)
|
||
return "behind me ";
|
||
else if (angle >= -157.5 && angle < -112.5)
|
||
return "behind me to my right";
|
||
else if (angle >= -112.5 && angle < -67.5)
|
||
return "to my right";
|
||
else // angle >= -67.5 && angle < -22.5
|
||
return "front-right";
|
||
}
|
||
|
||
// Same as getRelativeDirection but with 180° flip for avatars
|
||
string getAvatarDirection(vector npc_pos, rotation npc_rot, vector obj_pos)
|
||
{
|
||
// 1) Vector from NPC to object in world coords
|
||
vector to_obj = obj_pos - npc_pos;
|
||
|
||
// 2) Rotate into NPC‐local space (no extra flips needed)
|
||
vector local = to_obj / npc_rot;
|
||
|
||
// 3) Compute the local heading in degrees
|
||
float angle = llAtan2(local.y, local.x) * RAD_TO_DEG;
|
||
|
||
// 4) Normalize to [0,360)
|
||
if (angle < 0.0) angle += 360.0;
|
||
|
||
// 5) Bucket into eight 45° sectors
|
||
if (angle < 22.5 || angle >= 337.5) return "in front of me";
|
||
else if (angle < 67.5) return "in front of me to my left";
|
||
else if (angle < 112.5) return "to my left";
|
||
else if (angle < 157.5) return "behind me to my left";
|
||
else if (angle < 202.5) return "behind me";
|
||
else if (angle < 247.5) return "behind me to my right";
|
||
else if (angle < 292.5) return "to my right";
|
||
else return "in front of me to my right";
|
||
}
|
||
|
||
// Calculate relative height description
|
||
string getRelativeHeight(float npc_z, float obj_z)
|
||
{
|
||
float diff = obj_z - npc_z;
|
||
|
||
if (diff > 1.0)
|
||
return "above";
|
||
else if (diff < -1.0)
|
||
return "below";
|
||
else
|
||
return "level";
|
||
}
|
||
|
||
// FOV test: within radius and inside half-arc cone around +X forward, anchored to the prim
|
||
integer withinFOV(vector anchorpos, rotation anchorrot, vector targetpos)
|
||
{
|
||
float d = llVecDist(anchorpos, targetpos);
|
||
if (d > SENSES_RADIUS) return FALSE;
|
||
|
||
vector fwd = llRot2Fwd(anchorrot); // world-space forward (+X)
|
||
vector dir = llVecNorm(targetpos - anchorpos);
|
||
float dot = fwd.x*dir.x + fwd.y*dir.y + fwd.z*dir.z;
|
||
float halfArc = SENSES_ARC_DEG * DEG_TO_RAD * 0.5;
|
||
float cosLimit = llCos(halfArc);
|
||
|
||
if (dot >= cosLimit) return TRUE;
|
||
return FALSE;
|
||
}
|
||
|
||
// Sort and limit objects by distance
|
||
list sortObjectsByDistance(list objects)
|
||
{
|
||
integer count = llGetListLength(objects) / 9; // Changed from 6 to 9
|
||
if (count <= 1) return objects;
|
||
|
||
// Simple bubble sort by distance (index 3)
|
||
integer i;
|
||
integer j;
|
||
for (i = 0; i < count - 1; i++)
|
||
{
|
||
for (j = 0; j < count - i - 1; j++)
|
||
{
|
||
integer idx1 = j * 9 + 3; // distance index (changed from 6 to 9)
|
||
integer idx2 = (j + 1) * 9 + 3; // changed from 6 to 9
|
||
|
||
if (llList2Float(objects, idx1) > llList2Float(objects, idx2))
|
||
{
|
||
// Swap entire object records (9 elements each, changed from 6)
|
||
list temp = llList2List(objects, j * 9, j * 9 + 8); // Changed from +5 to +8
|
||
objects = llListReplaceList(objects,
|
||
llList2List(objects, (j + 1) * 9, (j + 1) * 9 + 8),
|
||
j * 9, j * 9 + 8);
|
||
objects = llListReplaceList(objects, temp,
|
||
(j + 1) * 9, (j + 1) * 9 + 8);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Limit to max objects
|
||
if (count > SENSES_MAX_OBJECTS)
|
||
{
|
||
integer keep = SENSES_MAX_OBJECTS * 9; // Changed from 6 to 9
|
||
objects = llList2List(objects, 0, keep - 1);
|
||
}
|
||
|
||
return objects;
|
||
}
|
||
|
||
// Determine object type description
|
||
string getObjectType(integer objtype)
|
||
{
|
||
if (objtype & SCRIPTED) return "scripted";
|
||
if (objtype & PASSIVE) return "furniture";
|
||
if (objtype & AGENT) return "attachment";
|
||
return "object";
|
||
}
|
||
|
||
// Send detected objects to Actions.lsl with proper data format
|
||
sendObjectsToActions() {
|
||
integer count = llGetListLength(detected_objects) / 6;
|
||
|
||
if (count == 0) {
|
||
llMessageLinked(LINK_SET, LM_SENSES_UPDATE, "", NULL_KEY);
|
||
return;
|
||
}
|
||
|
||
string objectData = "";
|
||
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);
|
||
|
||
// Use ~ separator and x:y:z format to avoid comma parsing issues
|
||
string posStr = (string)pos.x + ":" + (string)pos.y + ":" + (string)pos.z;
|
||
string objString = name + "~" + desc + "~" + posStr + "~" +
|
||
(string)dist + "~" + (string)objKey + "~" + objType;
|
||
|
||
if (i > 0) objectData += "|";
|
||
objectData += objString;
|
||
}
|
||
|
||
llMessageLinked(LINK_SET, LM_SENSES_UPDATE, objectData, NULL_KEY);
|
||
}
|
||
|
||
// NEW: Send avatar position data to Actions.lsl for occupancy detection
|
||
sendAvatarsToActions() {
|
||
integer count = llGetListLength(detected_avatars) / 3;
|
||
|
||
if (count == 0) {
|
||
llMessageLinked(LINK_SET, LM_AVATAR_UPDATE, "", NULL_KEY);
|
||
return;
|
||
}
|
||
|
||
string avatarData = "";
|
||
integer i;
|
||
|
||
for (i = 0; i < count; 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);
|
||
|
||
// Use ~ separator and x:y:z format
|
||
string posStr = (string)avatarPos.x + ":" + (string)avatarPos.y + ":" + (string)avatarPos.z;
|
||
string avatarString = (string)avatarKey + "~" + avatarName + "~" + posStr;
|
||
|
||
if (i > 0) avatarData += "|";
|
||
avatarData += avatarString;
|
||
}
|
||
|
||
llMessageLinked(LINK_SET, LM_AVATAR_UPDATE, avatarData, NULL_KEY);
|
||
}
|
||
|
||
// Build detailed objects field
|
||
string buildObjectsFieldDetailed()
|
||
{
|
||
if (llGetListLength(detected_objects) == 0)
|
||
{
|
||
return "OBJECTS=none_detected";
|
||
}
|
||
|
||
string result = "OBJECTS=";
|
||
integer count = llGetListLength(detected_objects) / 9; // Changed from 6 to 9
|
||
integer i;
|
||
|
||
vector npc_pos = getSelfPos();
|
||
rotation npc_rot = getAnchorRot();
|
||
|
||
for (i = 0; i < count; i++)
|
||
{
|
||
integer idx = i * 9; // Changed from 6 to 9
|
||
string name = llList2String(detected_objects, idx);
|
||
string desc = llList2String(detected_objects, idx + 1);
|
||
vector objpos = 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);
|
||
vector bbox_min = llList2Vector(detected_objects, idx + 6);
|
||
vector bbox_max = llList2Vector(detected_objects, idx + 7);
|
||
|
||
string objinfo = name;
|
||
if (desc != "" && desc != name)
|
||
{
|
||
objinfo += " (" + desc + ")";
|
||
}
|
||
|
||
// Calculate spatial information
|
||
string direction = getRelativeDirection(npc_pos, npc_rot, objpos);
|
||
string height_rel = getRelativeHeight(npc_pos.z, objpos.z);
|
||
|
||
// Calculate object size from bounding box
|
||
string size_str = "";
|
||
if (bbox_min != ZERO_VECTOR || bbox_max != ZERO_VECTOR)
|
||
{
|
||
vector size = bbox_max - bbox_min;
|
||
if (size.x >= 0.1 || size.y >= 0.1 || size.z >= 0.1)
|
||
{
|
||
string w = fmtFloat(size.x, 1);
|
||
string h = fmtFloat(size.z, 1); // Z is height
|
||
string d = fmtFloat(size.y, 1);
|
||
size_str = w + "×" + h + "×" + d + "m";
|
||
}
|
||
}
|
||
|
||
// Build the info string with enhanced spatial data
|
||
objinfo += " [" + objtype + ", " + direction;
|
||
|
||
if (height_rel != "level")
|
||
objinfo += ", " + height_rel;
|
||
|
||
objinfo += ", " + fmtFloat(dist, 1) + "m";
|
||
|
||
if (size_str != "")
|
||
objinfo += ", size:" + size_str;
|
||
|
||
objinfo += "]";
|
||
|
||
if (i > 0) result += ", ";
|
||
result += objinfo;
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
|
||
// Build top-K nearby human avatars using osGetAvatarList (stride: key, pos, name), anchored to the prim
|
||
// Returns up to k closest avatars from detected_avatars
|
||
// Returns up to k closest avatars with avatar‐flipped directions
|
||
list topKNearbyAvatars(integer k)
|
||
{
|
||
list out = [];
|
||
integer total = llGetListLength(detected_avatars) / 3;
|
||
if (total == 0) return out;
|
||
|
||
vector selfPos = llGetPos();
|
||
rotation selfRot = llGetRot();
|
||
list entries = [];
|
||
|
||
// Build distance‐indexed entries with direction and height
|
||
integer idx;
|
||
for (idx = 0; idx < total; ++idx)
|
||
{
|
||
integer base = idx * 3;
|
||
key akey = llList2Key(detected_avatars, base);
|
||
string aname = llList2String(detected_avatars, base + 1);
|
||
vector apos = llList2Vector(detected_avatars, base + 2);
|
||
float dist = llVecDist(selfPos, apos);
|
||
// Avatar‐flipped direction
|
||
string dir = getAvatarDirection(selfPos, selfRot, apos);
|
||
// Relative height
|
||
string hgt = getRelativeHeight(selfPos.z, apos.z);
|
||
|
||
// Each entry: distance, key, name, pos, dir, hgt
|
||
entries += [
|
||
dist,
|
||
(string)akey,
|
||
aname,
|
||
llDumpList2String([apos], ","),
|
||
dir,
|
||
hgt
|
||
];
|
||
}
|
||
|
||
// Sort by distance (stride=6, index=0)
|
||
entries = llListSort(entries, 6, TRUE);
|
||
|
||
// Determine count
|
||
integer totalEntries = llGetListLength(entries) / 6;
|
||
integer count = (totalEntries < k) ? totalEntries : k;
|
||
|
||
// Build output list
|
||
integer e;
|
||
for (e = 0; e < count; ++e)
|
||
{
|
||
integer off = e * 6;
|
||
float d = llList2Float(entries, off + 0);
|
||
key key_s = (key)llList2String(entries, off + 1);
|
||
string nm = llList2String(entries, off + 2);
|
||
vector pos = (vector)llList2Vector(
|
||
llParseString2List(
|
||
llList2String(entries, off + 3),
|
||
[","], []),
|
||
0);
|
||
string dir = llList2String(entries, off + 4);
|
||
string hgt = llList2String(entries, off + 5);
|
||
|
||
// Build CSV info: key,name,distance,direction,height
|
||
string info = (string)key_s + "," +
|
||
nm + "," +
|
||
(string)d + "m," +
|
||
dir + "," +
|
||
hgt;
|
||
out += [ info ];
|
||
}
|
||
|
||
return out;
|
||
}
|
||
|
||
|
||
|
||
// NEW: Enhanced buildSelfField with universal seating awareness
|
||
string buildSelfField()
|
||
{
|
||
vector p = getSelfPos();
|
||
string pos = fmtFloat(p.x,3) + "," + fmtFloat(p.y,3) + "," + fmtFloat(p.z,3);
|
||
|
||
string selfField = "SELF=pos:" + pos +
|
||
",sit:" + (sitting ? "1" : "0") +
|
||
",follow:" + (following ? "1" : "0") +
|
||
",followtarget:" + (string)followtarget;
|
||
|
||
// NEW: Add universal seating awareness information
|
||
if (seating_awareness_active && current_seating_info != "") {
|
||
selfField += ",sitting_on:" + current_seating_info;
|
||
}
|
||
|
||
return selfField;
|
||
}
|
||
|
||
string buildAvatarsFieldFromList(list K)
|
||
{
|
||
string s = "AVATARS=" + (string)llGetListLength(K);
|
||
key owner = llGetOwner();
|
||
integer i;
|
||
for (i = 0; i < llGetListLength(K); i++)
|
||
{
|
||
string avatarData = llList2String(K, i);
|
||
|
||
// Parse to get the avatar key (first field)
|
||
list fields = llParseString2List(avatarData, [","], []);
|
||
if (llGetListLength(fields) > 0)
|
||
{
|
||
key avatarKey = (key)llList2String(fields, 0);
|
||
string avatarName = llList2String(fields, 1);
|
||
|
||
// Label self and owner
|
||
if (avatarKey == NPC)
|
||
{
|
||
avatarName = "self:" + avatarName;
|
||
}
|
||
else if (avatarKey == owner)
|
||
{
|
||
avatarName = "owner:" + avatarName;
|
||
}
|
||
|
||
// Rebuild the data with labeled name
|
||
fields = llListReplaceList(fields, [avatarName], 1, 1);
|
||
avatarData = llDumpList2String(fields, ",");
|
||
}
|
||
|
||
s += "," + avatarData;
|
||
}
|
||
return s;
|
||
}
|
||
|
||
// Owner summary fragment using same filters as avatar FOV
|
||
string buildOwnerFragment()
|
||
{
|
||
vector Apos = getAnchorPos();
|
||
rotation Arot = getAnchorRot();
|
||
|
||
key owner = llGetOwner();
|
||
if (owner == NULL_KEY || osIsNpc(owner)) return "";
|
||
|
||
vector op = llList2Vector(llGetObjectDetails(owner, [OBJECT_POS]), 0);
|
||
if (op == ZERO_VECTOR) return "";
|
||
if (!withinFOV(Apos, Arot, op)) return "";
|
||
|
||
float od = llVecDist(Apos, op);
|
||
string on = llKey2Name(owner);
|
||
if (on == "") on = (string)owner;
|
||
|
||
return "owner:" + on + "," + fmtFloat(od,1) + "m";
|
||
}
|
||
|
||
string buildObjectsField()
|
||
{
|
||
if (SENSES_OBJECT_DETAILS)
|
||
{
|
||
return buildObjectsFieldDetailed();
|
||
}
|
||
else
|
||
{
|
||
// Original simple format for backward compatibility
|
||
return "OBJECTS=scripted:" + (string)fovscripted + ",passive:" + (string)fovpassive;
|
||
}
|
||
}
|
||
|
||
// buildsummaryFromK with universal seating awareness
|
||
string buildsummaryFromK(list K)
|
||
{
|
||
string objectInfo = buildObjectsField();
|
||
|
||
string summary = "SUMMARY=" +
|
||
(following ? "following" : "idle") +
|
||
(sitting ? ",sitting" : "") +
|
||
",avatars=" + (string)llGetListLength(K) +
|
||
"," + objectInfo;
|
||
|
||
// NEW: Add universal seating awareness to summary
|
||
if (seating_awareness_active && current_seating_info != "") {
|
||
summary += ",currently_sitting_on=" + current_seating_info;
|
||
}
|
||
|
||
return summary;
|
||
}
|
||
|
||
integer loadconfig()
|
||
{
|
||
string txt = getcard(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);
|
||
|
||
if (k == "CONFIG_CARD") CONFIG_CARD = v;
|
||
else if (k == "SENSES_ENABLED") SENSES_ENABLED = (integer)v;
|
||
else if (k == "SENSES_TIMER_INTERVAL") SENSES_TIMER_INTERVAL = (float)v;
|
||
else if (k == "SENSES_RADIUS") SENSES_RADIUS = (float)v;
|
||
else if (k == "SENSES_ARC_DEG") SENSES_ARC_DEG = (float)v;
|
||
else if (k == "SENSES_TOPK_AVATARS") SENSES_TOPK_AVATARS = (integer)v;
|
||
// Enhanced object detection config
|
||
else if (k == "SENSES_MAX_OBJECTS") SENSES_MAX_OBJECTS = (integer)v;
|
||
else if (k == "SENSES_INCLUDE_ATTACHMENTS") SENSES_INCLUDE_ATTACHMENTS = (integer)v;
|
||
else if (k == "SENSES_OBJECT_DETAILS") SENSES_OBJECT_DETAILS = (integer)v;
|
||
else if (k == "SENSES_DEBUG") SENSES_DEBUG = (integer)v;
|
||
|
||
@cont;
|
||
}
|
||
return TRUE;
|
||
}
|
||
|
||
// ---- DEFAULT
|
||
default
|
||
{
|
||
state_entry()
|
||
{
|
||
loadconfig();
|
||
if (SENSES_ENABLED) {
|
||
float arc = SENSES_ARC_DEG * DEG_TO_RAD;
|
||
llSensorRepeat(
|
||
"", // name filter
|
||
NULL_KEY, // key filter
|
||
AGENT | ACTIVE | PASSIVE, // detect avatars + objects
|
||
SENSES_RADIUS,
|
||
SENSES_ARC_DEG * DEG_TO_RAD,
|
||
SENSES_TIMER_INTERVAL
|
||
);
|
||
llSetTimerEvent(SENSES_TIMER_INTERVAL); // Keep for coordination
|
||
}
|
||
llOwnerSay("Senses: Ready with Master Timer Coordination");
|
||
}
|
||
|
||
sensor(integer n)
|
||
{
|
||
detected_objects = []; // Clear previous scan
|
||
detected_avatars = []; // Clear previous scan
|
||
integer sc = 0;
|
||
integer pc = 0;
|
||
vector mypos = getSelfPos();
|
||
rotation myrot = getAnchorRot();
|
||
|
||
integer i;
|
||
for (i = 0; i < n; i++)
|
||
{
|
||
key objkey = llDetectedKey(i);
|
||
string name = llDetectedName(i);
|
||
vector objpos = llDetectedPos(i);
|
||
integer objtype = llDetectedType(i);
|
||
if (objtype & AGENT)
|
||
{
|
||
key akey = llDetectedKey(i);
|
||
string aname = llDetectedName(i);
|
||
vector apos = llDetectedPos(i);
|
||
|
||
// Skip the NPC's own key - NPCs don't see themselves
|
||
if (akey == NPC) jump skip_self;
|
||
|
||
detected_avatars += [ akey, aname, apos ];
|
||
|
||
@skip_self;
|
||
}
|
||
|
||
// Keep original counting for backward compatibility
|
||
if (objtype & SCRIPTED) sc += 1;
|
||
else if (objtype & PASSIVE) pc += 1;
|
||
|
||
// Enhanced detection only if enabled
|
||
if (!SENSES_OBJECT_DETAILS) jump nextobj;
|
||
|
||
if (!withinFOV(mypos, myrot, objpos)) jump nextobj;
|
||
|
||
// Skip attachments if configured
|
||
if (!SENSES_INCLUDE_ATTACHMENTS && (objtype & AGENT)) jump nextobj;
|
||
|
||
// Get additional details via llGetObjectDetails
|
||
list details = llGetObjectDetails(objkey, [OBJECT_DESC]);
|
||
string desc = "";
|
||
if (llGetListLength(details) > 0)
|
||
{
|
||
desc = llList2String(details, 0);
|
||
}
|
||
|
||
|
||
// Get bounding box for size calculations
|
||
vector bbox_min = ZERO_VECTOR;
|
||
vector bbox_max = ZERO_VECTOR;
|
||
list bbox = llGetBoundingBox(objkey);
|
||
if (llGetListLength(bbox) == 2)
|
||
{
|
||
bbox_min = llList2Vector(bbox, 0);
|
||
bbox_max = llList2Vector(bbox, 1);
|
||
}
|
||
|
||
// Calculate distance for sorting
|
||
float dist = llVecDist(mypos, objpos);
|
||
|
||
// Get object type description
|
||
string typeDesc = getObjectType(objtype);
|
||
|
||
// Store: name, desc, pos, distance, key, type
|
||
detected_objects += [name, desc, objpos, dist, objkey, typeDesc, bbox_min, bbox_max, objpos];
|
||
|
||
@nextobj;
|
||
}
|
||
|
||
// Sort objects by distance and keep top objects
|
||
detected_objects = sortObjectsByDistance(detected_objects);
|
||
|
||
// Keep original counters
|
||
fovscripted = sc;
|
||
fovpassive = pc;
|
||
|
||
}
|
||
|
||
no_sensor()
|
||
{
|
||
fovscripted = 0;
|
||
fovpassive = 0;
|
||
detected_objects = [];
|
||
}
|
||
|
||
link_message(integer sender, integer num, string str, key id)
|
||
{
|
||
if (num == LM_CONFIG)
|
||
{
|
||
NPC = id;
|
||
}
|
||
else if (num == LM_FOLLOW)
|
||
{
|
||
following = (id != NULL_KEY);
|
||
followtarget = id;
|
||
sitting = FALSE;
|
||
// Clear seating awareness when starting to follow
|
||
seating_awareness_active = FALSE;
|
||
current_seating_info = "";
|
||
}
|
||
else if (num == LM_UNFOLLOW)
|
||
{
|
||
following = FALSE;
|
||
followtarget = NULL_KEY;
|
||
}
|
||
else if (num == LM_SIT)
|
||
{
|
||
sitting = TRUE;
|
||
following = FALSE;
|
||
}
|
||
else if (num == LM_STAND)
|
||
{
|
||
sitting = FALSE;
|
||
// Clear seating awareness when standing
|
||
seating_awareness_active = FALSE;
|
||
current_seating_info = "";
|
||
}
|
||
// NEW: Handle universal seating awareness updates from Actions.lsl
|
||
else if (num == LM_SEATING_UPDATE)
|
||
{
|
||
list parts = llParseString2List(str, ["|"], []);
|
||
if (llGetListLength(parts) >= 2)
|
||
{
|
||
string action = llList2String(parts, 0);
|
||
string seatingInfo = llList2String(parts, 1);
|
||
|
||
if (action == "sitting_on")
|
||
{
|
||
seating_awareness_active = TRUE;
|
||
current_seating_info = seatingInfo;
|
||
}
|
||
else if (action == "standing")
|
||
{
|
||
seating_awareness_active = FALSE;
|
||
current_seating_info = "";
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
timer()
|
||
{
|
||
if (!SENSES_ENABLED) return;
|
||
|
||
integer current_cycle = (integer)(llGetTime() / MASTER_TIMER_INTERVAL) % 4;
|
||
|
||
// Only process and send data on our designated cycle
|
||
if (current_cycle == SENSES_CYCLE_OFFSET) {
|
||
// Compute avatars once and reuse for both fields and summary
|
||
list nearby = topKNearbyAvatars(SENSES_TOPK_AVATARS);
|
||
|
||
if (SENSES_DEBUG)
|
||
{
|
||
llOwnerSay("DEBUG: Timer cycle " + (string)current_cycle +
|
||
" - Processing " + (string)llGetListLength(nearby) +
|
||
" top avatars");
|
||
|
||
// Show what we're sending
|
||
integer k;
|
||
for (k = 0; k < llGetListLength(nearby); k++)
|
||
{
|
||
llOwnerSay(" -> " + llList2String(nearby, k));
|
||
}
|
||
}
|
||
|
||
string payload = join_pipe([
|
||
buildSelfField(),
|
||
buildAvatarsFieldFromList(nearby),
|
||
buildObjectsField(),
|
||
buildsummaryFromK(nearby)
|
||
]);
|
||
|
||
llMessageLinked(LINK_SET, LM_SENSE, payload, NPC);
|
||
|
||
// Send object data to Actions.lsl for seating detection
|
||
sendObjectsToActions();
|
||
|
||
// Send avatar position data to Actions.lsl for occupancy detection
|
||
sendAvatarsToActions();
|
||
}
|
||
}
|
||
|
||
on_rez(integer p)
|
||
{
|
||
llResetScript();
|
||
}
|
||
|
||
changed(integer c)
|
||
{
|
||
if (c & CHANGED_OWNER)
|
||
{
|
||
llResetScript();
|
||
}
|
||
}
|
||
} |