// ============================================================ // [Diaper] Core v1.8 // Main logic for the OpenSim wearable diaper/incontinence system. // // Companion script: [Diaper] Persistence (must be in same prim) // Notecards: // .settings - EMERGENCY RECOVERY ONLY: touch_enabled=1 // All other settings are stored in llLinksetData // and changed via the in-world menu. // .whitelist - one UUID per line (managed by system) // .blacklist - one UUID per line (managed by system) // // RLV STUB: Search for "// RLV STUB" to find all RLV hook points. // // Changes v1.8: // - Settings split: .settings notecard now holds ONLY touch_enabled // (emergency recovery). All other settings persist in llLinksetData // via SAVE_CFG / LOAD_CFG. This fixes the bug where in-world settings // changes were silently overwritten on every script reset. // - saveSetting() replaced with saveCfg() which sends SAVE_CFG|key=value // to Persistence (individual key writes, no notecard rebuild). // - Startup sequence: LOAD_SETTINGS (notecard, touch_enabled only) -> // LOAD_CFG (llLinksetData, all other settings) -> LOAD_ALL (state) -> // LOAD_LIST x2. // - applySettings() now only acts on touch_enabled. // - applyCfg() handles all other settings from llLinksetData. // - Removed SAVE_SETTINGS message entirely. // - State Names menu hint updated: no longer refers to .settings notecard. // - finishInit() startup message no longer mentions osSetNotecard mode // (notecard mode removed in earlier versions, stale reference cleaned up). // Changes v1.7: // - Fixed: blank command_prefix= in .settings caused llGetSubString to return // the whole "key=" string instead of "" when = is the last character in the // pair. Fixed in applySettings() by explicitly checking eq >= llStringLength-1. // - Fixed: changing command channel via menu then using /N prefix did not work. // Root cause: g_listen_input was still registered when updateCommandListen() // fired. Fixed by moving listen cleanup to top of processInput() so it always // runs before any purpose handling. // Changes v1.4: // - Removed void return type (not supported by this grid's LSL compiler) // - Removed osSetNotecard/osGetPhysicsEngineType (OSSL not required) // - llLinksetData is sole persistence backend // Changes v1.2 (retained for reference): // - Removed premature llOwnerSay from state_entry (fired before Persistence ready) // - menuChannel() now clamps to never return 0 // - SETTINGS handler tolerates empty message (no notecard present = use defaults) // ============================================================ integer LM_CHANNEL = 1001; // ============================================================ // State variables // ============================================================ integer g_wetness = 0; // 0=Dry 1=Damp 2=Wet 3=Soaking 4=Leaking integer g_change_locked = 0; // 0=unlocked 1=wearer-locked 2=owner-locked integer g_removal_locked = 0; // 0=unlocked 1=owner-locked // RLV STUB integer g_diaper_on = 1; // 1=wearing diaper integer g_elapsed_timer = 0; // seconds since last auto-wet string g_rlv_owner = ""; // UUID string, empty = none // RLV STUB // ============================================================ // Settings // touch_enabled comes from .settings notecard (emergency recovery). // Everything else comes from llLinksetData (changed via in-world menus). // ============================================================ integer g_auto_wet_interval = 1800; integer g_announce_full = 0; integer g_announce_leak = 1; integer g_notify_wearer = 1; integer g_touch_enabled = 1; integer g_command_channel = 0; integer g_safeword_channel = 0; string g_safeword = "SAFEWORD"; string g_command_prefix = ""; string g_owner_title = "Daddy"; // RLV STUB - configurable caregiver title list g_state_names = ["Dry", "Damp", "Wet", "Soaking", "Leaking"]; // ============================================================ // Access lists // ============================================================ list g_whitelist = []; list g_blacklist = []; // ============================================================ // Runtime // ============================================================ integer g_persist_mode = 0; integer g_initialised = FALSE; integer g_lists_loaded = 0; // bitmask: 1=whitelist 2=blacklist // Listen handles integer g_listen_command = 0; integer g_listen_safeword = 0; integer g_listen_input = 0; integer g_input_purpose = 0; // Menu state key g_menu_user = NULL_KEY; integer g_menu_channel = 0; integer g_menu_listen = 0; integer TIMER_TICK = 60; // Announce target constants integer ANN_OWNER = 0; integer ANN_LOCAL = 1; integer ANN_ALL = 2; // ============================================================ // Helpers // ============================================================ string wetName() { return llList2String(g_state_names, g_wetness); } string ownerName() { return llKey2Name(llGetOwner()); } string lockStatus() { if (g_change_locked == 0) return "Unlocked"; if (g_change_locked == 1) return "Locked (by you)"; return "Locked (by " + g_owner_title + ")"; } string buildCommandPrefix() { string name = llGetDisplayName(llGetOwner()); if (name == "") name = llKey2Name(llGetOwner()); list parts = llParseString2List(name, [" "], []); string prefix = ""; integer i; for (i = 0; i < llGetListLength(parts) && i < 2; i++) { string part = llList2String(parts, i); if (llStringLength(part) > 0) prefix += llGetSubString(llToLower(part), 0, 0); } return prefix + "diaper"; } // ============================================================ // Access control // ============================================================ integer checkAccess(key agent) { if (agent == llGetOwner()) return TRUE; // RLV STUB: if (agent == (key)g_rlv_owner && g_rlv_owner != "") return TRUE; if (llGetListLength(g_whitelist) > 0) return (llListFindList(g_whitelist, [(string)agent]) != -1); return (llListFindList(g_blacklist, [(string)agent]) == -1); } integer isOwner(key agent) { return (agent == llGetOwner()); } // RLV STUB: integer isRLVOwner(key agent) { return (agent == (key)g_rlv_owner && g_rlv_owner != ""); } // ============================================================ // Announce // ============================================================ announce(string msg, integer target) { if (target == ANN_OWNER) llInstantMessage(llGetOwner(), msg); else llSay(0, msg); if (g_notify_wearer && target != ANN_OWNER) llInstantMessage(llGetOwner(), "[Diaper] " + msg); } // ============================================================ // State changes // ============================================================ applyWetting(integer units) { if (!g_diaper_on) return; integer prev = g_wetness; g_wetness += units; if (g_wetness > 4) g_wetness = 4; if (g_wetness == prev) return; if (g_notify_wearer) llInstantMessage(llGetOwner(), "[Diaper] Your diaper is now " + wetName() + "."); if (g_wetness == 3 && prev < 3) announce(ownerName() + "'s diaper is soaking and needs changing soon!", g_announce_full); else if (g_wetness == 4 && prev < 4) announce(ownerName() + "'s diaper is LEAKING!", g_announce_leak); saveState(); } changeDiaper(key changer) { g_wetness = 0; g_elapsed_timer = 0; g_diaper_on = 1; string changerName = llKey2Name(changer); if (changer == llGetOwner()) changerName = "themselves"; llSay(0, ownerName() + "'s diaper has been changed by " + changerName + ". Fresh and dry!"); if (g_notify_wearer && changer != llGetOwner()) llInstantMessage(llGetOwner(), "[Diaper] Your diaper was changed by " + llKey2Name(changer) + "."); saveState(); } // ============================================================ // Persistence - state // ============================================================ saveState() { string msg = "SAVE_ALL" + "|wetness=" + (string)g_wetness + "|change_locked=" + (string)g_change_locked + "|removal_locked=" + (string)g_removal_locked + "|elapsed_timer=" + (string)g_elapsed_timer + "|diaper_on=" + (string)g_diaper_on + "|rlv_owner=" + g_rlv_owner; llMessageLinked(LINK_ROOT, LM_CHANNEL, msg, NULL_KEY); } // ============================================================ // Persistence - settings (llLinksetData, individual key writes) // ============================================================ saveCfg(string k, string v) { llMessageLinked(LINK_ROOT, LM_CHANNEL, "SAVE_CFG|" + k + "=" + v, NULL_KEY); } // ============================================================ // Menu system // ============================================================ closeMenuListen() { if (g_menu_listen != 0) { llListenRemove(g_menu_listen); g_menu_listen = 0; g_menu_channel = 0; g_menu_user = NULL_KEY; } } integer menuChannel() { integer ch = -1 - (integer)llFrand(999998.0); return ch; } showRootMenu(key agent) { closeMenuListen(); g_menu_user = agent; g_menu_channel = menuChannel(); g_menu_listen = llListen(g_menu_channel, "", agent, ""); string title = "\n[Diaper System]\n" + "Status: " + wetName() + "\nLock: " + lockStatus(); list buttons; if (isOwner(agent)) { if (g_change_locked == 0) buttons = ["Check Status", "Change Diaper", "Wet", "Lock Diaper", "Settings", "Close"]; else buttons = ["Check Status", "Change Diaper", "Wet", "Unlock Diaper", "Settings", "Close"]; } else { buttons = ["Check Status", "Change Diaper", "Close"]; } llDialog(agent, title, buttons, g_menu_channel); } showSettingsMenu(key agent) { closeMenuListen(); g_menu_user = agent; g_menu_channel = menuChannel(); g_menu_listen = llListen(g_menu_channel, "", agent, ""); string title = "\n[Settings 1/2]\n" + "Auto-wet: " + (string)(g_auto_wet_interval/60) + " min\n" + "Full announce: " + announceLabel(g_announce_full) + "\n" + "Leak announce: " + announceLabel(g_announce_leak) + "\n" + "Touch: " + yesNo(g_touch_enabled) + "\n" + "Notify wearer: " + yesNo(g_notify_wearer) + "\n" + "Owner title: " + g_owner_title; list buttons = [ "Auto-wet Time", "Full Announce", "Leak Announce", "State Names", "Touch On/Off", "Notify Wearer", "Command Chan", "Safeword", "Owner Title", "More...", "Back" ]; llDialog(agent, title, buttons, g_menu_channel); } showSettingsMenu2(key agent) { closeMenuListen(); g_menu_user = agent; g_menu_channel = menuChannel(); g_menu_listen = llListen(g_menu_channel, "", agent, ""); string title = "\n[Settings 2/2]\n" + "Access lists and storage."; list buttons = [ "Add Whitelist", "Add Blacklist", "Reload Lists", "Reload Status", "Back2" ]; llDialog(agent, title, buttons, g_menu_channel); } showAnnounceMenu(key agent, string which) { closeMenuListen(); g_menu_user = agent; g_menu_channel = menuChannel(); g_menu_listen = llListen(g_menu_channel, "", agent, ""); if (which == "full") g_input_purpose = 10; else g_input_purpose = 11; llDialog(agent, "\nWho should be told when the diaper is " + which + "?", ["Owner IM", "Local Chat", "Everyone", "Back"], g_menu_channel); } string announceLabel(integer val) { if (val == 0) return "Owner IM"; if (val == 1) return "Local"; return "Everyone"; } string yesNo(integer val) { if (val) return "Yes"; return "No"; } // ============================================================ // Text input collection // ============================================================ // purpose: 1=addwhite 2=addblack 3=safeword 4=auto_wet_mins // 5=command_chan 6=safeword_chan 7=owner_title startListenForInput(key agent, integer purpose, string prompt) { if (g_listen_input != 0) llListenRemove(g_listen_input); g_input_purpose = purpose; g_listen_input = llListen(0, "", agent, ""); llInstantMessage(agent, "[Diaper] " + prompt); } processInput(string text, key agent) { // Clean up input listen first, before any purpose handling. // This ensures updateCommandListen() / updateSafewordListen() can register // cleanly without the input listen still being active. (v1.7 bug fix) llListenRemove(g_listen_input); g_listen_input = 0; integer purpose = g_input_purpose; g_input_purpose = 0; if (purpose == 1) // add whitelist { if (llGetSubString(llToLower(text), 0, 7) == "addwhite") { string uuid = llStringTrim(llGetSubString(text, 9, -1), STRING_TRIM); if (llStringLength(uuid) == 36) { if (llListFindList(g_whitelist, [uuid]) == -1) g_whitelist += [uuid]; writeListToLD("whitelist"); llInstantMessage(agent, "[Diaper] Added " + uuid + " to whitelist."); } else llInstantMessage(agent, "[Diaper] Invalid UUID. Try again."); } else llInstantMessage(agent, "[Diaper] Expected: addwhite "); } else if (purpose == 2) // add blacklist { if (llGetSubString(llToLower(text), 0, 7) == "addblack") { string uuid = llStringTrim(llGetSubString(text, 9, -1), STRING_TRIM); if (llStringLength(uuid) == 36) { if (llListFindList(g_blacklist, [uuid]) == -1) g_blacklist += [uuid]; writeListToLD("blacklist"); llInstantMessage(agent, "[Diaper] Added " + uuid + " to blacklist."); } else llInstantMessage(agent, "[Diaper] Invalid UUID. Try again."); } else llInstantMessage(agent, "[Diaper] Expected: addblack "); } else if (purpose == 3) // new safeword { string sw = llStringTrim(text, STRING_TRIM); if (sw != "") { g_safeword = sw; updateSafewordListen(); saveCfg("safeword", g_safeword); llInstantMessage(agent, "[Diaper] Safeword set to: " + g_safeword); } } else if (purpose == 4) // auto-wet interval { integer mins = (integer)text; if (mins > 0) { g_auto_wet_interval = mins * 60; saveCfg("auto_wet_interval", (string)g_auto_wet_interval); llInstantMessage(agent, "[Diaper] Auto-wet interval set to " + (string)mins + " minutes."); } else llInstantMessage(agent, "[Diaper] Please enter a number of minutes."); } else if (purpose == 5) // command channel { integer chan = (integer)text; g_command_channel = chan; saveCfg("command_channel", (string)chan); updateCommandListen(); llInstantMessage(agent, "[Diaper] Command channel set to " + (string)chan + "."); } else if (purpose == 6) // safeword channel { integer chan = (integer)text; g_safeword_channel = chan; saveCfg("safeword_channel", (string)chan); updateSafewordListen(); llInstantMessage(agent, "[Diaper] Safeword channel set to " + (string)chan + "."); } else if (purpose == 7) // owner title { string title = llStringTrim(text, STRING_TRIM); if (title != "") { g_owner_title = title; saveCfg("owner_title", g_owner_title); llInstantMessage(agent, "[Diaper] Owner title set to: " + g_owner_title); } else llInstantMessage(agent, "[Diaper] Title cannot be blank."); } } // ============================================================ // Access list persistence // ============================================================ writeListToLD(string which) { list theList = g_whitelist; if (which == "blacklist") theList = g_blacklist; string msg = "SAVE_LIST|" + which; integer i; for (i = 0; i < llGetListLength(theList); i++) msg += "|" + llList2String(theList, i); llMessageLinked(LINK_ROOT, LM_CHANNEL, msg, NULL_KEY); } // ============================================================ // Listen management // ============================================================ updateCommandListen() { if (g_listen_command != 0) llListenRemove(g_listen_command); g_listen_command = llListen(g_command_channel, "", NULL_KEY, ""); } updateSafewordListen() { if (g_listen_safeword != 0) llListenRemove(g_listen_safeword); g_listen_safeword = llListen(g_safeword_channel, "", llGetOwner(), ""); } // ============================================================ // Apply settings from SETTINGS message (notecard - touch_enabled only) // ============================================================ applySettings(list parts) { // Only touch_enabled is read from the .settings notecard. // It acts as an emergency override: if present, it wins over llLinksetData. // All other keys in the notecard are silently ignored. integer i; for (i = 1; i < llGetListLength(parts); i++) { string pair = llList2String(parts, i); integer eq = llSubStringIndex(pair, "="); if (eq < 1) jump skip_s; string k = llGetSubString(pair, 0, eq-1); string v; if (eq >= llStringLength(pair) - 1) v = ""; else v = llGetSubString(pair, eq+1, -1); if (k == "touch_enabled") g_touch_enabled = (integer)v; // All other keys deliberately ignored - they live in llLinksetData now. @skip_s; } } // ============================================================ // Apply cfg settings from CFG message (llLinksetData) // ============================================================ applyCfg(list parts) { // parts[0] = "CFG", parts[1..n] = "key=value" integer i; for (i = 1; i < llGetListLength(parts); i++) { string pair = llList2String(parts, i); integer eq = llSubStringIndex(pair, "="); if (eq < 1) jump skip_c; string k = llGetSubString(pair, 0, eq-1); string v; if (eq >= llStringLength(pair) - 1) v = ""; else v = llGetSubString(pair, eq+1, -1); if (k == "auto_wet_interval") g_auto_wet_interval = (integer)v; else if (k == "announce_full") g_announce_full = (integer)v; else if (k == "announce_leak") g_announce_leak = (integer)v; else if (k == "notify_wearer") g_notify_wearer = (integer)v; else if (k == "touch_enabled") g_touch_enabled = (integer)v; else if (k == "command_channel") g_command_channel = (integer)v; else if (k == "command_prefix") { if (v != "") g_command_prefix = v; } else if (k == "safeword") g_safeword = v; else if (k == "safeword_channel") g_safeword_channel = (integer)v; else if (k == "owner_title") { if (v != "") g_owner_title = v; } else if (k == "state_names") { list names = llCSV2List(v); if (llGetListLength(names) == 5) g_state_names = names; } @skip_c; } } // ============================================================ // Apply loaded runtime state // ============================================================ applyLoadedState(list parts) { integer i; for (i = 1; i < llGetListLength(parts); i++) { string pair = llList2String(parts, i); integer eq = llSubStringIndex(pair, "="); if (eq < 1) jump skip_l; string k = llGetSubString(pair, 0, eq-1); string v; if (eq >= llStringLength(pair) - 1) v = ""; else v = llGetSubString(pair, eq+1, -1); if (k == "wetness") g_wetness = (integer)v; else if (k == "change_locked") g_change_locked = (integer)v; else if (k == "removal_locked") g_removal_locked = (integer)v; else if (k == "elapsed_timer") g_elapsed_timer = (integer)v; else if (k == "diaper_on") g_diaper_on = (integer)v; else if (k == "rlv_owner") g_rlv_owner = v; @skip_l; } } // ============================================================ // Finish initialisation // ============================================================ finishInit() { if (g_command_prefix == "") g_command_prefix = buildCommandPrefix(); updateCommandListen(); updateSafewordListen(); llSetTimerEvent(TIMER_TICK); g_initialised = TRUE; llInstantMessage(llGetOwner(), "[Diaper] System ready. Storage: llLinksetData.\n" + "Status: " + wetName() + " | Lock: " + lockStatus() + "\nType '" + g_command_prefix + "' in chat" + (g_command_channel != 0 ? " on channel /" + (string)g_command_channel : "") + " to open menu" + (g_touch_enabled ? ", or touch." : ".")); } // ============================================================ // Menu response handler // ============================================================ handleMenuResponse(string response, key agent) { closeMenuListen(); if (response == "Close") return; if (response == "Check Status") { string status = "\n[Diaper Status]\n" + "Wetness: " + wetName() + "\nLocked: " + lockStatus() + "\nDiaper: " + (g_diaper_on ? "Wearing" : "Not wearing") + (isOwner(agent) ? "\nNext auto-wet in: " + (string)((g_auto_wet_interval - g_elapsed_timer)/60) + " min" : ""); llInstantMessage(agent, status); return; } if (response == "Change Diaper") { if (g_change_locked > 0) { llInstantMessage(agent, "[Diaper] The diaper is locked and cannot be changed."); return; } changeDiaper(agent); return; } if (response == "Wet") { if (!isOwner(agent)) return; applyWetting(1); llInstantMessage(agent, "[Diaper] Diaper wetted. Now: " + wetName()); return; } if (response == "Lock Diaper") { if (!isOwner(agent)) return; g_change_locked = 1; saveState(); llInstantMessage(agent, "[Diaper] Diaper locked."); return; } if (response == "Unlock Diaper") { if (!isOwner(agent)) return; // RLV STUB: if (g_change_locked == 2 && !isRLVOwner(agent)) { // llInstantMessage(agent, "[Diaper] Your " + g_owner_title + " has locked this. Use your safeword if needed."); // return; // } g_change_locked = 0; saveState(); llInstantMessage(agent, "[Diaper] Diaper unlocked."); return; } if (response == "Settings") { if (!isOwner(agent)) return; showSettingsMenu(agent); return; } if (response == "Back") { showRootMenu(agent); return; } if (response == "More...") { showSettingsMenu2(agent); return; } if (response == "Back2") { showSettingsMenu(agent); return; } if (response == "Auto-wet Time") { startListenForInput(agent, 4, "Enter auto-wet interval in MINUTES (current: " + (string)(g_auto_wet_interval/60) + "):"); return; } if (response == "Full Announce") { showAnnounceMenu(agent, "full"); return; } if (response == "Leak Announce") { showAnnounceMenu(agent, "leak"); return; } if (response == "Owner IM") { if (g_input_purpose == 10) { g_announce_full = 0; saveCfg("announce_full", "0"); } else { g_announce_leak = 0; saveCfg("announce_leak", "0"); } llInstantMessage(agent, "[Diaper] Announce set to Owner IM."); showSettingsMenu(agent); return; } if (response == "Local Chat") { if (g_input_purpose == 10) { g_announce_full = 1; saveCfg("announce_full", "1"); } else { g_announce_leak = 1; saveCfg("announce_leak", "1"); } llInstantMessage(agent, "[Diaper] Announce set to Local Chat."); showSettingsMenu(agent); return; } if (response == "Everyone") { if (g_input_purpose == 10) { g_announce_full = 2; saveCfg("announce_full", "2"); } else { g_announce_leak = 2; saveCfg("announce_leak", "2"); } llInstantMessage(agent, "[Diaper] Announce set to Everyone."); showSettingsMenu(agent); return; } if (response == "State Names") { llInstantMessage(agent, "[Diaper] Current state names: " + llList2CSV(g_state_names) + "\nTo change, use the in-world menu (State Names) or reset scripts after editing.\n" + "Format: state_names=Dry,Damp,Wet,Soaking,Leaking"); showSettingsMenu(agent); return; } if (response == "Touch On/Off") { g_touch_enabled = !g_touch_enabled; saveCfg("touch_enabled", (string)g_touch_enabled); llInstantMessage(agent, "[Diaper] Touch to open menu: " + yesNo(g_touch_enabled)); showSettingsMenu(agent); return; } if (response == "Notify Wearer") { g_notify_wearer = !g_notify_wearer; saveCfg("notify_wearer", (string)g_notify_wearer); llInstantMessage(agent, "[Diaper] Notify wearer: " + yesNo(g_notify_wearer)); showSettingsMenu(agent); return; } if (response == "Command Chan") { startListenForInput(agent, 5, "Enter command channel number (0 = open chat, current: " + (string)g_command_channel + "):"); return; } if (response == "Safeword") { closeMenuListen(); g_menu_user = agent; g_menu_channel = menuChannel(); g_menu_listen = llListen(g_menu_channel, "", agent, ""); llDialog(agent, "\n[Safeword Settings]\nCurrent: " + g_safeword + "\nChannel: " + (string)g_safeword_channel, ["Change Word", "Change Channel", "Back"], g_menu_channel); return; } if (response == "Change Word") { startListenForInput(agent, 3, "Type your new safeword (one word, no spaces):"); return; } if (response == "Change Channel") { startListenForInput(agent, 6, "Enter safeword channel (0 = open chat, current: " + (string)g_safeword_channel + "):"); return; } if (response == "Owner Title") { startListenForInput(agent, 7, "Type the title for your caregiver (e.g. Daddy, Mummy, Master, Mistress).\nCurrent: " + g_owner_title); return; } if (response == "Add Whitelist") { startListenForInput(agent, 1, "Type: addwhite \nExample: addwhite 00000000-0000-0000-0000-000000000000"); return; } if (response == "Add Blacklist") { startListenForInput(agent, 2, "Type: addblack \nExample: addblack 00000000-0000-0000-0000-000000000000"); return; } if (response == "Reload Lists") { g_whitelist = []; g_blacklist = []; g_lists_loaded = 0; llMessageLinked(LINK_ROOT, LM_CHANNEL, "LOAD_LIST|whitelist", NULL_KEY); llMessageLinked(LINK_ROOT, LM_CHANNEL, "LOAD_LIST|blacklist", NULL_KEY); llInstantMessage(agent, "[Diaper] Reloading access lists..."); return; } if (response == "Reload Status") { llMessageLinked(LINK_ROOT, LM_CHANNEL, "LOAD_ALL", NULL_KEY); llInstantMessage(agent, "[Diaper] Reloading status from storage..."); return; } } // ============================================================ // Safeword // ============================================================ processSafeword() { if (g_change_locked > 0 || g_removal_locked > 0) { g_change_locked = 0; g_removal_locked = 0; saveState(); llSay(0, ownerName() + " has used their safeword!"); llInstantMessage(llGetOwner(), "[Diaper] Safeword accepted. All locks removed."); // RLV STUB: send @detach=y, @unlockoutfit=y etc here } else { llInstantMessage(llGetOwner(), "[Diaper] Safeword heard but nothing is locked."); } } // ============================================================ // Default state // ============================================================ default { state_entry() { g_initialised = FALSE; g_lists_loaded = 0; g_wetness = 0; g_change_locked = 0; g_removal_locked = 0; g_elapsed_timer = 0; g_diaper_on = 1; g_rlv_owner = ""; g_whitelist = []; g_blacklist = []; // No llOwnerSay here - Persistence hasn't started yet. // Startup message is sent by finishInit() once everything is loaded. } on_rez(integer param) { llResetScript(); } attach(key id) { if (id != NULL_KEY) llResetScript(); } touch_start(integer num) { if (!g_touch_enabled) return; if (!g_initialised) { llOwnerSay("[Diaper] Still starting up, please wait..."); return; } key agent = llDetectedKey(0); if (!checkAccess(agent)) { llInstantMessage(agent, "[Diaper] Access denied."); return; } showRootMenu(agent); } listen(integer channel, string name, key id, string message) { // Safeword if (channel == g_safeword_channel && id == llGetOwner()) { if (llToLower(llStringTrim(message, STRING_TRIM)) == llToLower(g_safeword)) { processSafeword(); return; } } // Command prefix if (channel == g_command_channel) { if (llToLower(llStringTrim(message, STRING_TRIM)) == llToLower(g_command_prefix)) { if (!g_initialised) { llOwnerSay("[Diaper] Still starting up..."); return; } if (!checkAccess(id)) { llInstantMessage(id, "[Diaper] Access denied."); return; } showRootMenu(id); return; } } // Text input collection (ch0 from owner) if (channel == 0 && g_listen_input != 0 && id == llGetOwner()) { processInput(message, id); return; } // Menu dialog response if (channel == g_menu_channel && id == g_menu_user) { handleMenuResponse(message, id); return; } } timer() { if (!g_initialised) return; if (!g_diaper_on) return; g_elapsed_timer += TIMER_TICK; if (g_elapsed_timer >= g_auto_wet_interval) { g_elapsed_timer = 0; applyWetting(1); } else { saveState(); } } link_message(integer sender, integer num, string msg, key id) { if (num != LM_CHANNEL) return; list parts = llParseString2List(msg, ["|"], []); string cmd = llList2String(parts, 0); if (cmd == "PERSIST_MODE") { // Persistence is ready. Load .settings notecard first (touch_enabled override), // then cfg (all other settings from llLinksetData). llMessageLinked(LINK_ROOT, LM_CHANNEL, "LOAD_SETTINGS", NULL_KEY); } else if (cmd == "SETTINGS") { // Apply touch_enabled from notecard if present, ignore everything else. applySettings(parts); // Now load all other settings from llLinksetData. llMessageLinked(LINK_ROOT, LM_CHANNEL, "LOAD_CFG", NULL_KEY); } else if (cmd == "CFG") { // All settings from llLinksetData applied. Now load runtime state. applyCfg(parts); llMessageLinked(LINK_ROOT, LM_CHANNEL, "LOAD_ALL", NULL_KEY); } else if (cmd == "LOADED") { if (llGetListLength(parts) > 1) applyLoadedState(parts); llMessageLinked(LINK_ROOT, LM_CHANNEL, "LOAD_LIST|whitelist", NULL_KEY); llMessageLinked(LINK_ROOT, LM_CHANNEL, "LOAD_LIST|blacklist", NULL_KEY); } else if (cmd == "LIST") { string which = llList2String(parts, 1); list uuids = []; integer i; for (i = 2; i < llGetListLength(parts); i++) { string u = llList2String(parts, i); if (u != "") uuids += [u]; } if (which == "whitelist") { g_whitelist = uuids; g_lists_loaded = g_lists_loaded | 1; } else if (which == "blacklist") { g_blacklist = uuids; g_lists_loaded = g_lists_loaded | 2; } if (g_lists_loaded == 3 && !g_initialised) finishInit(); } } }