diff --git a/Bot_Manager.lsl b/Bot_Manager.lsl index 64e6729..f9be2e0 100644 --- a/Bot_Manager.lsl +++ b/Bot_Manager.lsl @@ -47,6 +47,7 @@ integer listenHandle = -1; #define MEMORY_WARNING_THRESHOLD 0.8 // Warn when using >80% of memory // Track game state to validate bot commands +list players = []; list names = []; list lives = []; string perilPlayer = ""; @@ -236,6 +237,7 @@ default { // Handle full reset from main controller if (num == MSG_RESET_ALL && str == "FULL_RESET") { // Reset bot manager state + players = []; names = []; lives = []; perilPlayer = ""; @@ -247,7 +249,7 @@ default { // Handle game state sync to track player eliminations if (num == MSG_SYNC_GAME_STATE) { - list parts = llParseString2List(str, ["~"], []); + list parts = llParseStringKeepNulls(str, ["~"], []); if (llGetListLength(parts) >= 4) { lives = llCSV2List(llList2String(parts, 0)); // Don't need picks data for validation, skip parts[1] @@ -274,13 +276,12 @@ default { sentBotMessages = []; dbg("šŸ¤– [Bot Manager] New round detected (peril change), cleared processed commands and sent messages"); } else { - // Only clear if we detect truly empty picks data during an active game - // Don't clear during initial registration (when picks = "EMPTY") + // Standardized ~EMPTY~ token logic string picksStr = llList2String(parts, 1); - if (picksStr != "EMPTY" && (picksStr == "" || llSubStringIndex(picksStr, "|") == -1) && perilPlayer != "") { + if (picksStr == "~EMPTY~" && perilPlayer != "") { processedBotCommands = []; sentBotMessages = []; - dbg("šŸ¤– [Bot Manager] New round detected (picks cleared), cleared processed commands and sent messages"); + dbg("šŸ¤– [Bot Manager] Turn reset detected (picks cleared), cleared processed commands and sent messages"); } } } diff --git a/FURWARE_Leaderboard_Communication_Linkset.lsl b/FURWARE_Leaderboard_Communication_Linkset.lsl index 95afdf6..838efef 100644 --- a/FURWARE_Leaderboard_Communication_Linkset.lsl +++ b/FURWARE_Leaderboard_Communication_Linkset.lsl @@ -91,8 +91,9 @@ default { // Set the Top Line Header on the main box llMessageLinked(LINK_SET, 0, "=== WORLD RANKING ===", (key)"fw_data:Leaderboard"); - // Trigger Leaderboard Manager to download from KVP Database - llMessageLinked(LINK_SET, MSG_RESET_LEADERBOARD, "START_SYNC", NULL_KEY); + // Note: Leaderboard sync should be triggered by the Main Controller or Game Manager + // during game resets, rather than every time the bridge initializes. + // llMessageLinked(LINK_SET, MSG_RESET_LEADERBOARD, "START_SYNC", NULL_KEY); // Start Blink Timer (0.5s intervals) llSetTimerEvent(0.5); @@ -148,5 +149,6 @@ default { } } } + else if (num == MSG_RESET_ALL) { llResetScript(); } } } diff --git a/Floater_Manager.lsl b/Floater_Manager.lsl index 16dbdd1..5e549c2 100644 --- a/Floater_Manager.lsl +++ b/Floater_Manager.lsl @@ -123,6 +123,7 @@ default { // Handle full reset from main controller if (num == MSG_RESET_ALL && str == "FULL_RESET") { allPlayerNames = []; + allPlayerKeys = []; names = []; players = []; lives = []; @@ -198,7 +199,7 @@ default { integer syncIdx = llListFindList(names, [name]); key avKey = id; - integer lifeCount = 0; + integer lifeCount = 3; // DEFAULT TO HEALTHY (Prevent Red Flashes) if (syncIdx != -1) { if (syncIdx < llGetListLength(players)) avKey = llList2Key(players, syncIdx); @@ -228,14 +229,15 @@ default { string txt = "šŸŽ² Peril Dice\nšŸ‘¤ " + name + "\n" + perilStatus + "\nšŸ”¢ Picks: " + picksDisplay; - if (lifeCount <= 0) { - glow = 0.3; - color = <1,0,0>; // Red - txt = "šŸŽ² Peril Dice\nšŸ‘¤ " + name + "\nšŸ”¢ Picks: " + picksDisplay + "\nšŸ’€ ELIMINATED! šŸ’€"; - } else if (livingPlayers == 1 && llGetListLength(names) >= 2) { + // COLOR PRIORITY: Green (Victory) > Red (Eliminated) > Yellow (Peril) + if (livingPlayers == 1 && llGetListLength(names) >= 2 && lifeCount > 0) { glow = 0.3; color = <0,1,0>; // Green txt = "šŸŽ² Peril Dice\nšŸ‘¤ " + name + "\n✨ ULTIMATE VICTORY! ✨\nšŸ† ULTIMATE SURVIVOR šŸ†\nšŸ”¢ Final Picks: " + picksDisplay; + } else if (lifeCount <= 0) { + glow = 0.3; + color = <1,0,0>; // Red + txt = "šŸŽ² Peril Dice\nšŸ‘¤ " + name + "\nšŸ”¢ Picks: " + picksDisplay + "\nšŸ’€ ELIMINATED! šŸ’€"; } else if (name == perilPlayer) { glow = 0.2; color = <1,1,0>; // Yellow @@ -309,12 +311,12 @@ default { } } else if (num == MSG_SYNC_GAME_STATE) { - list parts = llParseString2List(str, ["~"], []); + list parts = llParseStringKeepNulls(str, ["~"], []); if (llGetListLength(parts) < 4) return; lives = llCSV2List(llList2String(parts, 0)); string picksDataStr = llList2String(parts, 1); - if (picksDataStr == "" || picksDataStr == "EMPTY") { + if (picksDataStr == "" || picksDataStr == "EMPTY" || picksDataStr == "~EMPTY~") { picksData = []; } else { picksData = llParseString2List(picksDataStr, ["^"], []); diff --git a/Game_Leaderboard_Manager.lsl b/Game_Leaderboard_Manager.lsl index d5ea870..f9fa3c5 100644 --- a/Game_Leaderboard_Manager.lsl +++ b/Game_Leaderboard_Manager.lsl @@ -1,5 +1,9 @@ #include "Peril_Constants.lsl" +debugMsg(string msg) { + if (DEBUG_LOGS) llMessageLinked(LINK_SET, MSG_DEBUG_TEXT, msg, NULL_KEY); +} + // ============================================================================= // PERIL WORLD RANKING - Leaderboard Manager // ============================================================================= @@ -11,7 +15,9 @@ list leaderboardData = []; // Stride 3: [Name, Wins, Losses] integer currentLoadPage = 1; key kvpReadReq; key kvpWriteReq; -key kvpLockReq; // Request for claiming leaderboard lock +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 @@ -21,18 +27,25 @@ string currentWriteKey; string currentWriteVal; // --- Lock & Sync State --- -string pendingPlayer = ""; -string pendingResult = ""; +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; - dbg("šŸ† [Leaderboard] Save sequence complete. Releasing lock..."); - // releaseLock(); - kvpLockReq = llUpdateKeyValue(LB_LOCK_KEY, "", TRUE, myLockVal); + // 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; @@ -42,6 +55,12 @@ processWriteQueue() { 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; @@ -76,20 +95,30 @@ generateShardedData() { } loadLBPage(integer pageNum) { - if (isBusy && pageNum == 1) return; // Prevent double loads on reset - if (pageNum > 1) llSleep(1.0); // Anti-Throttle Delay + if ((isBusy || isSaving || isLocking) && 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); // 30 second watchdog (Lag Resistant) + 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 Read FAILED: Not authorized for " + keyName); - llSetTimerEvent(0.0); - } else { - dbg("šŸ“” [Leaderboard] Loading " + keyName + "..."); + dbg("āš ļø [Leaderboard] KVP Request FAILED (Throttle/NULL_KEY). Retrying " + keyName + " in 2s..."); + isBusy = FALSE; // Unlock for retry + llSetTimerEvent(2.0); // Use timer to trigger the retry + return; } + + dbg("šŸ“” [Leaderboard] Loading " + keyName + "..."); } list getSortedSlice(integer offset) { @@ -135,15 +164,19 @@ updatePlayerRecord(string pName, string pResult) { } startUpdateSequence(string pName, string pResult) { - if (isLocking || isSaving) { - dbg("āš ļø [Leaderboard] Update already in progress. Retrying in 2s..."); - llSleep(2.0); + 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; } - pendingPlayer = pName; - pendingResult = pResult; isLocking = TRUE; - dbg("šŸ” [Leaderboard] Checking global lock for update..."); kvpLockReq = llReadKeyValue(LB_LOCK_KEY); } @@ -153,18 +186,45 @@ default { state_entry() { REPORT_MEMORY(); - dbg("ā³ [Leaderboard] Startup: Handshake link active."); + 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) { - if (_qid == kvpReadReq) { + 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 - integer _c = llSubStringIndex(_qd, ","); - string _st = _qd; string _vl = ""; - if (_c != -1) { - _st = llGetSubString(_qd, 0, _c - 1); - _vl = llGetSubString(_qd, _c + 1, -1); + 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) { @@ -189,35 +249,55 @@ default { } } - if (currentLoadPage < 10) { + if (isBusy && currentLoadPage < 10) { retryCount = 0; // Reset retries for next page loadLBPage(currentLoadPage + 1); } else { - isBusy = FALSE; - 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 + if (isBusy) { + isBusy = FALSE; + 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 + } // IF WE WERE SYNCING FOR AN UPDATE, APPLY IT NOW - if (isLocking) { - isLocking = FALSE; - updatePlayerRecord(pendingPlayer, pendingResult); + if (isSaving && llGetListLength(pendingRecords) > 0) { + isSaving = FALSE; + string nextName = llList2String(pendingRecords, 0); + string nextRes = llList2String(pendingRecords, 1); + pendingRecords = llDeleteSubList(pendingRecords, 0, 1); + updatePlayerRecord(nextName, nextRes); } } - } else if (_st == "0" && _vl == "1") { - // XP_ERROR_THROTTLED - dbg("āš ļø [Leaderboard] Read throttled. Retrying Shard " + (string)currentLoadPage + " in 2s..."); - llSleep(2.0); - isBusy = FALSE; - loadLBPage(currentLoadPage); - } else { // Empty or Error - isBusy = FALSE; - llSetTimerEvent(0); - if (_st == "0") { dbg("āŒ [Leaderboard] KVP Error on Shard " + (string)currentLoadPage + ": " + _vl); } - 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); + } else if (_st == "0") { + if (_vl == "1") { // XP_ERROR_THROTTLED + dbg("āš ļø [Leaderboard] Read throttled. Retrying Shard " + (string)currentLoadPage + " in 2s..."); + llSleep(2.0); + isBusy = FALSE; + loadLBPage(currentLoadPage); + } else if (_vl == "14") { // XP_ERROR_KEY_NOT_FOUND (Value is empty) + isBusy = FALSE; + llSetTimerEvent(0); + dbg("šŸ† [Leaderboard] Shard " + (string)currentLoadPage + " is empty (New)."); + + if (currentLoadPage < 10) { + loadLBPage(currentLoadPage + 1); + } else if (isBusy) { + 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); + } + } 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) { @@ -228,64 +308,94 @@ default { _vl = llGetSubString(_qd, _c + 1, -1); } - if (_st == "0" && _vl == "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 { - if (_st == "0") { dbg("āŒ [Leaderboard] Write Error (" + _vl + ") on " + currentWriteKey); } + dbg("āŒ [Leaderboard] Write Error (" + _st + ":" + _vl + ") on " + currentWriteKey); processWriteQueue(); } } - else if (_qid == kvpLockReq) { - integer _c = llSubStringIndex(_qd, ","); - string _st = _qd; string _vl = ""; - if (_c != -1) { - _st = llGetSubString(_qd, 0, _c - 1); - _vl = llGetSubString(_qd, _c + 1, -1); + 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) { - // STAGE 1: Current Lock Read Result - if (_st == "1") { // Success read - string _curVal = _vl; + if (_st == "1" && _c != -1) { + string _curVal = _rawVal; // USE RAW DATA FOR ATOMIC CHECK integer _isStale = FALSE; + list _p = []; + integer _ts = 0; + if (_curVal != "") { - list _p = llParseString2List(_curVal, [":"], []); - integer _ts = (integer)llList2String(_p, 1); + _p = llParseString2List(_curVal, [":"], []); + _ts = (integer)llList2String(_p, 1); if (llGetUnixTime() - _ts > LB_LOCK_TIMEOUT) _isStale = TRUE; } - if (_curVal == "" || _isStale) { - dbg("šŸ” [Leaderboard] Lock available. Claiming..."); + if (llList2String(_p, 0) == (string)llGetKey()) { + dbg("šŸ” [Leaderboard] Lock already held by this board (Self-Sync). Proceeding..."); + myLockVal = _curVal; // CRITICAL: Sync timestamp to allow future release + isLocking = FALSE; + isSaving = TRUE; // MARK THAT WE ARE SYNCING FOR A SAVE + leaderboardData = []; // PURGE STALE MEMORY + loadLBPage(1); + return; // Done + } + + if (_curVal == "" || _curVal == "OFF" || _isStale) { + dbg("šŸ” [Leaderboard] Lock available (or stale). Claiming..."); myLockVal = (string)llGetKey() + ":" + (string)llGetUnixTime(); - // ATOMIC UPDATE: Only write if value is still what we just read - kvpLockReq = llUpdateKeyValue(LB_LOCK_KEY, myLockVal, TRUE, _curVal); + dbg("šŸ”“ [Leaderboard] CLAIMING WITH: [" + myLockVal + "] (Len: " + (string)llStringLength(myLockVal) + ")"); + isClaiming = TRUE; + kvpClaimReq = llUpdateKeyValue(LB_LOCK_KEY, myLockVal, TRUE, _curVal); } else { - dbg("ā³ [Leaderboard] Lock held by another board. Retrying in 5s..."); + 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(); + dbg("šŸ”“ [Leaderboard] CLAIMING WITH: [" + myLockVal + "] (Len: " + (string)llStringLength(myLockVal) + ")"); + isClaiming = TRUE; + // INITIAL CREATION: Use kvpClaimReq to match Stage 2 handler + 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 if (_st == "14") { // SUCCESSFUL CLAIM (Atomic check passed) - dbg("šŸ” [Leaderboard] LOCK ACQUIRED. Starting fresh sync..."); - leaderboardData = []; // PURGE STALE MEMORY - loadLBPage(1); - } else if (_st == "0" && _vl == "15") { // ATOMIC CHECK FAILED (Someone else grabbed it) - dbg("ā³ [Leaderboard] Lock race lost. Retrying..."); - llSleep(1.0); - kvpLockReq = llReadKeyValue(LB_LOCK_KEY); } else { - dbg("āŒ [Leaderboard] Lock Error: (" + _st + ") " + _vl); + dbg("āŒ [Leaderboard] Lock logic failed (Code " + _st + ":" + _vl + ")"); isLocking = FALSE; } } else { - // RELEASE RESULT - if (_st == "14") { dbg("šŸ”“ [Leaderboard] Lock released."); } - else { dbg("āš ļø [Leaderboard] Lock release issues: " + _vl); } + // RELEASE RESULT: Check for Status 1 (Success) + if (_st == "1") { dbg("šŸ”“ [Leaderboard] Lock released."); } + else { dbg("āš ļø [Leaderboard] Lock release report: " + _st + ":" + _vl); } } } } @@ -297,16 +407,16 @@ default { return; } - if (isBusy) { + if (isBusy || (kvpReadReq == NULL_KEY && currentLoadPage > 0)) { if (retryCount < 3) { retryCount++; - dbg("āš ļø [Leaderboard] Shard " + (string)currentLoadPage + " timed out. Retrying (" + (string)retryCount + "/3)..."); + 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 timeout on Shard " + (string)currentLoadPage + ")"); + dbg("šŸ† [Leaderboard] SYNC GAVE UP (Persistent issues on Shard " + (string)currentLoadPage + ")"); llMessageLinked(LINK_SET, MSG_LB_REQUEST_DISPLAY, "0", NULL_KEY); } } else { @@ -316,7 +426,9 @@ default { link_message(integer sender, integer num, string str, key id) { if (num == MSG_LB_RECORD_PLAYER) { - startUpdateSequence(str, (string)id); + // 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); @@ -385,6 +497,8 @@ default { generateShardedData(); } } - else if (num == MSG_RESET_ALL) { llResetScript(); } + else if (num == MSG_RESET_ALL) { + if (str == "FULL_RESET" || str == "HARD_RESET") llResetScript(); + } } } diff --git a/Game_Manager.lsl b/Game_Manager.lsl index efa300e..a2381cc 100644 --- a/Game_Manager.lsl +++ b/Game_Manager.lsl @@ -27,6 +27,7 @@ integer roundStarted = FALSE; integer diceTypeProcessed = FALSE; // Track if we've already processed dice type for this round integer ignorePicksSync = FALSE; // Temporarily ignore picks data sync during round initialization integer roundContinueInProgress = FALSE; // Prevent multiple rapid calls to continueCurrentRound +integer lastPlayerCount = 0; // Memory for dice type calculation (skips redundant requests) integer lastSyncProcessTime = 0; // Track when we last processed a sync to prevent rapid duplicates // Duplicate request prevention @@ -149,24 +150,28 @@ integer continueCurrentRound() { } currentPickerIdx = 0; - dbg("šŸŽÆ [Game Manager] Pick queue created: " + llList2CSV(pickQueue) + ", requesting dice type"); + dbg("šŸŽÆ [Game Manager] Pick queue created: " + llList2CSV(pickQueue) + ", checking dice memory..."); - // Request dice type directly from Calculator for the new round - llMessageLinked(LINK_SET, MSG_GET_DICE_TYPE, (string)llGetListLength(names), NULL_KEY); + integer currentPlayerCount = llGetListLength(names); + if (currentPlayerCount == lastPlayerCount && diceType > 0) { + dbg("šŸŽÆ [Game Manager] Player count unchanged (" + (string)currentPlayerCount + "), reusing stored dice type: d" + (string)diceType); + + // NEW: Add a brief buffer to let the previous outcome status stay on screen + llSleep(DELAY_MEDIUM_SYNC); + + // Reuse stored dice type and skip redundant request/status Flicker + diceTypeProcessed = TRUE; + llMessageLinked(LINK_SET, MSG_STATUS_TEXT, "NEW ROUND STARTING!\nPeril: " + perilPlayer, NULL_KEY); + showNextPickerDialog(); + } else { + lastPlayerCount = currentPlayerCount; + // NEW: Update status bar for new round initialization + llMessageLinked(LINK_SET, MSG_STATUS_TEXT, "NEW ROUND STARTING!\nPeril: " + perilPlayer, NULL_KEY); + // Request dice type directly from Calculator for the new round + llMessageLinked(LINK_SET, MSG_GET_DICE_TYPE, (string)currentPlayerCount, NULL_KEY); + } - // NEW: Update status bar for new round initialization - llMessageLinked(LINK_SET, MSG_STATUS_TEXT, "PREPARING ROUND...\nCalculating dice size...", NULL_KEY); - - // Dramatic pause to let the preparation message be read - llSleep(DELAY_LONG_SYNC); - - // NEW: Update status bar for new round with colored highlight - llMessageLinked(LINK_SET, MSG_STATUS_TEXT, "NEW ROUND STARTING!\nPeril: " + perilPlayer, NULL_KEY); - - // Dramatic pause to let the peril player be seen before the dice type is shown - llSleep(DELAY_LONG_SYNC); - - // Note: showNextPickerDialog() will be called when dice type result is received + // Note: showNextPickerDialog() will be called when dice type result is received (or immediately if cached) return 0; } @@ -249,16 +254,25 @@ integer startNextRound() { currentPickerIdx = 0; dbg("šŸŽÆ Debug - Created pickQueue: " + llList2CSV(pickQueue) + ", currentPickerIdx: " + (string)currentPickerIdx); - // Request dice type for this round - llMessageLinked(LINK_SET, MSG_GET_DICE_TYPE, (string)llGetListLength(names), NULL_KEY); + integer currentPlayerCount = llGetListLength(names); + if (currentPlayerCount == lastPlayerCount && diceType > 0) { + dbg("šŸŽÆ [Game Manager] Player count unchanged (" + (string)currentPlayerCount + "), reusing stored dice type: d" + (string)diceType); + + // NEW: Add a brief buffer to let the previous outcome status stay on screen + llSleep(DELAY_MEDIUM_SYNC); + + diceTypeProcessed = TRUE; + llMessageLinked(LINK_SET, MSG_STATUS_TEXT, "ROUND STARTING!\nPeril: " + perilPlayer, NULL_KEY); + showNextPickerDialog(); + } else { + lastPlayerCount = currentPlayerCount; + // Request dice type for this round + llMessageLinked(LINK_SET, MSG_GET_DICE_TYPE, (string)currentPlayerCount, NULL_KEY); + // NEW: Update status bar for round start + llMessageLinked(LINK_SET, MSG_STATUS_TEXT, "ROUND STARTING!\nPeril: " + perilPlayer, NULL_KEY); + } - // NEW: Update status bar for new round initialization - llMessageLinked(LINK_SET, MSG_STATUS_TEXT, "PREPARING ROUND...\nCalculating dice size...", NULL_KEY); - - // Dramatic pause to let the preparation message be read - llSleep(DELAY_LONG_SYNC); - - dbg("šŸŽÆ Game Manager round setup complete, requesting dice type..."); + dbg("šŸŽÆ Game Manager round setup complete."); return 0; } @@ -484,7 +498,7 @@ default { // Only skip sync messages if we're explicitly ignoring them // Allow player registration syncs even when game is not active - list parts = llParseString2List(str, ["~"], []); + list parts = llParseStringKeepNulls(str, ["~"], []); if (llGetListLength(parts) >= 4) { list newLives = llCSV2List(llList2String(parts, 0)); string currentLivesStr = llList2CSV(lives); @@ -509,7 +523,7 @@ default { if (livesChanged || perilChanged) { string encodedPicksDataStr = llList2String(parts, 1); list newPicksData = []; - if (encodedPicksDataStr != "") { + if (encodedPicksDataStr != "" && encodedPicksDataStr != "~EMPTY~") { list encodedEntries = llParseString2List(encodedPicksDataStr, ["^"], []); integer syncI; for (syncI = 0; syncI < llGetListLength(encodedEntries); syncI++) { @@ -551,10 +565,8 @@ default { } } - if (livesChanged && newPerilPlayer != "" && newPerilPlayer != "NONE" && roundStarted && allPicksEmpty) { - dbg("šŸŽÆ Post-roll state update detected - continuing round"); - continueCurrentRound(); - } + // Note: Round continuation is now handled by the Main Controller's explicit MSG_CONTINUE_ROUND signal + // This prevents duplicate round starts and race conditions during Plot Twists. } } return; @@ -778,7 +790,7 @@ default { // Use complete avoid list for re-shown dialog too list updatedCompleteAvoidList = buildCompleteAvoidanceList(); string dialogPayload = playerName + "|" + (string)diceType + "|" + (string)picksNeeded + "|" + llList2CSV(updatedCompleteAvoidList); - string dialogRequest = "SHOW_DIALOG|" + playerName + "|" + dialogPayload; + string dialogRequest = "SHOW_DIALOG~" + playerName + "~" + dialogPayload; llMessageLinked(LINK_SET, MSG_DIALOG_FORWARD_REQUEST, dialogRequest, NULL_KEY); } return; @@ -839,7 +851,7 @@ default { // Send roll dialog through Player_RegistrationManager dbg("šŸŽÆ [Game Manager] Sending roll dialog request for: " + perilPlayer); - string rollRequest = "SHOW_ROLL_DIALOG|" + perilPlayer + "|" + perilPlayer; + string rollRequest = "SHOW_ROLL_DIALOG~" + perilPlayer + "~" + perilPlayer; llMessageLinked(LINK_SET, MSG_DIALOG_FORWARD_REQUEST, rollRequest, NULL_KEY); } } @@ -898,7 +910,7 @@ default { llMessageLinked(LINK_SET, MSG_BOT_COMMAND, "CLOSE_ALL_DIALOGS", NULL_KEY); // Send roll dialog through Player_RegistrationManager - string rollRequest = "SHOW_ROLL_DIALOG|" + perilPlayer + "|" + perilPlayer; + string rollRequest = "SHOW_ROLL_DIALOG~" + perilPlayer + "~" + perilPlayer; llMessageLinked(LINK_SET, MSG_DIALOG_FORWARD_REQUEST, rollRequest, NULL_KEY); } return; // Ignore duplicate - bot already has real picks @@ -1008,7 +1020,7 @@ default { llMessageLinked(LINK_SET, MSG_BOT_COMMAND, "CLOSE_ALL_DIALOGS", NULL_KEY); // Send roll dialog through Player_RegistrationManager - string rollRequest = "SHOW_ROLL_DIALOG|" + perilPlayer + "|" + perilPlayer; + string rollRequest = "SHOW_ROLL_DIALOG~" + perilPlayer + "~" + perilPlayer; llMessageLinked(LINK_SET, MSG_DIALOG_FORWARD_REQUEST, rollRequest, NULL_KEY); } else { // Move to next picker @@ -1065,6 +1077,7 @@ default { currentPicker = NULL_KEY; roundStarted = FALSE; diceTypeProcessed = FALSE; // Reset for new game + lastPlayerCount = 0; // Reset dice memory for new game roundContinueInProgress = FALSE; // Reset protection flag lastSyncProcessTime = 0; // Reset sync timing dbg("šŸŽÆ Game Manager reset - ready for new game!"); diff --git a/Game_Scoreboard_Manager_Linkset.lsl b/Game_Scoreboard_Manager_Linkset.lsl index 63ad448..10f0405 100644 --- a/Game_Scoreboard_Manager_Linkset.lsl +++ b/Game_Scoreboard_Manager_Linkset.lsl @@ -107,9 +107,10 @@ clearAllPlayers() { integer _hL = getHeartsPrimLink(_ca_i); integer _oL = getOverlayPrimLink(_ca_i); if (_pL > 0) { - llSetLinkPrimitiveParamsFast(_pL, [PRIM_COLOR, ALL_SIDES, <1,1,1>, 0.0]); - llSetLinkPrimitiveParamsFast(_hL, [PRIM_COLOR, ALL_SIDES, <1,1,1>, 0.0]); - llSetLinkPrimitiveParamsFast(_oL, [PRIM_COLOR, ALL_SIDES, <1,1,1>, 0.0]); + // Explicitly reset textures to blank to prevent profile persistence + llSetLinkPrimitiveParamsFast(_pL, [PRIM_TEXTURE, ALL_SIDES, BLANK_TEXTURE, <1,1,0>, <0,0,0>, 0.0, PRIM_COLOR, ALL_SIDES, <1,1,1>, 0.0]); + llSetLinkPrimitiveParamsFast(_hL, [PRIM_TEXTURE, ALL_SIDES, TEXTURE_0_HEARTS, <1,1,0>, <0,0,0>, 0.0, PRIM_COLOR, ALL_SIDES, <1,1,1>, 0.0]); + llSetLinkPrimitiveParamsFast(_oL, [PRIM_TEXTURE, ALL_SIDES, BLANK_TEXTURE, <1,1,0>, <0,0,0>, 0.0, PRIM_COLOR, ALL_SIDES, <1,1,1>, 0.0]); } } } @@ -232,13 +233,15 @@ updatePlayerDisplay(string pName, integer pLives, string pUUID) { } default { - on_rez(integer _sp) { discoverLinks(); llResetScript(); } + on_rez(integer _sp) { discoverLinks(); clearAllPlayers(); llResetScript(); } state_entry() { discoverLinks(); llListen(1, "", llGetOwner(), ""); REPORT_MEMORY(); + clearAllPlayers(); // Ensure board is clean on initialization resetBackgroundPrim(); resetManagerCube(); + updateActionsPrim("Title"); updatePlayerGlowEffects(); dbg("Ready"); } @@ -256,8 +259,10 @@ default { currentPerilPlayer = ""; currentWinner = ""; updatePlayerGlowEffects(); llMessageLinked(LINK_SET, MSG_LB_REQUEST_DISPLAY, "", NULL_KEY); } - else if (_n == MSG_GAME_WON) llMessageLinked(LINK_SET, MSG_LB_RECORD_PLAYER, _st + "|WIN", NULL_KEY); - else if (_n == MSG_GAME_LOST) llMessageLinked(LINK_SET, MSG_LB_RECORD_PLAYER, _st + "|LOSS", NULL_KEY); + else if (_n == MSG_GAME_WON || _n == MSG_GAME_LOST) { + // Visual winning/losing logic is already handled by specific update messages + // Record keeping is now centralized in Main Controller + } else if (_n == MSG_LB_DISPLAY_DATA) { string _tl = "WORLD RANKING RECORD"; string _sp = " "; integer _m = (LEADERBOARD_WIDTH - llStringLength(_tl)) / 2; @@ -307,9 +312,9 @@ default { activePlayers = llListReplaceList(activePlayers, [_pU], (_pIdx * 3) + 2, (_pIdx * 3) + 2); integer _pL = getProfilePrimLink(_pIdx); if (_pL > 0) { - llSetLinkPrimitiveParamsFast(_pL, [PRIM_TEXTURE, ALL_SIDES, _pU, <1,1,0>, <0,0,0>, 0.0, PRIM_COLOR, ALL_SIDES, <1,1,1>, 1.0, PRIM_GLOW, 0.01]); + llSetLinkPrimitiveParamsFast(_pL, [PRIM_TEXTURE, ALL_SIDES, _pU, <1,1,0>, <0,0,0>, 0.0, PRIM_COLOR, ALL_SIDES, <1,1,1>, 1.0, PRIM_GLOW, ALL_SIDES, 0.01]); llSleep(DELAY_SCOREBOARD_REFRESH); - llSetLinkPrimitiveParamsFast(_pL, [PRIM_GLOW, 0.0]); + llSetLinkPrimitiveParamsFast(_pL, [PRIM_GLOW, ALL_SIDES, 0.0]); } } } diff --git a/Main_Controller_Linkset.lsl b/Main_Controller_Linkset.lsl index 8a820da..89033ac 100644 --- a/Main_Controller_Linkset.lsl +++ b/Main_Controller_Linkset.lsl @@ -68,6 +68,7 @@ integer victoryInProgress = FALSE; // Lockout system variables integer isLocked = FALSE; +integer resetInProgress = FALSE; // Lockout flag for reset synchronization key gameOwner; list pickQueue = []; @@ -179,23 +180,34 @@ checkMemoryUsage(string context) { // REMOVED: emergencyMemoryCleanup() - delegated to Controller_Memory.lsl // REMOVED: reportMemoryStats() - delegated to Controller_Memory.lsl -// Experience Sentinel - Functional probe to check if Experience is allowed on land +// Experience Sentinel - Functional probe to check if KVP is active +// NOTE FOR FUTURE AI/DEVS: KVP (World Rankings) ONLY requires the script to be +// compiled with the Experience. Land-Scope whitelisting (llAgentInExperience) +// is ONLY required for HUD auto-attachments and Animations. checkExperience() { - dbg("šŸ” [Peril Dice] Experience Sentinel: Starting functional diagnostic..."); - llOwnerSay("šŸ›”ļø [Peril Dice] Experience Sentinel: Verifying Land-Scope permissions for 'Final Girlz I.N.C.'..."); + dbg("šŸ” [Peril Dice] Experience Sentinel: Starting functional heartbeat..."); - // PROBE 1: Owner Agent Check (Instant indicator for land-readiness) + // PROBE 1: Land-Scope Check (Crucial for HUD auto-attachment) if (!llAgentInExperience(llGetOwner())) { - llOwnerSay("āš ļø [Peril Dice] SYSTEM WARNING: Experience Features are currently BLOCKED on this parcel."); - llOwnerSay("šŸ›”ļø [Peril Dice] TO FIX: Open 'About Land' -> 'Experiences' -> 'Add' and search for 'Final Girlz I.N.C.'"); + llOwnerSay("šŸ›”ļø [Peril Dice] LAND NOTICE: Experience 'Final Girlz I.N.C.' is NOT enabled on this land."); + llOwnerSay("āš ļø [Peril Dice] Automatic HUD attachments will FAIL. To fix this:"); + llOwnerSay("šŸ‘‰ 1. Right-click the ground -> 'About Land'"); + llOwnerSay("šŸ‘‰ 2. Go to the 'Experiences' tab."); + llOwnerSay("šŸ‘‰ 3. Click the 'Add' button."); + llOwnerSay("šŸ‘‰ 4. Search for 'Final Girlz I.N.C.' and click 'OK'."); - // Final probe check + // Silent probe for KVP (which works regardless) sentinelQueryID = llReadKeyValue("PERIL_SENTINEL"); - llSetTimerEvent(10.0); // Increased to 10s for reliability + llSetTimerEvent(10.0); currentTimerMode = TIMER_XP_CHECK; } else { - llOwnerSay("āœ… [Peril Dice] Experience 'Final Girlz I.N.C.' is ENABLED on this parcel."); + llOwnerSay("āœ… [Peril Dice] Experience 'Final Girlz I.N.C.' is ACTIVE. HUD Auto-attachment ready."); EX_READY = TRUE; + + // Full KVP heartbeat + sentinelQueryID = llReadKeyValue("PERIL_SENTINEL"); + llSetTimerEvent(10.0); + currentTimerMode = TIMER_XP_CHECK; } } @@ -227,13 +239,17 @@ sendStatusMessage(string status) { syncGameState() { // Build sync message WITHOUT redundant player keys to save memory // Format: lives~picks~perilPlayer~names + string picksStr = (string)llDumpList2String(picksData, "^"); + if (picksStr == "") picksStr = "EMPTY"; // Standardized token + + // DELIMITER FIX: Ensure each of the 4 parts is explicitly separated by ~ string syncMsg = llList2CSV(lives) + "~" + - (string)llDumpList2String(picksData, "^") + "~" + + picksStr + "~" + (string)llList2String([perilPlayer, "NONE"], (perilPlayer == "")) + "~" + llList2CSV(names); llMessageLinked(LINK_SET, MSG_SYNC_GAME_STATE, syncMsg, NULL_KEY); - dbg("šŸŒ [Main Controller] šŸ“” Global State Sync broadcasted."); + dbg("šŸŒ [Main Controller] šŸ“” Global State Sync broadcasted (Picks: " + picksStr + ")"); } @@ -288,14 +304,13 @@ updateFloatingText() { } resetGame() { + resetInProgress = TRUE; // Start lockout cleanupListeners(); // Let Floater Manager handle cleanup intelligently - // Always request cleanup on a reset to ensure no orphans are left behind llMessageLinked(LINK_SET, MSG_CLEANUP_ALL_FLOATERS, "RESET", NULL_KEY); - llSleep(DELAY_STATE_TRANSITION); - - llSleep(DELAY_STATE_TRANSITION); + // Reduced delay for snappy restart + llSleep(0.1); players = names = lives = picksData = globalPickedNumbers = readyPlayers = []; floaterChannels = []; @@ -317,6 +332,13 @@ resetGame() { llMessageLinked(LINK_SCOREBOARD, MSG_UPDATE_WINNER, "", NULL_KEY); // Clear winner glow llMessageLinked(LINK_SET, MSG_CLEAR_DICE, "", NULL_KEY); + // Give other scripts time to finish their reset before starting sync + // INCREASED to 2.0s for high-speed Mono startup with Debug Logs OFF + llSleep(2.0); + + // NEW: Trigger Leaderboard to reload rankings from KVP on reset + llMessageLinked(LINK_SET, MSG_RESET_LEADERBOARD, "START_SYNC", NULL_KEY); + dbg("šŸŽ® Game reset! All state cleared (including scoreboard)."); statusTimer = 0; @@ -328,15 +350,12 @@ resetGame() { // NEW: Update status bar for idle state sendStatusMessage("PERIL DICE GAME\nTouch to Join!"); - llSleep(DELAY_STATE_TRANSITION); + // Quick buffer for Linkset to settle + llSleep(0.2); llSetTimerEvent(0); initListeners(); - - // SAFETY: Only call updateHelpers if we have actual game data to prevent startup issues - if (llGetListLength(names) > 0) { - updateHelpers(); - } + resetInProgress = FALSE; // End lockout } default { @@ -384,15 +403,17 @@ default { } touch_start(integer total_number) { - // IGNORE TOUCHES INTENDED FOR THE LEADERBOARD - string _rawNm = llGetLinkName(llDetectedLinkNumber(0)); - string _linkNm = llToLower(_rawNm); + integer clickedLink = llDetectedLinkNumber(0); + string clickedName = llGetLinkName(clickedLink); - // TEMPORARY DEBUG TO CATCH THE EXACT LINK NAME - dbg("šŸŽÆ [Touch Filter] Clicked Link Name: '" + _rawNm + "'"); + // Ensure core links are discovered to prevent misidentification + if (LINK_LEADERBOARD_BRIDGE == -1) { DISCOVER_CORE_LINKS(); } - if (llSubStringIndex(_linkNm, "leaderboard") != -1 || llSubStringIndex(_linkNm, "furware") != -1 || llSubStringIndex(_linkNm, "lb_") != -1) { - dbg("šŸ›‘ [Touch Filter] Bypassing menu for Leaderboard element!"); + // IGNORE TOUCHES ON UI BRIDGES ONLY + // Always allow touch on Root (link 1) or any prim containing 'Scoreboard' or NOT containing 'FURWARE' + // ENHANCED: Also filter out click on Leaderboard elements which was causing redundant diagnostic logs + if (clickedLink != 1 && (llSubStringIndex(clickedName, "FURWARE") != -1 || llSubStringIndex(clickedName, "LB_") != -1)) { + dbg("šŸ›‘ [Touch Filter] Bypassing menu for Bridge element: " + clickedName); return; } @@ -544,8 +565,11 @@ default { if (pidx != -1) { lives = llListReplaceList(lives, [newLifeCount], pidx, pidx); dbg("šŸ›”ļø [Main Controller] šŸ’— Damage Report Confirmed: " + hitPlayer + " -> " + (string)newLifeCount + " hearts."); - // Broadcast the OFFICIAL ledger to the entire Linkset syncGameState(); + + // AUTHORITATIVE: Signal Game Manager to continue to the next round after the damage report + llSleep(DELAY_SYNC_PROPAGATION); + llMessageLinked(LINK_SET, MSG_CONTINUE_ROUND, perilPlayer, NULL_KEY); } } return; @@ -612,6 +636,9 @@ default { llMessageLinked(LINK_SCOREBOARD, MSG_PLAYER_UPDATE, eliminatedPlayer + "|0|" + (string)llList2Key(players, idx), NULL_KEY); + // RECORD LOSS: Send eliminated player to World Ranking database + llMessageLinked(LINK_SET, MSG_LB_RECORD_PLAYER, eliminatedPlayer + "|LOSS", NULL_KEY); + // CRITICAL: Check for victory condition BEFORE assigning new peril player integer willCauseVictory = (llGetListLength(names) <= 2); // After removing one player string potentialWinner = ""; @@ -629,6 +656,9 @@ default { @found_winner; if (potentialWinner != "") { + // NEW: Broadcast sync BEFORE redraw to ensure Floater Manager has fresh heart count + syncGameState(); + // SEND WINNER GLOW to scoreboard and trigger floater update llMessageLinked(LINK_SCOREBOARD, MSG_UPDATE_WINNER, potentialWinner, NULL_KEY); llMessageLinked(LINK_SET, MSG_UPDATE_FLOAT, potentialWinner, NULL_KEY); // Update winner's floater @@ -755,7 +785,10 @@ default { llMessageLinked(LINK_SCOREBOARD, MSG_UPDATE_WINNER, winner, NULL_KEY); llMessageLinked(LINK_SET, MSG_UPDATE_FLOAT, winner, NULL_KEY); // Update winner's floater - // CHANGED: Send to scoreboard, which will handle leaderboard updates + // RECORD WIN: Send ultimate survivor to World Ranking database + llMessageLinked(LINK_SET, MSG_LB_RECORD_PLAYER, winner + "|WIN", NULL_KEY); + + // CHANGED: Send to scoreboard, which will handle visual updates (no more duplicate records) llMessageLinked(LINK_SCOREBOARD, MSG_GAME_WON, winner, NULL_KEY); // Use timer instead of llSleep to avoid immediate reset @@ -843,6 +876,18 @@ default { return; } + // Handle peril player updates from other modules + if (num == MSG_UPDATE_PERIL_PLAYER) { + perilPlayer = str; + dbg("šŸŽÆ [Main Controller] Peril Player Updated: " + perilPlayer); + syncGameState(); + + // AUTHORITATIVE: Signal Game Manager to continue to the next round after Plot Twist + llSleep(DELAY_SYNC_PROPAGATION); + llMessageLinked(LINK_SET, MSG_CONTINUE_ROUND, perilPlayer, NULL_KEY); + return; + } + // Receive lightweight updates from Game Manager if (num == 9070) { list parts = llParseString2List(str, ["|"], []); @@ -953,11 +998,11 @@ default { string action = llList2String(parts, 0); if (action == "LEAVE_GAME" || action == "KICK_PLAYER") { string leavingName = llList2String(parts, 1); - // Find and remove the player from main controller lists - integer idx = llListFindList(names, [leavingName]); - if (idx != -1) { - // CHANGED: Remove player from scoreboard BEFORE updating lists - llMessageLinked(LINK_SCOREBOARD, MSG_REMOVE_PLAYER, leavingName, NULL_KEY); + // Find and remove the player from main controller lists + integer idx = llListFindList(names, [leavingName]); + if (idx != -1) { + // CRITICAL: Inform all scripts (Registration Manager) to scrub this player + llMessageLinked(LINK_SET, MSG_REMOVE_PLAYER, leavingName, NULL_KEY); // Only announce if it's not the end of the game if (llGetListLength(names) > 1) { @@ -1135,7 +1180,7 @@ default { llRegionSayTo(id, 0, "🚫 Access Denied: Only the Global Admin can perform this action."); return; } - llMessageLinked(LINK_SCOREBOARD, MSG_RESET_LEADERBOARD, "WIPE", id); // Use 'id' to pass user key + llMessageLinked(LINK_SET, MSG_RESET_LEADERBOARD, "WIPE", id); // Use LINK_SET llOwnerSay("šŸ† GLOBAL LEADERBOARD WIPE confirmed and executed."); return; } @@ -1149,7 +1194,7 @@ default { return; } resetGame(); - llMessageLinked(LINK_SCOREBOARD, MSG_RESET_LEADERBOARD, "WIPE", id); // Use 'id' for security check + llMessageLinked(LINK_SET, MSG_RESET_LEADERBOARD, "WIPE", id); // Use LINK_SET llOwnerSay("šŸ”„ FULL RESET confirmed and executed."); return; } diff --git a/Peril_Constants.lsl b/Peril_Constants.lsl index 806a68d..16ef32d 100644 --- a/Peril_Constants.lsl +++ b/Peril_Constants.lsl @@ -43,6 +43,8 @@ key EXPERIENCE_ID = "06926390-26e6-11f1-a452-0242ac110003"; // Final Girlz I.N.C #define dbg(msg) ; #endif +#define MSG_DEBUG_TEXT 7000 + #define MSG_RESET_ALL -99999 #define MSG_EMERGENCY_RESET -99998 #define MSG_DEBUG_PICKS_ON 7001 diff --git a/Player_RegistrationManager.lsl b/Player_RegistrationManager.lsl index f175950..ea89699 100644 --- a/Player_RegistrationManager.lsl +++ b/Player_RegistrationManager.lsl @@ -17,6 +17,8 @@ integer FLOATER_BASE_CHANNEL; integer roundStarted = FALSE; integer gameStarting = FALSE; +integer resetInProgress = FALSE; // Lockout for reset syncs + integer calculateChannel(integer offset) { string ownerStr = (string)llGetOwner(); string objectStr = (string)llGetLinkKey(1); @@ -154,6 +156,10 @@ handlePlayerRegistration(string regData, key requesterId) { if (newKey == llGetOwner()) { integer isStarter = (humanCount <= 1); llMessageLinked(LINK_SET, MSG_SHOW_MENU, "owner|" + (string)isStarter, newKey); + } else { + // NEW: Also show menu for non-owners immediately after registration + integer isStarter = (humanCount <= 1); + llMessageLinked(LINK_SET, MSG_SHOW_MENU, "player|" + (string)isStarter, newKey); } ownerMsg("JOIN|" + newName); @@ -190,77 +196,150 @@ default { // Handle game state sync from Main Controller if (num == MSG_SYNC_GAME_STATE) { + if (resetInProgress) return; // IGNORE stale syncs during reset lockout + // PRESERVE PLAYER KEYS: Don't overwrite our authoritative player registry - // We only sync lives and names, but keep our real player keys - list parts = llParseString2List(str, ["~"], []); + list parts = llParseStringKeepNulls(str, ["~"], []); if (llGetListLength(parts) >= 4) { list newLives = llCSV2List(llList2String(parts, 0)); + string picksPart = llList2String(parts, 1); list newNames = llCSV2List(llList2String(parts, 3)); - // Only update lives if list length matches (safety check) if (llGetListLength(newLives) == llGetListLength(players)) { lives = newLives; } - - // Only update names if they match our current registry - // This prevents sync messages from overwriting our authoritative data if (llGetListLength(newNames) == llGetListLength(players)) { names = newNames; } - // KEEP our player keys - they are authoritative dbg("šŸ”„ [RegMgr] Synced game state, preserving " + (string)llGetListLength(players) + " player keys"); } return; } + + // --- Status Queries --- + if (num == MSG_QUERY_READY_STATE) { + list parts = llParseString2List(str, ["|"], []); + if (llGetListLength(parts) >= 2) { + string name = llList2String(parts, 0); + string requestId = llList2String(parts, 1); + + integer isReady = (llListFindList(readyPlayers, [name]) != -1); + integer isBot = (llSubStringIndex(name, "Bot") == 0); + + llMessageLinked(LINK_SET, MSG_READY_STATE_RESULT, + name + "|" + (string)isReady + "|" + (string)isBot + "|" + requestId, id); + } + return; + } + if (num == MSG_QUERY_OWNER_STATUS) { + list parts = llParseString2List(str, ["|"], []); + if (llGetListLength(parts) >= 2) { + string name = llList2String(parts, 0); + string requestId = llList2String(parts, 1); + + integer isRegistered = (llListFindList(players, [id]) != -1); + integer isPending = FALSE; + + integer isStarter = FALSE; + if (isRegistered) { + integer starterIdx = -1; + integer i; + for (i = 0; i < llGetListLength(names) && starterIdx == -1; i++) { + if (llSubStringIndex(llList2String(names, i), "Bot") != 0) starterIdx = i; + } + if (starterIdx != -1 && llList2Key(players, starterIdx) == id) isStarter = TRUE; + } + + dbg("šŸ“¤ [RegMgr] Responding to OWNER STATUS (" + requestId + ") - Registered: " + (string)isRegistered); + llMessageLinked(LINK_SET, MSG_OWNER_STATUS_RESULT, + name + "|" + (string)isRegistered + "|" + (string)isPending + "|" + (string)isStarter + "|" + requestId, id); + } + return; + } + + if (num == MSG_TOGGLE_READY) { + string name = str; + integer idx = llListFindList(readyPlayers, [name]); + if (idx == -1) { + readyPlayers += [name]; + publicMsg("GAME|" + name + " is NOW READY!"); + } else { + readyPlayers = llDeleteSubList(readyPlayers, idx, idx); + publicMsg("GAME|" + name + " is NO LONGER READY."); + } + // Broadcast the official state list to all scripts (Main Controller needs this) + llMessageLinked(LINK_SET, MSG_UPDATE_MAIN_LISTS, "READY_LIST|" + llList2CSV(readyPlayers), NULL_KEY); + return; + } + + if (num == MSG_REMOVE_PLAYER) { + string leavingName = str; + integer idx = llListFindList(names, [leavingName]); + if (idx != -1) { + // Scrub from all synchronized lists + players = llDeleteSubList(players, idx, idx); + names = llDeleteSubList(names, idx, idx); + lives = llDeleteSubList(lives, idx, idx); + + // ALSO scrub from ready list if they were there + integer rIdx = llListFindList(readyPlayers, [leavingName]); + if (rIdx != -1) readyPlayers = llDeleteSubList(readyPlayers, rIdx, rIdx); + + dbg("šŸŽÆ [RegMgr] Deep Scrub Complete: " + leavingName + " removed from all registries."); + + // Broadcast updated lists to keep Main Controller in sync + llMessageLinked(LINK_SET, MSG_UPDATE_MAIN_LISTS, "READY_LIST|" + llList2CSV(readyPlayers), NULL_KEY); + } + return; + } + // Handle dialog forwarding requests from Game Manager if (num == MSG_DIALOG_FORWARD_REQUEST) { - list parts = llParseString2List(str, ["|"], []); + list parts = llParseString2List(str, ["~"], []); // Fixed delimiter to match forwarder if (llGetListLength(parts) >= 3) { - string dialogType = llList2String(parts, 0); // "SHOW_DIALOG" or "SHOW_ROLL_DIALOG" + string dialogType = llList2String(parts, 0); string targetPlayerName = llList2String(parts, 1); - // Reconstruct dialog payload from remaining parts string dialogPayload = ""; integer i; for (i = 2; i < llGetListLength(parts); i++) { - if (i > 2) dialogPayload += "|"; // Add separator between parts + if (i > 2) dialogPayload += "|"; dialogPayload += llList2String(parts, i); } - // Find player key in our authoritative registry integer playerIdx = llListFindList(names, [targetPlayerName]); if (playerIdx != -1 && playerIdx < llGetListLength(players)) { key playerKey = llList2Key(players, playerIdx); if (playerKey != NULL_KEY) { if (dialogType == "SHOW_DIALOG") { llMessageLinked(LINK_SET, MSG_SHOW_DIALOG, dialogPayload, playerKey); - dbg("šŸ“‹ [RegMgr] Forwarded dialog to " + targetPlayerName + ": " + dialogPayload); } else if (dialogType == "SHOW_ROLL_DIALOG") { llMessageLinked(LINK_SET, MSG_SHOW_ROLL_DIALOG, dialogPayload, playerKey); - dbg("šŸŽ² [RegMgr] Forwarded roll dialog to " + targetPlayerName); } - } else { - dbg("āš ļø [RegMgr] Player " + targetPlayerName + " has NULL_KEY"); } - } else { - dbg("āš ļø [RegMgr] Player " + targetPlayerName + " not found in registry"); } } return; } if (num == MSG_RESET_ALL && str == "FULL_RESET") { - - players = []; - names = []; - lives = []; - readyPlayers = []; - roundStarted = FALSE; - gameStarting = FALSE; - dbg("šŸŽÆ [RegMgr] Player Registration Manager reset complete"); + resetInProgress = TRUE; + players = []; names = []; lives = []; readyPlayers = []; + roundStarted = FALSE; gameStarting = FALSE; + + // Activate brief lockout timer to ignore stale syncs while Linkset settles + llSetTimerEvent(0.2); + + dbg("šŸŽÆ [RegMgr] Player Registration Manager reset complete (Lockout Active)"); return; } } + + timer() { + resetInProgress = FALSE; + llSetTimerEvent(0); + dbg("šŸŽÆ [RegMgr] Reset lockout complete - sync active."); + } } \ No newline at end of file diff --git a/Roll_ConfettiModule.lsl b/Roll_ConfettiModule.lsl index 8be39b8..927aa3c 100644 --- a/Roll_ConfettiModule.lsl +++ b/Roll_ConfettiModule.lsl @@ -214,7 +214,7 @@ default { string picksDataStr = llList2String(parts, 1); dbg("šŸŽ² [Roll Module] šŸ” [Roll Module] Received picksDataStr: '" + picksDataStr + "'"); - if (picksDataStr == "" || picksDataStr == "EMPTY") { + if (picksDataStr == "" || picksDataStr == "EMPTY" || picksDataStr == "~EMPTY~") { picksData = []; dbg("šŸŽ² [Roll Module] šŸ” [Roll Module] Set picksData to empty"); } else { @@ -270,9 +270,18 @@ default { llSleep(DELAY_CONFETTI_START); rollInProgress = TRUE; // Set roll in progress for bot - shouldRoll = TRUE; // Set flag to perform roll when dice type is received - // Request current dice type from Calculator - llMessageLinked(LINK_SET, MSG_GET_DICE_TYPE, (string)llGetListLength(names), NULL_KEY); + + // NEW: Use the dice type already received from Game Manager at round start! + if (diceType > 0) { + dbg("šŸŽ² [Roll Module] šŸ¤– Bot using stored dice type: d" + (string)diceType); + // Trigger the roll directly + llMessageLinked(LINK_THIS, MSG_ROLL_RESULT, (string)diceType, NULL_KEY); + } else { + // Fallback: If for some reason we don't have it, request it + dbg("šŸŽ² [Roll Module] šŸ¤– Fallback: Bot requesting dice type..."); + shouldRoll = TRUE; + llMessageLinked(LINK_SET, MSG_GET_DICE_TYPE, (string)llGetListLength(names), NULL_KEY); + } } else { // Reset any previous roll state before showing dialog rollInProgress = FALSE; @@ -385,11 +394,12 @@ default { llMessageLinked(LINK_SCOREBOARD, MSG_GAME_STATUS, "Plot Twist", NULL_KEY); llMessageLinked(LINK_SET, MSG_STATUS_TEXT, "PLOT TWIST!\nShield Up! Target shifted!", NULL_KEY); - // IMPORTANT: Send peril player update to scoreboard for glow effect - llMessageLinked(LINK_SCOREBOARD, MSG_UPDATE_PERIL_PLAYER, newPeril, NULL_KEY); // MSG_UPDATE_PERIL_PLAYER - // Add delay to let status display before next phase - llSleep(DELAY_LONG_SYNC); + llSleep(DELAY_LONG_SYNC + DELAY_MEDIUM_SYNC); + + // NEW: Move peril player update signal DOWN to after the sleep + // This prevents Game Manager from starting new round too soon + llMessageLinked(LINK_SET, MSG_UPDATE_PERIL_PLAYER, newPeril, NULL_KEY); // Update floaters immediately to show correct peril player before sync llMessageLinked(LINK_SET, MSG_UPDATE_FLOAT, newPeril, NULL_KEY); @@ -406,9 +416,8 @@ default { llMessageLinked(LINK_SCOREBOARD, MSG_PLAYER_UPDATE, perilPlayer + "|" + (string)(currentLives - 1) + "|" + "NULL_KEY", NULL_KEY); dbg("šŸŽ² [Roll Module] šŸ’— Player lives updated: " + perilPlayer + " now has " + (string)(currentLives - 1) + " lives"); - // NEW: REPORT LIFE REDUCTION to Main Controller (Source of Truth) - // This replaces the unreliable self-syncing and allows the Controller to broadcast the official state. - llMessageLinked(LINK_SET, MSG_UPDATE_LIFE, perilPlayer + "|" + (string)(currentLives - 1), NULL_KEY); + // NEW: Move life update signal DOWN to after the outcome sleeps + // This prevents Game Manager/Controller from starting next round too soon // Check if peril player picked the rolled number list perilPicks = getPicksFor(perilPlayer); @@ -424,21 +433,30 @@ default { // Direct scoreboard status update (texture and text) llMessageLinked(LINK_SCOREBOARD, MSG_GAME_STATUS, "Direct Hit", NULL_KEY); llMessageLinked(LINK_SET, MSG_STATUS_TEXT, "DIRECT HIT!\n" + perilPlayer + " matched!", NULL_KEY); + // Add delay to let status display before next phase - llSleep(DELAY_LONG_SYNC); + llSleep(DELAY_LONG_SYNC + DELAY_MEDIUM_SYNC); + + // SIGNAL DOWN: Now report life reduction to Controller + llMessageLinked(LINK_SET, MSG_UPDATE_LIFE, perilPlayer + "|" + (string)(currentLives - 1), NULL_KEY); } else if (!matched) { llSay(0, "🩸 NO SHIELD! Nobody picked " + resultStr + " - " + perilPlayer + " takes the hit from the d" + (string)diceType + "! 🩸"); // Direct scoreboard status update (texture and text) llMessageLinked(LINK_SCOREBOARD, MSG_GAME_STATUS, "No Shield", NULL_KEY); llMessageLinked(LINK_SET, MSG_STATUS_TEXT, "NO SHIELD!\n" + perilPlayer + " took the hit!", NULL_KEY); + // Add delay to let status display before next phase - llSleep(DELAY_LONG_SYNC); + llSleep(DELAY_LONG_SYNC + DELAY_MEDIUM_SYNC); + + // SIGNAL DOWN: Now report life reduction to Controller + llMessageLinked(LINK_SET, MSG_UPDATE_LIFE, perilPlayer + "|" + (string)(currentLives - 1), NULL_KEY); } else { // This should never happen in normal game flow since Plot Twist case is handled above // But if it does, it means someone else picked it - this should have been Plot Twist dbg("šŸŽ² [Roll Module] āš ļø LOGIC ERROR: Someone picked " + resultStr + " but not handled as Plot Twist!"); llSay(0, "🩸 SHIELD FAILED! " + perilPlayer + " takes the hit despite someone picking " + resultStr + "! 🩸"); llSleep(DELAY_ELIMINATION_NOTICE); + llMessageLinked(LINK_SET, MSG_UPDATE_LIFE, perilPlayer + "|" + (string)(currentLives - 1), NULL_KEY); } llMessageLinked(LINK_SET, MSG_UPDATE_FLOAT, perilPlayer, NULL_KEY); @@ -477,38 +495,8 @@ default { // Note: Win condition checking is handled by Main Controller after elimination - // Validate perilPlayer is not empty before syncing - string perilForSync = perilPlayer; - if (perilForSync == "") { - perilForSync = "NONE"; - } - - // MEMORY OPTIMIZED: Direct sync message construction to avoid temporary string allocations - // Uses 4-part format to remain compatible with Game Manager - llMessageLinked(LINK_SET, MSG_SYNC_GAME_STATE, - llList2CSV(lives) + "~" + - llDumpList2String(picksData, "^") + "~" + - perilForSync + "~" + - llList2CSV(names), NULL_KEY); - llSleep(DELAY_DIALOG_REFRESH); - - // Additional floater updates to ensure correct peril player display - dbg("šŸŽ² [Roll Module] šŸ”„ Updating all floaters with new peril player: " + perilPlayer); - integer f; - for (f = 0; f < llGetListLength(names); f++) { - string fname = llList2String(names, f); - llMessageLinked(LINK_SET, MSG_UPDATE_FLOAT, fname, NULL_KEY); - } - - // After roll is processed and sync is sent, send continuation directly to Game Manager - dbg("šŸŽ² [Roll Module] šŸŽÆ Round complete - sending continuation directly to Game Manager"); - - // Brief delay to ensure sync propagates - llSleep(DELAY_SYNC_PROPAGATION); - - // Send continuation directly to Game Manager (more efficient than through Main Controller) - // For non-elimination cases, continue with current peril player - llMessageLinked(LINK_SET, MSG_CONTINUE_GAME, perilForSync, NULL_KEY); + // Note: State sync and round continuation are now handled exclusively by the Main Controller + // after it receives the Damage/Elimination reports, ensuring a single source of truth. // Clear roll protection after processing is complete rollInProgress = FALSE;