#include "peril/Peril_Constants.lsl" debugMsg(string msg) { if (DEBUG_LOGS) llMessageLinked(LINK_SET, MSG_DEBUG_TEXT, msg, NULL_KEY); } // ============================================================================= // PERIL WORLD RANKING - Leaderboard Manager // ============================================================================= // Handles sharded KVP storage (up to 10 keys) and sorted ranking. // Capacity: 500+ Players (Max 10 keys x 50 entries) // ============================================================================= list leaderboardData = []; // Stride 3: [Name, Wins, Losses] integer currentLoadPage = 1; key kvpReadReq; key kvpWriteReq; key kvpLockReq; // Stage 1: Check Lock key kvpClaimReq; // Stage 2: Claim Lock key kvpSentinelReq; // Startup Probe list writeQueue = []; // Pairs of [KeyName, DataString] integer isSaving = FALSE; integer isBusy = FALSE; // Read lock integer displayOffset = 0; // Current ranking page offset (0, 10, 20...) integer retryCount = 0; // Track number of timeouts string currentWriteKey; string currentWriteVal; // --- Lock & Sync State --- list pendingRecords = []; // Stride 2: [Name, Result] integer isLocking = FALSE; string myLockVal = ""; integer isClaiming = FALSE; // Track if we are in the middle of a WRITE claim // Helper: Process the next KVP write in the queue processWriteQueue() { if (llGetListLength(writeQueue) == 0) { isSaving = FALSE; // Batch Processing: Check if more records are queued before releasing lock if (llGetListLength(pendingRecords) > 0) { string nextName = llList2String(pendingRecords, 0); string nextRes = llList2String(pendingRecords, 1); pendingRecords = llDeleteSubList(pendingRecords, 0, 1); dbg("🔄 [Leaderboard] Processing next queued record: " + nextName); updatePlayerRecord(nextName, nextRes); } else { releaseLock(); } return; } isSaving = TRUE; currentWriteKey = llList2String(writeQueue, 0); currentWriteVal = llList2String(writeQueue, 1); writeQueue = llDeleteSubList(writeQueue, 0, 1); kvpWriteReq = llUpdateKeyValue(currentWriteKey, currentWriteVal, FALSE, ""); } // Helper: Release the lock immediately releaseLock() { dbg("🏆 [Leaderboard] Releasing global lock..."); kvpLockReq = llUpdateKeyValue(LB_LOCK_KEY, "OFF", TRUE, myLockVal); } // Convert the whole list into sharded strings and start saving generateShardedData() { integer _shardI; integer _totalCount = llGetListLength(leaderboardData) / 3; writeQueue = []; for (_shardI = 0; _shardI < MAX_LB_PAGES; _shardI++) { integer _start = _shardI * MAX_ENTRIES_PER_PAGE; if (_start >= _totalCount) jump shard_exit; string _serialized = ""; integer _j; for (_j = 0; _j < MAX_ENTRIES_PER_PAGE && (_start + _j) < _totalCount; _j++) { integer _base = (_start + _j) * 3; // NEW FORMAT: Wins,Losses,Name (CSV-like within colon segments) string _entry = (string)llList2Integer(leaderboardData, _base) + "," + (string)llList2Integer(leaderboardData, _base + 1) + "," + llList2String(leaderboardData, _base + 2); if (_serialized != "") _serialized += "|"; _serialized += _entry; } writeQueue += ["Peril_LB_" + (string)(_shardI + 1), _serialized]; } @shard_exit; // If no data to write (reset case), release lock now if (llGetListLength(writeQueue) == 0) { kvpLockReq = llUpdateKeyValue(LB_LOCK_KEY, "", TRUE, myLockVal); } else { processWriteQueue(); } } loadLBPage(integer pageNum) { if ((isBusy || isSaving) && pageNum == 1) { dbg("⏳ [Leaderboard] Sync requested while Busy/Saving. Ignoring duplicate reset."); return; } // ANTI-THROTTLE: Add small 0.2s pause for Mono speed compatibility if (pageNum == 1) llSleep(0.2); else llSleep(1.0); // 1.0s delay for sharded pages isBusy = TRUE; llSetTimerEvent(30.0); // Lag Resistant watchdog currentLoadPage = pageNum; string keyName = LB_KEY_PREFIX + (string)pageNum; kvpReadReq = llReadKeyValue(keyName); if (kvpReadReq == NULL_KEY) { dbg("⚠️ [Leaderboard] KVP Request FAILED (Throttle/NULL_KEY). Retrying " + keyName + " in 2s..."); // KEEP isBusy = TRUE to prevent collisions during retry llSetTimerEvent(2.0); return; } dbg("📡 [Leaderboard] Loading " + keyName + "..."); } list getSortedSlice(integer offset) { if (llGetListLength(leaderboardData) == 0) return []; // In LSL, llListSort sorts by the FIRST element of each stride. // Our data format is [Wins, Losses, Name]. // Sort by Wins (descending). list sorted = llListSort(leaderboardData, 3, FALSE); integer start = offset * 3; return llList2List(sorted, start, start + 29); } updatePlayerRecord(string pName, string pResult) { // Search for Name (index 2 of each 3-stride entry) integer _idx = -1; integer _it; for (_it = 2; _it < llGetListLength(leaderboardData); _it += 3) { if (llList2String(leaderboardData, _it) == pName) { _idx = _it; jump found; } } @found; if (_idx == -1) { if (llGetListLength(leaderboardData) / 3 >= MAX_WORLD_RANKING) return; if (pResult == "WIN") leaderboardData += [1, 0, pName]; else leaderboardData += [0, 1, pName]; } else { integer _updBase = _idx - 2; integer _updVal; if (pResult == "WIN") { _updVal = llList2Integer(leaderboardData, _updBase); leaderboardData = llListReplaceList(leaderboardData, [_updVal + 1], _updBase, _updBase); } else { _updVal = llList2Integer(leaderboardData, _updBase + 1); leaderboardData = llListReplaceList(leaderboardData, [_updVal + 1], _updBase + 1, _updBase + 1); } } // SORT before saving leaderboardData = llListSort(leaderboardData, 3, FALSE); generateShardedData(); } startUpdateSequence(string pName, string pResult) { llSetTimerEvent(0); // GHOST-BUSTER: Kill any pending retries/syncs // Check if record already in queue to avoid duplicates if (llListFindList(pendingRecords, [pName, pResult]) != -1) return; pendingRecords += [pName, pResult]; // Add to Victory Queue if (isBusy || isLocking || isSaving) { dbg("⏳ [Leaderboard] Record for [" + pName + "] added to queue (" + (string)(llGetListLength(pendingRecords)/2) + " pending)."); return; } isLocking = TRUE; dbg("🔐 [Leaderboard] Checking global lock for update..."); kvpLockReq = llReadKeyValue(LB_LOCK_KEY); } default { on_rez(integer _p) { llResetScript(); } state_entry() { REPORT_MEMORY(); pendingRecords = []; dbg("⏳ [Leaderboard] v1.55 (Atomic Endurance) - Handshake link active. Probing Experience..."); // SENTINEL CHECK: Use a dedicated ID to prevent shard-read collisions kvpSentinelReq = llReadKeyValue(LB_KEY_PREFIX + "1"); llSetTimerEvent(10.0); // 10s watchdog for sentinel } dataserver(key _qid, string _qd) { integer _c = llSubStringIndex(_qd, ","); string _st = _qd; string _vl = ""; string _rawVal = ""; if (_c != -1) { _st = llGetSubString(_qd, 0, _c - 1); _rawVal = llGetSubString(_qd, _c + 1, -1); _vl = llStringTrim(_rawVal, STRING_TRIM); } if (_qid == kvpSentinelReq) { llSetTimerEvent(0.0); // Reset watchdog kvpSentinelReq = NULL_KEY; // Kill it once used if (_st == "1" || _st == "3") { // Success (1) or Not Found (3) prove access dbg("✅ [Leaderboard] Experience Sentinel: KVP Access Confirmed. Starting Auto-Sync..."); loadLBPage(1); } else if (_st == "0") { string sentinelErr = _vl; if (_vl == "1") sentinelErr = "Experience Throttled (Error 1)"; else if (_vl == "3") sentinelErr = "Invalid Parameters (Error 3)"; else if (_vl == "17") sentinelErr = "Experience Disabled/Not Permitted (Error 17)"; else sentinelErr = "KVP Error Status: " + _vl; dbg("🚫 [Leaderboard] Experience Sentinel: FATAL " + sentinelErr); llOwnerSay("🚫 [Leaderboard] WORLD RANKING DISABLED: " + sentinelErr); } return; } if (_qid == kvpReadReq) { if (isBusy == FALSE && isLocking == FALSE) return; // Ignore stale reads if (_st == "1") { // SUCCESS: Data found if (_vl != "" && llSubStringIndex(_vl, "Error") == -1) { list _ent = llParseString2List(_vl, ["|"], []); integer _ei; for (_ei = 0; _ei < llGetListLength(_ent); _ei++) { string _rawEntry = llList2String(_ent, _ei); if (llSubStringIndex(_rawEntry, ",") != -1) { // NEW FORMAT: Wins,Losses,Name list _itNew = llParseString2List(_rawEntry, [","], []); if (llGetListLength(_itNew) >= 3) { leaderboardData += [(integer)llList2String(_itNew, 0), (integer)llList2String(_itNew, 1), llList2String(_itNew, 2)]; } } else { // OLD LEGACY FORMAT: Name:Wins:Losses list _itOld = llParseString2List(_rawEntry, [":"], []); if (llGetListLength(_itOld) >= 3) { // Translate to new stride structure in memory leaderboardData += [(integer)llList2String(_itOld, 1), (integer)llList2String(_itOld, 2), llList2String(_itOld, 0)]; } } } } if (isBusy && currentLoadPage < 10) { retryCount = 0; // Reset retries for next page loadLBPage(currentLoadPage + 1); } else { // SYNC COMPLETE (Natural end) llSetTimerEvent(0); dbg("🏆 [Leaderboard] SYNC COMPLETE - " + (string)(llGetListLength(leaderboardData)/3) + " players."); llMessageLinked(LINK_SET, MSG_LB_LOAD_COMPLETE, "", NULL_KEY); llMessageLinked(LINK_SET, MSG_LB_REQUEST_DISPLAY, "0", NULL_KEY); // Show page 1 // Check if any records were queued while we were busy or saving if (llGetListLength(pendingRecords) > 0) { dbg("🔄 [Leaderboard] Processing queued record for [" + llList2String(pendingRecords, 0) + "]..."); string nextName = llList2String(pendingRecords, 0); string nextRes = llList2String(pendingRecords, 1); pendingRecords = llDeleteSubList(pendingRecords, 0, 1); isBusy = FALSE; isSaving = FALSE; updatePlayerRecord(nextName, nextRes); } else { isBusy = FALSE; isSaving = FALSE; } } } else if (_st == "0") { if (_vl == "1") { // XP_ERROR_THROTTLED dbg("⚠️ [Leaderboard] Read throttled. Retrying Shard " + (string)currentLoadPage + " in 2s..."); llSleep(2.0); // RETAIN isBusy/isSaving status during retry loadLBPage(currentLoadPage); } else if (_vl == "14") { // XP_ERROR_KEY_NOT_FOUND (Value is empty) if (currentLoadPage < 10) { loadLBPage(currentLoadPage + 1); } else { // SYNC COMPLETE (Empty trailing shards) llSetTimerEvent(0); dbg("🏆 [Leaderboard] SYNC COMPLETE - " + (string)(llGetListLength(leaderboardData)/3) + " players total."); llMessageLinked(LINK_SET, MSG_LB_LOAD_COMPLETE, "", NULL_KEY); llMessageLinked(LINK_SET, MSG_LB_REQUEST_DISPLAY, "0", NULL_KEY); if (llGetListLength(pendingRecords) > 0) { dbg("🔄 [Leaderboard] Processing queued record after empty shards..."); string nextName = llList2String(pendingRecords, 0); string nextRes = llList2String(pendingRecords, 1); pendingRecords = llDeleteSubList(pendingRecords, 0, 1); isBusy = FALSE; isSaving = FALSE; updatePlayerRecord(nextName, nextRes); } else { isBusy = FALSE; isSaving = FALSE; } } } else { dbg("❌ [Leaderboard] Read Error (Code " + _vl + ") on Shard " + (string)currentLoadPage); isBusy = FALSE; llSetTimerEvent(0); } } else if (_st == "3") { // Not Found isBusy = FALSE; llSetTimerEvent(0); dbg("🏆 [Leaderboard] Shard " + (string)currentLoadPage + " not found."); if (currentLoadPage < 10) loadLBPage(currentLoadPage + 1); } } else if (_qid == kvpWriteReq) { integer _c = llSubStringIndex(_qd, ","); string _st = _qd; string _vl = ""; if (_c != -1) { _st = llGetSubString(_qd, 0, _c - 1); _vl = llGetSubString(_qd, _c + 1, -1); } if (_st == "1") { dbg("✅ [Leaderboard] Write success to " + currentWriteKey); processWriteQueue(); } else if (_st == "0" && _vl == "1") { // XP_ERROR_THROTTLED dbg("⚠️ [Leaderboard] Write throttled. Retrying " + currentWriteKey + " in 2s..."); llSleep(2.0); kvpWriteReq = llUpdateKeyValue(currentWriteKey, currentWriteVal, FALSE, ""); } else { dbg("❌ [Leaderboard] Write Error (" + _st + ":" + _vl + ") on " + currentWriteKey); processWriteQueue(); } } else if (_qid == kvpClaimReq) { kvpClaimReq = NULL_KEY; // DUPLICATE DEFENSE: Prevent zombie processing if (_st == "1") { dbg("🔐 [Leaderboard] LOCK ACQUIRED. Starting fresh sync..."); isClaiming = FALSE; isLocking = FALSE; isSaving = TRUE; leaderboardData = []; loadLBPage(1); } else if (_vl == "15") { // ATOMIC CHECK FAILED dbg("⏳ [Leaderboard] Race conflict (Loss). Yielding and retrying in 5s..."); isClaiming = FALSE; isLocking = TRUE; // Stay in locking mode for retry llSetTimerEvent(5.0); } else { dbg("❌ [Leaderboard] Lock claim failed. Status: " + _st + ":" + _vl); isClaiming = FALSE; isLocking = FALSE; } } else if (_qid == kvpLockReq) { kvpLockReq = NULL_KEY; // DUPLICATE DEFENSE: Prevent zombie processing // --- STAGE 1: Check Result (Success Pulse = 1) --- if (isLocking) { if (_st == "1" && _c != -1) { string _curVal = _rawVal; // USE RAW DATA FOR ATOMIC CHECK integer _isStale = FALSE; list _p = []; integer _ts = 0; if (_curVal != "") { _p = llParseString2List(_curVal, [":"], []); _ts = (integer)llList2String(_p, 1); if (llGetUnixTime() - _ts > LB_LOCK_TIMEOUT) _isStale = TRUE; } if (llList2String(_p, 0) == (string)llGetKey()) { dbg("🔐 [Leaderboard] Lock already held by this board (Self-Sync). Proceeding..."); myLockVal = _curVal; isLocking = FALSE; // Set isBusy before loading to ensure the guard is handled correctly isBusy = TRUE; leaderboardData = []; loadLBPage(1); return; } if (_curVal == "" || _curVal == "OFF" || _isStale) { dbg("🔐 [Leaderboard] Lock available (or stale). Claiming..."); myLockVal = (string)llGetKey() + ":" + (string)llGetUnixTime(); isClaiming = TRUE; kvpClaimReq = llUpdateKeyValue(LB_LOCK_KEY, myLockVal, TRUE, _curVal); } else { dbg("⏳ [Leaderboard] Lock held by board [" + llList2String(_p, 0) + "] for " + (string)(llGetUnixTime() - _ts) + "s. Retrying in 5s..."); llSetTimerEvent(5.0); } } else if (_st == "0" && _vl == "14") { // KEY_NOT_FOUND (Lock key doesn't exist) dbg("🔐 [Leaderboard] Lock doesn't exist. Creating initial lock..."); myLockVal = (string)llGetKey() + ":" + (string)llGetUnixTime(); isClaiming = TRUE; kvpClaimReq = llUpdateKeyValue(LB_LOCK_KEY, myLockVal, FALSE, ""); } else if (_st == "0" && _vl == "1") { // Throttled dbg("⚠️ [Leaderboard] Lock check throttled. Retrying..."); llSleep(2.0); kvpLockReq = llReadKeyValue(LB_LOCK_KEY); } else { dbg("❌ [Leaderboard] Lock logic failed (Code " + _st + ":" + _vl + ")"); isLocking = FALSE; } } else { // RELEASE RESULT: Check for Status 1 (Success) if (_st == "1") { dbg("🔓 [Leaderboard] Lock released."); } else { dbg("⚠️ [Leaderboard] Lock release report: " + _st + ":" + _vl); } } } } timer() { if (isLocking) { llSetTimerEvent(0); kvpLockReq = llReadKeyValue(LB_LOCK_KEY); return; } if (isBusy) { // DO NOT check isSaving here, as isBusy should be the authoritative lock for READS if (retryCount < 3) { retryCount++; dbg("⏳ [Leaderboard] Retrying Shard " + (string)currentLoadPage + " (" + (string)retryCount + "/3)..."); isBusy = FALSE; // Unlock so loadLBPage can run loadLBPage(currentLoadPage); } else { isBusy = FALSE; llSetTimerEvent(0); dbg("🏆 [Leaderboard] SYNC GAVE UP (Persistent issues on Shard " + (string)currentLoadPage + ")"); llMessageLinked(LINK_SET, MSG_LB_REQUEST_DISPLAY, "0", NULL_KEY); } } else { llSetTimerEvent(0); } } link_message(integer sender, integer num, string str, key id) { if (num == MSG_LB_RECORD_PLAYER) { // str is "Name|Result" (e.g. "Bot1|WIN") list _p = llParseString2List(str, ["|"], []); startUpdateSequence(llList2String(_p, 0), llList2String(_p, 1)); } else if (num == MSG_DISPLAY_LEADERBOARD) { if (str == "REFRESH_STARTUP") loadLBPage(1); } else if (num == MSG_LB_REQUEST_DISPLAY) { integer _offset = (integer)str; displayOffset = _offset; // SYNC GLOBAL VARIABLE list _sorted = getSortedSlice(_offset); string _rankCol = ""; string _nameCol = ""; string _statsCol = ""; integer _i; integer _total = llGetListLength(_sorted) / 3; integer _globalTotal = llGetListLength(leaderboardData) / 3; for (_i = 0; _i < 10; _i++) { integer _rn = _offset + _i + 1; string _rank = (string)_rn + "."; if (_rn < 10) _rank = "0" + _rank; _rankCol += _rank + "\n"; if (_i < _total) { integer _b = _i * 3; _statsCol += "W:" + (string)llList2Integer(_sorted, _b) + "/L:" + (string)llList2Integer(_sorted, _b+1) + "\n"; string _n = llList2String(_sorted, _b+2); if (llStringLength(_n) > 16) _n = llGetSubString(_n, 0, 13) + "..."; _nameCol += _n + "\n"; } else { _nameCol += "----------\n"; _statsCol += "W:0/L:0\n"; } } string _p = " "; if (_offset > 0) _p = "[ < PREV ]"; string _n = " "; if (_offset + 10 < _globalTotal) _n = "[ NEXT > ]"; llMessageLinked(LINK_SET, MSG_DISPLAY_LEADERBOARD, "COLUMNS|" + _rankCol + "|" + _nameCol + "|" + _statsCol + "|" + _p + "|" + _n + "|" + (string)_offset, NULL_KEY); } else if (num == MSG_LB_PAGE_NEXT) { integer _maxEntries = llGetListLength(leaderboardData) / 3; if (displayOffset + 10 < _maxEntries) { displayOffset += 10; llMessageLinked(LINK_SET, MSG_LB_REQUEST_DISPLAY, (string)displayOffset, NULL_KEY); } } else if (num == MSG_LB_PAGE_PREV) { if (displayOffset >= 10) { displayOffset -= 10; llMessageLinked(LINK_SET, MSG_LB_REQUEST_DISPLAY, (string)displayOffset, NULL_KEY); } } else if (num == MSG_RESET_LEADERBOARD) { leaderboardData = []; currentLoadPage = 1; isBusy = FALSE; displayOffset = 0; if (str == "START_SYNC") { loadLBPage(1); } else if (str == "WIPE" || str == "HARD_RESET") { if (id != (key)GLOBAL_ADMIN) { llOwnerSay("🚫 [Security] Unauthorized WIPE attempt blocked from: " + (string)id); return; } // Soft Wipe: Clear all shards to empty in world datastore integer _i; for (_i = 1; _i <= MAX_LB_PAGES; _i++) { llUpdateKeyValue(LB_KEY_PREFIX + (string)_i, "", FALSE, ""); } dbg("🚫 [Leaderboard Manager] GLOBAL WIPE: All 10 datastore shards have been set to empty."); } else if (str == "DEBUG_GENERATE" && llGetOwner() == llGetCreator()) { generateShardedData(); } } else if (num == MSG_RESET_ALL) { if (str == "HARD_RESET" || str == "ADMIN_RESET") llResetScript(); } } }