diff --git a/Bot_Manager.lsl b/Bot_Manager.lsl index 7a0c17d..67004fa 100644 --- a/Bot_Manager.lsl +++ b/Bot_Manager.lsl @@ -57,6 +57,9 @@ list processedBotCommands = []; // Track sent BOT_PICKED messages to prevent duplicate sending list sentBotMessages = []; +// Verbose logging control - toggled by owner +integer VERBOSE_LOGGING = FALSE; + // Helper function to check and report memory usage checkMemoryUsage(string context) { integer usedMemory = llGetUsedMemory(); @@ -71,7 +74,7 @@ checkMemoryUsage(string context) { // Helper to parse and respond to pick commands doBotPick(string botName, integer count, integer diceMax, list avoidNumbers) { - llOwnerSay("[Bot Manager] ๐Ÿ”ง doBotPick ENTRY - botName:" + botName + " count:" + (string)count + " diceMax:" + (string)diceMax); + if (VERBOSE_LOGGING) llOwnerSay("[Bot Manager] ๐Ÿ”ง doBotPick ENTRY - botName:" + botName + " count:" + (string)count + " diceMax:" + (string)diceMax); checkMemoryUsage("doBotPick start"); // Validate bot exists and is still alive @@ -80,14 +83,14 @@ doBotPick(string botName, integer count, integer diceMax, list avoidNumbers) { llOwnerSay("[Bot Manager] โŒ Bot '" + botName + "' not found in game - ignoring command"); return; } - llOwnerSay("[Bot Manager] ๐Ÿ”ง Bot found at index:" + (string)botIdx); + if (VERBOSE_LOGGING) llOwnerSay("[Bot Manager] ๐Ÿ”ง Bot found at index:" + (string)botIdx); integer botLives = llList2Integer(lives, botIdx); if (botLives <= 0) { llOwnerSay("[Bot Manager] โŒ Bot '" + botName + "' is eliminated (lives=" + (string)botLives + ") - ignoring command"); return; } - llOwnerSay("[Bot Manager] ๐Ÿ”ง Bot lives check passed:" + (string)botLives); + if (VERBOSE_LOGGING) llOwnerSay("[Bot Manager] ๐Ÿ”ง Bot lives check passed:" + (string)botLives); // IMPROVED ALGORITHM: Calculate available numbers first list availableNumbers = []; @@ -143,8 +146,8 @@ doBotPick(string botName, integer count, integer diceMax, list avoidNumbers) { string response = "BOT_PICKED:" + botName + ":" + pickString; // Check if we've already sent this exact message to prevent duplicates - llOwnerSay("[Bot Manager] ๐Ÿ” DEBUG - Checking if message already sent: " + response); - llOwnerSay("[Bot Manager] ๐Ÿ” DEBUG - sentBotMessages list: " + llDumpList2String(sentBotMessages, "|")); + if (VERBOSE_LOGGING) llOwnerSay("[Bot Manager] ๐Ÿ” DEBUG - Checking if message already sent: " + response); + if (VERBOSE_LOGGING) llOwnerSay("[Bot Manager] ๐Ÿ” DEBUG - sentBotMessages list: " + llDumpList2String(sentBotMessages, "|")); if (llListFindList(sentBotMessages, [response]) != -1) { llOwnerSay("[Bot Manager] โš ๏ธ DUPLICATE SEND - Already sent message: " + response); return; @@ -152,7 +155,7 @@ doBotPick(string botName, integer count, integer diceMax, list avoidNumbers) { // Mark this message as sent BEFORE sending to prevent race conditions sentBotMessages += [response]; - llOwnerSay("[Bot Manager] ๐Ÿ” DEBUG - Added to sentBotMessages, new list: " + llDumpList2String(sentBotMessages, "|")); + if (VERBOSE_LOGGING) llOwnerSay("[Bot Manager] ๐Ÿ” DEBUG - Added to sentBotMessages, new list: " + llDumpList2String(sentBotMessages, "|")); llOwnerSay("[Bot Manager] ๐Ÿค– " + botName + " picked: " + llList2CSV(picks)); llOwnerSay("[Bot Manager] ๐Ÿ“ฌ SENDING BOT_PICKED: " + response); @@ -234,6 +237,20 @@ default { } link_message(integer sender, integer num, string str, key id) { + // Handle verbose logging toggle from Main Controller + if (num == 9011 && llSubStringIndex(str, "VERBOSE_LOGGING|") == 0) { + list parts = llParseString2List(str, ["|"], []); + if (llGetListLength(parts) >= 2) { + VERBOSE_LOGGING = (integer)llList2String(parts, 1); + if (VERBOSE_LOGGING) { + llOwnerSay("๐Ÿ” [Bot Manager] Verbose logging ON"); + } else { + llOwnerSay("๐Ÿ” [Bot Manager] Verbose logging OFF"); + } + } + return; + } + // Handle full reset from main controller if (num == -99999 && str == "FULL_RESET") { // Reset bot manager state @@ -261,10 +278,11 @@ default { } names = llCSV2List(llList2String(parts, 3)); - // Clear processed commands when peril player changes (new round) + // Clear processed commands and sent messages when peril player changes (new round) if (oldPerilPlayer != perilPlayer && perilPlayer != "") { processedBotCommands = []; - llOwnerSay("[Bot Manager] New round detected, cleared processed commands"); + sentBotMessages = []; + llOwnerSay("[Bot Manager] New round detected, cleared processed commands and sent messages"); } } return; @@ -309,7 +327,7 @@ default { } } // Debug: Show the game state before calling doBotPick - llOwnerSay("[Bot Manager] ๐Ÿ”ง Game state - names:" + llList2CSV(names) + " lives:" + llList2CSV(lives)); + if (VERBOSE_LOGGING) llOwnerSay("[Bot Manager] ๐Ÿ”ง Game state - names:" + llList2CSV(names) + " lives:" + llList2CSV(lives)); doBotPick(botName, count, diceMax, avoidNumbers); } } diff --git a/Controller_Memory.lsl b/Controller_Memory.lsl index 7df9d21..4f3996f 100644 --- a/Controller_Memory.lsl +++ b/Controller_Memory.lsl @@ -2,6 +2,10 @@ // Handles memory monitoring and reporting for the Main Controller // Communicates with other scripts via link messages +// Verbose logging control +integer VERBOSE_LOGGING = TRUE; // Global flag for verbose debug logs +integer MSG_TOGGLE_VERBOSE_LOGS = 9998; // Message to toggle verbose logging + // Memory monitoring system float MEMORY_WARNING_THRESHOLD = 0.8; // Warn at 80% usage float MEMORY_CRITICAL_THRESHOLD = 0.9; // Critical at 90% usage @@ -33,9 +37,11 @@ checkMemoryUsage(string context) { } else if (memoryPercentage > MEMORY_WARNING_THRESHOLD) { // Only warn once per interval to avoid spam if ((currentTime - lastMemoryCheck) >= MEMORY_CHECK_INTERVAL) { - llOwnerSay("โš ๏ธ [Memory Monitor] High memory in " + context + ": " + - (string)usedMemory + " bytes (" + - (string)llRound(memoryPercentage * 100.0) + "% of 64KB limit)"); + if (VERBOSE_LOGGING) { + llOwnerSay("โš ๏ธ [Memory Monitor] High memory in " + context + ": " + + (string)usedMemory + " bytes (" + + (string)llRound(memoryPercentage * 100.0) + "% of 64KB limit)"); + } lastMemoryCheck = currentTime; } } @@ -131,6 +137,17 @@ default { } link_message(integer sender, integer num, string str, key id) { + // Handle verbose logging toggle + if (num == MSG_TOGGLE_VERBOSE_LOGS) { + VERBOSE_LOGGING = !VERBOSE_LOGGING; + if (VERBOSE_LOGGING) { + llOwnerSay("๐Ÿ”Š [Memory Monitor] Verbose logging ENABLED"); + } else { + llOwnerSay("๐Ÿ”Š [Memory Monitor] Verbose logging DISABLED"); + } + return; + } + if (num == MSG_MEMORY_CHECK) { // Manual memory check request checkMemoryUsage(str); diff --git a/Controller_MessageHandler.lsl b/Controller_MessageHandler.lsl index 49b7cdb..03bce6a 100644 --- a/Controller_MessageHandler.lsl +++ b/Controller_MessageHandler.lsl @@ -2,6 +2,10 @@ // Handles non-critical message processing for the Main Controller // Reduces Main Controller size and improves modularity +// Verbose logging control +integer VERBOSE_LOGGING = TRUE; // Global flag for verbose debug logs +integer MSG_TOGGLE_VERBOSE_LOGS = 9998; // Message to toggle verbose logging + // Message constants for communication with main controller integer MSG_REQUEST_PLAYER_DATA = 8001; integer MSG_REQUEST_PICK_DATA = 8002; @@ -106,13 +110,17 @@ handlePickAction(string actionData) { if (action == "ADD_PICK") { if (llListFindList(pickList, [pick]) == -1) { pickList += [pick]; - llOwnerSay("โž• [Message Handler] Added " + pick + " to " + name); + if (VERBOSE_LOGGING) { + llOwnerSay("โž• [Message Handler] Added " + pick + " to " + name); + } } } else if (action == "REMOVE_PICK") { integer idx = llListFindList(pickList, [pick]); if (idx != -1) { pickList = llDeleteSubList(pickList, idx, idx); - llOwnerSay("โž– [Message Handler] Removed " + pick + " from " + name); + if (VERBOSE_LOGGING) { + llOwnerSay("โž– [Message Handler] Removed " + pick + " from " + name); + } } } @@ -128,7 +136,9 @@ handlePickAction(string actionData) { // Handle dice type requests handleDiceTypeRequest(key requester) { - llOwnerSay("๐ŸŽฒ [Message Handler] Sending dice type: " + (string)diceType); + if (VERBOSE_LOGGING) { + llOwnerSay("๐ŸŽฒ [Message Handler] Sending dice type: " + (string)diceType); + } llMessageLinked(LINK_SET, 102, (string)diceType, requester); // MSG_ROLL_RESULT } @@ -142,16 +152,19 @@ handleLeaveGameRequest(string requestData) { integer idx = llListFindList(names, [leavingName]); // Find by name instead of key if (idx != -1) { - // Remove player's float using the tracked channel - integer ch = FLOATER_BASE_CHANNEL + idx; - llMessageLinked(LINK_SET, MSG_CLEANUP_FLOAT, (string)ch, NULL_KEY); - - // Update local lists + // Update local lists FIRST to prevent race conditions players = llDeleteSubList(players, idx, idx); names = llDeleteSubList(names, idx, idx); lives = llDeleteSubList(lives, idx, idx); picksData = llDeleteSubList(picksData, idx, idx); + // THEN send floater cleanup using the previously calculated channel + // Note: We DON'T send MSG_CLEANUP_FLOAT here anymore to avoid double-cleanup + // The Main Controller will handle floater cleanup after list synchronization + if (VERBOSE_LOGGING) { + llOwnerSay("๐Ÿ“ [Message Handler] Updated local lists for " + leavingName + ", deferring floater cleanup to Main Controller"); + } + // Remove from ready list if present integer readyIdx = llListFindList(readyPlayers, [leavingName]); if (readyIdx != -1) { @@ -172,7 +185,9 @@ handleLeaveGameRequest(string requestData) { // Check if game should end (0 players = reset, 1 player = victory) if (llGetListLength(names) == 0) { - llOwnerSay("๐Ÿ”„ [Message Handler] All players left - game will reset"); + if (VERBOSE_LOGGING) { + llOwnerSay("๐Ÿ”„ [Message Handler] All players left - game will reset"); + } // Main Controller will handle the reset } else if (llGetListLength(names) == 1 && roundStarted && perilPlayer != "" && perilPlayer != "NONE") { // Only declare victory if the game has actually started (round active and peril player assigned) @@ -183,7 +198,9 @@ handleLeaveGameRequest(string requestData) { } else if (llGetListLength(names) == 1) { // Game not started yet, just inform but don't declare victory string remainingPlayer = llList2String(names, 0); - llOwnerSay("๐Ÿ“ [Message Handler] Only " + remainingPlayer + " remains, but game hasn't started yet"); + if (VERBOSE_LOGGING) { + llOwnerSay("๐Ÿ“ [Message Handler] Only " + remainingPlayer + " remains, but game hasn't started yet"); + } } } } @@ -192,6 +209,23 @@ handleLeaveGameRequest(string requestData) { // Synchronize game state from Main Controller syncGameState(string stateData) { list parts = llParseString2List(stateData, ["~"], []); + + // Handle special RESET sync message + if (llGetListLength(parts) >= 5 && llList2String(parts, 0) == "RESET") { + if (VERBOSE_LOGGING) { + llOwnerSay("๐Ÿ”„ [Message Handler] Received reset sync - clearing all state"); + } + // Clear all state during reset + players = []; + names = []; + lives = []; + picksData = []; + readyPlayers = []; + perilPlayer = ""; + roundStarted = FALSE; + return; + } + if (llGetListLength(parts) >= 4) { lives = llCSV2List(llList2String(parts, 0)); @@ -227,6 +261,19 @@ syncGameState(string stateData) { } names = llCSV2List(llList2String(parts, 3)); + + // SAFETY: Validate peril player exists in current names list AFTER updating names + if (perilPlayer != "" && perilPlayer != "NONE") { + integer perilIdx = llListFindList(names, [perilPlayer]); + if (perilIdx == -1) { + // Only warn if we actually have players - during reset this is expected + if (llGetListLength(names) > 0 && VERBOSE_LOGGING) { + llOwnerSay("โš ๏ธ [Message Handler] WARNING: Peril player '" + perilPlayer + "' not found in names list - clearing peril status"); + } + perilPlayer = ""; + roundStarted = FALSE; + } + } } } @@ -237,6 +284,17 @@ default { } link_message(integer sender, integer num, string str, key id) { + // Handle verbose logging toggle + if (num == MSG_TOGGLE_VERBOSE_LOGS) { + VERBOSE_LOGGING = !VERBOSE_LOGGING; + if (VERBOSE_LOGGING) { + llOwnerSay("๐Ÿ”Š [Message Handler] Verbose logging ENABLED"); + } else { + llOwnerSay("๐Ÿ”Š [Message Handler] Verbose logging DISABLED"); + } + return; + } + // Handle player list requests (for pick management only) if (num == 202 && str == "REQUEST_PLAYER_LIST") { handlePlayerListRequest(id); @@ -247,9 +305,11 @@ default { if (num == 8009 && str == "REQUEST_PLAYER_LIST_KICK") { // Send back kick-specific message ID to avoid conflict with pick management string namesCSV = llList2CSV(names); - llOwnerSay("๐Ÿ” [Message Handler] Received request: '" + str + "'"); - llOwnerSay("๐Ÿ” [Message Handler] Names list: " + llDumpList2String(names, ", ")); - llOwnerSay("๐Ÿ” [Message Handler] Sending kick player list: '" + namesCSV + "'"); + if (VERBOSE_LOGGING) { + llOwnerSay("๐Ÿ” [Message Handler] Received request: '" + str + "'"); + llOwnerSay("๐Ÿ” [Message Handler] Names list: " + llDumpList2String(names, ", ")); + llOwnerSay("๐Ÿ” [Message Handler] Sending kick player list: '" + namesCSV + "'"); + } llMessageLinked(LINK_SET, 8009, namesCSV, id); return; } diff --git a/Floater_Manager.lsl b/Floater_Manager.lsl index 30227a7..64b2d21 100644 --- a/Floater_Manager.lsl +++ b/Floater_Manager.lsl @@ -9,6 +9,10 @@ integer MSG_REZ_FLOAT = 105; integer MSG_REGISTER_PLAYER = 106; integer MSG_SYNC_GAME_STATE = 107; +// Debug control message constants +integer MSG_DEBUG_PICKS_ON = 7001; +integer MSG_DEBUG_PICKS_OFF = 7002; + // ============================================================================= // DYNAMIC CHANNEL CONFIGURATION FOR FLOATERS // ============================================================================= @@ -44,6 +48,12 @@ initializeChannels() { // Maximum number of players allowed in the game integer MAX_PLAYERS = 10; +// Debug control - set to TRUE for verbose pick debugging, FALSE for normal operation +integer DEBUG_PICKS = FALSE; + +// Verbose logging control - affects general debug messages, toggled by owner +integer VERBOSE_LOGGING = FALSE; + list players = []; list names = []; list lives = []; @@ -52,9 +62,13 @@ string perilPlayer = ""; // Returns a list of picks for the given player name, filtering out invalid values list getPicksFor(string nameInput) { + // ENHANCED DEBUG: Log the request and current data state + if (DEBUG_PICKS) llOwnerSay("๐Ÿ” [Floater Manager] getPicksFor(" + nameInput + "), picksData entries: " + (string)llGetListLength(picksData)); + integer i; for (i = 0; i < llGetListLength(picksData); i++) { string entry = llList2String(picksData, i); + if (DEBUG_PICKS) llOwnerSay("๐Ÿ” [Floater Manager] Examining entry [" + (string)i + "]: " + entry); if (llSubStringIndex(entry, "|") == -1) { if (entry != "") llOwnerSay("โš ๏ธ Malformed picks entry (missing pipe): " + entry); @@ -69,25 +83,45 @@ list getPicksFor(string nameInput) { } else if (llList2String(parts, 0) == nameInput) { string pickString = llList2String(parts, 1); + if (DEBUG_PICKS) llOwnerSay("๐Ÿ” [Floater Manager] Found match for " + nameInput + ", pickString: '" + pickString + "'"); + if (pickString == "") { // Empty picks are normal during initialization, don't warn + if (DEBUG_PICKS) llOwnerSay("๐Ÿ” [Floater Manager] Empty picks for " + nameInput + ", returning []"); return []; } else { - // Convert semicolons back to commas for display - list pickList = llParseString2List(pickString, [";"], []); + // IMPROVED PARSING: Handle both comma AND semicolon delimited picks + // First, try parsing as comma-separated (most common format from Game Manager) + list pickList = llParseString2List(pickString, [","], []); + + // If that gives us only one item, try semicolon-separated + if (llGetListLength(pickList) == 1 && llSubStringIndex(pickString, ";") != -1) { + pickList = llParseString2List(pickString, [";"], []); + if (DEBUG_PICKS) llOwnerSay("๐Ÿ” [Floater Manager] Using semicolon parsing for: " + pickString); + } else { + if (DEBUG_PICKS) llOwnerSay("๐Ÿ” [Floater Manager] Using comma parsing for: " + pickString + ", got " + (string)llGetListLength(pickList) + " picks"); + } + list filtered = []; integer j; for (j = 0; j < llGetListLength(pickList); j++) { string val = llStringTrim(llList2String(pickList, j), STRING_TRIM); if (val != "" && (string)((integer)val) == val) { filtered += [val]; + if (DEBUG_PICKS) llOwnerSay("๐Ÿ” [Floater Manager] Added valid pick: " + val); + } else if (val != "") { + if (DEBUG_PICKS) llOwnerSay("โš ๏ธ [Floater Manager] Filtered out invalid pick: '" + val + "'"); } } + + if (DEBUG_PICKS) llOwnerSay("๐Ÿ” [Floater Manager] Final filtered picks for " + nameInput + ": " + llList2CSV(filtered)); return filtered; } } } } + + if (DEBUG_PICKS) llOwnerSay("๐Ÿ” [Floater Manager] No picks found for " + nameInput + ", returning []"); return []; } @@ -142,6 +176,20 @@ default { return; } + // Handle verbose logging toggle from Main Controller + if (num == 9011 && llSubStringIndex(str, "VERBOSE_LOGGING|") == 0) { + list parts = llParseString2List(str, ["|"], []); + if (llGetListLength(parts) >= 2) { + VERBOSE_LOGGING = (integer)llList2String(parts, 1); + if (VERBOSE_LOGGING) { + llOwnerSay("๐Ÿ” [Floater Manager] Verbose logging ON"); + } else { + llOwnerSay("๐Ÿ” [Floater Manager] Verbose logging OFF"); + } + } + return; + } + if (num == MSG_REGISTER_PLAYER) { // Enforce the maximum number of players if (llGetListLength(players) >= MAX_PLAYERS) { @@ -221,13 +269,17 @@ default { else if (num == MSG_UPDATE_FLOAT) { string name = str; integer idx = llListFindList(names, [name]); - if (idx == -1) return; + if (idx == -1) { + llOwnerSay("โš ๏ธ [Floater Manager] MSG_UPDATE_FLOAT: Player '" + name + "' not found in names list"); + return; + } key avKey = llList2Key(players, idx); integer ch = FLOATER_BASE_CHANNEL + idx; integer lifeCount = llList2Integer(lives, idx); + if (VERBOSE_LOGGING) llOwnerSay("๐Ÿ”„ [Floater Manager] Updating floater for " + name + " (lives: " + (string)lifeCount + ")"); list picks = getPicksFor(name); - + if (VERBOSE_LOGGING) llOwnerSay("๐Ÿ”„ [Floater Manager] Retrieved picks for " + name + ": " + llList2CSV(picks)); string perilName; // Show proper peril status based on current game state @@ -261,29 +313,76 @@ default { // Always send cleanup message to the channel (even for orphaned floaters) llRegionSay(ch, "CLEANUP"); - // Only clean up internal lists if this corresponds to a valid player + // IMPROVED: Clean up internal lists more carefully to handle race conditions + // Instead of relying on index calculation, find by channel and remove consistently if (idx >= 0 && idx < llGetListLength(players)) { - players = llDeleteSubList(players, idx, idx); - names = llDeleteSubList(names, idx, idx); - lives = llDeleteSubList(lives, idx, idx); - picksData = llDeleteSubList(picksData, idx, idx); + // Store player info before removal for debugging + string removedPlayer = ""; + if (idx < llGetListLength(names)) { + removedPlayer = llList2String(names, idx); + } + + // Remove from all lists at the same index to maintain synchronization + if (idx < llGetListLength(players)) players = llDeleteSubList(players, idx, idx); + if (idx < llGetListLength(names)) names = llDeleteSubList(names, idx, idx); + if (idx < llGetListLength(lives)) lives = llDeleteSubList(lives, idx, idx); + if (idx < llGetListLength(picksData)) picksData = llDeleteSubList(picksData, idx, idx); + + if (removedPlayer != "") { + llOwnerSay("๐Ÿงน [Floater Manager] Cleaned up floater for: " + removedPlayer + " (channel: " + (string)ch + ")"); + + // CRITICAL: Update all remaining floaters to refresh peril status after player removal + // This prevents stale peril player information from persisting in floaters + llSleep(0.2); // Brief delay to ensure cleanup completes + integer i; + for (i = 0; i < llGetListLength(names); i++) { + string remainingPlayerName = llList2String(names, i); + key remainingPlayerKey = NULL_KEY; + if (i < llGetListLength(players)) { + remainingPlayerKey = llList2Key(players, i); + } + llMessageLinked(LINK_SET, MSG_UPDATE_FLOAT, remainingPlayerName, remainingPlayerKey); + } + } + } else { + // Orphaned floater cleanup + llOwnerSay("๐Ÿงน [Floater Manager] Cleaned up orphaned floater (channel: " + (string)ch + ")"); } } else if (num == MSG_SYNC_GAME_STATE) { // Synchronize the lists for lives and picksData when receiving a new game state list parts = llParseString2List(str, ["~"], []); + + // Handle special RESET sync message + if (llGetListLength(parts) >= 5 && llList2String(parts, 0) == "RESET") { + llOwnerSay("๐Ÿ”„ [Floater Manager] Received reset sync - ignoring during reset"); + return; + } + if (llGetListLength(parts) < 4) { llOwnerSay("โš ๏ธ Floater Manager: Incomplete sync message received, parts: " + (string)llGetListLength(parts)); return; } string livesStr = llList2String(parts, 0); lives = llCSV2List(livesStr); - // Use ^ delimiter for picksData to avoid comma conflicts + + // ENHANCED DEBUG: Log the received picks data string picksDataStr = llList2String(parts, 1); + if (VERBOSE_LOGGING) llOwnerSay("๐Ÿ“ฅ [Floater Manager] Received picks data: '" + picksDataStr + "'"); + + // Use ^ delimiter for picksData to avoid comma conflicts if (picksDataStr == "" || picksDataStr == "EMPTY") { picksData = []; + if (VERBOSE_LOGGING) llOwnerSay("๐Ÿ“ฅ [Floater Manager] Picks data empty, cleared picksData list"); } else { picksData = llParseString2List(picksDataStr, ["^"], []); + if (VERBOSE_LOGGING) { + llOwnerSay("๐Ÿ“ฅ [Floater Manager] Parsed " + (string)llGetListLength(picksData) + " picks data entries:"); + integer debugIdx; + for (debugIdx = 0; debugIdx < llGetListLength(picksData); debugIdx++) { + llOwnerSay(" [" + (string)debugIdx + "]: " + llList2String(picksData, debugIdx)); + } + } } string receivedPeril = llList2String(parts, 2); string oldPeril = perilPlayer; @@ -307,10 +406,9 @@ default { llOwnerSay("๐ŸŽฏ Floater Manager: Peril player updated from '" + oldPeril + "' to '" + perilPlayer + "'"); } - // After synchronizing the game state, update all existing floats so - // they reflect the current peril status. Without this, floats - // created in previous rounds may continue to display an old - // peril name until another manual update occurs. + // CRITICAL: After synchronizing the game state, ALWAYS update all existing floats + // This ensures they reflect the current peril status and prevents desynchronization + // Force update even if peril player didn't change - other state may have changed integer idx; for (idx = 0; idx < llGetListLength(names); idx++) { string n = llList2String(names, idx); @@ -323,6 +421,33 @@ default { } llMessageLinked(LINK_SET, MSG_UPDATE_FLOAT, n, k); } + + // Additional safety: If there was a peril player change, add a brief delay and update again + // This handles edge cases where floater updates might race with other state changes + if (oldPeril != perilPlayer && perilPlayer != "" && perilPlayer != "NONE") { + llOwnerSay("๐Ÿ”„ [Floater Manager] Peril player changed - scheduling secondary update for stability"); + llSleep(0.3); // Brief delay to let initial updates complete + for (idx = 0; idx < llGetListLength(names); idx++) { + string n = llList2String(names, idx); + key k; + if (idx < llGetListLength(players)) { + k = llList2Key(players, idx); + } else { + k = NULL_KEY; + } + llMessageLinked(LINK_SET, MSG_UPDATE_FLOAT, n, k); + } + } + } + + // Handle debug control messages + else if (num == MSG_DEBUG_PICKS_ON) { + DEBUG_PICKS = TRUE; + llOwnerSay("๐Ÿ” [Floater Manager] Pick debugging ENABLED"); + } + else if (num == MSG_DEBUG_PICKS_OFF) { + DEBUG_PICKS = FALSE; + llOwnerSay("๐Ÿ”‡ [Floater Manager] Pick debugging DISABLED"); } } -} \ No newline at end of file +} diff --git a/Game_Calculator.lsl b/Game_Calculator.lsl index 355863a..79370ca 100644 --- a/Game_Calculator.lsl +++ b/Game_Calculator.lsl @@ -3,6 +3,10 @@ // This version chooses a dice size based on the number of players, // ensuring at least three numbers are available per player. +// Verbose logging control +integer VERBOSE_LOGGING = TRUE; // Global flag for verbose debug logs +integer MSG_TOGGLE_VERBOSE_LOGS = 9998; // Message to toggle verbose logging + integer MSG_GET_DICE_TYPE = 1001; integer MSG_DICE_TYPE_RESULT = 1005; integer MSG_GET_PICKS_REQUIRED = 1002; @@ -80,12 +84,16 @@ integer showPickManager(string player, key id) { string testName = llList2String(names, i); if (llSubStringIndex(testName, searchName) != -1) { idx = i; - llOwnerSay("โœ… Matched pick list for: " + testName); + if (VERBOSE_LOGGING) { + llOwnerSay("โœ… Matched pick list for: " + testName); + } i = llGetListLength(names); } } if (idx == -1) { - llOwnerSay("โš ๏ธ Could not find player: " + player); + if (VERBOSE_LOGGING) { + llOwnerSay("โš ๏ธ Could not find player: " + player); + } return 0; } } @@ -111,7 +119,9 @@ integer showPickManager(string player, key id) { } // Don't show hardcoded dialog - let the Owner Dialog Handler manage this - llOwnerSay("๐Ÿงฎ Calculator delegating pick management to dialog handler for: " + (string)id); + if (VERBOSE_LOGGING) { + llOwnerSay("๐Ÿงฎ Calculator delegating pick management to dialog handler for: " + (string)id); + } // Return without showing dialog - this should be handled by the proper dialog handler // Return dummy value since LSL does not have a void return type @@ -130,7 +140,9 @@ default { } on_rez(integer start_param) { - llOwnerSay("๐Ÿ”„ Game Calculator rezzed - reinitializing..."); + if (VERBOSE_LOGGING) { + llOwnerSay("๐Ÿ”„ Game Calculator rezzed - reinitializing..."); + } // Reset all state variables on rez lives = []; @@ -139,10 +151,23 @@ default { names = []; pickQueue = []; - llOwnerSay("โœ… Game Calculator reset complete after rez!"); + if (VERBOSE_LOGGING) { + llOwnerSay("โœ… Game Calculator reset complete after rez!"); + } } link_message(integer sender, integer num, string str, key id) { + // Handle verbose logging toggle + if (num == MSG_TOGGLE_VERBOSE_LOGS) { + VERBOSE_LOGGING = !VERBOSE_LOGGING; + if (VERBOSE_LOGGING) { + llOwnerSay("๐Ÿ”Š [Game Calculator] Verbose logging ENABLED"); + } else { + llOwnerSay("๐Ÿ”Š [Game Calculator] Verbose logging DISABLED"); + } + return; + } + // Handle full reset from main controller if (num == -99999 && str == "FULL_RESET") { // Reset helper state @@ -157,8 +182,20 @@ default { if (num == MSG_SYNC_GAME_STATE) { list parts = llParseString2List(str, ["~"], []); + + // Handle special RESET sync message + if (llGetListLength(parts) >= 5 && llList2String(parts, 0) == "RESET") { + if (VERBOSE_LOGGING) { + llOwnerSay("๐Ÿ”„ [Game Calculator] Received reset sync - ignoring during reset"); + } + return; + } + if (llGetListLength(parts) < 4) { - llOwnerSay("โš ๏ธ Game Calculator: Incomplete sync message received, parts: " + (string)llGetListLength(parts)); + if (VERBOSE_LOGGING) { + llOwnerSay("โš ๏ธ Game Calculator: Incomplete sync message received, parts: " + (string)llGetListLength(parts)); + llOwnerSay("โš ๏ธ Incomplete game state received."); + } return; } lives = llCSV2List(llList2String(parts, 0)); @@ -183,7 +220,9 @@ default { } else if (num == MSG_GET_DICE_TYPE) { integer playerCount = (integer)str; - llOwnerSay("๐Ÿ“ˆ Calculator: Calculating dice type for " + (string)playerCount + " players"); + if (VERBOSE_LOGGING) { + llOwnerSay("๐Ÿ“ˆ Calculator: Calculating dice type for " + (string)playerCount + " players"); + } integer result = getDiceType(playerCount); // Send result only to the requester (sender), not broadcast to everyone llMessageLinked(sender, MSG_DICE_TYPE_RESULT, (string)result, NULL_KEY); diff --git a/Game_Manager.lsl b/Game_Manager.lsl index f6af518..ddd789b 100644 --- a/Game_Manager.lsl +++ b/Game_Manager.lsl @@ -31,12 +31,16 @@ 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 lastSyncProcessTime = 0; // Track when we last processed a sync to prevent rapid duplicates // Duplicate request prevention string lastHumanPickMessage = ""; string lastBotPickMessage = ""; integer lastProcessTime = 0; +// Verbose logging control - toggled by owner +integer VERBOSE_LOGGING = FALSE; + // Message constants integer MSG_SHOW_DIALOG = 101; integer MSG_SHOW_ROLL_DIALOG = 301; @@ -48,12 +52,53 @@ integer MSG_PLAYER_WON = 551; integer MSG_UPDATE_FLOAT = 2010; continueCurrentRound() { + // CRITICAL PROTECTION: Don't start rounds if game is ending or players are eliminated + if (llGetListLength(names) <= 1) { + llOwnerSay("๐Ÿ›‘ [Game Manager] continueCurrentRound: Game ending (" + (string)llGetListLength(names) + " players) - aborting round start"); + return; + } + + // Check for eliminated players (0 lives) - don't continue if any exist + integer i; + for (i = 0; i < llGetListLength(names); i++) { + string playerName = llList2String(names, i); + integer playerIdx = llListFindList(names, [playerName]); + if (playerIdx != -1 && playerIdx < llGetListLength(lives)) { + integer playerLives = llList2Integer(lives, playerIdx); + if (playerLives <= 0) { + llOwnerSay("๐Ÿ›‘ [Game Manager] continueCurrentRound: Found eliminated player " + playerName + " with " + (string)playerLives + " lives - aborting round start"); + return; + } + } + } + + if (perilPlayer == "" || perilPlayer == "NONE") { + llOwnerSay("๐Ÿ›‘ [Game Manager] continueCurrentRound: Invalid peril player (" + perilPlayer + ") - aborting round start"); + return; + } + + // Verify peril player exists and has lives + integer perilIdx = llListFindList(names, [perilPlayer]); + if (perilIdx == -1) { + llOwnerSay("๐Ÿ›‘ [Game Manager] continueCurrentRound: Peril player " + perilPlayer + " not found in names list - aborting round start"); + return; + } + if (perilIdx >= llGetListLength(lives)) { + llOwnerSay("๐Ÿ›‘ [Game Manager] continueCurrentRound: Peril player " + perilPlayer + " index out of bounds - aborting round start"); + return; + } + integer perilLives = llList2Integer(lives, perilIdx); + if (perilLives <= 0) { + llOwnerSay("๐Ÿ›‘ [Game Manager] continueCurrentRound: Peril player " + perilPlayer + " has " + (string)perilLives + " lives - aborting round start"); + return; + } + // ALWAYS start fresh for a new round - clear all picks and global picked numbers // After a roll, we need everyone to pick new numbers picksData = []; globalPickedNumbers = []; - llOwnerSay("๐Ÿ”„ [Game Manager] continueCurrentRound: Cleared picks data, starting fresh"); + llOwnerSay("๐Ÿ”„ [Game Manager] continueCurrentRound: All validation passed, starting fresh round with " + (string)llGetListLength(names) + " players"); // Set round as started for the new round roundStarted = TRUE; @@ -61,7 +106,6 @@ continueCurrentRound() { // Create new pick queue with peril player first pickQueue = [perilPlayer]; - integer i; for (i = 0; i < llGetListLength(names); i++) { string playerName = llList2String(names, i); if (playerName != perilPlayer) { @@ -146,6 +190,12 @@ startNextRound() { } showNextPickerDialog() { + // CRITICAL PROTECTION: Don't show dialogs if game is ending or players eliminated + if (llGetListLength(names) <= 1) { + llOwnerSay("๐Ÿ›‘ [Game Manager] showNextPickerDialog: Game ending (" + (string)llGetListLength(names) + " players) - aborting dialog"); + return; + } + if (diceType <= 0) { llOwnerSay("โŒ Cannot show picker dialog: diceType not set (" + (string)diceType + ")"); return; @@ -162,6 +212,24 @@ showNextPickerDialog() { return; } + // CRITICAL: Verify this player is still alive and in the game + integer playerIdx = llListFindList(names, [firstName]); + if (playerIdx == -1) { + llOwnerSay("โŒ Cannot show picker dialog: player " + firstName + " not found in current game"); + return; + } + + if (playerIdx >= llGetListLength(lives)) { + llOwnerSay("โŒ Cannot show picker dialog: player " + firstName + " index out of bounds for lives list"); + return; + } + + integer playerLives = llList2Integer(lives, playerIdx); + if (playerLives <= 0) { + llOwnerSay("โŒ Cannot show picker dialog: player " + firstName + " has been eliminated (" + (string)playerLives + " lives)"); + return; + } + // CHECK: Don't show dialog if this player already has picks to prevent loops integer alreadyHasPicks = FALSE; integer i; @@ -371,10 +439,17 @@ default { } } + // Handle special RESET sync message + if (llGetListLength(parts) >= 5 && llList2String(parts, 0) == "RESET") { + llOwnerSay("๐Ÿ”„ [Game Manager] Received reset sync - ignoring during reset"); + return; + } + if (llGetListLength(parts) < 4) { llOwnerSay("โš ๏ธ [Game Manager] Incomplete sync message received, parts: " + (string)llGetListLength(parts) + " - IGNORING"); return; - } else { + } + { list newLives = llCSV2List(llList2String(parts, 0)); string picksDataStr = llList2String(parts, 1); @@ -463,36 +538,91 @@ default { if (llGetListLength(newNames) == 1) { string winner = llList2String(newNames, 0); llOwnerSay("๐Ÿ† [Game Manager] Victory detected: " + winner + " wins!"); - // Reset round state to prevent loops + // IMMEDIATE FULL STATE RESET to prevent any further round processing roundStarted = FALSE; perilPlayer = ""; + currentPickerIdx = 0; + pickQueue = []; + diceTypeProcessed = FALSE; + roundContinueInProgress = FALSE; + llOwnerSay("๐Ÿ”’ [Game Manager] Game state locked - no further round processing until reset"); // Let Main Controller handle the victory celebration and reset return; } else { llOwnerSay("๐Ÿ’€ [Game Manager] No survivors remain!"); - // Reset round state to prevent loops + // IMMEDIATE FULL STATE RESET to prevent any further round processing roundStarted = FALSE; perilPlayer = ""; + currentPickerIdx = 0; + pickQueue = []; + diceTypeProcessed = FALSE; + roundContinueInProgress = FALSE; + llOwnerSay("๐Ÿ”’ [Game Manager] Game state locked - no further round processing until reset"); return; } } - // STOP processing if game is over or peril player is invalid - if (!roundStarted || perilPlayer == "" || perilPlayer == "NONE" || llGetListLength(newNames) <= 1) { - // Game is ending or already ended, don't continue rounds + // ADDITIONAL PROTECTION: Check for eliminated players in the current game + // Don't continue rounds if any player has 0 lives (elimination in progress) + integer hasEliminatedPlayers = FALSE; + integer e; + for (e = 0; e < llGetListLength(newLives) && !hasEliminatedPlayers; e++) { + integer playerLives = llList2Integer(newLives, e); + if (playerLives <= 0) { + hasEliminatedPlayers = TRUE; + string eliminatedPlayerName = llList2String(newNames, e); + llOwnerSay("โš ๏ธ [Game Manager] ELIMINATION DETECTED: " + eliminatedPlayerName + " has 0 lives - stopping round progression"); + } + } + + if (hasEliminatedPlayers) { + llOwnerSay("๐Ÿ›‘ [Game Manager] Elimination in progress - waiting for Main Controller to handle victory/elimination sequence"); + // IMMEDIATE STATE RESET to prevent further processing during elimination + roundStarted = FALSE; + perilPlayer = ""; + currentPickerIdx = 0; + pickQueue = []; + diceTypeProcessed = FALSE; + roundContinueInProgress = FALSE; + llOwnerSay("๐Ÿ”’ [Game Manager] Round state reset due to elimination - awaiting Main Controller reset"); + return; // Don't continue rounds during elimination + } + + // Only process game continuation if we have a valid active game + // During initial player joining (roundStarted=FALSE, single player), just skip processing quietly + if (!roundStarted) { + // Game hasn't started yet (normal during player joining) - no warning needed + return; + } + + // Check for actual game ending conditions only during active games + if (perilPlayer == "" || perilPlayer == "NONE") { + llOwnerSay("โš ๏ธ [Game Manager] Game ending - invalid peril player (" + perilPlayer + ")"); + return; + } + + // Check for game ending when we had multiple players but now have 1 or less + if (llGetListLength(newNames) <= 1 && llGetListLength(pickQueue) > 1) { + llOwnerSay("โš ๏ธ [Game Manager] Game ending - only " + (string)llGetListLength(newNames) + " player(s) remaining"); return; } // Only show debug and continue if game is actually active - llOwnerSay("๐Ÿ” [Game Manager] DEBUG - allPicksEmpty: " + (string)allPicksEmpty + ", perilPlayer: " + perilPlayer + ", roundStarted: " + (string)roundStarted); + if (VERBOSE_LOGGING) llOwnerSay("๐Ÿ” [Game Manager] DEBUG - allPicksEmpty: " + (string)allPicksEmpty + ", perilPlayer: " + perilPlayer + ", roundStarted: " + (string)roundStarted); // If picks are empty, round started, and we have a valid peril player, continue to next round // This works for ALL outcomes: Direct Hit, No Shield, AND Plot Twist // PROTECTION: Prevent rapid consecutive calls to continueCurrentRound // ADDITIONAL PROTECTION: Verify peril player still exists in the game // CRITICAL FIX: Prevent double processing during Plot Twist scenarios + // NEW PROTECTION: Prevent duplicate processing within short time windows + integer currentTime = llGetUnixTime(); integer perilPlayerExists = (perilPlayer != "" && perilPlayer != "NONE" && llListFindList(newNames, [perilPlayer]) != -1); - if (allPicksEmpty && roundStarted && perilPlayerExists && llGetListLength(newNames) > 1 && !roundContinueInProgress) { + integer timingOk = ((currentTime - lastSyncProcessTime) > 1); + integer gameActive = (roundStarted && llGetListLength(newNames) > 1); + integer canContinue = (allPicksEmpty && perilPlayerExists && !roundContinueInProgress); + + if (gameActive && canContinue && timingOk) { // ADDITIONAL CHECK: Don't start new rounds if we just received a peril player update // This prevents double round starts during Plot Twist scenarios if (receivedPerilPlayer != "NONE" && receivedPerilPlayer != "" && receivedPerilPlayer != originalPerilPlayer) { @@ -503,6 +633,7 @@ default { llOwnerSay("๐ŸŽฏ [Game Manager] Round complete - starting next round with peril player: " + perilPlayer); roundContinueInProgress = TRUE; // Set flag to prevent rapid calls + lastSyncProcessTime = currentTime; // Record processing time llSleep(2.0); // Brief pause for dramatic effect continueCurrentRound(); // Use continueCurrentRound instead of startNextRound roundContinueInProgress = FALSE; // Clear flag after completion @@ -517,7 +648,9 @@ default { // Receive pick queue updates if (num == MSG_SYNC_PICKQUEUE) { list newPickQueue = llCSV2List(str); - llOwnerSay("๐Ÿ” [Game Manager] Received pickQueue sync from sender " + (string)sender + ": '" + str + "' (" + (string)llGetListLength(newPickQueue) + " items)"); + string senderStr = (string)sender; + string queueLenStr = (string)llGetListLength(newPickQueue); + llOwnerSay("๐Ÿ” [Game Manager] Received pickQueue sync from sender " + senderStr + ": '" + str + "' (" + queueLenStr + " items)"); // STRONGEST PROTECTION: Never accept empty pickQueues during any active round if (roundStarted && llGetListLength(pickQueue) > 0 && (str == "" || llGetListLength(newPickQueue) == 0)) { @@ -692,7 +825,23 @@ default { llOwnerSay("๐ŸŸข " + playerName + " picks saved: " + picksStr); llSay(0, "๐ŸŽฏ " + playerName + " stakes their life on numbers: " + picksStr + " ๐ŸŽฒ"); - // Don't sync to Main here - it creates loops + + // IMMEDIATE FLOATER UPDATE: Update this player's floater right after their pick is saved + // First sync the picks data so Floater Manager has the latest picks, then update the floater + llOwnerSay("๐Ÿ”„ [Game Manager] Syncing picks and updating floater immediately for " + playerName + " with new picks"); + + // Send just the updated picks data to Floater Manager (lightweight sync) + string picksDataStr = llDumpList2String(picksData, "^"); + string lightSync = llList2CSV(lives) + "~" + picksDataStr + "~" + perilPlayer + "~" + llList2CSV(names); + llMessageLinked(LINK_SET, MSG_SYNC_GAME_STATE, lightSync, NULL_KEY); + + // Brief delay to ensure sync reaches Floater Manager before update request + llSleep(0.1); + + // Now update the floater + llMessageLinked(LINK_SET, MSG_UPDATE_FLOAT, playerName, llList2Key(players, idx)); + + // Don't do full sync to Main here - it creates loops, but targeted floater update is safe llSleep(HUMAN_PICK_DELAY); // Move to next picker @@ -708,7 +857,7 @@ default { // CRITICAL: Sync the updated picks data to all modules before roll phase llOwnerSay("๐Ÿ”„ [Game Manager] Syncing final picks data before roll phase..."); syncStateToMain(); - llSleep(0.2); // Brief delay to ensure sync propagates + llSleep(1.0); // Longer delay to ensure sync propagates to all modules integer perilIdx = llListFindList(names, [perilPlayer]); if (perilIdx != -1) { @@ -768,7 +917,7 @@ default { // CRITICAL: Sync the updated picks data to all modules before roll phase llOwnerSay("๐Ÿ”„ [Game Manager] Syncing final picks data before roll phase..."); syncStateToMain(); - llSleep(0.2); // Brief delay to ensure sync propagates + llSleep(1.0); // Longer delay to ensure sync propagates to all modules // Force close any active number picker dialogs llOwnerSay("๐Ÿšซ [Game Manager] Sending CLOSE_ALL_DIALOGS command"); @@ -815,7 +964,23 @@ default { } llSay(0, "๐ŸŽฏ " + playerName + " (bot) stakes their digital life on numbers: " + picksStr + " ๐ŸŽฒ"); - // Don't sync to Main here - it creates loops + + // IMMEDIATE FLOATER UPDATE: Update this bot's floater right after their pick is saved + // First sync the picks data so Floater Manager has the latest picks, then update the floater + llOwnerSay("๐Ÿ”„ [Game Manager] Syncing picks and updating floater immediately for " + playerName + " (bot) with new picks"); + + // Send just the updated picks data to Floater Manager (lightweight sync) + string picksDataStr = llDumpList2String(picksData, "^"); + string lightSync = llList2CSV(lives) + "~" + picksDataStr + "~" + perilPlayer + "~" + llList2CSV(names); + llMessageLinked(LINK_SET, MSG_SYNC_GAME_STATE, lightSync, NULL_KEY); + + // Brief delay to ensure sync reaches Floater Manager before update request + llSleep(0.1); + + // Now update the floater + llMessageLinked(LINK_SET, MSG_UPDATE_FLOAT, playerName, NULL_KEY); + + // Don't do full sync to Main here - it creates loops, but targeted floater update is safe llSleep(BOT_PICK_DELAY); // Check if all players now have picks (more reliable than picker index) @@ -870,6 +1035,20 @@ default { return; } + // Handle verbose logging toggle from Main Controller + if (num == 9011 && llSubStringIndex(str, "VERBOSE_LOGGING|") == 0) { + list parts = llParseString2List(str, ["|"], []); + if (llGetListLength(parts) >= 2) { + VERBOSE_LOGGING = (integer)llList2String(parts, 1); + if (VERBOSE_LOGGING) { + llOwnerSay("๐Ÿ” [Game Manager] Verbose logging ON"); + } else { + llOwnerSay("๐Ÿ” [Game Manager] Verbose logging OFF"); + } + } + return; + } + // Handle reset if (num == -99999 && str == "FULL_RESET") { players = names = lives = picksData = globalPickedNumbers = pickQueue = []; @@ -880,7 +1059,22 @@ default { roundStarted = FALSE; diceTypeProcessed = FALSE; // Reset for new game roundContinueInProgress = FALSE; // Reset protection flag - llOwnerSay("๐ŸŽฏ Game Manager reset!"); + lastSyncProcessTime = 0; // Reset sync timing + llOwnerSay("๐ŸŽฏ Game Manager reset - ready for new game!"); + return; + } + + // Handle emergency state reset (for when game gets stuck) + if (num == -99998 && str == "EMERGENCY_RESET") { + llOwnerSay("๐Ÿšจ [Game Manager] Emergency reset triggered!"); + roundStarted = FALSE; + perilPlayer = ""; + currentPickerIdx = 0; + pickQueue = []; + diceTypeProcessed = FALSE; + roundContinueInProgress = FALSE; + lastSyncProcessTime = 0; // Reset sync timing + llOwnerSay("๐Ÿ”’ [Game Manager] Emergency state reset complete"); return; } } diff --git a/Game_Scoreboard_Manager_Linkset.lsl b/Game_Scoreboard_Manager_Linkset.lsl index 059b41e..a71db6a 100644 --- a/Game_Scoreboard_Manager_Linkset.lsl +++ b/Game_Scoreboard_Manager_Linkset.lsl @@ -6,6 +6,10 @@ // LINKSET COMMUNICATION - NO DISCOVERY NEEDED // ============================================================================= +// Verbose logging control +integer VERBOSE_LOGGING = TRUE; // Global flag for verbose debug logs +integer MSG_TOGGLE_VERBOSE_LOGS = 9998; // Message to toggle verbose logging + // Message constants for link communication // Scoreboard messages (from link 1 - controller) integer MSG_GAME_STATUS = 3001; @@ -50,6 +54,7 @@ string TEXTURE_DIRECT_HIT = "ecd2dba2-3969-6c39-ad59-319747307f55"; // Direct Hi string TEXTURE_NO_SHIELD = "2440174f-e385-44e2-8016-ac34934f11f5"; // No Shield texture string TEXTURE_PLOT_TWIST = "ec533379-4f7f-8183-e877-e68af703dcce"; // Plot Twist texture string TEXTURE_TITLE = "624bb7a7-e856-965c-bae8-94d75226c1bc"; // Title texture +string TEXTURE_ELIMINATED_X = "90524092-03b0-1b3c-bcea-3ea5118c6dba"; // Red X overlay for eliminated players // Player data - used for BOTH current game display AND leaderboard list playerNames = []; // Current game players for display @@ -97,7 +102,9 @@ updateActionsPrim(string status) { // SAFETY CHECK - prevent overflow into leaderboard prims if (ACTIONS_PRIM > LAST_PLAYER_PRIM) { - llOwnerSay("โŒ ERROR: ACTIONS_PRIM (" + (string)ACTIONS_PRIM + ") exceeds safety boundary (" + (string)LAST_PLAYER_PRIM + ")"); + if (VERBOSE_LOGGING) { + llOwnerSay("โŒ ERROR: ACTIONS_PRIM (" + (string)ACTIONS_PRIM + ") exceeds safety boundary (" + (string)LAST_PLAYER_PRIM + ")"); + } return; } @@ -326,8 +333,10 @@ clearAllPlayers() { // SAFETY CHECK - prevent overflow into leaderboard prims if (profilePrimIndex > LAST_PLAYER_PRIM || heartsPrimIndex > LAST_PLAYER_PRIM) { - llOwnerSay("โŒ ERROR: Player prim index overflow! Attempted: " + (string)profilePrimIndex + - " or " + (string)heartsPrimIndex + ", max allowed: " + (string)LAST_PLAYER_PRIM); + if (VERBOSE_LOGGING) { + llOwnerSay("โŒ ERROR: Player prim index overflow! Attempted: " + (string)profilePrimIndex + + " or " + (string)heartsPrimIndex + ", max allowed: " + (string)LAST_PLAYER_PRIM); + } return; // Don't modify prims outside our range! } @@ -359,7 +368,9 @@ removePlayer(string playerName) { // Refresh the entire display to shift remaining players up refreshPlayerDisplay(); - llOwnerSay("๐Ÿ“Š Removed " + playerName + " from the scoreboard and shifted remaining players."); + if (VERBOSE_LOGGING) { + llOwnerSay("๐Ÿ“Š Removed " + playerName + " from the scoreboard and shifted remaining players."); + } } } @@ -402,12 +413,17 @@ refreshPlayerDisplay() { // SAFETY CHECK if (profilePrimIndex > LAST_PLAYER_PRIM || heartsPrimIndex > LAST_PLAYER_PRIM) { - llOwnerSay("โš ๏ธ Warning: Player " + name + " cannot be displayed - out of prim range"); + if (VERBOSE_LOGGING) { + llOwnerSay("โš ๏ธ Warning: Player " + name + " cannot be displayed - out of prim range"); + } return; } - // Determine profile texture to use - if (isBot(name)) { + // Determine profile texture to use - check for elimination first + if (lives <= 0) { + // Player is eliminated - show red X overlay + profileTexture = TEXTURE_ELIMINATED_X; + } else if (isBot(name)) { profileTexture = TEXTURE_BOT_PROFILE; } else if (profileTexture == "" || profileTexture == "00000000-0000-0000-0000-000000000000") { profileTexture = TEXTURE_DEFAULT_PROFILE; @@ -453,7 +469,9 @@ updatePlayerDisplay(string playerName, integer lives, string profileUUID) { // New player - find next available slot playerIndex = llGetListLength(playerNames); if (playerIndex >= 10) { - llOwnerSay("WARNING: Maximum 10 players supported, ignoring: " + playerName); + if (VERBOSE_LOGGING) { + llOwnerSay("WARNING: Maximum 10 players supported, ignoring: " + playerName); + } return; } @@ -474,37 +492,51 @@ updatePlayerDisplay(string playerName, integer lives, string profileUUID) { // SAFETY CHECK - prevent overflow into leaderboard prims if (profilePrimIndex > LAST_PLAYER_PRIM || heartsPrimIndex > LAST_PLAYER_PRIM) { - llOwnerSay("โŒ ERROR: Player prim index overflow! Attempted: " + (string)profilePrimIndex + - " or " + (string)heartsPrimIndex + ", max allowed: " + (string)LAST_PLAYER_PRIM); + if (VERBOSE_LOGGING) { + llOwnerSay("โŒ ERROR: Player prim index overflow! Attempted: " + (string)profilePrimIndex + + " or " + (string)heartsPrimIndex + ", max allowed: " + (string)LAST_PLAYER_PRIM); + } return; // Don't modify prims outside our range! } - // Update profile prim texture - string profileTexture = profileUUID; + // Update profile prim texture - show red X if eliminated, otherwise show profile + string profileTexture; - // Check if we already have a cached profile texture for this player - if (playerIndex < llGetListLength(playerProfiles)) { - string cachedTexture = llList2String(playerProfiles, playerIndex); - if (cachedTexture != "" && cachedTexture != profileUUID && - cachedTexture != TEXTURE_DEFAULT_PROFILE && cachedTexture != TEXTURE_BOT_PROFILE) { - // We have a previously fetched profile picture, use it - profileTexture = cachedTexture; + if (lives <= 0) { + // Player is eliminated - show red X (replaces profile picture) + profileTexture = TEXTURE_ELIMINATED_X; + if (VERBOSE_LOGGING) { + llOwnerSay("๐Ÿ’€ Scoreboard: Showing elimination X for " + playerName); } - } - - // If no cached texture or using original UUID, determine what to use - if (profileTexture == profileUUID) { - if (isBot(playerName)) { - profileTexture = TEXTURE_BOT_PROFILE; - } else if (profileTexture == "" || profileTexture == "00000000-0000-0000-0000-000000000000") { - profileTexture = TEXTURE_DEFAULT_PROFILE; - } else { - // Request profile picture via HTTP - string URL_RESIDENT = "https://world.secondlife.com/resident/"; - key httpRequestID = llHTTPRequest(URL_RESIDENT + profileUUID, [HTTP_METHOD, "GET"], ""); - - // Store the HTTP request mapping: requestID -> playerIndex - httpRequests += [httpRequestID, playerIndex]; + } else { + // Player is alive - determine profile texture to use + profileTexture = profileUUID; + + // Check if we already have a cached profile texture for this player + if (playerIndex < llGetListLength(playerProfiles)) { + string cachedTexture = llList2String(playerProfiles, playerIndex); + if (cachedTexture != "" && cachedTexture != profileUUID && + cachedTexture != TEXTURE_DEFAULT_PROFILE && cachedTexture != TEXTURE_BOT_PROFILE && + cachedTexture != TEXTURE_ELIMINATED_X) { + // We have a previously fetched profile picture, use it + profileTexture = cachedTexture; + } + } + + // If no cached texture or using original UUID, determine what to use + if (profileTexture == profileUUID) { + if (isBot(playerName)) { + profileTexture = TEXTURE_BOT_PROFILE; + } else if (profileTexture == "" || profileTexture == "00000000-0000-0000-0000-000000000000") { + profileTexture = TEXTURE_DEFAULT_PROFILE; + } else { + // Request profile picture via HTTP + string URL_RESIDENT = "https://world.secondlife.com/resident/"; + key httpRequestID = llHTTPRequest(URL_RESIDENT + profileUUID, [HTTP_METHOD, "GET"], ""); + + // Store the HTTP request mapping: requestID -> playerIndex + httpRequests += [httpRequestID, playerIndex]; + } } } @@ -525,8 +557,10 @@ updatePlayerDisplay(string playerName, integer lives, string profileUUID) { default { state_entry() { - llOwnerSay("๐Ÿ“Š Scoreboard Manager ready! (Linkset Version)"); - llOwnerSay("๐Ÿ“Š Managing prims " + (string)FIRST_PLAYER_PRIM + "-" + (string)LAST_PLAYER_PRIM); + if (VERBOSE_LOGGING) { + llOwnerSay("๐Ÿ“Š Scoreboard Manager ready! (Linkset Version)"); + llOwnerSay("๐Ÿ“Š Managing prims " + (string)FIRST_PLAYER_PRIM + "-" + (string)LAST_PLAYER_PRIM); + } // Initialize profile picture extraction constants profile_key_prefix_length = llStringLength(profile_key_prefix); @@ -538,11 +572,24 @@ default { // Generate initial leaderboard generateLeaderboardText(); - llOwnerSay("โœ… Linkset communication active - no discovery needed!"); - llOwnerSay("๐Ÿ“Š Loaded " + (string)llGetListLength(leaderboardNames) + " players from leaderboard data"); + if (VERBOSE_LOGGING) { + llOwnerSay("โœ… Linkset communication active - no discovery needed!"); + llOwnerSay("๐Ÿ“Š Loaded " + (string)llGetListLength(leaderboardNames) + " players from leaderboard data"); + } } link_message(integer sender, integer num, string str, key id) { + // Handle verbose logging toggle + if (num == MSG_TOGGLE_VERBOSE_LOGS) { + VERBOSE_LOGGING = !VERBOSE_LOGGING; + if (VERBOSE_LOGGING) { + llOwnerSay("๐Ÿ”Š [Scoreboard] Verbose logging ENABLED"); + } else { + llOwnerSay("๐Ÿ”Š [Scoreboard] Verbose logging DISABLED"); + } + return; + } + // Only listen to messages from the main controller (link 1) if (sender != 1) return; @@ -557,7 +604,9 @@ default { string profileUUID = llList2String(parts, 2); // Debug: Show what data scoreboard received - llOwnerSay("๐Ÿ“Š Scoreboard received update: " + playerName + " has " + (string)lives + " hearts"); + if (VERBOSE_LOGGING) { + llOwnerSay("๐Ÿ“Š Scoreboard received update: " + playerName + " has " + (string)lives + " hearts"); + } updatePlayerDisplay(playerName, lives, profileUUID); } diff --git a/Leaderboard_Communication_Linkset.lsl b/Leaderboard_Communication_Linkset.lsl index 5523b37..d633724 100644 --- a/Leaderboard_Communication_Linkset.lsl +++ b/Leaderboard_Communication_Linkset.lsl @@ -7,6 +7,10 @@ // LINKSET COMMUNICATION - NO DISCOVERY NEEDED // ============================================================================= +// Verbose logging control +integer VERBOSE_LOGGING = TRUE; // Global flag for verbose debug logs +integer MSG_TOGGLE_VERBOSE_LOGS = 9998; // Message to toggle verbose logging + // Message constants for link communication // Leaderboard messages (from link 2 - scoreboard) integer MSG_GAME_WON = 3010; @@ -112,16 +116,29 @@ distributeToBank(string text, list linkNumbers) { default { state_entry() { - llOwnerSay("๐Ÿ“‹ Leaderboard Communication Script ready! (Linkset Version)"); - llOwnerSay("๐Ÿ“‹ This is link " + (string)llGetLinkNumber() + " - should be link 25"); - llOwnerSay("๐Ÿ“‹ Managing XyzzyText links 25-72 (48 prims total)"); - llOwnerSay("โœ… Linkset communication active - listening for messages from link 2!"); + if (VERBOSE_LOGGING) { + llOwnerSay("๐Ÿ“‹ Leaderboard Communication Script ready! (Linkset Version)"); + llOwnerSay("๐Ÿ“‹ This is link " + (string)llGetLinkNumber() + " - should be link 25"); + llOwnerSay("๐Ÿ“‹ Managing XyzzyText links 25-72 (48 prims total)"); + llOwnerSay("โœ… Linkset communication active - listening for messages from link 2!"); + } // Start with blank display - real data will come from scoreboard script distributeFullText(""); } link_message(integer sender, integer num, string str, key id) { + // Handle verbose logging toggle + if (num == MSG_TOGGLE_VERBOSE_LOGS) { + VERBOSE_LOGGING = !VERBOSE_LOGGING; + if (VERBOSE_LOGGING) { + llOwnerSay("๐Ÿ”Š [Leaderboard] Verbose logging ENABLED"); + } else { + llOwnerSay("๐Ÿ”Š [Leaderboard] Verbose logging DISABLED"); + } + return; + } + // Only listen to messages from the scoreboard (link 2) if (sender != 2) { return; @@ -154,7 +171,9 @@ default { else if (llSubStringIndex(str, "LEADERBOARD") == 0) { // Legacy leaderboard format - not used with linkset version // Scoreboard now sends pre-formatted text via FORMATTED_TEXT - llOwnerSay("โš ๏ธ [Leaderboard] Received legacy LEADERBOARD format - ignoring"); + if (VERBOSE_LOGGING) { + llOwnerSay("โš ๏ธ [Leaderboard] Received legacy LEADERBOARD format - ignoring"); + } } } } diff --git a/Main_Controller_Linkset.lsl b/Main_Controller_Linkset.lsl index 2c77e76..b10af76 100644 --- a/Main_Controller_Linkset.lsl +++ b/Main_Controller_Linkset.lsl @@ -77,6 +77,12 @@ integer TIMER_DISCOVERY = 3; integer currentTimerMode = 0; // Track what the timer is currently doing float timerInterval = 1.0; // How often timer() is called for checks +// Debug control - set to TRUE for verbose pick debugging, FALSE for normal operation +integer DEBUG_PICKS = FALSE; + +// Verbose logging system - affects all modules when toggled +integer VERBOSE_LOGGING = FALSE; + // Status message timing float STATUS_DISPLAY_TIME = 8.0; // How long to show status messages on scoreboard integer statusTimer = 0; // Track when status messages were sent @@ -92,6 +98,9 @@ list picksData = []; list readyPlayers = []; list floaterChannels = []; // Track actual floater channels for cleanup +// Victory state tracking +integer victoryInProgress = FALSE; + // Lockout system variables integer isLocked = FALSE; key gameOwner; @@ -260,6 +269,12 @@ sendStatusMessage(string status) { // Forward game state to helpers when it changes updateHelpers() { + // Skip sending sync messages during victory sequence to prevent stale data broadcasts + if (victoryInProgress) { + llOwnerSay("๐Ÿ† [Main Controller] Skipping updateHelpers during victory sequence"); + return; + } + checkMemoryUsage("updateHelpers_start"); string perilForSync = "NONE"; @@ -272,11 +287,40 @@ updateHelpers() { if (dataCount > 0) { picksDataStr = llDumpList2String(picksData, "^"); checkMemoryUsage("updateHelpers_after_picks_processing"); + + // DEBUG: Log what we're sending + llOwnerSay("๐Ÿ“ค [Main Controller] Sending picks data (" + (string)dataCount + " entries):"); + integer debugIdx; + for (debugIdx = 0; debugIdx < llGetListLength(picksData); debugIdx++) { + llOwnerSay(" [" + (string)debugIdx + "]: " + llList2String(picksData, debugIdx)); + } + llOwnerSay("๐Ÿ“ค [Main Controller] Encoded picks data: '" + picksDataStr + "'"); + } else { + llOwnerSay("๐Ÿ“ค [Main Controller] No picks data to send (empty)"); } // Send core game state to internal scripts (include players list for floater management) - string syncMessage = llList2CSV(lives) + "~" + picksDataStr + "~" + perilForSync + "~" + llList2CSV(names) + "~" + llList2CSV(players); - llMessageLinked(LINK_SET, MSG_SYNC_GAME_STATE, syncMessage, NULL_KEY); + // CRITICAL: Always ensure we have at least 4 parts in sync message to prevent incomplete sync warnings + string livesStr = llList2CSV(lives); + string namesStr = llList2CSV(names); + string playersStr = llList2CSV(players); + + // During reset/empty states, ensure we have proper empty values instead of completely empty strings + if (livesStr == "") livesStr = "EMPTY_LIVES"; + if (namesStr == "") namesStr = "EMPTY_NAMES"; + if (playersStr == "") playersStr = "EMPTY_PLAYERS"; + + string syncMessage = livesStr + "~" + picksDataStr + "~" + perilForSync + "~" + namesStr + "~" + playersStr; + llOwnerSay("๐Ÿ“ค [Main Controller] Full sync message: " + syncMessage); + + // Only send sync if we have actual game data OR it's a proper reset sync + if (llGetListLength(names) > 0 || picksDataStr != "EMPTY" || perilForSync != "NONE") { + llMessageLinked(LINK_SET, MSG_SYNC_GAME_STATE, syncMessage, NULL_KEY); + } else { + // This is a true empty state (reset), send a special reset sync instead + llOwnerSay("๐Ÿ“ค [Main Controller] Sending reset sync instead of empty sync"); + llMessageLinked(LINK_SET, MSG_SYNC_GAME_STATE, "RESET~RESET~RESET~RESET~RESET", NULL_KEY); + } // Send scoreboard updates using link messages integer i; @@ -342,6 +386,7 @@ resetGame() { currentPickerIdx = 0; roundStarted = FALSE; gameStarting = FALSE; + victoryInProgress = FALSE; currentPicker = NULL_KEY; timeoutTimer = 0; lastWarning = 0; @@ -405,16 +450,52 @@ default { key toucher = llDetectedKey(0); integer idx = llListFindList(players, [toucher]); - // Check if player has a current dialog they can recover + // Enhanced disconnect/reconnect handling - check if this player should be picking if (idx != -1 && toucher != llGetOwner() && roundStarted) { string playerName = llList2String(names, idx); - if (currentPicker == toucher) { - llRegionSayTo(toucher, 0, "๐Ÿ”„ Restoring your number picking dialog..."); + + // ENHANCED: Check if this player is supposed to be the current picker based on game state + // This handles cases where currentPicker got corrupted due to disconnect + integer shouldBePicking = FALSE; + if (currentPickerIdx < llGetListLength(pickQueue)) { + string expectedPickerName = llList2String(pickQueue, currentPickerIdx); + if (VERBOSE_LOGGING) { + llOwnerSay("๐Ÿ” [Touch Debug] Player: " + playerName + ", Expected picker: " + expectedPickerName + ", PickerIdx: " + (string)currentPickerIdx); + llOwnerSay("๐Ÿ” [Touch Debug] CurrentPicker: " + (string)currentPicker + ", Toucher: " + (string)toucher); + } + if (playerName == expectedPickerName) { + shouldBePicking = TRUE; + // Fix corrupted currentPicker state + if (currentPicker != toucher) { + llOwnerSay("๐Ÿ”„ [Disconnect Recovery] Fixing currentPicker for returning player: " + playerName); + llOwnerSay("๐Ÿ”„ [Disconnect Recovery] Old currentPicker: " + (string)currentPicker + " -> New: " + (string)toucher); + currentPicker = toucher; + } + } + } else if (VERBOSE_LOGGING) { + llOwnerSay("๐Ÿ” [Touch Debug] Pick phase complete - currentPickerIdx (" + (string)currentPickerIdx + ") >= pickQueue length (" + (string)llGetListLength(pickQueue) + ")"); + } + + // FALLBACK: If game is stuck and no one is set as current picker, but we're in pick phase, + // let any registered player attempt to resume (with owner confirmation) + if (!shouldBePicking && currentPicker == NULL_KEY && currentPickerIdx < llGetListLength(pickQueue)) { + if (toucher == llGetOwner()) { + llOwnerSay("โš ๏ธ [Recovery] Game appears stuck - no current picker set!"); + llOwnerSay("โš ๏ธ [Recovery] Expected picker: " + llList2String(pickQueue, currentPickerIdx)); + llOwnerSay("โš ๏ธ [Recovery] You (owner) touched - fixing by assigning you as current picker"); + currentPicker = toucher; + shouldBePicking = TRUE; + } + } + + // Check for dialog recovery (original logic OR enhanced detection) + if (currentPicker == toucher || shouldBePicking) { + llRegionSayTo(toucher, 0, "๐Ÿ”„ Welcome back! Restoring your number picking dialog..."); llMessageLinked(LINK_SET, MSG_GET_CURRENT_DIALOG, playerName, toucher); return; } else if (playerName == perilPlayer && currentPickerIdx >= llGetListLength(pickQueue)) { - llRegionSayTo(toucher, 0, "๐Ÿ”„ Restoring your roll dialog..."); + llRegionSayTo(toucher, 0, "๐Ÿ”„ Welcome back! Restoring your roll dialog..."); llMessageLinked(LINK_SET, MSG_SHOW_ROLL_DIALOG, perilPlayer, toucher); return; } @@ -614,14 +695,8 @@ default { floaterChannels = llDeleteSubList(floaterChannels, idx, idx); - // CHANGED: Send to scoreboard, which will handle leaderboard updates - llMessageLinked(SCOREBOARD_LINK, MSG_GAME_LOST, eliminatedPlayer, NULL_KEY); - - sendStatusMessage("Elimination"); - updateHelpers(); - - // CRITICAL: Update perilPlayer if the eliminated player was the peril player - // This prevents sending stale sync messages in updateHelpers() + // CRITICAL: Update perilPlayer BEFORE any updateHelpers() calls + // This prevents sending stale sync messages with eliminated players if (perilPlayer == eliminatedPlayer) { // Find first remaining alive player to be new peril player string newPerilPlayer = ""; @@ -646,10 +721,32 @@ default { } } - // Check for victory condition AFTER the 0 hearts display AND removal + // CHANGED: Send to scoreboard, which will handle leaderboard updates + llMessageLinked(SCOREBOARD_LINK, MSG_GAME_LOST, eliminatedPlayer, NULL_KEY); + + sendStatusMessage("Elimination"); + + // Check for victory condition BEFORE calling updateHelpers() if (llGetListLength(names) <= 1) { if (llGetListLength(names) == 1) { string winner = llList2String(names, 0); + + // CRITICAL: Set victory flag immediately to prevent sync processing + victoryInProgress = TRUE; + + // CRITICAL: Clear all game state immediately before any sync messages + // This prevents broadcasting stale winner data to other scripts + players = []; + names = []; + lives = []; + picksData = []; + perilPlayer = ""; + pickQueue = []; + currentPickerIdx = 0; + roundStarted = FALSE; + gameStarting = FALSE; + currentPicker = NULL_KEY; + llSay(0, "โœจ ULTIMATE VICTORY! " + winner + " is the Ultimate Survivor!"); llMessageLinked(LINK_SET, MSG_PLAYER_WON, winner, NULL_KEY); @@ -662,6 +759,19 @@ default { sendStatusMessage("Victory"); llSleep(STATUS_DISPLAY_TIME + 1.0); } else { + // CRITICAL: Set victory flag and clear state for no survivors scenario too + victoryInProgress = TRUE; + players = []; + names = []; + lives = []; + picksData = []; + perilPlayer = ""; + pickQueue = []; + currentPickerIdx = 0; + roundStarted = FALSE; + gameStarting = FALSE; + currentPicker = NULL_KEY; + llSay(0, "๐Ÿ’€ DESPAIR WINS! No Ultimate Survivors remain!"); llSleep(STATUS_DISPLAY_TIME + 1.0); } @@ -669,12 +779,27 @@ default { return; } + // Only call updateHelpers() if victory was NOT detected + updateHelpers(); + + // Final updateHelpers() call after all elimination processing is complete updateHelpers(); } } return; } + // Handle emergency reset for stuck games + if (num == -99998 && str == "EMERGENCY_RESET") { + llOwnerSay("๐Ÿšจ [Main Controller] Emergency reset triggered - sending emergency reset to all scripts"); + // Signal all scripts to emergency reset + llMessageLinked(LINK_SET, -99998, "EMERGENCY_RESET", NULL_KEY); + llSleep(0.5); + // Then do full reset + resetGame(); + return; + } + // Handle game won if (num == 998) { list parts = llParseString2List(str, ["|"], []); @@ -690,6 +815,12 @@ default { // Handle incoming sync updates from Roll Confetti Module if (num == MSG_SYNC_GAME_STATE) { + // Skip processing sync messages during victory sequence + if (victoryInProgress) { + llOwnerSay("๐Ÿ† [Main Controller] Ignoring sync message during victory sequence"); + return; + } + list parts = llParseString2List(str, ["~"], []); if (llGetListLength(parts) >= 4) { list newLives = llCSV2List(llList2String(parts, 0)); @@ -905,6 +1036,33 @@ default { integer ch = FLOATER_BASE_CHANNEL + idx; llMessageLinked(LINK_SET, MSG_CLEANUP_FLOAT, (string)ch, NULL_KEY); + // CRITICAL: If the leaving player was the peril player, update before updateHelpers() + if (perilPlayer == leavingName) { + // Find first remaining alive player to be new peril player + string newPerilPlayer = ""; + integer k; + for (k = 0; k < llGetListLength(names) && newPerilPlayer == ""; k++) { + string candidateName = llList2String(names, k); + if (candidateName != leavingName) { // Skip the player being removed + integer candidateIdx = llListFindList(names, [candidateName]); + if (candidateIdx != -1 && candidateIdx < llGetListLength(lives)) { + integer candidateLives = llList2Integer(lives, candidateIdx); + if (candidateLives > 0) { + newPerilPlayer = candidateName; + } + } + } + } + + if (newPerilPlayer != "") { + llOwnerSay("๐ŸŽฏ [Main Controller] Peril player left - assigning new peril player: " + newPerilPlayer); + perilPlayer = newPerilPlayer; + } else { + llOwnerSay("โš ๏ธ [Main Controller] No valid peril player candidates found after player left!"); + perilPlayer = "NONE"; + } + } + // Update main controller's lists to match the message handler players = llDeleteSubList(players, idx, idx); names = llDeleteSubList(names, idx, idx); @@ -965,6 +1123,22 @@ default { return; } + // Handle verbose logging toggle + if (num == 9010 && str == "TOGGLE_VERBOSE_LOGS") { + VERBOSE_LOGGING = !VERBOSE_LOGGING; + if (VERBOSE_LOGGING) { + llOwnerSay("๐Ÿ” [Main Controller] Verbose logging turned ON"); + llSay(0, "๐Ÿ” Verbose logging system-wide: ON"); + } else { + llOwnerSay("๐Ÿ” [Main Controller] Verbose logging turned OFF"); + llSay(0, "๐Ÿ” Verbose logging system-wide: OFF"); + } + + // Broadcast the setting to all modules + llMessageLinked(LINK_SET, 9011, "VERBOSE_LOGGING|" + (string)VERBOSE_LOGGING, id); + return; + } + // Handle memory monitor messages if (num == MSG_EMERGENCY_CLEANUP) { emergencyMemoryCleanup(); diff --git a/NumberPicker_DialogHandler.lsl b/NumberPicker_DialogHandler.lsl index 37e1147..65d06d7 100644 --- a/NumberPicker_DialogHandler.lsl +++ b/NumberPicker_DialogHandler.lsl @@ -3,6 +3,10 @@ integer MSG_SHOW_DIALOG = 101; integer MSG_GET_CURRENT_DIALOG = 302; +// Verbose logging control +integer VERBOSE_LOGGING = TRUE; // Global flag for verbose debug logs +integer MSG_TOGGLE_VERBOSE_LOGS = 9998; // Message to toggle verbose logging + // ============================================================================= // DYNAMIC CHANNEL CONFIGURATION // ============================================================================= @@ -35,8 +39,10 @@ initializeChannels() { NUMBERPICK_CHANNEL = calculateChannel(2); // ~-77200 range to match Main.lsl // Report channel to owner for debugging - llOwnerSay("๐Ÿ”ง [Number Picker Dialog] Dynamic channel initialized:"); - llOwnerSay(" Number Pick: " + (string)NUMBERPICK_CHANNEL); + if (VERBOSE_LOGGING) { + llOwnerSay("๐Ÿ”ง [Number Picker Dialog] Dynamic channel initialized:"); + llOwnerSay(" Number Pick: " + (string)NUMBERPICK_CHANNEL); + } } integer numberPickChannel; // Legacy variable, will be set dynamically @@ -82,7 +88,9 @@ list getNumbersForPage(integer page, integer diceType) { } showPickDialog(string name, key id, integer diceType, integer picks) { - llOwnerSay("๐Ÿ“‹ [NumberPicker] showPickDialog called for " + name + ", dice:" + (string)diceType + ", picks:" + (string)picks); + if (VERBOSE_LOGGING) { + llOwnerSay("๐Ÿ“‹ [NumberPicker] showPickDialog called for " + name + ", dice:" + (string)diceType + ", picks:" + (string)picks); + } if (diceType <= 0) { llOwnerSay("โš ๏ธ Cannot show dialog: diceType is 0 or invalid."); @@ -91,7 +99,9 @@ showPickDialog(string name, key id, integer diceType, integer picks) { // Initialize or update picking session if (currentPlayer != name) { - llOwnerSay("๐Ÿ“‹ [NumberPicker] Starting new session for " + name); + if (VERBOSE_LOGGING) { + llOwnerSay("๐Ÿ“‹ [NumberPicker] Starting new session for " + name); + } currentPlayer = name; currentPlayerKey = id; currentDiceType = diceType; @@ -99,7 +109,9 @@ showPickDialog(string name, key id, integer diceType, integer picks) { currentPicks = []; currentPage = 0; } else { - llOwnerSay("๐Ÿ“‹ [NumberPicker] Updating existing session for " + name); + if (VERBOSE_LOGGING) { + llOwnerSay("๐Ÿ“‹ [NumberPicker] Updating existing session for " + name); + } } list options; @@ -180,7 +192,9 @@ default { } on_rez(integer start_param) { - llOwnerSay("๐Ÿ”„ Number Picker rezzed - reinitializing..."); + if (VERBOSE_LOGGING) { + llOwnerSay("๐Ÿ”„ Number Picker rezzed - reinitializing..."); + } // Re-initialize dynamic channels initializeChannels(); @@ -202,20 +216,39 @@ default { // Set up managed listener with dynamic channel listenHandle = llListen(numberPickChannel, "", NULL_KEY, ""); - llOwnerSay("โœ… Number Picker reset complete after rez!"); + if (VERBOSE_LOGGING) { + llOwnerSay("โœ… Number Picker reset complete after rez!"); + } } link_message(integer sender, integer num, string str, key id) { + // Handle verbose logging toggle + if (num == MSG_TOGGLE_VERBOSE_LOGS) { + VERBOSE_LOGGING = !VERBOSE_LOGGING; + if (VERBOSE_LOGGING) { + llOwnerSay("๐Ÿ”Š [NumberPicker] Verbose logging ENABLED"); + } else { + llOwnerSay("๐Ÿ”Š [NumberPicker] Verbose logging DISABLED"); + } + return; + } + // Handle dialog close command if (num == -9999 && str == "CLOSE_ALL_DIALOGS") { - llOwnerSay("๐Ÿšซ [NumberPicker] CLOSE_ALL_DIALOGS received! currentPlayer: " + currentPlayer + ", key: " + (string)currentPlayerKey); + if (VERBOSE_LOGGING) { + llOwnerSay("๐Ÿšซ [NumberPicker] CLOSE_ALL_DIALOGS received! currentPlayer: " + currentPlayer + ", key: " + (string)currentPlayerKey); + } if (currentPlayerKey != NULL_KEY) { - llOwnerSay("๐Ÿšซ [NumberPicker] Forced dialog close for " + currentPlayer); + if (VERBOSE_LOGGING) { + llOwnerSay("๐Ÿšซ [NumberPicker] Forced dialog close for " + currentPlayer); + } currentPlayer = ""; currentPlayerKey = NULL_KEY; currentPicks = []; } else { - llOwnerSay("๐Ÿšซ [NumberPicker] No active dialog to close"); + if (VERBOSE_LOGGING) { + llOwnerSay("๐Ÿšซ [NumberPicker] No active dialog to close"); + } } return; } @@ -265,7 +298,9 @@ default { string playerName = str; // Only show dialog if this player is the current picker if (currentPlayer == playerName && currentPlayerKey == id) { - llOwnerSay("๐Ÿ”„ Restoring dialog for " + playerName); + if (VERBOSE_LOGGING) { + llOwnerSay("๐Ÿ”„ Restoring dialog for " + playerName); + } showPickDialog(currentPlayer, currentPlayerKey, currentDiceType, picksNeeded); } else { llRegionSayTo(id, 0, "โŒ You don't have an active dialog to restore."); @@ -276,13 +311,17 @@ default { listen(integer channel, string name, key id, string message) { // Ignore any responses if no active dialog session if (currentPlayerKey == NULL_KEY || currentPlayer == "") { - llOwnerSay("๐Ÿšซ [NumberPicker] Ignoring stale dialog response (no active session). Player: " + name + ", Key: " + (string)id + ", Message: " + message); + if (VERBOSE_LOGGING) { + llOwnerSay("๐Ÿšซ [NumberPicker] Ignoring stale dialog response (no active session). Player: " + name + ", Key: " + (string)id + ", Message: " + message); + } return; } // Ignore responses from wrong player if (id != currentPlayerKey) { - llOwnerSay("๐Ÿšซ [NumberPicker] Ignoring dialog response from wrong player. Expected: " + (string)currentPlayerKey + ", Got: " + (string)id + ", Message: " + message); + if (VERBOSE_LOGGING) { + llOwnerSay("๐Ÿšซ [NumberPicker] Ignoring dialog response from wrong player. Expected: " + (string)currentPlayerKey + ", Got: " + (string)id + ", Message: " + message); + } return; } @@ -291,13 +330,17 @@ default { if (messageNum > 0 && messageNum <= currentDiceType) { // This looks like a number selection - verify it's not already globally picked if (llListFindList(globallyPickedNumbers, [message]) != -1) { - llOwnerSay("๐Ÿšซ [NumberPicker] Ignoring stale dialog response - number " + message + " already globally picked. Player: " + name); + if (VERBOSE_LOGGING) { + llOwnerSay("๐Ÿšซ [NumberPicker] Ignoring stale dialog response - number " + message + " already globally picked. Player: " + name); + } return; } // Also verify it's not already in current picks if (llListFindList(currentPicks, [message]) != -1) { - llOwnerSay("๐Ÿšซ [NumberPicker] Ignoring stale dialog response - number " + message + " already in current picks. Player: " + name); + if (VERBOSE_LOGGING) { + llOwnerSay("๐Ÿšซ [NumberPicker] Ignoring stale dialog response - number " + message + " already in current picks. Player: " + name); + } return; } } @@ -325,19 +368,27 @@ default { // Handle completion if (message == "โœ… Done") { - llOwnerSay("๐Ÿ“‹ [NumberPicker] Done button clicked by " + name + ", currentPicks: " + llList2CSV(currentPicks) + ", needed: " + (string)picksNeeded); + if (VERBOSE_LOGGING) { + llOwnerSay("๐Ÿ“‹ [NumberPicker] Done button clicked by " + name + ", currentPicks: " + llList2CSV(currentPicks) + ", needed: " + (string)picksNeeded); + } if (llGetListLength(currentPicks) >= picksNeeded) { string picksStr = llList2CSV(currentPicks); string response = "HUMAN_PICKED:" + currentPlayer + ":" + picksStr; - llOwnerSay("๐Ÿ“ค [NumberPicker] SENDING HUMAN_PICKED: " + response); + if (VERBOSE_LOGGING) { + llOwnerSay("๐Ÿ“ค [NumberPicker] SENDING HUMAN_PICKED: " + response); + } llMessageLinked(LINK_SET, -9998, response, NULL_KEY); // Reset state - llOwnerSay("๐Ÿ“‹ [NumberPicker] Resetting session state for " + currentPlayer); + if (VERBOSE_LOGGING) { + llOwnerSay("๐Ÿ“‹ [NumberPicker] Resetting session state for " + currentPlayer); + } currentPlayer = ""; currentPlayerKey = NULL_KEY; currentPicks = []; } else { - llOwnerSay("โš ๏ธ " + currentPlayer + " needs " + (string)(picksNeeded - llGetListLength(currentPicks)) + " more picks"); + if (VERBOSE_LOGGING) { + llOwnerSay("โš ๏ธ " + currentPlayer + " needs " + (string)(picksNeeded - llGetListLength(currentPicks)) + " more picks"); + } showPickDialog(currentPlayer, currentPlayerKey, currentDiceType, picksNeeded); } return; diff --git a/Player_DialogHandler.lsl b/Player_DialogHandler.lsl index 7ee3543..064743b 100644 --- a/Player_DialogHandler.lsl +++ b/Player_DialogHandler.lsl @@ -96,7 +96,7 @@ list mainOwnerOptionsUnlocked = ["๐Ÿ‘ฅ Player Management", "๐Ÿ”„ Reset Options", // Sub-menu options for each category list playerManagementOptions = ["Add Test Player", "Kick Player", "โฌ…๏ธ Back to Main"]; list resetOptions = ["Reset Game", "Reset Leaderboard", "Reset All", "โฌ…๏ธ Back to Main"]; -list troubleshootingOptions = ["Cleanup Floaters", "Force Floaters", "โฌ…๏ธ Back to Main"]; +list troubleshootingOptions = ["Cleanup Floaters", "Force Floaters", "๐Ÿ” Toggle Verbose Logs", "โฌ…๏ธ Back to Main"]; // State tracking for dynamic ready menu with race condition protection key pendingMenuPlayer = NULL_KEY; @@ -640,6 +640,13 @@ default { // This will be handled by Main Controller's dialog listener // No need to forward via link_message - the Main Controller is already listening on DIALOG_CHANNEL } + else if (msg == "๐Ÿ” Toggle Verbose Logs") { + // Toggle verbose logging system-wide + llOwnerSay("๐Ÿ” Toggling verbose logging system-wide..."); + llMessageLinked(LINK_SET, 9010, "TOGGLE_VERBOSE_LOGS", id); + // Return to troubleshooting menu after toggling + showTroubleshootingMenu(id); + } else if (msg == "Manage Picks") { llMessageLinked(LINK_THIS, 202, "REQUEST_PLAYER_LIST", id); } diff --git a/Roll_ConfettiModule.lsl b/Roll_ConfettiModule.lsl index 07f1c15..3f44bf3 100644 --- a/Roll_ConfettiModule.lsl +++ b/Roll_ConfettiModule.lsl @@ -69,6 +69,9 @@ integer rollInProgress = FALSE; // Prevent multiple simultaneous rolls integer lastRollTime = 0; // Track when last roll happened integer diceTypeProcessed = FALSE; // Prevent duplicate dice type processing +// Verbose logging control - toggled by owner +integer VERBOSE_LOGGING = FALSE; + list getPicksFor(string nameInput) { integer i; for (i = 0; i < llGetListLength(picksData); i++) { @@ -185,6 +188,20 @@ default { llOwnerSay("๐ŸŽฒ [Roll Module] RECEIVED GET_DICE_TYPE: " + str); } + // Handle verbose logging toggle from Main Controller + if (num == 9011 && llSubStringIndex(str, "VERBOSE_LOGGING|") == 0) { + list parts = llParseString2List(str, ["|"], []); + if (llGetListLength(parts) >= 2) { + VERBOSE_LOGGING = (integer)llList2String(parts, 1); + if (VERBOSE_LOGGING) { + llOwnerSay("๐Ÿ” [Roll Module] Verbose logging ON"); + } else { + llOwnerSay("๐Ÿ” [Roll Module] Verbose logging OFF"); + } + } + return; + } + // Handle full reset from main controller if (num == -99999 && str == "FULL_RESET") { // Reset roll confetti module state @@ -200,15 +217,27 @@ default { // Handle game state sync from main controller if (num == MSG_SYNC_GAME_STATE) { + if (VERBOSE_LOGGING) llOwnerSay("๐Ÿ” [Roll Module] Received sync: " + str); list parts = llParseString2List(str, ["~"], []); + if (VERBOSE_LOGGING) llOwnerSay("๐Ÿ” [Roll Module] Parsed into " + (string)llGetListLength(parts) + " parts"); + + // Handle special RESET sync message + if (llGetListLength(parts) >= 5 && llList2String(parts, 0) == "RESET") { + llOwnerSay("๐Ÿ”„ [Roll Module] Received reset sync - ignoring during reset"); + return; + } + if (llGetListLength(parts) >= 4) { lives = llCSV2List(llList2String(parts, 0)); // Use ^ delimiter for picksData to avoid comma conflicts string picksDataStr = llList2String(parts, 1); + if (VERBOSE_LOGGING) llOwnerSay("๐Ÿ” [Roll Module] Received picksDataStr: '" + picksDataStr + "'"); if (picksDataStr == "" || picksDataStr == "EMPTY") { picksData = []; + if (VERBOSE_LOGGING) llOwnerSay("๐Ÿ” [Roll Module] Set picksData to empty"); } else { picksData = llParseString2List(picksDataStr, ["^"], []); + if (VERBOSE_LOGGING) llOwnerSay("๐Ÿ” [Roll Module] Set picksData: " + llDumpList2String(picksData, " | ")); } string receivedPeril = llList2String(parts, 2); if (receivedPeril == "NONE") { @@ -217,6 +246,7 @@ default { perilPlayer = receivedPeril; } names = llCSV2List(llList2String(parts, 3)); + if (VERBOSE_LOGGING) llOwnerSay("๐Ÿ” [Roll Module] Updated names: " + llDumpList2String(names, ", ")); } return; } @@ -290,6 +320,25 @@ default { return; } + // CRITICAL: Verify we have picks data for at least some players before processing roll + integer hasAnyPicks = FALSE; + integer validationIdx; + for (validationIdx = 0; validationIdx < llGetListLength(picksData) && !hasAnyPicks; validationIdx++) { + string entry = llList2String(picksData, validationIdx); + list entryParts = llParseString2List(entry, ["|"], []); + if (llGetListLength(entryParts) >= 2 && llList2String(entryParts, 1) != "") { + hasAnyPicks = TRUE; + } + } + + if (!hasAnyPicks) { + llOwnerSay("โš ๏ธ [Roll Module] No picks data available - delaying roll to wait for sync"); + rollInProgress = FALSE; // Reset roll state + // Request a re-sync of game state + llMessageLinked(LINK_SET, 998, "REQUEST_SYNC", NULL_KEY); + return; + } + integer diceType = (integer)str; integer result = rollDice(diceType); string resultStr = (string)result; @@ -306,14 +355,19 @@ default { // Find all players who picked the rolled number + llOwnerSay("๐Ÿ” [Roll Module] Checking picks for rolled number: " + resultStr); + llOwnerSay("๐Ÿ” [Roll Module] Current picksData: " + llDumpList2String(picksData, " | ")); for (i = 0; i < llGetListLength(names); i++) { string pname = llList2String(names, i); list picks = getPicksFor(pname); + llOwnerSay("๐Ÿ” [Roll Module] " + pname + "'s picks: " + llDumpList2String(picks, ",")); if (llListFindList(picks, [resultStr]) != -1) { matched = TRUE; matchedPlayers += [pname]; + llOwnerSay("๐Ÿ” [Roll Module] MATCH! " + pname + " picked " + resultStr); } } + llOwnerSay("๐Ÿ” [Roll Module] Final matched status: " + (string)matched + ", players: " + llDumpList2String(matchedPlayers, ",")); // If anyone matched, pick the first non-peril player as new peril diff --git a/System_Debugger.lsl b/System_Debugger.lsl index 413d88f..08b465f 100644 --- a/System_Debugger.lsl +++ b/System_Debugger.lsl @@ -1,6 +1,10 @@ // === System Debugger - Game State Validation === // Validates game state consistency and reports issues +// Verbose logging control +integer VERBOSE_LOGGING = TRUE; // Global flag for verbose debug logs +integer MSG_TOGGLE_VERBOSE_LOGS = 9998; // Message to toggle verbose logging + integer MSG_SYNC_GAME_STATE = 107; list validatePicksData(list picksData, list names) { @@ -41,7 +45,7 @@ list validatePicksData(list picksData, list names) { } // Only show picks if player has actually picked something - if (picks != "") { + if (picks != "" && VERBOSE_LOGGING) { llOwnerSay("โœ… " + name + "'s picks: " + llList2CSV(pickNums)); } } else if (entry != "") { @@ -73,6 +77,17 @@ default { } link_message(integer sender, integer num, string str, key id) { + // Handle verbose logging toggle + if (num == MSG_TOGGLE_VERBOSE_LOGS) { + VERBOSE_LOGGING = !VERBOSE_LOGGING; + if (VERBOSE_LOGGING) { + llOwnerSay("๐Ÿ”Š [System Debugger] Verbose logging ENABLED"); + } else { + llOwnerSay("๐Ÿ”Š [System Debugger] Verbose logging DISABLED"); + } + return; + } + // Handle full reset from main controller if (num == -99999 && str == "FULL_RESET") { // Debugger doesn't maintain state, but acknowledge reset @@ -83,7 +98,9 @@ default { if (num == MSG_SYNC_GAME_STATE) { list parts = llParseString2List(str, ["~"], []); if (llGetListLength(parts) < 4) { - llOwnerSay("โš ๏ธ Incomplete game state received."); + if (VERBOSE_LOGGING) { + llOwnerSay("โš ๏ธ Incomplete game state received."); + } return; } diff --git a/WARP.md b/WARP.md index 6910501..48c962a 100644 --- a/WARP.md +++ b/WARP.md @@ -6,7 +6,7 @@ This file provides guidance to WARP (warp.dev) when working with code in this re **Peril Dice** is a multiplayer elimination game for Second Life, built using LSL (Linden Scripting Language). It's a professional-grade linkset system where players select numbers before dice rolls, with the peril player losing lives when their number comes up. The project features a single 74-prim linkset architecture with sophisticated inter-script communication. -**Current Version**: 2.8.2 - Scoreboard Spam & Display Fixes +**Current Version**: 2.8.4 - Disconnect Recovery & System-Wide Debug Control **Technology**: LSL (Linden Scripting Language) for Second Life virtual world **Architecture**: Single linkset with 16 modular LSL scripts communicating via `llMessageLinked()` diff --git a/XyzzyText_Dice_Bridge_Linkset.lsl b/XyzzyText_Dice_Bridge_Linkset.lsl index 5fbe0fe..22a42fa 100644 --- a/XyzzyText_Dice_Bridge_Linkset.lsl +++ b/XyzzyText_Dice_Bridge_Linkset.lsl @@ -7,6 +7,10 @@ // LINKSET COMMUNICATION - NO DISCOVERY NEEDED // ============================================================================= +// Verbose logging control +integer VERBOSE_LOGGING = TRUE; // Global flag for verbose debug logs +integer MSG_TOGGLE_VERBOSE_LOGS = 9998; // Message to toggle verbose logging + // Message constants for link communication // Dice messages (from link 1 - controller) integer MSG_DICE_ROLL = 3020; @@ -36,23 +40,40 @@ displayDiceText(string text) { llMessageLinked(DICE_PRIM_1, DISPLAY_STRING, leftText, ""); llMessageLinked(DICE_PRIM_2, DISPLAY_STRING, rightText, ""); - llOwnerSay("๐ŸŽฒ Dice display updated: '" + text + "'"); + if (VERBOSE_LOGGING) { + llOwnerSay("๐ŸŽฒ Dice display updated: '" + text + "'"); + } } default { state_entry() { - llOwnerSay("๐ŸŽฒ Dice Display Bridge ready! (Linkset Version)"); - llOwnerSay("๐ŸŽฒ This is link " + (string)llGetLinkNumber() + " - should be link 73"); - llOwnerSay("๐ŸŽฒ Managing dice display links 73-74 (2 prims total)"); - llOwnerSay("โœ… Linkset communication active - listening for messages from link 1!"); + if (VERBOSE_LOGGING) { + llOwnerSay("๐ŸŽฒ Dice Display Bridge ready! (Linkset Version)"); + llOwnerSay("๐ŸŽฒ This is link " + (string)llGetLinkNumber() + " - should be link 73"); + llOwnerSay("๐ŸŽฒ Managing dice display links 73-74 (2 prims total)"); + llOwnerSay("โœ… Linkset communication active - listening for messages from link 1!"); + } // Initialize dice display with blank text (10 spaces each prim) displayDiceText(" "); // 20 spaces - llOwnerSay("Dice display initialized - using simple XyzzyText v1.0 (like leaderboard)"); + if (VERBOSE_LOGGING) { + llOwnerSay("Dice display initialized - using simple XyzzyText v1.0 (like leaderboard)"); + } } link_message(integer sender, integer num, string str, key id) { + // Handle verbose logging toggle + if (num == MSG_TOGGLE_VERBOSE_LOGS) { + VERBOSE_LOGGING = !VERBOSE_LOGGING; + if (VERBOSE_LOGGING) { + llOwnerSay("๐Ÿ”Š [Dice Bridge] Verbose logging ENABLED"); + } else { + llOwnerSay("๐Ÿ”Š [Dice Bridge] Verbose logging DISABLED"); + } + return; + } + // Only listen to messages from the main controller (link 1) if (sender != 1) return; @@ -83,7 +104,9 @@ default { else if (num == MSG_CLEAR_DICE) { // Clear dice display (20 spaces) displayDiceText(" "); - llOwnerSay("๐ŸŽฒ Dice display cleared"); + if (VERBOSE_LOGGING) { + llOwnerSay("๐ŸŽฒ Dice display cleared"); + } } }