// ============================================================ // [Diaper] Core v2.8 // Main logic for the OpenSim wearable diaper/incontinence system. // // Companion script: [Diaper] Persistence (must be in same prim) // // Changes v2.8: // - RLV Phase 1 implementation. // - g_rlv_owner (single string) replaced with g_rlv_owners (list, max 6). // - isRLVOwner() now a real function checking g_rlv_owners list. // - checkAccess() wired to grant RLV owners access. // - g_change_locked semantics clarified: // 0 = unlocked (anyone with access can change) // 1 = wearer-locked (only wearer and RLV owners can change) // 2 = owner-locked (only RLV owners can change) // - Lock/Unlock Diaper button: shown to RLV owners and (if no RLV owner // set) to wearer for 0<->1 toggle. Never shown to third parties. // - HUD lock (removal_locked): shown only to RLV owners, never to wearer. // - RLV owners can Wet from main menu. // - Locking (change or removal) sends @detach=n via RLV. // - Unlocking/safeword sends @detach=y via RLV. // - processSafeword() now sends RLV release commands. // - applyWetting() now notifies all RLV owners by IM. // - RLV submenu expanded: Add RLV Owner, Del RLV Owner, Show RLV Owners, // Safeword Word, Safeword Chan, Lock Diaper, Lock HUD, Unlock All, Back. // - Announce target "Owner IM" renamed "Private IM" (sends to wearer). // RLV owner notification on wetting is automatic, not a target choice. // - writeListToLD() now handles "rlv_owners" list. // - Startup sequence requests LOAD_LIST|rlv_owners from Persistence. // - g_lists_loaded bitmask extended: 1=whitelist 2=blacklist 4=rlv_owners. // - finishInit() now waits for all three lists (bitmask == 7). // - resetToDefaults() clears RLV owner list. // - lockStatus() and changeLockStatus() separated for clarity. // - All "// RLV STUB" comments replaced with live code. // - "Lock Diaper" renamed "Lock Changes". // - RLV owners get sub-dialog: Wearer Lock (=1) or Owner Lock (=2). // - announce() now always copies RLV owners regardless of target. // Changes v2.7: // - Full menu refactor per Menu Structure document. // - Added g_access_mode, g_announce_change. // - SAFEWORD button on main menu. // Changes v2.6: // - Auto-wet 0 disables timer. // Changes v2.5: // - Minimum region maturity setting. // Changes v2.4: // - state_entry() does not zero state vars (hypergrid safety). // Changes v2.3: // - Reset to Defaults. // Changes v2.2: // - State Names and Cmd Prefix via text input. // Changes v2.1: // - Show Lists, Del Whitelist, Del Blacklist. // Changes v2.0: // - Prim description storage backend. // ============================================================ 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 integer g_diaper_on = 1; // 1=wearing diaper integer g_elapsed_timer = 0; // seconds since last auto-wet // ============================================================ // Settings // ============================================================ integer g_auto_wet_interval = 1800; // 0 = disabled integer g_announce_full = 0; // 0=Private IM 1=Local 2=Everyone integer g_announce_leak = 1; integer g_announce_change = 1; integer g_notify_wearer = 1; integer g_touch_enabled = 1; integer g_access_mode = 0; // 0=Open/List 1=Owner Only integer g_command_channel = 0; integer g_safeword_channel = 0; string g_safeword = "SAFEWORD"; string g_command_prefix = ""; string g_owner_title = "Daddy"; list g_state_names = ["Dry", "Damp", "Wet", "Soaking", "Leaking"]; integer g_min_maturity = 0; // 0=None 1=Moderate 2=Adult integer g_region_rating = 0; // 0=PG/General 1=Moderate 2=Adult key g_rating_query = NULL_KEY; // ============================================================ // Access lists // ============================================================ list g_whitelist = []; list g_blacklist = []; list g_rlv_owners = []; // max 6 UUIDs // ============================================================ // Runtime // ============================================================ integer g_initialised = FALSE; integer g_lists_loaded = 0; // bitmask: 1=whitelist 2=blacklist 4=rlv_owners // 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_PRIVATE = 0; // IM to wearer integer ANN_LOCAL = 1; integer ANN_ALL = 2; // ============================================================ // Helpers // ============================================================ string wetName() { return llList2String(g_state_names, g_wetness); } string ownerName() { return llKey2Name(llGetOwner()); } integer maturityFromString(string s) { s = llToLower(llStringTrim(s, STRING_TRIM)); if (s == "moderate") return 1; if (s == "adult") return 2; return 0; } string maturityLabel(integer m) { if (m == 1) return "Moderate"; if (m == 2) return "Adult"; return "None"; } refreshRegionRating() { g_rating_query = llRequestSimulatorData(llGetRegionName(), DATA_SIM_RATING); } string changeLockStatus() { if (g_change_locked == 0) return "Unlocked"; if (g_change_locked == 1) return "Wearer locked"; return "Locked by " + g_owner_title; } string removalLockStatus() { if (g_removal_locked == 0) return "Unlocked"; return "Locked by " + g_owner_title; } string accessModeLabel(integer m) { if (m == 1) return "Owner Only"; return "Open/List"; } 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"; } // ============================================================ // RLV commands // ============================================================ rlvLock() { // Called when any lock is applied. Restricts detach. llOwnerSay("@detach=n"); } rlvRelease() { // Called when all locks are cleared. Releases detach restriction. // Only release if nothing else is still locked. if (g_change_locked == 0 && g_removal_locked == 0) llOwnerSay("@detach=y"); } // ============================================================ // Access control // ============================================================ integer isRLVOwner(key agent) { if (llGetListLength(g_rlv_owners) == 0) return FALSE; return (llListFindList(g_rlv_owners, [(string)agent]) != -1); } integer checkAccess(key agent) { if (agent == llGetOwner()) return TRUE; if (isRLVOwner(agent)) return TRUE; if (g_access_mode == 1) return FALSE; // Owner Only 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()); } // Can this agent change the diaper right now? integer canChange(key agent) { if (isRLVOwner(agent)) return TRUE; // RLV owners always can if (isOwner(agent)) { // Wearer blocked only if owner-locked and no RLV owner (shouldn't happen // in normal use, but be safe). If RLV owner exists and locked to 2, // wearer is blocked. if (g_change_locked == 2 && llGetListLength(g_rlv_owners) > 0) return FALSE; return TRUE; } // Third parties if (g_change_locked >= 1) return FALSE; // wearer-locked or owner-locked return TRUE; } // ============================================================ // Announce // ============================================================ string announceLabel(integer val) { if (val == 0) return "Private IM"; if (val == 1) return "Local"; return "Everyone"; } announce(string msg, integer target) { if (target == ANN_PRIVATE) { llInstantMessage(llGetOwner(), "[Diaper] " + msg); notifyRLVOwners("[Diaper] " + msg); return; } if (g_min_maturity == 0 || g_region_rating >= g_min_maturity) llSay(0, msg); if (g_notify_wearer) llInstantMessage(llGetOwner(), "[Diaper] " + msg); notifyRLVOwners("[Diaper] " + msg); } notifyRLVOwners(string msg) { integer i; for (i = 0; i < llGetListLength(g_rlv_owners); i++) { key ownr = (key)llList2String(g_rlv_owners, i); if (ownr != llGetOwner()) llInstantMessage(ownr, msg); } } // ============================================================ // State changes // ============================================================ applyWetting(integer units) { if (!g_diaper_on) return; g_wetness += units; if (g_wetness > 4) g_wetness = 4; if (g_notify_wearer) llInstantMessage(llGetOwner(), "[Diaper] You just wet yourself and your diaper is now " + wetName() + "."); notifyRLVOwners(ownerName() + "'s diaper is now " + wetName() + "."); if (g_wetness == 3) announce(ownerName() + "'s diaper is soaking and needs changing soon!", g_announce_full); else if (g_wetness == 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"; announce(ownerName() + "'s diaper has been changed by " + changerName + ". Fresh and dry!", g_announce_change); 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; llMessageLinked(LINK_ROOT, LM_CHANNEL, msg, NULL_KEY); } // ============================================================ // Persistence - settings (individual key writes to child prim 1) // ============================================================ saveCfg(string k, string v) { llMessageLinked(LINK_ROOT, LM_CHANNEL, "SAVE_CFG|" + k + "=" + v, NULL_KEY); } // ============================================================ // Access list persistence // ============================================================ writeListToLD(string which) { list theList = g_whitelist; if (which == "blacklist") theList = g_blacklist; if (which == "rlv_owners") theList = g_rlv_owners; 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); } // ============================================================ // Menu helpers // ============================================================ 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() { return -1 - (integer)llFrand(999998.0); } openDialog(key agent, string title, list buttons) { closeMenuListen(); g_menu_user = agent; g_menu_channel = menuChannel(); g_menu_listen = llListen(g_menu_channel, "", agent, ""); llDialog(agent, title, buttons, g_menu_channel); } string yesNo(integer val) { if (val) return "Yes"; return "No"; } // ============================================================ // Text input collection // ============================================================ // purpose: 1=addwhite 2=addblack 3=safeword_word 4=auto_wet_mins // 5=cmd_chan 6=sw_chan 7=owner_title 8=del_whitelist // 9=del_black 10=state_names 11=cmd_prefix 12=add_rlv_owner // 13=del_rlv_owner 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); } showLists(key agent) { string out = "[Diaper] Whitelist:"; if (llGetListLength(g_whitelist) == 0) out += " Empty"; else { integer i; for (i = 0; i < llGetListLength(g_whitelist); i++) out += "\n " + (string)(i+1) + ". " + llList2String(g_whitelist, i); } out += "\n[Diaper] Blacklist:"; if (llGetListLength(g_blacklist) == 0) out += " Empty"; else { integer i; for (i = 0; i < llGetListLength(g_blacklist); i++) out += "\n " + (string)(i+1) + ". " + llList2String(g_blacklist, i); } llInstantMessage(agent, out); } showRLVOwners(key agent) { string out = "[Diaper] RLV Owners:"; if (llGetListLength(g_rlv_owners) == 0) out += " None"; else { integer i; for (i = 0; i < llGetListLength(g_rlv_owners); i++) out += "\n " + (string)(i+1) + ". " + llList2String(g_rlv_owners, i); } llInstantMessage(agent, out); } startDeleteFromList(key agent, integer purpose) { list theList = g_whitelist; string which = "whitelist"; if (purpose == 9) { theList = g_blacklist; which = "blacklist"; } if (purpose == 13) { theList = g_rlv_owners; which = "RLV owner list"; } if (llGetListLength(theList) == 0) { llInstantMessage(agent, "[Diaper] " + which + " is empty."); if (purpose == 13) showMenuRLV(agent); else showMenuAccess(agent); return; } string out = "[Diaper] Type the number to remove from " + which + " (0 to cancel):"; integer i; for (i = 0; i < llGetListLength(theList); i++) out += "\n " + (string)(i+1) + ". " + llList2String(theList, i); startListenForInput(agent, purpose, out); } // ============================================================ // Menu show functions // ============================================================ showRootMenu(key agent) { string title = "\n[Diaper System]\n" + "Status: " + wetName() + "\n" + "Change lock: " + changeLockStatus() + "\n" + "HUD lock: " + removalLockStatus(); list buttons = ["Check Status", "Close"]; if (isOwner(agent)) buttons += ["SAFEWORD"]; // Change Diaper: visible unless owner-locked and agent cannot change if (canChange(agent)) buttons += ["Change Diaper"]; // Wet: wearer and RLV owners only if (isOwner(agent) || isRLVOwner(agent)) buttons += ["Wet"]; // Lock/Unlock controls for change lock // RLV owners see it always. // Wearer sees it only if no RLV owner set (0<->1 toggle). if (isRLVOwner(agent)) { if (g_change_locked == 0) buttons += ["Lock Changes"]; else buttons += ["Unlock Changes"]; } else if (isOwner(agent) && llGetListLength(g_rlv_owners) == 0) { if (g_change_locked == 0) buttons += ["Lock Changes"]; else buttons += ["Unlock Changes"]; } // HUD lock: RLV owners only, never shown to wearer if (isRLVOwner(agent)) { if (g_removal_locked == 0) buttons += ["Lock HUD"]; else buttons += ["Unlock HUD"]; } // Settings: wearer and RLV owners only if (isOwner(agent) || isRLVOwner(agent)) buttons += ["Settings"]; openDialog(agent, title, buttons); } showMenuSettings(key agent) { string title = "\n[Settings]\nChoose a category."; list buttons = ["Announce", "Access", "Identity", "General", "RLV", "Back"]; openDialog(agent, title, buttons); } showMenuAnnounce(key agent) { string title = "\n[Settings > Announce]\n" + "Full: " + announceLabel(g_announce_full) + "\n" + "Leak: " + announceLabel(g_announce_leak) + "\n" + "Change: " + announceLabel(g_announce_change) + "\n" + "Notify wearer: " + yesNo(g_notify_wearer) + "\n" + "Min maturity: " + maturityLabel(g_min_maturity); list buttons = ["Full Announce", "Leak Announce", "Change Announce", "Notify Wearer", "Min Maturity", "Back"]; openDialog(agent, title, buttons); } showMenuAccess(key agent) { string title = "\n[Settings > Access]\n" + "Touch: " + yesNo(g_touch_enabled) + "\n" + "Access mode: " + accessModeLabel(g_access_mode) + "\n" + "Cmd channel: " + (string)g_command_channel + "\n" + "Cmd prefix: " + g_command_prefix; list buttons = ["Touch On/Off", "Access Mode", "Add Whitelist", "Del Whitelist", "Add Blacklist", "Del Blacklist", "Show Lists", "Cmd Channel", "Cmd Prefix", "Back"]; openDialog(agent, title, buttons); } showMenuIdentity(key agent) { string title = "\n[Settings > Identity]\n" + "Owner title: " + g_owner_title + "\n" + "State names: " + llList2CSV(g_state_names); list buttons = ["Owner Title", "State Names", "Back"]; openDialog(agent, title, buttons); } showMenuGeneral(key agent) { string title = "\n[Settings > General]\n" + "Auto-wet: " + (g_auto_wet_interval > 0 ? (string)(g_auto_wet_interval/60) + " min" : "Disabled"); list buttons = ["Auto-wet", "Reset Defaults", "Reload Status", "Reload Lists", "Back"]; openDialog(agent, title, buttons); } showMenuRLV(key agent) { integer ownerCount = llGetListLength(g_rlv_owners); string title = "\n[Settings > RLV]\n" + "RLV Owners: " + (string)ownerCount + "/6\n" + "Safeword: " + g_safeword + "\n" + "SW channel: " + (string)g_safeword_channel; list buttons; if (isRLVOwner(agent)) buttons = ["Add RLV Owner", "Del RLV Owner", "Show Owners", "Safeword Word", "Safeword Chan", "Back"]; else if (llGetListLength(g_rlv_owners) == 0) buttons = ["Add RLV Owner", "Show Owners", "Safeword Word", "Safeword Chan", "Back"]; else buttons = ["Show Owners", "Safeword Word", "Safeword Chan", "Back"]; openDialog(agent, title, buttons); } showAnnounceTargetMenu(key agent, string which) { // which = "full", "leak", or "change" // Store context in g_input_purpose: 20=full 21=leak 22=change if (which == "full") g_input_purpose = 20; else if (which == "leak") g_input_purpose = 21; else g_input_purpose = 22; openDialog(agent, "\nWho should be told when the diaper is " + which + "?", ["Private IM", "Local Chat", "Everyone", "Back"]); } showMinMaturityMenu(key agent) { openDialog(agent, "\n[Min Region Maturity]\nPublic announces suppressed below this level.\nCurrent: " + maturityLabel(g_min_maturity), ["None", "Moderate", "Adult", "Back"]); } showAccessModeMenu(key agent) { openDialog(agent, "\n[Access Mode]\nOwner always has access.\n\nOpen/List: anyone can access unless lists apply.\nOwner Only: only the owner can open menus.\n\nCurrent: " + accessModeLabel(g_access_mode), ["Open/List", "Owner Only", "Back"]); } // ============================================================ // processInput - handles free-text responses // ============================================================ processInput(string text, key agent) { // Clean up input listen FIRST (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."); } else llInstantMessage(agent, "[Diaper] Expected: addwhite "); showMenuAccess(agent); } 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."); } else llInstantMessage(agent, "[Diaper] Expected: addblack "); showMenuAccess(agent); } else if (purpose == 3) // safeword word { string sw = llStringTrim(text, STRING_TRIM); if (sw != "") { g_safeword = sw; updateSafewordListen(); saveCfg("safeword", g_safeword); llInstantMessage(agent, "[Diaper] Safeword set to: " + g_safeword); } showMenuRLV(agent); } 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); if (mins == 0) llInstantMessage(agent, "[Diaper] Auto-wet disabled."); else llInstantMessage(agent, "[Diaper] Auto-wet interval set to " + (string)mins + " minutes."); } else llInstantMessage(agent, "[Diaper] Please enter a number of minutes (0 to disable)."); showMenuGeneral(agent); } 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 + "."); showMenuAccess(agent); } 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 + "."); showMenuRLV(agent); } 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."); showMenuIdentity(agent); } else if (purpose == 8) // delete from whitelist { integer num = (integer)text; if (num == 0) llInstantMessage(agent, "[Diaper] Cancelled."); else if (num < 1 || num > llGetListLength(g_whitelist)) llInstantMessage(agent, "[Diaper] Invalid number."); else { string removed = llList2String(g_whitelist, num-1); g_whitelist = llDeleteSubList(g_whitelist, num-1, num-1); writeListToLD("whitelist"); llInstantMessage(agent, "[Diaper] Removed " + removed + " from whitelist."); } showMenuAccess(agent); } else if (purpose == 9) // delete from blacklist { integer num = (integer)text; if (num == 0) llInstantMessage(agent, "[Diaper] Cancelled."); else if (num < 1 || num > llGetListLength(g_blacklist)) llInstantMessage(agent, "[Diaper] Invalid number."); else { string removed = llList2String(g_blacklist, num-1); g_blacklist = llDeleteSubList(g_blacklist, num-1, num-1); writeListToLD("blacklist"); llInstantMessage(agent, "[Diaper] Removed " + removed + " from blacklist."); } showMenuAccess(agent); } else if (purpose == 10) // state names { string trimmed = llStringTrim(text, STRING_TRIM); list names = llCSV2List(trimmed); if (llGetListLength(names) == 5) { g_state_names = names; saveCfg("state_names", trimmed); llInstantMessage(agent, "[Diaper] State names updated: " + trimmed); } else llInstantMessage(agent, "[Diaper] Please enter exactly 5 comma-separated names."); showMenuIdentity(agent); } else if (purpose == 11) // command prefix { string prefix = llStringTrim(text, STRING_TRIM); if (prefix == "auto" || prefix == "reset") { g_command_prefix = buildCommandPrefix(); saveCfg("command_prefix", ""); updateCommandListen(); llInstantMessage(agent, "[Diaper] Command prefix reset to: " + g_command_prefix); } else if (prefix != "" && llSubStringIndex(prefix, " ") == -1) { g_command_prefix = prefix; saveCfg("command_prefix", prefix); updateCommandListen(); llInstantMessage(agent, "[Diaper] Command prefix set to: " + prefix); } else llInstantMessage(agent, "[Diaper] Prefix must be one word (no spaces), or type 'auto' to reset."); showMenuAccess(agent); } else if (purpose == 12) // add RLV owner { if (llGetSubString(llToLower(text), 0, 7) == "addowner") { string uuid = llStringTrim(llGetSubString(text, 9, -1), STRING_TRIM); if (llStringLength(uuid) == 36) { if (llGetListLength(g_rlv_owners) >= 6) { llInstantMessage(agent, "[Diaper] RLV owner list is full (max 6)."); } else if (llListFindList(g_rlv_owners, [uuid]) != -1) { llInstantMessage(agent, "[Diaper] That UUID is already an RLV owner."); } else { g_rlv_owners += [uuid]; writeListToLD("rlv_owners"); llInstantMessage(agent, "[Diaper] Added " + uuid + " as RLV owner."); } } else llInstantMessage(agent, "[Diaper] Invalid UUID."); } else llInstantMessage(agent, "[Diaper] Expected: addowner "); showMenuRLV(agent); } else if (purpose == 13) // delete from RLV owner list { integer num = (integer)text; if (num == 0) llInstantMessage(agent, "[Diaper] Cancelled."); else if (num < 1 || num > llGetListLength(g_rlv_owners)) llInstantMessage(agent, "[Diaper] Invalid number."); else { string removed = llList2String(g_rlv_owners, num-1); g_rlv_owners = llDeleteSubList(g_rlv_owners, num-1, num-1); writeListToLD("rlv_owners"); llInstantMessage(agent, "[Diaper] Removed " + removed + " from RLV owner list."); // If no more RLV owners, release any RLV locks if (llGetListLength(g_rlv_owners) == 0 && (g_change_locked > 0 || g_removal_locked > 0)) { g_change_locked = 0; g_removal_locked = 0; saveState(); llOwnerSay("@detach=y"); llInstantMessage(agent, "[Diaper] All RLV owners removed. Locks released."); } } showMenuRLV(agent); } } // ============================================================ // 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 (touch_enabled only) // ============================================================ applySettings(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_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; @skip_s; } } // ============================================================ // Apply cfg settings from CFG message (child prim 1) // ============================================================ applyCfg(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_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 == "announce_change") g_announce_change = (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 == "access_mode") g_access_mode = (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 == "min_maturity") g_min_maturity = (integer)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; @skip_l; } } // ============================================================ // Reset all settings to defaults // ============================================================ resetToDefaults() { g_wetness = 0; g_change_locked = 0; g_removal_locked = 0; g_elapsed_timer = 0; g_diaper_on = 1; g_auto_wet_interval = 1800; g_announce_full = 0; g_announce_leak = 1; g_announce_change = 1; g_notify_wearer = 1; g_touch_enabled = 1; g_access_mode = 0; g_command_channel = 0; g_safeword_channel = 0; g_safeword = "SAFEWORD"; g_command_prefix = buildCommandPrefix(); g_owner_title = "Daddy"; g_state_names = ["Dry", "Damp", "Wet", "Soaking", "Leaking"]; g_min_maturity = 0; g_whitelist = []; g_blacklist = []; g_rlv_owners = []; saveState(); saveCfg("auto_wet_interval", "1800"); saveCfg("announce_full", "0"); saveCfg("announce_leak", "1"); saveCfg("announce_change", "1"); saveCfg("notify_wearer", "1"); saveCfg("touch_enabled", "1"); saveCfg("access_mode", "0"); saveCfg("command_channel", "0"); saveCfg("safeword_channel", "0"); saveCfg("safeword", "SAFEWORD"); saveCfg("command_prefix", ""); saveCfg("owner_title", "Daddy"); saveCfg("state_names", "Dry,Damp,Wet,Soaking,Leaking"); saveCfg("min_maturity", "0"); writeListToLD("whitelist"); writeListToLD("blacklist"); writeListToLD("rlv_owners"); // Release any active RLV locks llOwnerSay("@detach=y"); updateCommandListen(); updateSafewordListen(); } // ============================================================ // Finish initialisation // ============================================================ finishInit() { if (g_command_prefix == "") g_command_prefix = buildCommandPrefix(); updateCommandListen(); updateSafewordListen(); llSetTimerEvent(TIMER_TICK); refreshRegionRating(); g_initialised = TRUE; string rlvStatus = ""; if (llGetListLength(g_rlv_owners) > 0) rlvStatus = " | RLV owners: " + (string)llGetListLength(g_rlv_owners); llInstantMessage(llGetOwner(), "[Diaper] System ready. Storage: Prim Description (hypergrid-safe).\n" + "Status: " + wetName() + " | Change lock: " + changeLockStatus() + rlvStatus + "\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(); // ---- Main menu ---- if (response == "Close") return; if (response == "Check Status") { if (isOwner(agent) || isRLVOwner(agent)) { string status = "\n[Diaper Status]\n" + "Wetness: " + wetName() + "\n" + "Change lock: " + changeLockStatus() + "\n" + "HUD lock: " + removalLockStatus() + "\n" + "RLV owners: " + (string)llGetListLength(g_rlv_owners) + "\n" + "Next auto-wet: " + (g_auto_wet_interval > 0 ? (string)((g_auto_wet_interval - g_elapsed_timer)/60) + " min" : "Disabled"); llInstantMessage(agent, status); } else { llInstantMessage(agent, llGetDisplayName(llGetOwner()) + "'s diaper is " + wetName() + "."); } showRootMenu(agent); return; } if (response == "Change Diaper") { if (!canChange(agent)) { if (g_change_locked == 2) llInstantMessage(agent, "[Diaper] The diaper is locked by your " + g_owner_title + " and cannot be changed."); else llInstantMessage(agent, "[Diaper] The diaper is locked."); showRootMenu(agent); return; } changeDiaper(agent); showRootMenu(agent); return; } if (response == "Wet") { if (!isOwner(agent) && !isRLVOwner(agent)) { showRootMenu(agent); return; } applyWetting(1); if (!isOwner(agent)) llInstantMessage(agent, llGetDisplayName(llGetOwner()) + " just wet themselves! Status: " + wetName()); showRootMenu(agent); return; } if (response == "Lock Changes") { if (isRLVOwner(agent)) { openDialog(agent, "\n[Lock Changes]\nWearer lock: only you and other RLV owners can change.\nOwner lock: only RLV owners can change.\n\nCurrent: " + changeLockStatus(), ["Wearer Lock", "Owner Lock", "No Change"]); return; } else if (isOwner(agent) && llGetListLength(g_rlv_owners) == 0) { g_change_locked = 1; saveState(); llInstantMessage(agent, "[Diaper] Changes locked (wearer lock — third parties cannot change)."); } showRootMenu(agent); return; } if (response == "Wearer Lock") { if (!isRLVOwner(agent)) { showRootMenu(agent); return; } g_change_locked = 1; saveState(); llInstantMessage(agent, "[Diaper] Changes locked at wearer level."); llInstantMessage(llGetOwner(), "[Diaper] Your " + g_owner_title + " has locked your changes. You can still change yourself."); showRootMenu(agent); return; } if (response == "Owner Lock") { if (!isRLVOwner(agent)) { showRootMenu(agent); return; } g_change_locked = 2; saveState(); llInstantMessage(agent, "[Diaper] Changes locked at owner level. Only you can change."); llInstantMessage(llGetOwner(), "[Diaper] Your " + g_owner_title + " has locked your changes. Only they can change you now."); showRootMenu(agent); return; } if (response == "Unlock Changes") { if (isRLVOwner(agent)) { g_change_locked = 0; saveState(); llInstantMessage(agent, "[Diaper] Changes unlocked."); llInstantMessage(llGetOwner(), "[Diaper] Your " + g_owner_title + " has unlocked your changes."); } else if (isOwner(agent) && llGetListLength(g_rlv_owners) == 0) { g_change_locked = 0; saveState(); llInstantMessage(agent, "[Diaper] Changes unlocked."); } else { llInstantMessage(agent, "[Diaper] Your " + g_owner_title + " has locked this. Use your safeword if needed."); } showRootMenu(agent); return; } if (response == "Lock HUD") { if (!isRLVOwner(agent)) { showRootMenu(agent); return; } g_removal_locked = 1; saveState(); rlvLock(); llInstantMessage(agent, "[Diaper] HUD locked. Wearer cannot remove it."); llInstantMessage(llGetOwner(), "[Diaper] Your " + g_owner_title + " has locked your HUD. You cannot remove it."); showRootMenu(agent); return; } if (response == "Unlock HUD") { if (!isRLVOwner(agent)) { showRootMenu(agent); return; } g_removal_locked = 0; saveState(); rlvRelease(); llInstantMessage(agent, "[Diaper] HUD unlocked."); llInstantMessage(llGetOwner(), "[Diaper] Your " + g_owner_title + " has unlocked your HUD."); showRootMenu(agent); return; } if (response == "SAFEWORD") { if (g_change_locked > 0 || g_removal_locked > 0 || llGetListLength(g_rlv_owners) > 0) processSafeword(); else llInstantMessage(agent, "[Diaper] No locks or RLV owners are currently active. Your safeword is: " + g_safeword); showRootMenu(agent); return; } if (response == "Settings") { if (!isOwner(agent) && !isRLVOwner(agent)) { showRootMenu(agent); return; } showMenuSettings(agent); return; } // ---- Settings top level ---- if (response == "Back") { showRootMenu(agent); return; } if (response == "Announce") { showMenuAnnounce(agent); return; } if (response == "Access") { showMenuAccess(agent); return; } if (response == "Identity") { showMenuIdentity(agent); return; } if (response == "General") { showMenuGeneral(agent); return; } if (response == "RLV") { showMenuRLV(agent); return; } // ---- Announce submenu ---- if (response == "Full Announce") { showAnnounceTargetMenu(agent, "full"); return; } if (response == "Leak Announce") { showAnnounceTargetMenu(agent, "leak"); return; } if (response == "Change Announce") { showAnnounceTargetMenu(agent, "change"); 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)); showMenuAnnounce(agent); return; } if (response == "Min Maturity") { showMinMaturityMenu(agent); return; } // Announce target sub-dialog responses (purpose in g_input_purpose 20/21/22) if (response == "Private IM") { integer tgt = g_input_purpose; if (tgt == 20) { g_announce_full = 0; saveCfg("announce_full", "0"); } if (tgt == 21) { g_announce_leak = 0; saveCfg("announce_leak", "0"); } if (tgt == 22) { g_announce_change = 0; saveCfg("announce_change", "0"); } g_input_purpose = 0; llInstantMessage(agent, "[Diaper] Announce set to Private IM."); showMenuAnnounce(agent); return; } if (response == "Local Chat") { integer tgt = g_input_purpose; if (tgt == 20) { g_announce_full = 1; saveCfg("announce_full", "1"); } if (tgt == 21) { g_announce_leak = 1; saveCfg("announce_leak", "1"); } if (tgt == 22) { g_announce_change = 1; saveCfg("announce_change", "1"); } g_input_purpose = 0; llInstantMessage(agent, "[Diaper] Announce set to Local Chat."); showMenuAnnounce(agent); return; } if (response == "Everyone") { integer tgt = g_input_purpose; if (tgt == 20) { g_announce_full = 2; saveCfg("announce_full", "2"); } if (tgt == 21) { g_announce_leak = 2; saveCfg("announce_leak", "2"); } if (tgt == 22) { g_announce_change = 2; saveCfg("announce_change", "2"); } g_input_purpose = 0; llInstantMessage(agent, "[Diaper] Announce set to Everyone."); showMenuAnnounce(agent); return; } // Min maturity responses if (response == "None") { g_min_maturity = 0; saveCfg("min_maturity", "0"); llInstantMessage(agent, "[Diaper] Min maturity: None (always announce)."); showMenuAnnounce(agent); return; } if (response == "Moderate") { g_min_maturity = 1; saveCfg("min_maturity", "1"); llInstantMessage(agent, "[Diaper] Min maturity: Moderate."); showMenuAnnounce(agent); return; } if (response == "Adult") { g_min_maturity = 2; saveCfg("min_maturity", "2"); llInstantMessage(agent, "[Diaper] Min maturity: Adult only."); showMenuAnnounce(agent); return; } // ---- Access submenu ---- 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)); showMenuAccess(agent); return; } if (response == "Access Mode") { showAccessModeMenu(agent); return; } if (response == "Open/List") { g_access_mode = 0; saveCfg("access_mode", "0"); llInstantMessage(agent, "[Diaper] Access mode: Open/List."); showMenuAccess(agent); return; } if (response == "Owner Only") { g_access_mode = 1; saveCfg("access_mode", "1"); llInstantMessage(agent, "[Diaper] Access mode: Owner Only."); showMenuAccess(agent); return; } if (response == "Add Whitelist") { startListenForInput(agent, 1, "Type: addwhite \nExample: addwhite 00000000-0000-0000-0000-000000000000"); return; } if (response == "Del Whitelist") { startDeleteFromList(agent, 8); return; } if (response == "Add Blacklist") { startListenForInput(agent, 2, "Type: addblack \nExample: addblack 00000000-0000-0000-0000-000000000000"); return; } if (response == "Del Blacklist") { startDeleteFromList(agent, 9); return; } if (response == "Show Lists") { showLists(agent); showMenuAccess(agent); return; } if (response == "Cmd Channel") { startListenForInput(agent, 5, "Enter command channel number (0 = open chat, current: " + (string)g_command_channel + "):"); return; } if (response == "Cmd Prefix") { startListenForInput(agent, 11, "Type a new command prefix (no spaces), or 'auto' to reset.\nCurrent: " + g_command_prefix); return; } // ---- Identity submenu ---- if (response == "Owner Title") { startListenForInput(agent, 7, "Type the title for your caregiver (e.g. Daddy, Mummy, Master).\nCurrent: " + g_owner_title); return; } if (response == "State Names") { startListenForInput(agent, 10, "Type 5 comma-separated state names (e.g. Dry,Damp,Wet,Soaking,Leaking).\nCurrent: " + llList2CSV(g_state_names)); return; } // ---- General submenu ---- if (response == "Auto-wet") { startListenForInput(agent, 4, "Enter auto-wet interval in MINUTES (0 to disable, current: " + (string)(g_auto_wet_interval/60) + "):"); return; } if (response == "Reset Defaults") { openDialog(agent, "\n[Reset to Defaults]\nThis will reset ALL settings, state, and RLV owners to defaults.\nAre you sure?", ["Confirm Reset", "Cancel"]); return; } if (response == "Confirm Reset") { resetToDefaults(); llInstantMessage(agent, "[Diaper] All settings and state reset to defaults."); showMenuGeneral(agent); return; } if (response == "Cancel") { showMenuGeneral(agent); return; } if (response == "Reload Status") { llMessageLinked(LINK_ROOT, LM_CHANNEL, "LOAD_ALL", NULL_KEY); llInstantMessage(agent, "[Diaper] Reloading status from storage..."); showMenuGeneral(agent); return; } if (response == "Reload Lists") { g_whitelist = []; g_blacklist = []; g_rlv_owners = []; g_lists_loaded = 0; llMessageLinked(LINK_ROOT, LM_CHANNEL, "LOAD_LIST|whitelist", NULL_KEY); llMessageLinked(LINK_ROOT, LM_CHANNEL, "LOAD_LIST|blacklist", NULL_KEY); llMessageLinked(LINK_ROOT, LM_CHANNEL, "LOAD_LIST|rlv_owners", NULL_KEY); llInstantMessage(agent, "[Diaper] Reloading access lists..."); showMenuGeneral(agent); return; } // ---- RLV submenu ---- if (response == "Add RLV Owner") { if (!isRLVOwner(agent) && llGetListLength(g_rlv_owners) > 0) { llInstantMessage(agent, "[Diaper] An RLV owner is already set. Only an RLV owner can add more."); showMenuRLV(agent); return; } if (llGetListLength(g_rlv_owners) >= 6) { llInstantMessage(agent, "[Diaper] RLV owner list is full (max 6). Remove one first."); showMenuRLV(agent); return; } startListenForInput(agent, 12, "Type: addowner \nExample: addowner 00000000-0000-0000-0000-000000000000"); return; } if (response == "Del RLV Owner") { if (!isRLVOwner(agent)) { llInstantMessage(agent, "[Diaper] Only an RLV owner can remove owners. Use safeword to exit the arrangement."); showMenuRLV(agent); return; } startDeleteFromList(agent, 13); return; } if (response == "Show Owners") { showRLVOwners(agent); showMenuRLV(agent); return; } if (response == "Safeword Word") { startListenForInput(agent, 3, "Type your new safeword (one word, no spaces):"); return; } if (response == "Safeword Chan") { startListenForInput(agent, 6, "Enter safeword channel (0 = open chat, current: " + (string)g_safeword_channel + "):"); return; } } // ============================================================ // Safeword // ============================================================ processSafeword() { if (g_change_locked > 0 || g_removal_locked > 0 || llGetListLength(g_rlv_owners) > 0) { g_change_locked = 0; g_removal_locked = 0; notifyRLVOwners(ownerName() + " has used their safeword. All locks and owner access removed."); g_rlv_owners = []; writeListToLD("rlv_owners"); saveState(); llOwnerSay("@detach=y"); llSay(0, ownerName() + " has used their safeword!"); llInstantMessage(llGetOwner(), "[Diaper] Safeword accepted. All locks removed and RLV owner list cleared."); } else { llInstantMessage(llGetOwner(), "[Diaper] Safeword heard but nothing is locked and no RLV owners are set."); } } // ============================================================ // Default state // ============================================================ default { dataserver(key query_id, string data) { if (query_id == g_rating_query) { g_region_rating = maturityFromString(data); g_rating_query = NULL_KEY; } } changed(integer change) { if (change & CHANGED_REGION) refreshRegionRating(); } state_entry() { g_initialised = FALSE; g_lists_loaded = 0; // State variables intentionally NOT zeroed here - prim storage // holds the authoritative values and will be loaded by Persistence. // Zeroing here would clobber state on foreign grids where scripts // restart but Persistence cannot write back. } 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 channel 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; } } // Free-text input (ch0 from owner or RLV owner currently interacting) //if (channel == 0 && g_listen_input != 0 && id == g_menu_user) if (channel == 0 && g_listen_input != 0 && (id == llGetOwner() || isRLVOwner(id))) { 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; if (g_auto_wet_interval > 0) g_elapsed_timer += TIMER_TICK; if (g_auto_wet_interval > 0 && 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") { llMessageLinked(LINK_ROOT, LM_CHANNEL, "LOAD_SETTINGS", NULL_KEY); } else if (cmd == "SETTINGS") { applySettings(parts); llMessageLinked(LINK_ROOT, LM_CHANNEL, "LOAD_CFG", NULL_KEY); } else if (cmd == "CFG") { 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); llMessageLinked(LINK_ROOT, LM_CHANNEL, "LOAD_LIST|rlv_owners", 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; } else if (which == "rlv_owners") { g_rlv_owners = uuids; g_lists_loaded = g_lists_loaded | 4; } if (g_lists_loaded == 7 && !g_initialised) finishInit(); } } }