mirror of
https://github.com/DM67-Developer/opensim-ai-npc-framework.git
synced 2026-08-14 00:47:59 +00:00
524 lines
22 KiB
Plaintext
524 lines
22 KiB
Plaintext
// Main.lsl — NPC controller with ENHANCED Brain rotation sync
|
|
// Requires OSSL: osNpcCreate, osNpcGetOwner, osNpcSay, osNpcSayTo, osIsNpc, osGetNotecard, osNpcGetPos, osNpcGetRot
|
|
|
|
// ---- Opcodes (must match in Chat.lsl and Actions.lsl)
|
|
integer LM_INIT = 1;
|
|
integer LM_CONFIG = 300;
|
|
integer LM_USER_MSG = 100; // str: name|msg , id: speaker key
|
|
integer LM_AI_SAY = 110; // from Chat: str: say|private|action , id: target key
|
|
integer LM_ACTION = 200; // general action pass-through
|
|
integer LM_FOLLOW = 210; // str: mode|key , id: target key
|
|
integer LM_UNFOLLOW = 211; // stop following
|
|
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
|
|
|
|
|
|
// ---- Brain Controller opcodes (new)
|
|
integer LM_BRAIN_VISIBLE = 220; // make brain visible and moveable
|
|
integer LM_BRAIN_INVISIBLE = 221; // make brain invisible and phantom
|
|
integer LM_BRAIN_POSITION = 222; // position brain in NPC head
|
|
|
|
// ---- Config variables (loaded from general.cfg)
|
|
string APPEARANCE_CARD = "appearance";
|
|
string CONTEXT_CARD = "personality.cfg";
|
|
string CONFIG_CARD = "general.cfg";
|
|
float MASTER_TIMER_BASE_INTERVAL = 1.0; // Base timer interval
|
|
float MASTER_TIMER_STAGGER_OFFSET = 0.0; // Configurable stagger offset
|
|
float CALCULATED_MASTER_INTERVAL = 1.0; // Final calculated interval
|
|
|
|
string API_URL = "http://127.0.0.1:5001/api/v1/generate";
|
|
integer MESSAGE_LIMIT = 400;
|
|
integer LOG_LIMIT = 16;
|
|
string NPC_FIRST = "Delphina";
|
|
string NPC_LAST = "II";
|
|
string WAKE_WORDS = "Delphina|Delphina II";
|
|
integer MAX_TOKENS = 100;
|
|
integer REPLY_CHAR_CAP = 800;
|
|
|
|
// Display and timing configuration
|
|
integer LISTEN_CHANNEL = 0;
|
|
float MAIN_TIMER_INTERVAL = 1.0;
|
|
vector NPC_SPAWN_OFFSET = <1.0, 0.0, 1.0>;
|
|
float TEXT_ALPHA = 1.0;
|
|
vector STATUS_COLOR_WHITE = <1,1,1>;
|
|
vector STATUS_COLOR_RED = <1,0.3,0.3>;
|
|
vector STATUS_COLOR_GREEN = <0.6,1,0.6>;
|
|
|
|
// Brain Controller Configuration (new)
|
|
float BRAIN_EYE_OFFSET = 1.7; // Height offset for brain position (eye level)
|
|
vector BRAIN_WORK_OFFSET = <2.0, 0.0, 1.0>; // Offset when made visible for work
|
|
string BRAIN_VISIBLE_COMMAND = "brain work"; // Owner command to make brain visible
|
|
string BRAIN_HIDE_COMMAND = "brain hide"; // Owner command to hide brain
|
|
string BRAIN_STATUS_COMMAND = "brain status"; // Owner command to check brain status
|
|
vector BRAIN_VISIBLE_COLOR = <1.0, 0.8, 0.2>; // Yellow glow when visible
|
|
float BRAIN_GLOW = 0.3; // Glow intensity when visible
|
|
|
|
// Status messages
|
|
string MSG_STARTING = "Main: starting...";
|
|
string MSG_CREATION_FAILED = "Main: creation failed.";
|
|
string MSG_READY = "Main: ready. Say 'Delphina' or use @Delphina.";
|
|
string MSG_THINKING = "Main: thinking...";
|
|
string MSG_CONTROLLER_FAILED = "Controller: NPC creation failed; check appearance and OSSL permissions.";
|
|
string MSG_TOUCH_HELP = "Use @Delphina to whisper; say 'Delphina' to address publicly.";
|
|
string MSG_BRAIN_VISIBLE = "Brain: controller visible - you can now work on it.";
|
|
string MSG_BRAIN_HIDDEN = "Brain: controller hidden and following NPC.";
|
|
|
|
// ---- Runtime
|
|
key NPC = NULL_KEY;
|
|
key OWNER = NULL_KEY;
|
|
integer brain_visible = FALSE;
|
|
integer brain_following = TRUE;
|
|
vector last_npc_pos = ZERO_VECTOR;
|
|
rotation last_npc_rot = ZERO_ROTATION; // NEW: Track NPC rotation changes
|
|
|
|
// ---- Helpers
|
|
string get_card(string name) {
|
|
if (llGetInventoryType(name) != INVENTORY_NOTECARD) return "";
|
|
return osGetNotecard(name);
|
|
}
|
|
|
|
integer contains_ci(string hay, string needle) {
|
|
if (needle == "") return FALSE;
|
|
return (llSubStringIndex(llToLower(hay), llToLower(needle)) >= 0);
|
|
}
|
|
|
|
list split_pipe(string s) { return llParseString2List(s, ["|"], []); }
|
|
string join_pipe(list L) { return llDumpList2String(L, "|"); }
|
|
|
|
integer starts_with_at_name(string msg) {
|
|
if (llStringLength(msg) < 2) return FALSE;
|
|
if (llGetSubString(msg,0,0) != "@") return FALSE;
|
|
integer sp = llSubStringIndex(msg, " ");
|
|
string token;
|
|
if (sp > 1) token = llGetSubString(msg,1,sp-1);
|
|
else token = llGetSubString(msg,1,-1);
|
|
string t = llToLower(llStringTrim(token, STRING_TRIM));
|
|
string a = llToLower(NPC_FIRST);
|
|
string b = llToLower(NPC_FIRST + " " + NPC_LAST);
|
|
if (t == a) return TRUE;
|
|
if (t == b) return TRUE;
|
|
return FALSE;
|
|
}
|
|
|
|
integer addressed_to_me(string msg) {
|
|
if (starts_with_at_name(msg)) return TRUE;
|
|
string low = llToLower(msg);
|
|
if (contains_ci(low, NPC_FIRST)) return TRUE;
|
|
if (contains_ci(low, NPC_FIRST + " " + NPC_LAST)) return TRUE;
|
|
list extras = split_pipe(WAKE_WORDS);
|
|
integer i; for (i = 0; i < llGetListLength(extras); ++i) {
|
|
if (contains_ci(low, llList2String(extras,i))) return TRUE;
|
|
}
|
|
return FALSE;
|
|
}
|
|
|
|
integer is_human_speaker(key id) {
|
|
if (osIsNpc(id)) return FALSE;
|
|
vector sz = llGetAgentSize(id);
|
|
if (sz == ZERO_VECTOR) return FALSE;
|
|
return TRUE;
|
|
}
|
|
|
|
integer say_public(string msg) { if (NPC != NULL_KEY) osNpcSay(NPC, 0, msg); return 0; }
|
|
integer say_private(key who, string msg) { if (NPC != NULL_KEY) osNpcSayTo(NPC, who, 0, msg); return 0; }
|
|
|
|
float calculate_stagger_offset()
|
|
{
|
|
if (MASTER_TIMER_STAGGER_OFFSET != 0.0) {
|
|
// Use configured stagger offset
|
|
return MASTER_TIMER_STAGGER_OFFSET;
|
|
}
|
|
|
|
// Auto-calculate using UUID if no manual stagger configured
|
|
string key_str = llGetKey();
|
|
integer hash = 0;
|
|
integer i;
|
|
for (i = 0; i < 8; i++) {
|
|
integer ascii_val = llOrd(key_str, i); // Get ASCII value at position i
|
|
if (ascii_val >= 48 && ascii_val <= 57) { // '0' to '9'
|
|
hash += ascii_val - 48;
|
|
} else if (ascii_val >= 97 && ascii_val <= 102) { // 'a' to 'f'
|
|
hash += ascii_val - 87;
|
|
} else if (ascii_val >= 65 && ascii_val <= 70) { // 'A' to 'F'
|
|
hash += ascii_val - 55;
|
|
}
|
|
}
|
|
float offset = (float)(hash % 10) / 10.0;
|
|
return offset;
|
|
}
|
|
|
|
// Safe division function to prevent division by zero
|
|
float safe_divide(float numerator, float divisor, float fallback) {
|
|
if (divisor == 0.0) {
|
|
llOwnerSay("WARNING: Division by zero prevented in timer calculation!");
|
|
return fallback;
|
|
}
|
|
return numerator / divisor;
|
|
}
|
|
|
|
integer setup_master_timer()
|
|
{
|
|
float stagger = calculate_stagger_offset();
|
|
CALCULATED_MASTER_INTERVAL = MASTER_TIMER_BASE_INTERVAL + stagger;
|
|
|
|
// STEP 2: Add validation with minimum threshold
|
|
if (CALCULATED_MASTER_INTERVAL <= 0.0) {
|
|
llOwnerSay("ERROR: Invalid timer interval calculated: " + (string)CALCULATED_MASTER_INTERVAL);
|
|
llOwnerSay("Falling back to default 1.0 second interval");
|
|
CALCULATED_MASTER_INTERVAL = 1.0; // Safe fallback
|
|
}
|
|
|
|
// Additional safety: ensure reasonable bounds
|
|
if (CALCULATED_MASTER_INTERVAL < 0.1) {
|
|
llOwnerSay("WARNING: Timer interval too small, adjusting to 0.1s minimum");
|
|
CALCULATED_MASTER_INTERVAL = 0.1;
|
|
}
|
|
if (CALCULATED_MASTER_INTERVAL > 60.0) {
|
|
llOwnerSay("WARNING: Timer interval too large, adjusting to 60s maximum");
|
|
CALCULATED_MASTER_INTERVAL = 60.0;
|
|
}
|
|
|
|
llSetTimerEvent(CALCULATED_MASTER_INTERVAL);
|
|
|
|
// Rest of function remains the same...
|
|
string timer_config = join_pipe([
|
|
"MASTER_INTERVAL=" + (string)CALCULATED_MASTER_INTERVAL,
|
|
"BASE_INTERVAL=" + (string)MASTER_TIMER_BASE_INTERVAL,
|
|
"STAGGER_OFFSET=" + (string)stagger
|
|
]);
|
|
llMessageLinked(LINK_SET, LM_TIMER_CONFIG, timer_config, NPC);
|
|
|
|
llOwnerSay("Master timer: " + (string)CALCULATED_MASTER_INTERVAL + "s (stagger: " + (string)stagger + "s)");
|
|
return 0;
|
|
}
|
|
|
|
// ENHANCED Brain positioning function with rotation sync
|
|
update_brain_position() {
|
|
if (NPC == NULL_KEY || !brain_following) return;
|
|
|
|
vector npc_pos = osNpcGetPos(NPC);
|
|
rotation npc_rot = osNpcGetRot(NPC); // NEW: Get NPC rotation
|
|
|
|
if (npc_pos == ZERO_VECTOR || npc_rot == ZERO_ROTATION) return;
|
|
|
|
// Check if position OR rotation has changed significantly
|
|
float pos_change = llVecDist(npc_pos, last_npc_pos);
|
|
float rot_change = llVecDist(llRot2Euler(npc_rot), llRot2Euler(last_npc_rot));
|
|
|
|
if (pos_change < 0.1 && rot_change < 0.1) return;
|
|
|
|
// Update brain position and rotation to match NPC
|
|
last_npc_pos = npc_pos;
|
|
last_npc_rot = npc_rot; // NEW: Remember NPC rotation
|
|
|
|
vector brain_pos = npc_pos + <0, 0, BRAIN_EYE_OFFSET>;
|
|
llSetPos(brain_pos);
|
|
llSetRot(npc_rot); // ✅ NEW: Brain now faces same direction as NPC!
|
|
}
|
|
|
|
set_brain_visible(integer visible) {
|
|
if (visible) {
|
|
// Make visible and solid for work
|
|
brain_visible = TRUE;
|
|
brain_following = FALSE;
|
|
llSetAlpha(1.0, ALL_SIDES);
|
|
llSetText("BRAIN CONTROLLER - WORK MODE", BRAIN_VISIBLE_COLOR, 1.0);
|
|
llSetPrimitiveParams([PRIM_GLOW, ALL_SIDES, BRAIN_GLOW]);
|
|
llSetStatus(STATUS_PHANTOM, FALSE);
|
|
|
|
// Move to work position (maintain current rotation when working)
|
|
if (NPC != NULL_KEY) {
|
|
vector npc_pos = osNpcGetPos(NPC);
|
|
if (npc_pos != ZERO_VECTOR) {
|
|
llSetPos(npc_pos + BRAIN_WORK_OFFSET);
|
|
}
|
|
}
|
|
llOwnerSay(MSG_BRAIN_VISIBLE);
|
|
} else {
|
|
// Make invisible and phantom
|
|
brain_visible = FALSE;
|
|
brain_following = TRUE;
|
|
llSetAlpha(0.0, ALL_SIDES);
|
|
llSetText("", ZERO_VECTOR, 0.0);
|
|
llSetPrimitiveParams([PRIM_GLOW, ALL_SIDES, 0.0]);
|
|
llSetStatus(STATUS_PHANTOM, TRUE);
|
|
|
|
// Return to NPC head position with proper rotation sync
|
|
update_brain_position();
|
|
llOwnerSay(MSG_BRAIN_HIDDEN);
|
|
}
|
|
}
|
|
|
|
check_owner_commands(string msg, key speaker) {
|
|
if (speaker != llGetOwner()) return;
|
|
|
|
string low = llToLower(llStringTrim(msg, STRING_TRIM));
|
|
|
|
if (low == BRAIN_VISIBLE_COMMAND) {
|
|
set_brain_visible(TRUE);
|
|
} else if (low == BRAIN_HIDE_COMMAND) {
|
|
set_brain_visible(FALSE);
|
|
} else if (low == BRAIN_STATUS_COMMAND) {
|
|
string status = "Brain: Status: ";
|
|
status += brain_visible ? "VISIBLE/WORK MODE" : "HIDDEN/PHANTOM MODE";
|
|
status += " | Following: " + (string)brain_following;
|
|
if (NPC != NULL_KEY) {
|
|
vector npc_pos = osNpcGetPos(NPC);
|
|
rotation npc_rot = osNpcGetRot(NPC); // NEW: Include rotation info
|
|
vector my_pos = llGetPos();
|
|
rotation my_rot = llGetRot();
|
|
|
|
float distance = llVecDist(my_pos, npc_pos);
|
|
float rounded_distance = llRound(distance * 100.0) / 100.0;
|
|
|
|
// NEW: Calculate rotation alignment
|
|
float rot_diff = llVecDist(llRot2Euler(npc_rot), llRot2Euler(my_rot));
|
|
float rot_alignment = llRound((1.0 - (rot_diff / PI)) * 100.0);
|
|
if (rot_alignment < 0) rot_alignment = 0;
|
|
|
|
status += " | Distance to NPC: " + (string)rounded_distance + "m";
|
|
status += " | Rotation sync: " + (string)rot_alignment + "%"; // NEW: Show alignment
|
|
}
|
|
llOwnerSay(status);
|
|
}
|
|
}
|
|
|
|
integer create_npc() {
|
|
vector pos = llGetPos() + NPC_SPAWN_OFFSET;
|
|
NPC = osNpcCreate(NPC_FIRST, NPC_LAST, pos, APPEARANCE_CARD);
|
|
if (NPC == NULL_KEY) {
|
|
llOwnerSay(MSG_CONTROLLER_FAILED);
|
|
return FALSE;
|
|
}
|
|
|
|
OWNER = llGetOwner();
|
|
llOwnerSay("Controller: NPC created: " + (string)NPC + " as " + NPC_FIRST + " " + NPC_LAST + ".");
|
|
|
|
// Initialize brain in phantom mode with proper rotation
|
|
set_brain_visible(FALSE);
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
integer broadcast_config() {
|
|
string payload = join_pipe([
|
|
"API_URL=" + API_URL,
|
|
"MAX_TOKENS=" + (string)MAX_TOKENS,
|
|
"REPLY_CHAR_CAP=" + (string)REPLY_CHAR_CAP,
|
|
"LOG_LIMIT=" + (string)LOG_LIMIT,
|
|
"WAKE_WORDS=" + WAKE_WORDS,
|
|
"NPC_FIRST=" + NPC_FIRST,
|
|
"NPC_LAST=" + NPC_LAST,
|
|
"MASTER_INTERVAL=" + (string)CALCULATED_MASTER_INTERVAL,
|
|
"BASE_INTERVAL=" + (string)MASTER_TIMER_BASE_INTERVAL
|
|
]);
|
|
llMessageLinked(LINK_SET, LM_CONFIG, payload, NPC);
|
|
return 0;
|
|
}
|
|
|
|
// ---- Load configuration from general.cfg notecard
|
|
integer load_config() {
|
|
string txt = get_card(CONFIG_CARD);
|
|
if (txt != "") {
|
|
list lines = llParseString2List(txt, ["\n"], []);
|
|
integer i; for (i = 0; i < llGetListLength(lines); ++i) {
|
|
string line = llStringTrim(llList2String(lines, i), STRING_TRIM);
|
|
if (line == "" || llGetSubString(line,0,0) == "#" || llGetSubString(line,0,1) == "//") jump cont;
|
|
integer eq = llSubStringIndex(line, "=");
|
|
if (eq >= 0) {
|
|
string var = llStringTrim(llGetSubString(line, 0, eq-1), STRING_TRIM);
|
|
string val = llStringTrim(llGetSubString(line, eq+1, -1), STRING_TRIM);
|
|
|
|
// Basic NPC Identity
|
|
if (var == "NPC_FIRST") NPC_FIRST = val;
|
|
else if (var == "NPC_LAST") NPC_LAST = val;
|
|
else if (var == "WAKE_WORDS") WAKE_WORDS = val;
|
|
|
|
// File references
|
|
else if (var == "APPEARANCE_CARD") APPEARANCE_CARD = val;
|
|
else if (var == "CONTEXT_CARD") CONTEXT_CARD = val;
|
|
else if (var == "CONFIG_CARD") CONFIG_CARD = val;
|
|
|
|
// Brain Controller config
|
|
else if (var == "BRAIN_EYE_OFFSET") BRAIN_EYE_OFFSET = (float)val;
|
|
else if (var == "BRAIN_WORK_OFFSET") BRAIN_WORK_OFFSET = (vector)val;
|
|
else if (var == "BRAIN_VISIBLE_COMMAND") BRAIN_VISIBLE_COMMAND = val;
|
|
else if (var == "BRAIN_HIDE_COMMAND") BRAIN_HIDE_COMMAND = val;
|
|
else if (var == "BRAIN_STATUS_COMMAND") BRAIN_STATUS_COMMAND = val;
|
|
else if (var == "BRAIN_VISIBLE_COLOR") BRAIN_VISIBLE_COLOR = (vector)val;
|
|
else if (var == "BRAIN_GLOW") BRAIN_GLOW = (float)val;
|
|
|
|
// AI API Configuration
|
|
else if (var == "API_URL") API_URL = val;
|
|
else if (var == "MAX_TOKENS") MAX_TOKENS = (integer)val;
|
|
else if (var == "REPLY_CHAR_CAP") REPLY_CHAR_CAP = (integer)val;
|
|
else if (var == "MESSAGE_LIMIT") MESSAGE_LIMIT = (integer)val;
|
|
else if (var == "LOG_LIMIT") LOG_LIMIT = (integer)val;
|
|
|
|
// Display and timing configuration
|
|
else if (var == "LISTEN_CHANNEL") LISTEN_CHANNEL = (integer)val;
|
|
else if (var == "MAIN_TIMER_INTERVAL") MAIN_TIMER_INTERVAL = (float)val;
|
|
else if (var == "NPC_SPAWN_OFFSET") NPC_SPAWN_OFFSET = (vector)val;
|
|
else if (var == "TEXT_ALPHA") TEXT_ALPHA = (float)val;
|
|
else if (var == "STATUS_COLOR_WHITE") STATUS_COLOR_WHITE = (vector)val;
|
|
else if (var == "STATUS_COLOR_RED") STATUS_COLOR_RED = (vector)val;
|
|
else if (var == "STATUS_COLOR_GREEN") STATUS_COLOR_GREEN = (vector)val;
|
|
// ENHANCED: Validate MASTER_TIMER_BASE_INTERVAL
|
|
else if (var == "MASTER_TIMER_BASE_INTERVAL") {
|
|
float val_float = (float)val;
|
|
if (val_float <= 0.0) {
|
|
llOwnerSay("WARNING: Invalid MASTER_TIMER_BASE_INTERVAL in config: " + (string)val_float + ", using default 1.0");
|
|
MASTER_TIMER_BASE_INTERVAL = 1.0;
|
|
} else if (val_float < 0.1) {
|
|
llOwnerSay("WARNING: MASTER_TIMER_BASE_INTERVAL too small: " + (string)val_float + ", using minimum 0.1");
|
|
MASTER_TIMER_BASE_INTERVAL = 0.1;
|
|
} else if (val_float > 60.0) {
|
|
llOwnerSay("WARNING: MASTER_TIMER_BASE_INTERVAL too large: " + (string)val_float + ", using maximum 60.0");
|
|
MASTER_TIMER_BASE_INTERVAL = 60.0;
|
|
} else {
|
|
MASTER_TIMER_BASE_INTERVAL = val_float;
|
|
// Optional: Confirm valid config loaded
|
|
// llOwnerSay("Config: MASTER_TIMER_BASE_INTERVAL set to " + (string)val_float);
|
|
}
|
|
}
|
|
// ENHANCED: Validate MASTER_TIMER_STAGGER_OFFSET
|
|
else if (var == "MASTER_TIMER_STAGGER_OFFSET") {
|
|
float val_float = (float)val;
|
|
if (val_float < 0.0) {
|
|
llOwnerSay("WARNING: Negative MASTER_TIMER_STAGGER_OFFSET: " + (string)val_float + ", using 0.0");
|
|
MASTER_TIMER_STAGGER_OFFSET = 0.0;
|
|
} else if (val_float > 30.0) {
|
|
llOwnerSay("WARNING: MASTER_TIMER_STAGGER_OFFSET too large: " + (string)val_float + ", using maximum 5.0");
|
|
MASTER_TIMER_STAGGER_OFFSET = 5.0;
|
|
} else {
|
|
MASTER_TIMER_STAGGER_OFFSET = val_float;
|
|
// Optional: Confirm valid config loaded
|
|
// llOwnerSay("Config: MASTER_TIMER_STAGGER_OFFSET set to " + (string)val_float);
|
|
}
|
|
}
|
|
|
|
|
|
// Status messages
|
|
else if (var == "MSG_STARTING") MSG_STARTING = val;
|
|
else if (var == "MSG_CREATION_FAILED") MSG_CREATION_FAILED = val;
|
|
else if (var == "MSG_READY") MSG_READY = val;
|
|
else if (var == "MSG_THINKING") MSG_THINKING = val;
|
|
else if (var == "MSG_CONTROLLER_FAILED") MSG_CONTROLLER_FAILED = val;
|
|
else if (var == "MSG_TOUCH_HELP") MSG_TOUCH_HELP = val;
|
|
else if (var == "MSG_BRAIN_VISIBLE") MSG_BRAIN_VISIBLE = val;
|
|
else if (var == "MSG_BRAIN_HIDDEN") MSG_BRAIN_HIDDEN = val;
|
|
}
|
|
@cont;
|
|
}
|
|
return TRUE;
|
|
}
|
|
return FALSE;
|
|
}
|
|
|
|
// ---- DEFAULT
|
|
default
|
|
{
|
|
state_entry()
|
|
{
|
|
llSetText(MSG_STARTING, STATUS_COLOR_WHITE, TEXT_ALPHA);
|
|
load_config(); // Load config first
|
|
setup_master_timer(); // Setup staggered timer
|
|
|
|
OWNER = llGetOwner();
|
|
if (!create_npc()) {
|
|
llSetText(MSG_CREATION_FAILED, STATUS_COLOR_RED, TEXT_ALPHA);
|
|
return;
|
|
}
|
|
|
|
llListen(LISTEN_CHANNEL, "", NULL_KEY, "");
|
|
broadcast_config();
|
|
llMessageLinked(LINK_SET, LM_INIT, "ready", NPC);
|
|
llSetText(MSG_READY, STATUS_COLOR_GREEN, TEXT_ALPHA);
|
|
}
|
|
|
|
listen(integer ch, string name, key id, string msg) {
|
|
// Check for owner brain commands first
|
|
check_owner_commands(msg, id);
|
|
|
|
if (!is_human_speaker(id)) return;
|
|
if (llGetSubString(msg, 0, 0) == "/") return;
|
|
if (!addressed_to_me(msg)) return;
|
|
|
|
string payload = join_pipe([ name, msg ]);
|
|
llMessageLinked(LINK_SET, LM_USER_MSG, payload, id);
|
|
llSetText(MSG_THINKING, STATUS_COLOR_WHITE, TEXT_ALPHA);
|
|
}
|
|
|
|
link_message(integer sender, integer num, string str, key id) {
|
|
if (num == LM_AI_SAY) {
|
|
list L = split_pipe(str);
|
|
|
|
string say = "";
|
|
if (llGetListLength(L) > 0) say = llList2String(L, 0);
|
|
|
|
string privs = "false";
|
|
if (llGetListLength(L) > 1) privs = llList2String(L, 1);
|
|
|
|
string action = "";
|
|
if (llGetListLength(L) > 2) action = llList2String(L, 2);
|
|
|
|
integer priv = (llToLower(privs) == "true");
|
|
|
|
if (say != "") {
|
|
if (priv && id != NULL_KEY) say_private(id, say);
|
|
else say_public(say);
|
|
}
|
|
|
|
// Only AI-decided actions
|
|
if (action == "follow") {
|
|
llMessageLinked(LINK_SET, LM_FOLLOW, "avatar", id);
|
|
} else if (action == "unfollow") {
|
|
llMessageLinked(LINK_SET, LM_UNFOLLOW, "", NULL_KEY);
|
|
} else if (action == "sit") {
|
|
llMessageLinked(LINK_SET, LM_SIT, "", id);
|
|
} else if (action == "stand") {
|
|
llMessageLinked(LINK_SET, LM_STAND, "", id);
|
|
} else if (action == "not_for_me") {
|
|
llMessageLinked(LINK_SET, LM_IGNORE, "", id);
|
|
} else if (action != "") {
|
|
llMessageLinked(LINK_SET, LM_ACTION, action, id);
|
|
}
|
|
llSetText("Main: ready.", STATUS_COLOR_GREEN, TEXT_ALPHA);
|
|
}
|
|
}
|
|
|
|
timer()
|
|
{
|
|
// ENHANCED Update brain position AND rotation if following
|
|
if (brain_following && !brain_visible) {
|
|
update_brain_position();
|
|
}
|
|
|
|
// STEP 3: Safe timer cycle calculation with validation
|
|
integer timer_cycle = 0; // Default fallback
|
|
float safe_time_ratio = safe_divide(llGetTime(), CALCULATED_MASTER_INTERVAL, 1.0);
|
|
|
|
if (CALCULATED_MASTER_INTERVAL > 0.0) {
|
|
timer_cycle = (integer)safe_time_ratio % 4;
|
|
} else {
|
|
// Emergency fallback - shouldn't happen with Step 2, but double protection
|
|
llOwnerSay("CRITICAL: Timer interval is zero during timer event!");
|
|
CALCULATED_MASTER_INTERVAL = 1.0;
|
|
timer_cycle = (integer)(llGetTime()) % 4;
|
|
}
|
|
|
|
string tick_data = "CYCLE=" + (string)timer_cycle + "|TIME=" + (string)llGetTime();
|
|
llMessageLinked(LINK_SET, LM_TIMER_TICK, tick_data, NPC);
|
|
}
|
|
|
|
touch_start(integer n) {
|
|
if (NPC == NULL_KEY) { create_npc(); broadcast_config(); }
|
|
llOwnerSay(MSG_TOUCH_HELP);
|
|
}
|
|
|
|
on_rez(integer p) { llResetScript(); }
|
|
changed(integer change) { if (change & CHANGED_OWNER) llResetScript(); }
|
|
} |