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

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(); }
}