// ============================================================ // [HappyBumsPlus] Core v4.3 // Main logic for the OpenSim wearable diaper/incontinence system. // Plus version: wetting AND messing. // // Companion script: [HappyBumsPlus] Persistence (must be in same prim) // // Changes v4.3: // - Add g_mess, g_mess_elapsed globals for mess state and timer. // - Add g_auto_mess_interval, g_min_mess, g_mess_names globals. // - g_mess_capable flag (always TRUE in Plus version). // - applyMessing() function mirroring applyWetting(); blowout // special announce shares g_announce_leak target. // - changeDiaper() resets both g_mess and g_wetness to 0. // - Status wording: "[Name]'s diaper is [wet] and [mess]" with // special case "[Name]'s diaper is clean and dry" after a change. // - safeword resets both g_mess and g_wetness. // - Mess button on main menu (owner/RLV owner only). // - canChange() updated: either/or logic for min_wet OR min_mess. // - Identity settings: add Mess Names input (purpose 17). // - General settings: add Auto Mess button. // - saveState() includes mess= and mess_elapsed= fields. // - applyLoadedState() parses mess and mess_elapsed. // - applyCfg() parses auto_mess_interval, min_mess, mess_names. // - resetToDefaults() resets all mess fields. // - webPost() payload includes mess_state and mess_capable=1. // - sendFloatUpdate() includes mess state. // - timer() handles auto-mess counter alongside auto-wet. // - finishInit() startup IM shows both states. // - WEB_CMD handler: added "mess" and "auto_mess_interval" commands. // - Default auto-wet interval changed to 15 mins (was 30 mins). // - Default auto-mess interval: 60 mins. // // Changes v4.2: // - Finish implementing the webpage. // Changes v4.1: // - stubWebPost() replaced with real webPost() using llHTTPRequest. // - New g_web_request key tracks pending HTTP request. // - New http_response event handler processes PHP replies. // - webPost() called from finishInit(), applyWetting(), changeDiaper(). // - add_owner POST wired in processInput() purpose 12. // - reset_password POST wired in handleMenuResponse() Reset Password. // - New g_web_interval setting (heartbeat interval in seconds, default 180). // - New g_web_elapsed counter incremented by TIMER_TICK each tick. // - Heartbeat fires in timer() when g_web_elapsed >= g_web_interval. // - New "Web Interval" button in General settings menu. // - web_interval stored via Persistence (SAVE_CFG / CFG). // - resetToDefaults() resets web_interval to 180. // - g_pending_owner_uuid / g_pending_owner_pwd stored temporarily so // http_response can IM the owner after PHP confirms new vs existing. // Changes v4.0: // - Bumped to match persistence. Added owner/website management. // Changes v3.2: // - Added hover text controller for optional hover text wearable. // Changes v3.1: // - Bumped to v3.1 to match persistence refactor. // Changes v3.0: // - Implement min status level and change limits. // Changes v2.9: // - HUD check for locked status on initialisation. // - Notify on wear. // Changes v2.8: // - RLV Phase 1 implementation. // Changes v2.7: // - Full menu refactor. Added g_access_mode, g_announce_change. // - SAFEWORD button on main menu. // ============================================================ integer LM_CHANNEL = 1001; // ============================================================ // State variables // ============================================================ integer g_wetness = 0; // 0=Dry 1=Damp 2=Wet 3=Soaking 4=Leaking integer g_mess = 0; // 0=Clean 1=Soiled 2=Messy 3=Blowout 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 integer g_mess_elapsed = 0; // seconds since last auto-mess integer g_notify_on_wear = 0; // 0=off 1=on // ============================================================ // Settings // ============================================================ integer g_auto_wet_interval = 900; // 15 mins default (0 = disabled) integer g_auto_mess_interval = 3600; // 60 mins default (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 = "Caregiver"; integer g_min_change_state = 0; // 0=any wet state, 1-4=minimum wetness required integer g_min_mess = 0; // 0=any mess state, 1-3=minimum mess required integer g_change_limit = -1; // -1=unlimited, 0=owner only, n=n changes allowed integer g_change_counter = -1; // remaining changes (-1 = unlimited) integer g_hover_channel = 0; integer g_hover_enabled = 1; string g_hud_uuid = ""; string g_website_url = ""; integer g_web_interval = 180; // heartbeat interval in seconds (0=disabled) integer g_web_elapsed = 0; // seconds since last heartbeat string g_sim_url = ""; // simulator URL from WebRelay integer g_mess_capable = TRUE; // always TRUE in Plus version list g_state_names = ["Dry", "Damp", "Wet", "Soaking", "Leaking"]; list g_mess_names = ["Clean", "Soiled", "Messy", "Blowout"]; 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 8=hud_uuid 16=website_url // 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; // Web POST tracking key g_web_request = NULL_KEY; string g_pending_owner_uuid = ""; // set before add_owner POST, read in http_response string g_pending_owner_pwd = ""; // plain password sent to new owner on "new" reply // ============================================================ // Helpers // ============================================================ string wetName() { return llList2String(g_state_names, g_wetness); } string messName() { return llList2String(g_mess_names, g_mess); } string ownerName() { return llKey2Name(llGetOwner()); } // Combined status string: "clean and dry" special case after change, // otherwise "[wet] and [mess]". string diaperStatus() { if (g_wetness == 0 && g_mess == 0) return "clean and dry"; return wetName() + " and " + messName(); } string generatePassword() { string chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; string pwd = ""; integer i; for (i = 0; i < 15; i++) { integer idx = (integer)llFrand(62); pwd += llGetSubString(chars, idx, idx); } return pwd; } initHudUuid() { if (g_hud_uuid == "") { g_hud_uuid = (string)llGenerateKey(); llMessageLinked(LINK_ROOT, LM_CHANNEL, "SAVE_HUD_UUID|" + g_hud_uuid, NULL_KEY); } } regenerateUUID() { g_hud_uuid = (string)llGenerateKey(); llMessageLinked(LINK_ROOT, LM_CHANNEL, "SAVE_HUD_UUID|" + g_hud_uuid, NULL_KEY); resetToDefaults(); llMessageLinked(LINK_ROOT, LM_CHANNEL, "WEB_RELAY_INIT|" + g_hud_uuid, NULL_KEY); if (g_website_url != "") webPost(""); llInstantMessage(llGetOwner(), "[HappyBumsPlus] A new UUID has been generated and all settings reset to default.\n" + "New UUID: " + g_hud_uuid + "\n" + (g_website_url != "" ? "Status sent to website. Old UUID data will be cleaned up automatically after 365 days." : "No website configured — no web update sent.")); } 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 webIntervalLabel(integer secs) { if (secs == 0) return "Disabled"; if (secs == 60) return "1 min"; if (secs == 180) return "3 mins"; if (secs == 300) return "5 mins"; return (string)(secs/60) + " mins"; } 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() { llOwnerSay("@detach=n"); } rlvRelease() { 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; 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()); } integer canChange(key agent) { if (isRLVOwner(agent)) return TRUE; if (isOwner(agent)) { if (g_change_locked == 2 && llGetListLength(g_rlv_owners) > 0) return FALSE; if (g_change_limit == 0) return FALSE; if (g_change_limit > 0 && g_change_counter == 0) return FALSE; return TRUE; } if (g_change_locked >= 1) return FALSE; if (g_change_limit == 0) return FALSE; if (g_change_limit > 0 && g_change_counter == 0) return FALSE; return TRUE; } // Returns TRUE if the diaper state meets the minimum threshold to allow a change. // Either wet OR mess threshold being met is sufficient. integer meetsMinChangeState(key agent) { if (isRLVOwner(agent)) return TRUE; integer wet_ok = (g_min_change_state == 0 || g_wetness >= g_min_change_state); integer mess_ok = (g_min_mess == 0 || g_mess >= g_min_mess); return (wet_ok || mess_ok); } // ============================================================ // 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(), "[HappyBumsPlus] " + msg); notifyRLVOwners("[HappyBumsPlus] " + msg); return; } if (g_min_maturity == 0 || g_region_rating >= g_min_maturity) llSay(0, msg); if (g_notify_wearer) llInstantMessage(llGetOwner(), "[HappyBumsPlus] " + msg); notifyRLVOwners("[HappyBumsPlus] " + 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); } } // ============================================================ // Web POST // ============================================================ webPost(string json) { if (g_website_url == "") return; if (json == "") { string ownerList = llList2CSV(g_rlv_owners); json = llList2Json(JSON_OBJECT, [ "access_uuid", g_hud_uuid, "wearer_name", llGetDisplayName(llGetOwner()), "region_name", llGetRegionName(), "region_rating", (string)g_region_rating, "diaper_state", wetName(), "mess_state", messName(), "mess_capable", "1", "removal_locked", (string)g_removal_locked, "change_lock", (string)g_change_locked, "auto_wet_interval", (string)g_auto_wet_interval, "auto_mess_interval", (string)g_auto_mess_interval, "simulator_url", g_sim_url, "last_seen", (string)llGetUnixTime(), "owner_list", ownerList, "announce_full", (string)g_announce_full, "announce_leak", (string)g_announce_leak, "announce_change", (string)g_announce_change, "notify_wearer", (string)g_notify_wearer, "owner_title", g_owner_title, "state_names", llList2CSV(g_state_names), "mess_names", llList2CSV(g_mess_names) ]); } g_web_request = llHTTPRequest(g_website_url, [HTTP_METHOD, "POST", HTTP_MIMETYPE, "application/json", HTTP_VERIFY_CERT, TRUE], json); } sendFloatUpdate() { string msg = "STATE:" + (string)g_wetness + "|" + llGetDisplayName(llGetOwner()) + "|" + wetName() + "|MESS:" + (string)g_mess + "|" + messName(); llSay(g_hover_channel, 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(), "[HappyBumsPlus] You just wet yourself. 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); sendFloatUpdate(); saveState(); webPost(""); } applyMessing(integer units) { if (!g_diaper_on) return; g_mess += units; if (g_mess > 3) g_mess = 3; if (g_notify_wearer) llInstantMessage(llGetOwner(), "[HappyBumsPlus] You just messed yourself. Your diaper is now " + messName() + "."); notifyRLVOwners(ownerName() + "'s diaper is now " + messName() + "."); if (g_mess == 2) announce(ownerName() + "'s diaper is messy and needs changing soon!", g_announce_full); else if (g_mess == 3) announce(ownerName() + "'s diaper has had a blowout!", g_announce_leak); sendFloatUpdate(); saveState(); webPost(""); } changeDiaper(key changer) { g_wetness = 0; g_mess = 0; g_elapsed_timer = 0; g_mess_elapsed = 0; g_diaper_on = 1; string changerName = llKey2Name(changer); if (changer == llGetOwner()) changerName = "themselves"; if (changer == NULL_KEY) changerName = "WebRelay"; if (isRLVOwner(changer)) { g_change_counter = g_change_limit; saveCfg("change_counter", (string)g_change_counter); } else if (g_change_limit > 0) { g_change_counter--; if (g_change_counter < 0) g_change_counter = 0; saveCfg("change_counter", (string)g_change_counter); } announce(ownerName() + "'s diaper has been changed by " + changerName + ". Fresh and dry!", g_announce_change); if (g_notify_wearer && changer != llGetOwner()) { string changerDisplay; if (changer == NULL_KEY) changerDisplay = "WebRelay"; else changerDisplay = llKey2Name(changer); llInstantMessage(llGetOwner(), "[HappyBumsPlus] Your diaper was changed by " + changerDisplay + "."); } sendFloatUpdate(); saveState(); webPost(""); } // ============================================================ // Persistence - state // ============================================================ saveState() { string msg = "SAVE_ALL" + "|wetness=" + (string)g_wetness + "|mess=" + (string)g_mess + "|change_locked=" + (string)g_change_locked + "|removal_locked=" + (string)g_removal_locked + "|elapsed_timer=" + (string)g_elapsed_timer + "|mess_elapsed=" + (string)g_mess_elapsed + "|diaper_on=" + (string)g_diaper_on; llMessageLinked(LINK_ROOT, LM_CHANNEL, msg, NULL_KEY); } // ============================================================ // Persistence - settings (individual key writes) // ============================================================ 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_blacklist 10=state_names 11=cmd_prefix 12=add_rlv_owner // 13=del_rlv_owner 14=change_limit 15=min_change_state 16=website_url // 17=mess_names 18=auto_mess_mins 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, "[HappyBumsPlus] " + prompt); } showLists(key agent) { string out = "[HappyBumsPlus] 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[HappyBumsPlus] 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 = "[HappyBumsPlus] 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, "[HappyBumsPlus] " + which + " is empty."); if (purpose == 13) showMenuRLV(agent); else showMenuAccess(agent); return; } string out = "[HappyBumsPlus] 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; if (isOwner(agent) || isRLVOwner(agent)) title = "\n[HappyBumsPlus]\n" + "Status: " + diaperStatus() + "\n" + "Change lock: " + changeLockStatus() + "\n" + "HUD lock: " + removalLockStatus(); else title = "\n[HappyBumsPlus]\nTouch to interact."; list buttons = ["Check Status", "Close"]; if (isOwner(agent)) buttons += ["SAFEWORD"]; if (canChange(agent)) buttons += ["Change Diaper"]; if (isOwner(agent) || isRLVOwner(agent)) { buttons += ["Wet"]; buttons += ["Mess"]; } 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"]; } if (isRLVOwner(agent)) { if (g_removal_locked == 0) buttons += ["Lock HUD"]; else buttons += ["Unlock HUD"]; } 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" + "Wet names: " + llList2CSV(g_state_names) + "\n" + "Mess names: " + llList2CSV(g_mess_names); list buttons = ["Owner Title", "State Names", "Mess 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") + "\n" + "Auto-mess: " + (g_auto_mess_interval > 0 ? (string)(g_auto_mess_interval/60) + " min" : "Disabled") + "\n" + "Web interval: " + webIntervalLabel(g_web_interval) + "\n" + "Website: " + (g_website_url == "" ? "Not set" : "Set"); list buttons = ["Auto-wet", "Auto-mess", "Web Interval", "Website URL", "New UUID", "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 + "\n" + "Notify on wear: "+ yesNo(g_notify_on_wear) + "\n" + "Min wet: " + llList2String(g_state_names, g_min_change_state) + "\n" + "Min mess: " + llList2String(g_mess_names, g_min_mess) + "\n" + "Change limit: " + (g_change_limit == -1 ? "Unlimited" : (string)g_change_limit) + "\n" + "Changes left: " + (g_change_limit == -1 ? "Unlimited" : (string)g_change_counter) + "\n"; list buttons; if (isRLVOwner(agent)) buttons = ["Add RLV Owner", "Del RLV Owner", "Show Owners", "Safeword Word", "Safeword Chan", "Notify On Wear", "Change Limit", "Min Change State", "Min Mess State", "Hover Text", "Reset Password", "Back"]; else if (llGetListLength(g_rlv_owners) == 0) buttons = ["Add RLV Owner", "Show Owners", "Safeword Word", "Safeword Chan", "Notify On Wear", "Change Limit", "Min Change State", "Min Mess State", "Hover Text", "Back"]; else buttons = ["Show Owners", "Safeword Word", "Safeword Chan", "Notify On Wear", "Change Limit", "Min Change State", "Min Mess State", "Back"]; openDialog(agent, title, buttons); } showMenuHoverText(key agent) { string toggle = g_hover_enabled ? "Disable" : "Enable"; openDialog(agent, "\n[Settings > RLV > Hover Text]\nControls the optional float text HUD.\nCurrently: " + yesNo(g_hover_enabled), [toggle, "Up", "Down", "Reset Height", "Back"]); } showAnnounceTargetMenu(key agent, string which) { 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"]); } showWebIntervalMenu(key agent) { openDialog(agent, "\n[Web Heartbeat Interval]\nHow often the HUD sends status to the website.\nDisabled = no web updates.\nCurrent: " + webIntervalLabel(g_web_interval), ["Disabled", "1 min", "3 mins", "5 mins", "Back"]); } showMinChangeStateMenu(key agent) { openDialog(agent, "\n[Min Wet State for Change]\nMinimum wetness before change is allowed.\nRLV owners can always change.\nEither wet OR mess threshold being met allows a change.\nCurrent: " + llList2String(g_state_names, g_min_change_state), ["Any", llList2String(g_state_names, 1), llList2String(g_state_names, 2), llList2String(g_state_names, 3), llList2String(g_state_names, 4), "Back"]); } showMinMessStateMenu(key agent) { openDialog(agent, "\n[Min Mess State for Change]\nMinimum mess before change is allowed.\nRLV owners can always change.\nEither wet OR mess threshold being met allows a change.\nCurrent: " + llList2String(g_mess_names, g_min_mess), ["Any", llList2String(g_mess_names, 1), llList2String(g_mess_names, 2), llList2String(g_mess_names, 3), "Back"]); } // ============================================================ // processInput - handles free-text responses // ============================================================ processInput(string text, key agent) { 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, "[HappyBumsPlus] Added " + uuid + " to whitelist."); } else llInstantMessage(agent, "[HappyBumsPlus] Invalid UUID."); } else llInstantMessage(agent, "[HappyBumsPlus] 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, "[HappyBumsPlus] Added " + uuid + " to blacklist."); } else llInstantMessage(agent, "[HappyBumsPlus] Invalid UUID."); } else llInstantMessage(agent, "[HappyBumsPlus] 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, "[HappyBumsPlus] 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; g_elapsed_timer = 0; saveCfg("auto_wet_interval", (string)g_auto_wet_interval); if (mins == 0) llInstantMessage(agent, "[HappyBumsPlus] Auto-wet disabled."); else llInstantMessage(agent, "[HappyBumsPlus] Auto-wet interval set to " + (string)mins + " minutes."); } else llInstantMessage(agent, "[HappyBumsPlus] 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, "[HappyBumsPlus] 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, "[HappyBumsPlus] 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, "[HappyBumsPlus] Owner title set to: " + g_owner_title); } else llInstantMessage(agent, "[HappyBumsPlus] Title cannot be blank."); showMenuIdentity(agent); } else if (purpose == 8) // delete from whitelist { integer num = (integer)text; if (num == 0) llInstantMessage(agent, "[HappyBumsPlus] Cancelled."); else if (num < 1 || num > llGetListLength(g_whitelist)) llInstantMessage(agent, "[HappyBumsPlus] Invalid number."); else { string removed = llList2String(g_whitelist, num-1); g_whitelist = llDeleteSubList(g_whitelist, num-1, num-1); writeListToLD("whitelist"); llInstantMessage(agent, "[HappyBumsPlus] Removed " + removed + " from whitelist."); } showMenuAccess(agent); } else if (purpose == 9) // delete from blacklist { integer num = (integer)text; if (num == 0) llInstantMessage(agent, "[HappyBumsPlus] Cancelled."); else if (num < 1 || num > llGetListLength(g_blacklist)) llInstantMessage(agent, "[HappyBumsPlus] Invalid number."); else { string removed = llList2String(g_blacklist, num-1); g_blacklist = llDeleteSubList(g_blacklist, num-1, num-1); writeListToLD("blacklist"); llInstantMessage(agent, "[HappyBumsPlus] Removed " + removed + " from blacklist."); } showMenuAccess(agent); } else if (purpose == 10) // wet 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, "[HappyBumsPlus] Wet state names updated: " + trimmed); } else llInstantMessage(agent, "[HappyBumsPlus] 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, "[HappyBumsPlus] Command prefix reset to: " + g_command_prefix); } else if (prefix != "" && llSubStringIndex(prefix, " ") == -1) { g_command_prefix = prefix; saveCfg("command_prefix", prefix); updateCommandListen(); llInstantMessage(agent, "[HappyBumsPlus] Command prefix set to: " + prefix); } else llInstantMessage(agent, "[HappyBumsPlus] 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, "[HappyBumsPlus] RLV owner list is full (max 6)."); } else if (llListFindList(g_rlv_owners, [uuid]) != -1) { llInstantMessage(agent, "[HappyBumsPlus] That UUID is already an RLV owner."); } else { g_rlv_owners += [uuid]; writeListToLD("rlv_owners"); llInstantMessage(agent, "[HappyBumsPlus] Added " + uuid + " as RLV owner."); if (g_website_url != "") { string pwd = generatePassword(); g_pending_owner_uuid = uuid; g_pending_owner_pwd = pwd; string json = llList2Json(JSON_OBJECT, [ "action", "add_owner", "owner_uuid", uuid, "password", pwd, "access_uuid", g_hud_uuid ]); webPost(json); } else { llInstantMessage((key)uuid, "[HappyBumsPlus] You have been added as an owner by " + ownerName() + ". No web panel is currently configured."); } } } else llInstantMessage(agent, "[HappyBumsPlus] Invalid UUID."); } else llInstantMessage(agent, "[HappyBumsPlus] Expected: addowner "); showMenuRLV(agent); } else if (purpose == 13) // delete from RLV owner list { integer num = (integer)text; if (num == 0) llInstantMessage(agent, "[HappyBumsPlus] Cancelled."); else if (num < 1 || num > llGetListLength(g_rlv_owners)) llInstantMessage(agent, "[HappyBumsPlus] 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, "[HappyBumsPlus] Removed " + removed + " from RLV owner list."); 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, "[HappyBumsPlus] All RLV owners removed. Locks released."); } } showMenuRLV(agent); } else if (purpose == 14) // change limit { integer lim = (integer)llStringTrim(text, STRING_TRIM); if (lim < -1) { llInstantMessage(agent, "[HappyBumsPlus] Please enter -1 (unlimited), 0 (owner only), or a positive number."); } else { g_change_limit = lim; if (lim == -1) g_change_counter = -1; else if (g_change_counter > lim) g_change_counter = lim; saveCfg("change_limit", (string)g_change_limit); saveCfg("change_counter", (string)g_change_counter); if (lim == -1) llInstantMessage(agent, "[HappyBumsPlus] Change limit: Unlimited."); else if (lim == 0) llInstantMessage(agent, "[HappyBumsPlus] Change limit: Owner only (no free changes)."); else llInstantMessage(agent, "[HappyBumsPlus] Change limit set to " + (string)lim + "."); } showMenuRLV(agent); } else if (purpose == 16) // website URL { string url = llStringTrim(text, STRING_TRIM); if (url == "clear" || url == "none" || url == "") { g_website_url = ""; llMessageLinked(LINK_ROOT, LM_CHANNEL, "SAVE_WEBSITE_URL|", NULL_KEY); llInstantMessage(agent, "[HappyBumsPlus] Website URL cleared."); } else if (llGetSubString(url, 0, 6) == "http://" || llGetSubString(url, 0, 7) == "https://") { g_website_url = url; llMessageLinked(LINK_ROOT, LM_CHANNEL, "SAVE_WEBSITE_URL|" + url, NULL_KEY); llInstantMessage(agent, "[HappyBumsPlus] Website URL set to: " + url); } else { llInstantMessage(agent, "[HappyBumsPlus] URL must start with http:// or https:// (or type 'clear' to remove)."); } showMenuGeneral(agent); } else if (purpose == 17) // mess state names { string trimmed = llStringTrim(text, STRING_TRIM); list names = llCSV2List(trimmed); if (llGetListLength(names) == 4) { g_mess_names = names; saveCfg("mess_names", trimmed); llInstantMessage(agent, "[HappyBumsPlus] Mess state names updated: " + trimmed); } else llInstantMessage(agent, "[HappyBumsPlus] Please enter exactly 4 comma-separated names."); showMenuIdentity(agent); } else if (purpose == 18) // auto-mess interval { integer mins = (integer)text; if (mins >= 0) { g_auto_mess_interval = mins * 60; g_mess_elapsed = 0; saveCfg("auto_mess_interval", (string)g_auto_mess_interval); if (mins == 0) llInstantMessage(agent, "[HappyBumsPlus] Auto-mess disabled."); else llInstantMessage(agent, "[HappyBumsPlus] Auto-mess interval set to " + (string)mins + " minutes."); } else llInstantMessage(agent, "[HappyBumsPlus] Please enter a number of minutes (0 to disable)."); showMenuGeneral(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 // ============================================================ 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 == "auto_mess_interval") g_auto_mess_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 == "notify_on_wear") g_notify_on_wear = (integer)v; else if (k == "min_change_state") g_min_change_state = (integer)v; else if (k == "min_mess") g_min_mess = (integer)v; else if (k == "change_limit") g_change_limit = (integer)v; else if (k == "change_counter") g_change_counter = (integer)v; else if (k == "web_interval") g_web_interval = (integer)v; else if (k == "state_names") { list names = llCSV2List(v); if (llGetListLength(names) == 5) g_state_names = names; } else if (k == "mess_names") { list names = llCSV2List(v); if (llGetListLength(names) == 4) g_mess_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 == "mess") g_mess = (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 == "mess_elapsed") g_mess_elapsed = (integer)v; else if (k == "diaper_on") g_diaper_on = (integer)v; @skip_l; } } // ============================================================ // Reset all settings to defaults // ============================================================ resetToDefaults() { g_wetness = 0; g_mess = 0; g_change_locked = 0; g_removal_locked = 0; g_elapsed_timer = 0; g_mess_elapsed = 0; g_diaper_on = 1; g_auto_wet_interval = 900; g_auto_mess_interval = 3600; 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 = "Caregiver"; g_state_names = ["Dry", "Damp", "Wet", "Soaking", "Leaking"]; g_mess_names = ["Clean", "Soiled", "Messy", "Blowout"]; g_min_maturity = 0; g_whitelist = []; g_blacklist = []; g_rlv_owners = []; g_notify_on_wear = 0; g_min_change_state = 0; g_min_mess = 0; g_change_limit = -1; g_change_counter = -1; g_web_interval = 180; g_web_elapsed = 0; saveState(); saveCfg("auto_wet_interval", "900"); saveCfg("auto_mess_interval", "3600"); 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", "Caregiver"); saveCfg("state_names", "Dry,Damp,Wet,Soaking,Leaking"); saveCfg("mess_names", "Clean,Soiled,Messy,Blowout"); saveCfg("min_maturity", "0"); saveCfg("notify_on_wear", "0"); saveCfg("min_change_state", "0"); saveCfg("min_mess", "0"); saveCfg("change_limit", "-1"); saveCfg("change_counter", "-1"); saveCfg("web_interval", "180"); writeListToLD("whitelist"); writeListToLD("blacklist"); writeListToLD("rlv_owners"); // Note: Website URL and HUD UUID intentionally NOT reset. llOwnerSay("@detach=y"); updateCommandListen(); updateSafewordListen(); } // ============================================================ // Finish initialisation // ============================================================ finishInit() { g_hover_channel = -1 - (integer)("0x" + llGetSubString((string)llGetOwner(), 0, 6)); if (g_command_prefix == "") g_command_prefix = buildCommandPrefix(); updateCommandListen(); updateSafewordListen(); llSetTimerEvent(TIMER_TICK); refreshRegionRating(); g_initialised = TRUE; if (g_removal_locked > 0) rlvLock(); if (g_notify_on_wear && llGetListLength(g_rlv_owners) > 0) notifyRLVOwners("[HappyBumsPlus] " + ownerName() + " has worn their diaper. Status: " + diaperStatus()); string rlvStatus = ""; if (llGetListLength(g_rlv_owners) > 0) rlvStatus = " | RLV owners: " + (string)llGetListLength(g_rlv_owners); llInstantMessage(llGetOwner(), "[HappyBumsPlus] System ready. Storage: Prim Description (hypergrid-safe).\n" + "Status: " + diaperStatus() + " | 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." : ".")); sendFloatUpdate(); llMessageLinked(LINK_ROOT, LM_CHANNEL, "WEB_RELAY_INIT|" + g_hud_uuid, NULL_KEY); // Startup heartbeat is deferred — WebRelay will broadcast its URL via // WEB_RELAY_URL which triggers a fresh webPost() once g_sim_url is known. } // ============================================================ // 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" + "Mess: " + messName() + "\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") + "\n" + "Next auto-mess: " + (g_auto_mess_interval > 0 ? (string)((g_auto_mess_interval - g_mess_elapsed)/60) + " min" : "Disabled"); llInstantMessage(agent, status); } else { llInstantMessage(agent, llGetDisplayName(llGetOwner()) + "'s diaper is " + diaperStatus() + "."); } showRootMenu(agent); return; } if (response == "Change Diaper") { if (!canChange(agent)) { if (g_change_locked == 2) llInstantMessage(agent, "[HappyBumsPlus] The diaper is locked by your " + g_owner_title + " and cannot be changed."); else if (g_change_limit == 0 || g_change_counter == 0) llInstantMessage(agent, "[HappyBumsPlus] No fresh diapers available!"); else llInstantMessage(agent, "[HappyBumsPlus] The diaper is locked."); showRootMenu(agent); return; } if (!meetsMinChangeState(agent)) { llInstantMessage(agent, "[HappyBumsPlus] Diaper does not need changing yet. (" + diaperStatus() + " — needs at least " + llList2String(g_state_names, g_min_change_state) + " or " + llList2String(g_mess_names, g_min_mess) + ")"); 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 == "Mess") { if (!isOwner(agent) && !isRLVOwner(agent)) { showRootMenu(agent); return; } applyMessing(1); if (!isOwner(agent)) llInstantMessage(agent, llGetDisplayName(llGetOwner()) + " just messed themselves! Status: " + messName()); 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, "[HappyBumsPlus] 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, "[HappyBumsPlus] Changes locked at wearer level."); llInstantMessage(llGetOwner(), "[HappyBumsPlus] 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, "[HappyBumsPlus] Changes locked at owner level. Only you can change."); llInstantMessage(llGetOwner(), "[HappyBumsPlus] Your " + g_owner_title + " has locked your changes. Only they can change you now."); showRootMenu(agent); return; } if (response == "No Change") { showRootMenu(agent); return; } if (response == "Unlock Changes") { if (isRLVOwner(agent)) { g_change_locked = 0; saveState(); llInstantMessage(agent, "[HappyBumsPlus] Changes unlocked."); llInstantMessage(llGetOwner(), "[HappyBumsPlus] Your " + g_owner_title + " has unlocked your changes."); } else if (isOwner(agent) && llGetListLength(g_rlv_owners) == 0) { g_change_locked = 0; saveState(); llInstantMessage(agent, "[HappyBumsPlus] Changes unlocked."); } else { llInstantMessage(agent, "[HappyBumsPlus] 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, "[HappyBumsPlus] HUD locked. Wearer cannot remove it."); llInstantMessage(llGetOwner(), "[HappyBumsPlus] 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, "[HappyBumsPlus] HUD unlocked."); llInstantMessage(llGetOwner(), "[HappyBumsPlus] 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, "[HappyBumsPlus] 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, "[HappyBumsPlus] Notify wearer: " + yesNo(g_notify_wearer)); showMenuAnnounce(agent); return; } if (response == "Min Maturity") { showMinMaturityMenu(agent); return; } 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, "[HappyBumsPlus] 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, "[HappyBumsPlus] 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, "[HappyBumsPlus] Announce set to Everyone."); showMenuAnnounce(agent); return; } if (response == "None") { g_min_maturity = 0; saveCfg("min_maturity", "0"); llInstantMessage(agent, "[HappyBumsPlus] Min maturity: None (always announce)."); showMenuAnnounce(agent); return; } if (response == "Moderate") { g_min_maturity = 1; saveCfg("min_maturity", "1"); llInstantMessage(agent, "[HappyBumsPlus] Min maturity: Moderate."); showMenuAnnounce(agent); return; } if (response == "Adult") { g_min_maturity = 2; saveCfg("min_maturity", "2"); llInstantMessage(agent, "[HappyBumsPlus] 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, "[HappyBumsPlus] 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, "[HappyBumsPlus] Access mode: Open/List."); showMenuAccess(agent); return; } if (response == "Owner Only") { g_access_mode = 1; saveCfg("access_mode", "1"); llInstantMessage(agent, "[HappyBumsPlus] 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 wet state names (e.g. Dry,Damp,Wet,Soaking,Leaking).\nCurrent: " + llList2CSV(g_state_names)); return; } if (response == "Mess Names") { startListenForInput(agent, 17, "Type 4 comma-separated mess state names (e.g. Clean,Soiled,Messy,Blowout).\nCurrent: " + llList2CSV(g_mess_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 == "Auto-mess") { startListenForInput(agent, 18, "Enter auto-mess interval in MINUTES (0 to disable, current: " + (string)(g_auto_mess_interval/60) + "):"); return; } if (response == "Web Interval") { showWebIntervalMenu(agent); return; } if (response == "Disabled") { g_web_interval = 0; saveCfg("web_interval", "0"); g_web_elapsed = 0; llInstantMessage(agent, "[HappyBumsPlus] Web heartbeat disabled."); showMenuGeneral(agent); return; } if (response == "1 min") { g_web_interval = 60; saveCfg("web_interval", "60"); g_web_elapsed = 0; llInstantMessage(agent, "[HappyBumsPlus] Web heartbeat: 1 minute."); showMenuGeneral(agent); return; } if (response == "3 mins") { g_web_interval = 180; saveCfg("web_interval", "180"); g_web_elapsed = 0; llInstantMessage(agent, "[HappyBumsPlus] Web heartbeat: 3 minutes."); showMenuGeneral(agent); return; } if (response == "5 mins") { g_web_interval = 300; saveCfg("web_interval", "300"); g_web_elapsed = 0; llInstantMessage(agent, "[HappyBumsPlus] Web heartbeat: 5 minutes."); showMenuGeneral(agent); return; } if (response == "Website URL") { if (!isOwner(agent)) { llInstantMessage(agent, "[HappyBumsPlus] Only the wearer can set the website URL."); showMenuGeneral(agent); return; } startListenForInput(agent, 16, "Enter the website URL for the web control panel.\nType 'clear' to remove.\nCurrent: " + (g_website_url == "" ? "Not set" : g_website_url)); 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, "[HappyBumsPlus] 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, "[HappyBumsPlus] 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, "[HappyBumsPlus] Reloading access lists..."); showMenuGeneral(agent); return; } if (response == "New UUID") { if (!isOwner(agent)) { llInstantMessage(agent, "[HappyBumsPlus] Only the wearer can regenerate the UUID."); showMenuGeneral(agent); return; } openDialog(agent, "\n[New UUID]\nThis generates a fresh HUD identity.\nUseful if you gave a copy to someone.\nThis also resets all data to defaults.\nOld UUID data on the website will be cleaned up automatically after 365 days.\nAre you sure?", ["Confirm New UUID", "Cancel"]); return; } if (response == "Confirm New UUID") { regenerateUUID(); showMenuGeneral(agent); return; } // ---- RLV submenu ---- if (response == "Hover Text") { if (!isRLVOwner(agent) && llGetListLength(g_rlv_owners) > 0) { llInstantMessage(agent, "[HappyBumsPlus] Only your " + g_owner_title + " can access this."); showMenuRLV(agent); return; } showMenuHoverText(agent); return; } if (response == "Enable") { g_hover_enabled = 1; llSay(g_hover_channel, "TOGGLE:1"); sendFloatUpdate(); showMenuHoverText(agent); return; } if (response == "Disable") { g_hover_enabled = 0; llSay(g_hover_channel, "TOGGLE:0"); showMenuHoverText(agent); return; } if (response == "Up") { llSay(g_hover_channel, "HEIGHT:+0.1"); showMenuHoverText(agent); return; } if (response == "Down") { llSay(g_hover_channel, "HEIGHT:-0.1"); showMenuHoverText(agent); return; } if (response == "Reset Height") { llSay(g_hover_channel, "HEIGHT:0"); showMenuHoverText(agent); return; } if (response == "Add RLV Owner") { if (!isRLVOwner(agent) && llGetListLength(g_rlv_owners) > 0) { llInstantMessage(agent, "[HappyBumsPlus] An RLV owner is already set. Only an RLV owner can add more."); showMenuRLV(agent); return; } if (llGetListLength(g_rlv_owners) >= 6) { llInstantMessage(agent, "[HappyBumsPlus] 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, "[HappyBumsPlus] 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; } if (response == "Notify On Wear") { g_notify_on_wear = !g_notify_on_wear; saveCfg("notify_on_wear", (string)g_notify_on_wear); llInstantMessage(agent, "[HappyBumsPlus] Notify on wear: " + yesNo(g_notify_on_wear)); showMenuRLV(agent); return; } if (response == "Change Limit") { if (!isRLVOwner(agent) && llGetListLength(g_rlv_owners) > 0) { llInstantMessage(agent, "[HappyBumsPlus] Only your " + g_owner_title + " can change this."); showMenuRLV(agent); return; } startListenForInput(agent, 14, "Enter change limit (-1=unlimited, 0=owner only, n=number of changes).\nCurrent: " + (g_change_limit == -1 ? "Unlimited" : (string)g_change_limit)); return; } if (response == "Min Change State") { showMinChangeStateMenu(agent); return; } if (response == "Min Mess State") { showMinMessStateMenu(agent); return; } // Responses from showMinChangeStateMenu if (g_input_purpose == 15 || response == "Any" || response == llList2String(g_state_names, 1) || response == llList2String(g_state_names, 2) || response == llList2String(g_state_names, 3) || response == llList2String(g_state_names, 4)) { // Only handle if we actually came from Min Change State menu // We detect this by checking if the dialog was opened with purpose 15. // Since openDialog resets g_input_purpose indirectly via closeMenuListen, // we use a dedicated approach: showMinChangeStateMenu sets g_input_purpose = 15. if (response == "Any") { g_min_change_state = 0; saveCfg("min_change_state", "0"); llInstantMessage(agent, "[HappyBumsPlus] Min wet state for change: Any."); } else if (response == llList2String(g_state_names, 1)) { g_min_change_state = 1; saveCfg("min_change_state", "1"); llInstantMessage(agent, "[HappyBumsPlus] Min wet state: " + llList2String(g_state_names, 1) + "."); } else if (response == llList2String(g_state_names, 2)) { g_min_change_state = 2; saveCfg("min_change_state", "2"); llInstantMessage(agent, "[HappyBumsPlus] Min wet state: " + llList2String(g_state_names, 2) + "."); } else if (response == llList2String(g_state_names, 3)) { g_min_change_state = 3; saveCfg("min_change_state", "3"); llInstantMessage(agent, "[HappyBumsPlus] Min wet state: " + llList2String(g_state_names, 3) + "."); } else if (response == llList2String(g_state_names, 4)) { g_min_change_state = 4; saveCfg("min_change_state", "4"); llInstantMessage(agent, "[HappyBumsPlus] Min wet state: " + llList2String(g_state_names, 4) + "."); } g_input_purpose = 0; showMenuRLV(agent); return; } // Responses from showMinMessStateMenu (purpose 16 reused for mess — use 25 to distinguish) if (response == llList2String(g_mess_names, 1) || response == llList2String(g_mess_names, 2) || response == llList2String(g_mess_names, 3)) { if (response == llList2String(g_mess_names, 1)) { g_min_mess = 1; saveCfg("min_mess", "1"); llInstantMessage(agent, "[HappyBumsPlus] Min mess state: " + llList2String(g_mess_names, 1) + "."); } else if (response == llList2String(g_mess_names, 2)) { g_min_mess = 2; saveCfg("min_mess", "2"); llInstantMessage(agent, "[HappyBumsPlus] Min mess state: " + llList2String(g_mess_names, 2) + "."); } else if (response == llList2String(g_mess_names, 3)) { g_min_mess = 3; saveCfg("min_mess", "3"); llInstantMessage(agent, "[HappyBumsPlus] Min mess state: " + llList2String(g_mess_names, 3) + "."); } g_input_purpose = 0; showMenuRLV(agent); return; } if (response == "Reset Password") { if (!isRLVOwner(agent)) { llInstantMessage(agent, "[HappyBumsPlus] Only an RLV owner can reset their password."); showMenuRLV(agent); return; } if (g_website_url == "") { llInstantMessage(agent, "[HappyBumsPlus] No website URL is configured."); showMenuRLV(agent); return; } string pwd = generatePassword(); g_pending_owner_uuid = (string)agent; g_pending_owner_pwd = pwd; string json = llList2Json(JSON_OBJECT, [ "action", "reset_password", "owner_uuid", (string)agent, "password", pwd ]); webPost(json); showMenuRLV(agent); 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; g_wetness = 0; g_mess = 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(), "[HappyBumsPlus] Safeword accepted. All locks removed, RLV owner list cleared, and diaper state reset."); webPost(""); } else { llInstantMessage(llGetOwner(), "[HappyBumsPlus] 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; g_web_elapsed = 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("[HappyBumsPlus] Still starting up, please wait..."); return; } key agent = llDetectedKey(0); if (!checkAccess(agent)) { llInstantMessage(agent, "[HappyBumsPlus] 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("[HappyBumsPlus] Still starting up..."); return; } if (!checkAccess(id)) { llInstantMessage(id, "[HappyBumsPlus] Access denied."); return; } showRootMenu(id); return; } } // Free-text input 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; } } http_response(key requestid, integer status, list metadata, string body) { if (requestid != g_web_request) return; g_web_request = NULL_KEY; string result = llJsonGetValue(body, ["status"]); if (result == "new") { llInstantMessage((key)g_pending_owner_uuid, "[HappyBumsPlus] You have been added as an owner.\n" + "Website: " + g_website_url + "\n" + "Your UUID: " + g_pending_owner_uuid + "\n" + "Initial password: " + g_pending_owner_pwd + "\n" + "Please change your password on first login."); } else if (result == "existing") { llInstantMessage((key)g_pending_owner_uuid, "[HappyBumsPlus] You have been added as an owner of another wearer.\n" + "Website: " + g_website_url + "\n" + "Use your existing website password to log in."); } else if (result == "ok") { if (g_pending_owner_pwd != "") { llInstantMessage((key)g_pending_owner_uuid, "[HappyBumsPlus] Your password has been reset.\n" + "Website: " + g_website_url + "\n" + "New password: " + g_pending_owner_pwd + "\n" + "Please update your password on next login."); } } else if (result == "error") { llOwnerSay("[HappyBumsPlus] Website error: " + llJsonGetValue(body, ["message"])); } g_pending_owner_uuid = ""; g_pending_owner_pwd = ""; } timer() { if (!g_initialised) return; if (!g_diaper_on) return; // Auto-wet timer if (g_auto_wet_interval > 0) { g_elapsed_timer += TIMER_TICK; if (g_elapsed_timer >= g_auto_wet_interval) { g_elapsed_timer = 0; applyWetting(1); // applyWetting calls saveState/webPost internally } } // Auto-mess timer if (g_auto_mess_interval > 0) { g_mess_elapsed += TIMER_TICK; if (g_mess_elapsed >= g_auto_mess_interval) { g_mess_elapsed = 0; applyMessing(1); // applyMessing calls saveState/webPost internally } } // If neither timer fired, still persist elapsed counters periodically if ((g_auto_wet_interval == 0 || g_elapsed_timer != 0) && (g_auto_mess_interval == 0 || g_mess_elapsed != 0)) { saveState(); } // Web heartbeat timer if (g_web_interval > 0) { g_web_elapsed += TIMER_TICK; if (g_web_elapsed >= g_web_interval) { g_web_elapsed = 0; webPost(""); } } } 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); llMessageLinked(LINK_ROOT, LM_CHANNEL, "LOAD_HUD_UUID", NULL_KEY); llMessageLinked(LINK_ROOT, LM_CHANNEL, "LOAD_WEBSITE_URL", 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 == 31 && !g_initialised) finishInit(); } else if (cmd == "HUD_UUID") { g_hud_uuid = llList2String(parts, 1); initHudUuid(); g_lists_loaded = g_lists_loaded | 8; if (g_lists_loaded == 31 && !g_initialised) finishInit(); } else if (cmd == "WEBSITE_URL") { g_website_url = llList2String(parts, 1); g_lists_loaded = g_lists_loaded | 16; if (g_lists_loaded == 31 && !g_initialised) finishInit(); } else if (cmd == "WEB_RELAY_URL") { g_sim_url = llList2String(parts, 1); webPost(""); } else if (cmd == "WEB_CMD") { string wcmd = llList2String(parts, 1); if (wcmd == "wet") { applyWetting(1); } else if (wcmd == "mess") { applyMessing(1); } else if (wcmd == "change") { changeDiaper(NULL_KEY); } else if (wcmd == "change_lock") { g_change_locked = (integer)llList2String(parts, 2); llMessageLinked(LINK_ROOT, LM_CHANNEL, "SAVE_STATE|change_locked|" + (string)g_change_locked, NULL_KEY); webPost(""); } else if (wcmd == "hud_lock") { g_removal_locked = (integer)llList2String(parts, 2); saveState(); if (g_removal_locked == 1) { rlvLock(); llInstantMessage(llGetOwner(), "[HappyBumsPlus] Your " + g_owner_title + " has locked your HUD remotely. You cannot remove it."); } else { rlvRelease(); llInstantMessage(llGetOwner(), "[HappyBumsPlus] Your " + g_owner_title + " has unlocked your HUD remotely."); } webPost(""); } else if (wcmd == "autowet") { g_auto_wet_interval = (integer)llList2String(parts, 2); g_elapsed_timer = 0; llMessageLinked(LINK_ROOT, LM_CHANNEL, "SAVE_CFG|auto_wet_interval|" + (string)g_auto_wet_interval, NULL_KEY); webPost(""); } else if (wcmd == "automess") { g_auto_mess_interval = (integer)llList2String(parts, 2); g_mess_elapsed = 0; llMessageLinked(LINK_ROOT, LM_CHANNEL, "SAVE_CFG|auto_mess_interval|" + (string)g_auto_mess_interval, NULL_KEY); webPost(""); } else if (wcmd == "msg") { string text = llList2String(parts, 2); llInstantMessage(llGetOwner(), "[WebRelay] " + text); } else if (wcmd == "announce_full") { g_announce_full = (integer)llList2String(parts, 2); saveCfg("announce_full", (string)g_announce_full); } else if (wcmd == "announce_leak") { g_announce_leak = (integer)llList2String(parts, 2); saveCfg("announce_leak", (string)g_announce_leak); } else if (wcmd == "announce_change") { g_announce_change = (integer)llList2String(parts, 2); saveCfg("announce_change", (string)g_announce_change); } else if (wcmd == "notify_wearer") { g_notify_wearer = (integer)llList2String(parts, 2); saveCfg("notify_wearer", (string)g_notify_wearer); } else if (wcmd == "owner_title") { string t = llList2String(parts, 2); if (t != "") { g_owner_title = t; saveCfg("owner_title", g_owner_title); } } else if (wcmd == "state_names") { string raw = llList2String(parts, 2); list names = llCSV2List(raw); if (llGetListLength(names) == 5) { g_state_names = names; saveCfg("state_names", raw); } } else if (wcmd == "mess_names") { string raw = llList2String(parts, 2); list names = llCSV2List(raw); if (llGetListLength(names) == 4) { g_mess_names = names; saveCfg("mess_names", raw); } } } } }