// ============================================================ // [HappyBumsPlus] Persistence v4.3 // Handles all state storage for the Diaper system. // Communicates with [HappyBumsPlus] 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 (root + 20 child prims = 21 prims total): // Link 1 (root) - runtime state // Link 2 (child 1) - settings A1 (timer/announce/access flags) // Link 3 (child 2) - settings A2 (control/identity/limits) // Link 4 (child 3) - settings B (safeword, owner title) // Link 5 (child 4) - settings C (state names only) // Link 6 (child 5) - mess state names (raw CSV) // Link 7 (child 6) - whitelist page 1 (UUIDs 1-3) // Link 8 (child 7) - whitelist page 2 (UUIDs 4-6) // Link 9 (child 8) - whitelist page 3 (UUIDs 7-9) // Link 10 (child 9) - whitelist page 4 (UUIDs 10-12) // Link 11 (child 10) - blacklist page 1 (UUIDs 1-3) // Link 12 (child 11) - blacklist page 2 (UUIDs 4-6) // Link 13 (child 12) - blacklist page 3 (UUIDs 7-9) // Link 14 (child 13) - blacklist page 4 (UUIDs 10-12) // Link 15 (child 14) - RLV owners page 1 (UUIDs 1-3) // Link 16 (child 15) - RLV owners page 2 (UUIDs 4-6) // Link 17 (child 16) - HUD UUID (hu=) // Link 18 (child 17) - Website URL (raw string) // Link 19 (child 18) - spare // Link 20 (child 19) - spare // Link 21 (child 20) - spare // // IMPORTANT: Each prim description field is limited to 128 characters. // UUID lists use pipe separators: max 3 UUIDs per prim // (3 x 36 chars + 2 separators = 110 chars, safely within limit). // // Root prim (link 1) description format: // w=0,cl=0,rl=0,do=1,et=0,m=0,me=0,t=1 // Fields: w=wetness, cl=change_locked, rl=removal_locked, // do=diaper_on, et=elapsed_timer, m=mess, me=mess_elapsed, t=touch_enabled // NOTE: t= is always last for easy manual editing during lockout recovery. // // Settings A1 (link 2) description format: // awi=900,af=0,al=1,ac=0,ach=1,nw=1,cc=0,sc=0,mr=0,now=0,ami=0,mm=0 // Fields: awi=auto_wet_interval, af=announce_full, al=announce_leak, // ac=access_mode, ach=announce_change, nw=notify_wearer, // cc=command_channel, sc=safeword_channel, // mr=min_maturity, now=notify_on_wear, // ami=auto_mess_interval, mm=min_mess // // Settings A2 (link 3) description format: // cp=,mcs=0,chl=-1,chc=-1,wi=180 // Fields: cp=command_prefix, mcs=min_change_state, // chl=change_limit, chc=change_counter, wi=web_interval // // Settings B (link 4) description format: // sw=SAFEWORD,ot=Caregiver // Fields: sw=safeword, ot=owner_title // // Settings C (link 5) description format: // Dry,Damp,Wet,Soaking,Leaking // NOTE: Raw comma-separated state names only, no key= prefix, // to maximise available space for custom names. // // List prim description format (pipe-separated UUIDs, max 3 per prim): // uuid1|uuid2|uuid3 // // Message protocol: // SAVE_ALL|k=v|... -> encodes and writes runtime state to link 1 // SAVE_CFG|key=value -> updates single setting in correct child prim // SAVE_LIST|which|uuid|. -> writes list across correct child prims // LOAD_SETTINGS -> reads t= from link 1, replies SETTINGS|touch_enabled=n // LOAD_CFG -> reads all settings prims, replies CFG|key=val|... // LOAD_ALL -> reads link 1, replies LOADED|k=v|... // LOAD_LIST|which -> reads list prims, replies LIST|which|uuid|... // // Changes v4.1+v4.2: // - Bumped to v4.2 to match Core // Changes v4.0: // - Full storage refactor for 21-prim linkset (root + 20 children). // - Prim description 128-char limit properly respected: max 3 UUIDs per list prim. // - Settings A split into A1 (link 2) and A2 (link 3) to prevent overflow. // - Settings B (link 4) now holds only sw and ot (no state names). // - Settings C (link 5) holds state names only as raw CSV (no key= prefix). // - Whitelist expanded to 4 prims (links 7-10), max 12 UUIDs. // - Blacklist expanded to 4 prims (links 11-14), max 12 UUIDs. // - RLV owners expanded to 2 prims (links 15-16), max 6 UUIDs. // - Legacy ro= field removed from root prim (RLV owners now in list prims). // - Links 6, 17-21 reserved as spare for future use. // Changes v3.2: // - NO changes - bumped to match Core. // Changes v3.1: // - Settings split across link 2 (part A) and link 11 (part B) to fix // 128-character truncation issue. // Changes v3.0: // - Implement min status level to allow third parties and owner from changing diaper. // Changes v2.9: // - Changes for notify on wear and checking for lock on re-wear/login. // 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.0: // - Complete rewrite. llLinksetData replaced with prim description storage. // ============================================================ integer LM_CHANNEL = 1001; integer LINK_MESS_NAMES = 6; // mess state names (raw CSV) integer LINK_SETTINGS_A1 = 2; // timer/announce/access flags integer LINK_SETTINGS_A2 = 3; // control/identity/limits integer LINK_SETTINGS_B = 4; // safeword, owner title integer LINK_SETTINGS_C = 5; // state names (raw CSV) integer LINK_HUD_UUID = 17; integer LINK_WEBSITE_URL = 18; 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); } // ============================================================ // Generic compact key=value parser (comma-separated, no commas in values) // ============================================================ 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; } // ============================================================ // Runtime state encode/decode (link 1) // Format: w=0,cl=0,rl=0,do=1,et=0,t=1 // NOTE: ro= (legacy rlv_owner) removed - owners now stored in list prims. // ============================================================ string buildRuntimeDesc(list parts) { // Read existing desc to preserve all current values 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 t = getField(cur, "t", "1"); string m = getField(cur, "m", "0"); string me = getField(cur, "me", "0"); 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 == "mess") m = v; else if (k == "mess_elapsed") me = v; @next_r; } return "w=" + w + ",cl=" + cl + ",rl=" + rl + ",do=" + doo + ",et=" + et + ",m=" + m + ",me=" + me + ",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") + "|mess=" + getField(cur, "m", "0") + "|mess_elapsed=" + getField(cur, "me", "0"); } // ============================================================ // Settings A1 - link 2 (timer/announce/access flags) // Format: awi=1800,af=0,al=1,ac=0,ach=1,nw=1,cc=0,sc=0,mr=0,now=0 // ============================================================ string updateSettingA1(string desc, string fkey, string fvalue) { list pairs = parseCompact(desc); string awi = getField(pairs, "awi", "1800"); string af = getField(pairs, "af", "0"); string al = getField(pairs, "al", "1"); string ac = getField(pairs, "ac", "0"); string ach = getField(pairs, "ach", "1"); string nw = getField(pairs, "nw", "1"); string cc = getField(pairs, "cc", "0"); string sc = getField(pairs, "sc", "0"); string mr = getField(pairs, "mr", "0"); string now = getField(pairs, "now", "0"); string ami = getField(pairs, "ami", "0"); string mm = getField(pairs, "mm", "0"); if (fkey == "auto_wet_interval") awi = fvalue; else if (fkey == "announce_full") af = fvalue; else if (fkey == "announce_leak") al = fvalue; else if (fkey == "access_mode") ac = 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 == "safeword_channel") sc = fvalue; else if (fkey == "min_maturity") mr = fvalue; else if (fkey == "notify_on_wear") now = fvalue; else if (fkey == "auto_mess_interval") ami = fvalue; else if (fkey == "min_mess") mm = fvalue; return "awi=" + awi + ",af=" + af + ",al=" + al + ",ac=" + ac + ",ach=" + ach + ",nw=" + nw + ",cc=" + cc + ",sc=" + sc + ",mr=" + mr + ",now=" + now + ",ami=" + ami + ",mm=" + mm; } // ============================================================ // Settings A2 - link 3 (control/identity/limits) // Format: cp=,mcs=0,chl=-1,chc=-1 // ============================================================ string updateSettingA2(string desc, string fkey, string fvalue) { list pairs = parseCompact(desc); string cp = getField(pairs, "cp", ""); string mcs = getField(pairs, "mcs", "0"); string chl = getField(pairs, "chl", "-1"); string chc = getField(pairs, "chc", "-1"); string wi = getField(pairs, "wi", "180"); if (fkey == "command_prefix") cp = fvalue; else if (fkey == "min_change_state") mcs = fvalue; else if (fkey == "change_limit") chl = fvalue; else if (fkey == "change_counter") chc = fvalue; else if (fkey == "web_interval") wi = fvalue; return "cp=" + cp + ",mcs=" + mcs + ",chl=" + chl + ",chc=" + chc + ",wi=" + wi; } // ============================================================ // Settings B - link 4 (safeword, owner title) // Format: sw=SAFEWORD,ot=Caregiver // ============================================================ string updateSettingB(string desc, string fkey, string fvalue) { list pairs = parseCompact(desc); string sw = getField(pairs, "sw", "SAFEWORD"); string ot = getField(pairs, "ot", "Caregiver"); if (fkey == "safeword") sw = fvalue; else if (fkey == "owner_title") ot = fvalue; return "sw=" + sw + ",ot=" + ot; } // ============================================================ // Settings C - link 5 (state names, raw CSV) // Format: Dry,Damp,Wet,Soaking,Leaking // Stored as raw CSV with no key= prefix to maximise space for custom names. // ============================================================ string updateSettingC(string fvalue) { // fvalue is the full CSV string of 5 state names return fvalue; } // ============================================================ // Route a SAVE_CFG key to the correct settings prim // Returns the link number for the prim that owns this key, // or 0 if it is handled specially (touch_enabled -> root prim). // ============================================================ integer settingPrimForKey(string fkey) { // touch_enabled lives in root prim if (fkey == "touch_enabled") return 0; // Settings C if (fkey == "state_names") return LINK_SETTINGS_C; if (fkey == "mess_names") return LINK_MESS_NAMES; // Settings B if (fkey == "safeword" || fkey == "owner_title") return LINK_SETTINGS_B; // Settings A2 if (fkey == "command_prefix" || fkey == "min_change_state" || fkey == "change_limit" || fkey == "change_counter" || fkey == "web_interval") return LINK_SETTINGS_A2; // Everything else -> Settings A1 return LINK_SETTINGS_A1; } // ============================================================ // Build the full CFG reply from all settings prims // ============================================================ string buildCfgReply() { string descA1 = getPrimDesc(LINK_SETTINGS_A1); string descA2 = getPrimDesc(LINK_SETTINGS_A2); string descB = getPrimDesc(LINK_SETTINGS_B); string descC = getPrimDesc(LINK_SETTINGS_C); // A1 fields list pA1 = parseCompact(descA1); string awi = getField(pA1, "awi", "1800"); string af = getField(pA1, "af", "0"); string al = getField(pA1, "al", "1"); string ac = getField(pA1, "ac", "0"); string ach = getField(pA1, "ach", "1"); string nw = getField(pA1, "nw", "1"); string cc = getField(pA1, "cc", "0"); string sc = getField(pA1, "sc", "0"); string mr = getField(pA1, "mr", "0"); string now = getField(pA1, "now", "0"); string ami = getField(pA1, "ami", "0"); string mm = getField(pA1, "mm", "0"); // A2 fields list pA2 = parseCompact(descA2); string cp = getField(pA2, "cp", ""); string mcs = getField(pA2, "mcs", "0"); string chl = getField(pA2, "chl", "-1"); string chc = getField(pA2, "chc", "-1"); string wi = getField(pA2, "wi", "180"); // B fields list pB = parseCompact(descB); string sw = getField(pB, "sw", "SAFEWORD"); string ot = getField(pB, "ot", "Caregiver"); // C field - raw CSV state names string sn = descC; if (sn == "") sn = "Dry,Damp,Wet,Soaking,Leaking"; string mn = getPrimDesc(LINK_MESS_NAMES); if (mn == "") mn = "Clean,Soiled,Messy,Blowout"; // 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 + "|access_mode=" + ac + "|announce_change=" + ach + "|notify_wearer=" + nw + "|touch_enabled=" + te + "|command_channel=" + cc + "|safeword_channel=" + sc + "|min_maturity=" + mr + "|notify_on_wear=" + now + "|auto_mess_interval=" + ami + "|min_mess=" + mm + "|min_change_state=" + mcs + "|change_limit=" + chl + "|change_counter=" + chc + "|safeword=" + sw + "|owner_title=" + ot + "|state_names=" + sn + "|mess_names=" + mn + "|web_interval=" + wi; // Only include command_prefix if non-empty (Core auto-generates if not set) if (cp != "") out += "|command_prefix=" + cp; return out; } saveHudUuid(string uuid) { setPrimDesc(LINK_HUD_UUID, "hu=" + uuid); } string loadHudUuid() { string desc = getPrimDesc(LINK_HUD_UUID); if (desc == "") return ""; list pairs = parseCompact(desc); return getField(pairs, "hu", ""); } saveWebsiteUrl(string url) { setPrimDesc(LINK_WEBSITE_URL, url); } string loadWebsiteUrl() { return getPrimDesc(LINK_WEBSITE_URL); } // ============================================================ // Access list encode/decode // Links 7-10: whitelist (4 prims, 3 UUIDs each, max 12) // Links 11-14: blacklist (4 prims, 3 UUIDs each, max 12) // Links 15-16: rlv_owners (2 prims, 3 UUIDs each, max 6) // ============================================================ integer listBaseLink(string which) { if (which == "whitelist") return 7; if (which == "rlv_owners") return 15; return 11; // blacklist } integer listPageCount(string which) { if (which == "rlv_owners") return 2; return 4; // whitelist and blacklist } saveList(string which, list parts) { 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 pages = listPageCount(which); integer prim; for (prim = 0; prim < pages; prim++) { string desc = ""; integer j; for (j = 0; j < 3; j++) // max 3 UUIDs per prim { integer idx = prim * 3 + j; if (idx < llGetListLength(uuids)) { if (desc != "") desc += "|"; desc += llList2String(uuids, idx); } } setPrimDesc(base + prim, desc); } } string buildListReply(string which) { integer base = listBaseLink(which); integer pages = listPageCount(which); string out = "LIST|" + which; integer prim; for (prim = 0; prim < pages; 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_HUD_UUID") { saveHudUuid(llList2String(parts, 1)); } else if (cmd == "LOAD_HUD_UUID") { string uuid = loadHudUuid(); llMessageLinked(LINK_ROOT, LM_CHANNEL, "HUD_UUID|" + uuid, NULL_KEY); } else if (cmd == "SAVE_WEBSITE_URL") { saveWebsiteUrl(llList2String(parts, 1)); } else if (cmd == "LOAD_WEBSITE_URL") { string url = loadWebsiteUrl(); llMessageLinked(LINK_ROOT, LM_CHANNEL, "WEBSITE_URL|" + url, NULL_KEY); } else if (cmd == "SAVE_CFG") { // SAVE_CFG|key=value — route to correct settings prim 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); integer target = settingPrimForKey(k); if (target == 0) { // touch_enabled -> update t= in root prim 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 m = getField(cur, "m", "0"); string me = getField(cur, "me", "0"); setPrimDesc(LINK_ROOT, "w=" + w + ",cl=" + cl + ",rl=" + rl + ",do=" + doo + ",et=" + et + ",m=" + m + ",me=" + me + ",t=" + v); } else if (target == LINK_SETTINGS_A1) { string cur = getPrimDesc(LINK_SETTINGS_A1); setPrimDesc(LINK_SETTINGS_A1, updateSettingA1(cur, k, v)); } else if (target == LINK_SETTINGS_A2) { string cur = getPrimDesc(LINK_SETTINGS_A2); setPrimDesc(LINK_SETTINGS_A2, updateSettingA2(cur, k, v)); } else if (target == LINK_SETTINGS_B) { string cur = getPrimDesc(LINK_SETTINGS_B); setPrimDesc(LINK_SETTINGS_B, updateSettingB(cur, k, v)); } else if (target == LINK_SETTINGS_C) { setPrimDesc(LINK_SETTINGS_C, updateSettingC(v)); } else if (target == LINK_MESS_NAMES) { setPrimDesc(LINK_MESS_NAMES, v); } } else if (cmd == "SAVE_LIST") { saveList(llList2String(parts, 1), parts); } else if (cmd == "LOAD_SETTINGS") { 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; string test = getPrimDesc(LINK_ROOT); if (test == "") llOwnerSay("[HappyBumsPlus] 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); } }