// ============================================================ // [Diaper] Core v2.7 // Main logic for the OpenSim wearable diaper/incontinence system. // // Companion script: [Diaper] Persistence (must be in same prim) // // RLV STUB: Search for "// RLV STUB" to find all RLV hook points. // // Changes v2.7: // - Full menu refactor per Menu Structure document. // - Main menu: Check Status, Change Diaper, Wet, Lock/Unlock Diaper, // SAFEWORD, Settings, Close. // - Settings submenus: Announce, Access, Identity, General, RLV, Back. // - Announce submenu: Full Announce, Leak Announce, Change Announce, // Notify Wearer, Min Maturity, Back. // - Access submenu: Touch On/Off, Access Mode (owner-only vs open/list), // Add Whitelist, Del Whitelist, Add Blacklist, Del Blacklist, // Show Lists, Cmd Channel, Cmd Prefix, Back. // - Identity submenu: Owner Title, State Names, Back. // - General submenu: Auto-wet Time, Reset Defaults, Reload Status, // Reload Lists, Back. // - RLV submenu: Safeword Word, Safeword Channel, Back. // (All future RLV options will live here.) // - Added g_access_mode: 0=Open/List, 1=Owner Only. // - Added g_announce_change: who is told when a diaper is changed. // - changeDiaper() now uses announce() via g_announce_change. // - SAFEWORD button on main menu: if RLV active and locked, fires // safeword; otherwise just reminds wearer of their safeword word. // - Removed old flat Settings 1/2 and Settings 2/2 menus. // Changes v2.6: // - Changed autowet so setting it to 0 disables autowet. // Changes v2.5: // - Added minimum region maturity setting. // Changes v2.4: // - Commented out zeroing items in state_entry() to avoid overwriting // prim storage on reload on a foreign grid. // Changes v2.3: // - Added Reset to Defaults. // Changes v2.2: // - State Names menu button opens text input (purpose 10). // - Added Cmd Prefix button (purpose 11). // Changes v2.1: // - Added Show Lists, Del Whitelist, Del Blacklist. // Changes v2.0: // - Storage backend changed to prim description (hypergrid-safe). // Changes v1.9: // - Added leaking announcement if already leaking and wet again. // - Added notification to RLV owner stub. // Changes v1.8: // - Settings split: touch_enabled in root prim only; all other settings // in child prim 1 via SAVE_CFG / LOAD_CFG. // Changes v1.7: // - Fixed blank command_prefix= edge case in applySettings(). // - Fixed command-channel-change listen cleanup order. // Changes v1.4: // - Removed void return type; removed OSSL calls. // Changes v1.2: // - Removed premature llOwnerSay; menuChannel() clamped; SETTINGS // tolerates empty message. // ============================================================ 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 // ============================================================ integer g_auto_wet_interval = 1800; // 0 = disabled integer g_announce_full = 0; // 0=Owner IM 1=Local 2=Everyone integer g_announce_leak = 1; integer g_announce_change = 1; // who is told when diaper is changed 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"; // RLV STUB - configurable caregiver title 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; // cached: 0=PG/General 1=Moderate 2=Adult key g_rating_query = NULL_KEY; // ============================================================ // 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()); } 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 lockStatus() { if (g_change_locked == 0) return "Unlocked"; if (g_change_locked == 1) return "Locked (by you)"; 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"; } // ============================================================ // 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 (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()); } // 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); return; } if (g_min_maturity == 0 || g_region_rating >= g_min_maturity) llSay(0, msg); // If region is below minimum maturity, public announce is silently suppressed. // Owner IM via notify_wearer path still fires below. if (g_notify_wearer) llInstantMessage(llGetOwner(), "[Diaper] " + 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() + "."); // RLV STUB: if (g_rlv_owner != "" && (key)g_rlv_owner != llGetOwner()) // llInstantMessage((key)g_rlv_owner, ownerName() + "'s diaper is now " + wetName() + "."); // Threshold announcements re-enabled now that announce_full/leak route properly. 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 + "|rlv_owner=" + g_rlv_owner; 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); } // ============================================================ // 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() { integer ch = -1 - (integer)llFrand(999998.0); return ch; } 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 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_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 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); } startDeleteFromList(key agent, integer purpose) { list theList = g_whitelist; string which = "whitelist"; if (purpose == 9) { theList = g_blacklist; which = "blacklist"; } if (llGetListLength(theList) == 0) { llInstantMessage(agent, "[Diaper] " + which + " is empty."); 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() + "\nLock: " + lockStatus(); list buttons; if (isOwner(agent)) { if (g_change_locked == 0) buttons = ["Check Status", "Change Diaper", "Wet", "Lock Diaper", "SAFEWORD", "Settings", "Close"]; else buttons = ["Check Status", "Change Diaper", "Wet", "Unlock Diaper", "SAFEWORD", "Settings", "Close"]; } else { buttons = ["Check Status", "Change Diaper", "Close"]; } 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) { string title = "\n[Settings > RLV]\n" + "Safeword: " + g_safeword + "\n" + "SW channel: " + (string)g_safeword_channel + "\n\n" + "RLV features not yet active."; list buttons = ["Safeword Word", "Safeword Chan", "Back"]; openDialog(agent, title, buttons); } showAnnounceTargetMenu(key agent, string which) { // which = "full", "leak", or "change" // Store which announce we are editing 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 + "?", ["Owner 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 white/blacklists 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. Try again."); } 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. Try again."); } 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); } } // ============================================================ // 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 (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; else if (k == "rlv_owner") g_rlv_owner = 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_rlv_owner = ""; 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 = []; 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"); updateCommandListen(); updateSafewordListen(); } // ============================================================ // Finish initialisation // ============================================================ finishInit() { if (g_command_prefix == "") g_command_prefix = buildCommandPrefix(); updateCommandListen(); updateSafewordListen(); llSetTimerEvent(TIMER_TICK); refreshRegionRating(); g_initialised = TRUE; llInstantMessage(llGetOwner(), "[Diaper] System ready. Storage: Prim Description (hypergrid-safe).\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(); // ---- Main menu ---- if (response == "Close") return; if (response == "Check Status") { if (isOwner(agent)) { string status = "\n[Diaper Status]\n" + "Wetness: " + wetName() + "\nLocked: " + lockStatus() + "\nNext 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 (g_change_locked > 0) { llInstantMessage(agent, "[Diaper] The diaper is locked and cannot be changed."); showRootMenu(agent); return; } changeDiaper(agent); showRootMenu(agent); return; } if (response == "Wet") { if (!isOwner(agent)) { showRootMenu(agent); return; } applyWetting(1); if (agent != llGetOwner()) llInstantMessage(agent, llGetDisplayName(llGetOwner()) + " just wet themselves! Status: " + wetName()); showRootMenu(agent); return; } if (response == "Lock Diaper") { if (!isOwner(agent)) { showRootMenu(agent); return; } g_change_locked = 1; saveState(); llInstantMessage(agent, "[Diaper] Diaper locked."); showRootMenu(agent); return; } if (response == "Unlock Diaper") { if (!isOwner(agent)) { showRootMenu(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."); // showRootMenu(agent); return; // } g_change_locked = 0; saveState(); llInstantMessage(agent, "[Diaper] Diaper unlocked."); showRootMenu(agent); return; } if (response == "SAFEWORD") { // If RLV is active and something is locked, fire the safeword. // Otherwise just remind the wearer of their safeword. // RLV STUB: when RLV is implemented, check for active RLV locks here too. if (g_change_locked > 0 || g_removal_locked > 0) { processSafeword(); } else { llInstantMessage(agent, "[Diaper] No RLV restrictions are currently active. Your safeword is: " + g_safeword); } showRootMenu(agent); return; } if (response == "Settings") { if (!isOwner(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 stored in g_input_purpose 20/21/22) if (response == "Owner 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 Owner 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 and state 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_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..."); showMenuGeneral(agent); return; } // ---- RLV submenu ---- 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) { 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 { 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) 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; 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); } 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(); } } }