// ============================================================ // [Diaper] Persistence v1.8 // Handles all state storage for the Diaper system. // Communicates with [Diaper] Core via llMessageLinked on channel 1001. // // Message protocol: // SAVE_ALL|k1=v1|k2=v2 -> saves runtime state pairs to llLinksetData // SAVE_CFG|key=value -> saves a single setting to llLinksetData (cfg_ prefix) // SAVE_LIST|which|uuid|... -> saves whitelist or blacklist to llLinksetData // LOAD_SETTINGS -> reads .settings notecard (touch_enabled only), // replies SETTINGS|key=val|... // LOAD_CFG -> reads all cfg_ keys from llLinksetData, // replies CFG|key=val|... // LOAD_ALL -> reads runtime state, replies LOADED|k1=v1|... // LOAD_LIST|whitelist -> reads list from llLinksetData, // replies LIST|whitelist|uuid|... // LOAD_LIST|blacklist -> reads list from llLinksetData, // replies LIST|blacklist|uuid|... // // Storage layout in llLinksetData: // Runtime state keys: wetness, change_locked, removal_locked, // elapsed_timer, diaper_on, rlv_owner // Settings keys: cfg_auto_wet_interval, cfg_announce_full, // cfg_announce_leak, cfg_notify_wearer, // cfg_touch_enabled, cfg_command_channel, // cfg_command_prefix, cfg_safeword, // cfg_safeword_channel, cfg_owner_title, // cfg_state_names // Access lists: list_whitelist, list_blacklist // (pipe-separated UUIDs in a single key) // // Changes v1.8: // - Removed SAVE_SETTINGS (notecard rebuild) entirely. // - Removed ADD_TO_LIST (notecard write) entirely. // - Removed all notecard-mode (osSetNotecard) code paths - dead code cleanup. // - Removed notecard read for whitelist/blacklist - lists now live entirely // in llLinksetData (list_whitelist, list_blacklist keys). // - Added SAVE_CFG handler: writes a single cfg_ key to llLinksetData. // - Added LOAD_CFG handler: reads all cfg_ keys, replies CFG|key=val|... // - Added SAVE_LIST handler: writes pipe-separated list to llLinksetData. // - LOAD_LIST now reads from llLinksetData instead of notecard. // - LOAD_SETTINGS still reads .settings notecard (touch_enabled emergency // recovery), but no longer reads whitelist/blacklist or other settings. // - detectPersistMode() simplified: llLinksetData or none, no notecard path. // - Startup sequence simplified: no pre-load phase needed, 0.5s timer // delay is the only startup mechanism. // - Notecard reading infrastructure (startReadNotecard, processNotecard, // g_notecard_*, dataserver handler) retained but now used ONLY for // .settings notecard. All other reads removed. // FIX v1.2 (retained for reference): // - Deferred PERSIST_MODE until after .status pre-load completes (race fix) // - Pending-command queue so requests during a notecard read are not lost // - Startup retry timer so PERSIST_MODE isn't lost if Core hasn't started yet // ============================================================ integer LM_CHANNEL = 1001; integer g_startup_done = FALSE; // Notecard reading state (used only for .settings) integer g_reading_notecard = FALSE; string g_notecard_name = ""; integer g_notecard_line = 0; key g_notecard_query = NULL_KEY; list g_notecard_lines = []; // Pending command queue - holds commands that arrive during a notecard read list g_pending = []; // ============================================================ // llLinksetData helpers // ============================================================ saveLD(string k, string v) { llLinksetDataWrite(k, v); } string loadLD(string k, string def) { string v = llLinksetDataRead(k); if (v == "") return def; return v; } // ============================================================ // Build LOADED reply (runtime state) // ============================================================ string buildLoadedReply() { list keys = ["wetness","change_locked","removal_locked","elapsed_timer","diaper_on","rlv_owner"]; list defaults = ["0", "0", "0", "0", "1", ""]; string out = "LOADED"; integer i; for (i = 0; i < llGetListLength(keys); i++) out += "|" + llList2String(keys,i) + "=" + loadLD(llList2String(keys,i), llList2String(defaults,i)); return out; } // ============================================================ // Build CFG reply (all settings from llLinksetData) // ============================================================ string buildCfgReply() { // cfg_ keys with their hardcoded defaults (used on first run before any // in-world changes have been saved) list keys = ["auto_wet_interval","announce_full","announce_leak", "notify_wearer","touch_enabled","command_channel", "command_prefix","safeword","safeword_channel", "owner_title","state_names"]; list defaults = ["1800","0","1", "1","1","0", "","SAFEWORD","0", "Daddy","Dry,Damp,Wet,Soaking,Leaking"]; string out = "CFG"; integer i; for (i = 0; i < llGetListLength(keys); i++) { string k = llList2String(keys, i); string v = loadLD("cfg_" + k, llList2String(defaults, i)); // Only include keys that have a value (skip empties like command_prefix // so Core auto-generates from display name as intended) if (v != "") out += "|" + k + "=" + v; } return out; } // ============================================================ // Notecard reading (used only for .settings) // ============================================================ startReadNotecard(string name) { if (llGetInventoryType(name) != INVENTORY_NOTECARD) { // No .settings notecard - that's fine, send empty SETTINGS reply llMessageLinked(LINK_ROOT, LM_CHANNEL, "SETTINGS", NULL_KEY); drainPending(); return; } g_reading_notecard = TRUE; g_notecard_name = name; g_notecard_line = 0; g_notecard_lines = []; g_notecard_query = llGetNotecardLine(name, 0); } // ============================================================ // Process completed .settings notecard read // ============================================================ processNotecard() { string out = "SETTINGS"; integer i; for (i = 0; i < llGetListLength(g_notecard_lines); i++) { string line = llStringTrim(llList2String(g_notecard_lines, i), STRING_TRIM); if (line != "" && llGetSubString(line, 0, 0) != "#") out += "|" + line; } llMessageLinked(LINK_ROOT, LM_CHANNEL, out, NULL_KEY); drainPending(); } // ============================================================ // Send PERSIST_MODE to Core // ============================================================ sendPersistMode() { if (g_startup_done) return; g_startup_done = TRUE; llSetTimerEvent(0); llMessageLinked(LINK_ROOT, LM_CHANNEL, "PERSIST_MODE|linksetdata", NULL_KEY); } // ============================================================ // Pending command queue // ============================================================ queueCommand(string msg) { g_pending += [msg]; } drainPending() { while (llGetListLength(g_pending) > 0) { string msg = llList2String(g_pending, 0); g_pending = llDeleteSubList(g_pending, 0, 0); handleCommand(msg); } } // ============================================================ // Process a single command message // ============================================================ handleCommand(string msg) { list parts = llParseString2List(msg, ["|"], []); string cmd = llList2String(parts, 0); if (cmd == "SAVE_ALL") { // Save runtime state key/value pairs integer i; for (i = 1; i < llGetListLength(parts); i++) { string pair = llList2String(parts, i); integer eq = llSubStringIndex(pair, "="); if (eq > 0) saveLD(llGetSubString(pair, 0, eq-1), llGetSubString(pair, eq+1, -1)); } } else if (cmd == "SAVE_CFG") { // Save a single setting: SAVE_CFG|key=value string pair = llList2String(parts, 1); integer eq = llSubStringIndex(pair, "="); if (eq > 0) saveLD("cfg_" + llGetSubString(pair, 0, eq-1), llGetSubString(pair, eq+1, -1)); // Note: if value is empty (eq is last char) we write "" which is fine - // loadLD will return the default anyway. } else if (cmd == "SAVE_LIST") { // Save access list: SAVE_LIST|which|uuid|uuid|... string which = llList2String(parts, 1); string ldValue = ""; integer i; for (i = 2; i < llGetListLength(parts); i++) { string u = llStringTrim(llList2String(parts, i), STRING_TRIM); if (u != "") { if (ldValue != "") ldValue += "|"; ldValue += u; } } saveLD("list_" + which, ldValue); } else if (cmd == "LOAD_SETTINGS") { // Read .settings notecard (touch_enabled emergency recovery only) startReadNotecard(".settings"); return; // don't drainPending - mid notecard read } else if (cmd == "LOAD_CFG") { // Return all settings from llLinksetData llMessageLinked(LINK_ROOT, LM_CHANNEL, buildCfgReply(), NULL_KEY); } else if (cmd == "LOAD_ALL") { llMessageLinked(LINK_ROOT, LM_CHANNEL, buildLoadedReply(), NULL_KEY); } else if (cmd == "LOAD_LIST") { // Read access list from llLinksetData string which = llList2String(parts, 1); string stored = loadLD("list_" + which, ""); string out = "LIST|" + which; if (stored != "") { list uuids = llParseString2List(stored, ["|"], []); integer i; for (i = 0; i < llGetListLength(uuids); i++) out += "|" + llList2String(uuids, i); } llMessageLinked(LINK_ROOT, LM_CHANNEL, out, NULL_KEY); } } // ============================================================ // Default state // ============================================================ default { state_entry() { g_startup_done = FALSE; g_reading_notecard = FALSE; g_pending = []; // Verify llLinksetData is available integer result = llLinksetDataWrite("__test__", "1"); if (result == 0) { string val = llLinksetDataRead("__test__"); llLinksetDataDelete("__test__"); if (val != "1") { llOwnerSay("[Diaper] FATAL: llLinksetData read/write test failed. State will not persist."); // Continue anyway - Core will still function for the session } } else { llOwnerSay("[Diaper] FATAL: llLinksetData not available on this grid. State will not persist."); } // Small delay so Core's state_entry completes before we send PERSIST_MODE llSetTimerEvent(0.5); } on_rez(integer param) { llResetScript(); } attach(key id) { if (id != NULL_KEY) llResetScript(); } timer() { llSetTimerEvent(0); sendPersistMode(); } link_message(integer sender, integer num, string msg, key id) { if (num != LM_CHANNEL) return; // If a notecard read is in progress, queue incoming commands. // SAVE_ALL and SAVE_CFG are safe to process immediately (no I/O). if (g_reading_notecard) { string cmd = llList2String(llParseString2List(msg, ["|"], []), 0); if (cmd == "SAVE_ALL" || cmd == "SAVE_CFG" || cmd == "SAVE_LIST") handleCommand(msg); else queueCommand(msg); return; } handleCommand(msg); } dataserver(key query_id, string data) { if (!g_reading_notecard) return; if (query_id != g_notecard_query) return; if (data == EOF) { g_reading_notecard = FALSE; processNotecard(); return; } g_notecard_lines += [data]; g_notecard_line++; g_notecard_query = llGetNotecardLine(g_notecard_name, g_notecard_line); } }