// ============================================================ // [Diaper] Persistence v2.8 // Handles all state storage for the Diaper system. // Communicates with [Diaper] Core via llMessageLinked on channel 1001. // // Storage backend: prim descriptions (hypergrid-safe) // State survives hypergrid travel as prim descriptions are part of // the asset. llLinksetData is NOT used. // // Linkset layout: // Link 1 (root) - runtime state // Link 2 (child 1) - settings // Link 3 (child 2) - whitelist page 1 (UUIDs 1-6) // Link 4 (child 3) - whitelist page 2 (UUIDs 7-12) // Link 5 (child 4) - whitelist page 3 (UUIDs 13-18) // Link 6 (child 5) - blacklist page 1 (UUIDs 1-6) // Link 7 (child 6) - blacklist page 2 (UUIDs 7-12) // Link 8 (child 7) - blacklist page 3 (UUIDs 13-18) // Link 9 (child 8) - RLV owner list (max 6 UUIDs) // Link 10 (child 9) - RLV configuration // Link 11 (child 10) - reserved for future RLV use // // Root prim description format: // w=2,cl=1,rl=0,do=1,et=900,ro=,t=1 // // awi=1800,af=0,al=1,ac=0,ach=1,nw=1,cc=0,cp=jsdiaper,sw=SAFEWORD,sc=0,ot=Daddy,mr=0,sn=Dry,Damp,Wet,Soaking,Leaking // // List prim description format (pipe-separated UUIDs, up to 6 per prim): // uuid1|uuid2|uuid3|uuid4|uuid5|uuid6 // // Message protocol: // SAVE_ALL|k=v|... -> encodes and writes runtime state to link 1 // SAVE_CFG|key=value -> updates single setting in link 2 // SAVE_LIST|which|uuid|. -> writes list across 3 child prims // LOAD_SETTINGS -> reads t= from link 1, replies SETTINGS|touch_enabled=n // LOAD_CFG -> reads link 2, replies CFG|key=val|... // LOAD_ALL -> reads link 1, replies LOADED|k=v|... // LOAD_LIST|which -> reads 3 list prims, replies LIST|which|uuid|... // // Changes v2.8: // - Initial addition of RLV functions. // Changes v2.7: // - Added some config changes for min-maturity, change announce and access mode. // Changes v2.6: // - No changes made - just bumped version number to match Core // Changes v2.5: // - Changes for minimum region maturity. // Changes v2.4: // - No changes made - just bumped version number to match Core // Changes v2.3: // - No changes made - just bumped version number to match Core // Changes v2.1: // - No changes - bumping version number so it matches with core v2.1 // Changes v2.0: // - Complete rewrite. llLinksetData replaced with prim description storage. // - All notecard reading infrastructure removed. // - LOAD_SETTINGS now reads t= field from root prim description. // - LOAD_CFG reads child prim 1 (link 2) description. // - LOAD_ALL reads root prim (link 1) description. // - LOAD_LIST reads 3 child prims per list. // - SAVE_ALL encodes full runtime state as compact key=value string. // - SAVE_CFG does read/modify/write on child prim 1. // - SAVE_LIST encodes up to 18 UUIDs across 3 child prims (6 each). // - Startup sequence unchanged: 0.5s timer delay then PERSIST_MODE|primdesc. // - No pending queue needed (all operations are synchronous prim reads/writes). // ============================================================ integer LM_CHANNEL = 1001; integer g_startup_done = FALSE; // ============================================================ // Prim description helpers // ============================================================ setPrimDesc(integer link, string data) { llSetLinkPrimitiveParamsFast(link, [PRIM_DESC, data]); } string getPrimDesc(integer link) { return llList2String(llGetLinkPrimitiveParams(link, [PRIM_DESC]), 0); } // ============================================================ // Runtime state encode/decode (link 1) // Format: w=0,cl=0,rl=0,do=1,et=0,ro=,t=1 // ============================================================ // Parse a compact comma-separated key=value string into a list of "key=value" pairs list parseCompact(string data) { return llParseString2List(data, [","], []); } string getField(list pairs, string fkey, string fdefault) { integer i; for (i = 0; i < llGetListLength(pairs); i++) { string pair = llList2String(pairs, i); integer eq = llSubStringIndex(pair, "="); if (eq > 0) { if (llGetSubString(pair, 0, eq-1) == fkey) { if (eq >= llStringLength(pair) - 1) return ""; return llGetSubString(pair, eq+1, -1); } } } return fdefault; } string buildRuntimeDesc(list parts) { // parts is the pipe-split SAVE_ALL message (parts[0]="SAVE_ALL", rest are key=value) // Read existing desc so we preserve t= (touch_enabled) which is also in root string existing = getPrimDesc(LINK_ROOT); list cur = parseCompact(existing); string w = getField(cur, "w", "0"); string cl = getField(cur, "cl", "0"); string rl = getField(cur, "rl", "0"); string doo = getField(cur, "do", "1"); string et = getField(cur, "et", "0"); string ro = getField(cur, "ro", ""); string t = getField(cur, "t", "1"); integer i; for (i = 1; i < llGetListLength(parts); i++) { string pair = llList2String(parts, i); integer eq = llSubStringIndex(pair, "="); if (eq < 1) jump next_r; 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") w = v; else if (k == "change_locked") cl = v; else if (k == "removal_locked") rl = v; else if (k == "diaper_on") doo = v; else if (k == "elapsed_timer") et = v; else if (k == "rlv_owner") ro = v; @next_r; } return "w=" + w + ",cl=" + cl + ",rl=" + rl + ",do=" + doo + ",et=" + et + ",ro=" + ro + ",t=" + t; } string buildLoadedReply() { list cur = parseCompact(getPrimDesc(LINK_ROOT)); return "LOADED" + "|wetness=" + getField(cur, "w", "0") + "|change_locked=" + getField(cur, "cl", "0") + "|removal_locked=" + getField(cur, "rl", "0") + "|elapsed_timer=" + getField(cur, "et", "0") + "|diaper_on=" + getField(cur, "do", "1") + "|rlv_owner=" + getField(cur, "ro", ""); } // ============================================================ // Settings encode/decode (link 2) // Format: awi=1800,af=0,al=1,nw=1,cc=0,cp=,sw=SAFEWORD,sc=0,ot=Daddy,sn=Dry,Damp,Wet,Soaking,Leaking // NOTE: sn (state_names) uses commas internally, so it must be the LAST field. // ============================================================ // State names field contains commas so we can't use parseCompact for the whole string. // We split off everything after "sn=" manually. string getSettingField(string desc, string fkey, string fdefault) { // Special handling for sn= (last field, may contain commas) if (fkey == "sn") { integer snPos = llSubStringIndex(desc, ",sn="); if (snPos == -1) { // Maybe it starts with sn= (unlikely but safe) if (llGetSubString(desc, 0, 2) == "sn=") return llGetSubString(desc, 3, -1); return fdefault; } return llGetSubString(desc, snPos + 4, -1); } // For all other fields, parse the portion before ",sn=" to avoid comma collision integer snCut = llSubStringIndex(desc, ",sn="); string safe = desc; if (snCut != -1) safe = llGetSubString(desc, 0, snCut - 1); list pairs = parseCompact(safe); return getField(pairs, fkey, fdefault); } string updateSettingField(string desc, string fkey, string fvalue) { // Rebuild the settings string with one field changed. // We read all fields and reconstruct to keep ordering consistent. string awi = getSettingField(desc, "awi", "1800"); string af = getSettingField(desc, "af", "0"); string al = getSettingField(desc, "al", "1"); string nw = getSettingField(desc, "nw", "1"); string cc = getSettingField(desc, "cc", "0"); string cp = getSettingField(desc, "cp", ""); string sw = getSettingField(desc, "sw", "SAFEWORD"); string sc = getSettingField(desc, "sc", "0"); string ot = getSettingField(desc, "ot", "Daddy"); string sn = getSettingField(desc, "sn", "Dry,Damp,Wet,Soaking,Leaking"); string mr = getSettingField(desc, "mr", "0"); string ac = getSettingField(desc, "ac", "0"); string ach = getSettingField(desc, "ach", "1"); if (fkey == "auto_wet_interval") awi = fvalue; else if (fkey == "announce_full") af = fvalue; else if (fkey == "announce_leak") al = fvalue; else if (fkey == "announce_change") ach = fvalue; else if (fkey == "notify_wearer") nw = fvalue; else if (fkey == "command_channel") cc = fvalue; else if (fkey == "command_prefix") cp = fvalue; else if (fkey == "safeword") sw = fvalue; else if (fkey == "safeword_channel") sc = fvalue; else if (fkey == "owner_title") ot = fvalue; else if (fkey == "state_names") sn = fvalue; else if (fkey == "min_maturity") mr = fvalue; else if (fkey == "access_mode") ac = fvalue; else if (fkey == "touch_enabled") { // touch_enabled lives in root prim, not here - update root instead string rootDesc = getPrimDesc(LINK_ROOT); list cur = parseCompact(rootDesc); string w = getField(cur, "w", "0"); string cl = getField(cur, "cl", "0"); string rl = getField(cur, "rl", "0"); string doo = getField(cur, "do", "1"); string et = getField(cur, "et", "0"); string ro = getField(cur, "ro", ""); setPrimDesc(LINK_ROOT, "w=" + w + ",cl=" + cl + ",rl=" + rl + ",do=" + doo + ",et=" + et + ",ro=" + ro + ",t=" + fvalue); return desc; // settings prim unchanged } return "awi=" + awi + ",af=" + af + ",al=" + al + ",ac=" + ac + ",ach=" + ach + ",nw=" + nw + ",cc=" + cc + ",cp=" + cp + ",sw=" + sw + ",sc=" + sc + ",ot=" + ot + ",mr=" + mr + ",sn=" + sn; } string buildCfgReply() { string desc = getPrimDesc(2); // link 2 = child 1 = settings string awi = getSettingField(desc, "awi", "1800"); string af = getSettingField(desc, "af", "0"); string al = getSettingField(desc, "al", "1"); string nw = getSettingField(desc, "nw", "1"); string cc = getSettingField(desc, "cc", "0"); string cp = getSettingField(desc, "cp", ""); string sw = getSettingField(desc, "sw", "SAFEWORD"); string sc = getSettingField(desc, "sc", "0"); string ot = getSettingField(desc, "ot", "Daddy"); string sn = getSettingField(desc, "sn", "Dry,Damp,Wet,Soaking,Leaking"); string mr = getSettingField(desc, "mr", "0"); string ac = getSettingField(desc, "ac", "0"); string ach = getSettingField(desc, "ach", "1"); // Also read touch_enabled from root prim list rootCur = parseCompact(getPrimDesc(LINK_ROOT)); string te = getField(rootCur, "t", "1"); string out = "CFG" + "|auto_wet_interval=" + awi + "|announce_full=" + af + "|announce_leak=" + al + "|notify_wearer=" + nw + "|touch_enabled=" + te + "|command_channel=" + cc + "|safeword=" + sw + "|safeword_channel=" + sc + "|owner_title=" + ot + "|min_maturity=" + mr + "|state_names=" + sn + "|access_mode=" + ac + "|announce_change=" + ach; // Only include command_prefix if non-empty (so Core auto-generates if not set) if (cp != "") out += "|command_prefix=" + cp; return out; } // ============================================================ // Access list encode/decode (links 3-5 whitelist, 6-8 blacklist) // 6 UUIDs per prim, pipe-separated // ============================================================ integer listBaseLink(string which) { if (which == "whitelist") return 3; if (which == "rlv_owners") return 9; return 6; // blacklist } saveList(string which, list parts) { // parts[0]="SAVE_LIST", parts[1]=which, parts[2..n]=uuids list uuids = []; integer i; for (i = 2; i < llGetListLength(parts); i++) { string u = llStringTrim(llList2String(parts, i), STRING_TRIM); if (u != "") uuids += [u]; } integer base = listBaseLink(which); integer prim; for (prim = 0; prim < 3; prim++) { string desc = ""; integer j; for (j = 0; j < 6; j++) { integer idx = prim * 6 + j; if (idx < llGetListLength(uuids)) { if (desc != "") desc += "|"; desc += llList2String(uuids, idx); } } setPrimDesc(base + prim, desc); } } string buildListReply(string which) { integer base = listBaseLink(which); string out = "LIST|" + which; integer prim; for (prim = 0; prim < 3; prim++) { string desc = getPrimDesc(base + prim); if (desc != "") { list uuids = llParseString2List(desc, ["|"], []); integer j; for (j = 0; j < llGetListLength(uuids); j++) { string u = llStringTrim(llList2String(uuids, j), STRING_TRIM); if (u != "") out += "|" + u; } } } return out; } // ============================================================ // Send PERSIST_MODE to Core // ============================================================ sendPersistMode() { if (g_startup_done) return; g_startup_done = TRUE; llSetTimerEvent(0); llMessageLinked(LINK_ROOT, LM_CHANNEL, "PERSIST_MODE|primdesc", NULL_KEY); } // ============================================================ // Process a single command message // ============================================================ handleCommand(string msg) { list parts = llParseString2List(msg, ["|"], []); string cmd = llList2String(parts, 0); if (cmd == "SAVE_ALL") { setPrimDesc(LINK_ROOT, buildRuntimeDesc(parts)); } else if (cmd == "SAVE_CFG") { // SAVE_CFG|key=value string pair = llList2String(parts, 1); integer eq = llSubStringIndex(pair, "="); if (eq < 1) return; string k = llGetSubString(pair, 0, eq-1); string v; if (eq >= llStringLength(pair) - 1) v = ""; else v = llGetSubString(pair, eq+1, -1); string cur = getPrimDesc(2); setPrimDesc(2, updateSettingField(cur, k, v)); } else if (cmd == "SAVE_LIST") { saveList(llList2String(parts, 1), parts); } else if (cmd == "LOAD_SETTINGS") { // Read touch_enabled from root prim description list cur = parseCompact(getPrimDesc(LINK_ROOT)); string te = getField(cur, "t", "1"); llMessageLinked(LINK_ROOT, LM_CHANNEL, "SETTINGS|touch_enabled=" + te, NULL_KEY); } else if (cmd == "LOAD_CFG") { 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") { llMessageLinked(LINK_ROOT, LM_CHANNEL, buildListReply(llList2String(parts, 1)), NULL_KEY); } } // ============================================================ // Default state // ============================================================ default { state_entry() { g_startup_done = FALSE; // Verify we can read/write prim descriptions string test = getPrimDesc(LINK_ROOT); if (test == "") llOwnerSay("[Diaper] Note: Root prim description is empty. Will initialise on first save."); // 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; handleCommand(msg); } }