fix: replace ternary operators with conditional statements for LSL compatibility

- Fixed LSL syntax errors caused by unsupported ternary operator (condition ? value1 : value2)
- Replaced all ternary operators with proper if/else statements in verbose logging toggles
- Ensures 'ENABLED'/'DISABLED' messages display correctly without compilation errors
- Updated files: Bot_Manager.lsl, Controller_Memory.lsl, Controller_MessageHandler.lsl,
  Floater_Manager.lsl, Game_Calculator.lsl, Game_Manager.lsl, Game_Scoreboard_Manager_Linkset.lsl,
  Leaderboard_Communication_Linkset.lsl, Main_Controller_Linkset.lsl, NumberPicker_DialogHandler.lsl,
  Player_DialogHandler.lsl, Roll_ConfettiModule.lsl, System_Debugger.lsl, XyzzyText_Dice_Bridge_Linkset.lsl
- Updated WARP.md with latest v2.8.4 release information and fixes documentation
This commit is contained in:
RebeccaNod1 2025-09-14 08:29:56 -05:00
parent c2341d0a93
commit 8f95e792e2
15 changed files with 999 additions and 152 deletions

View file

@ -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);
}
}

View file

@ -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);

View file

@ -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;
}

View file

@ -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");
}
}
}
}

View file

@ -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);

View file

@ -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;
}
}

View file

@ -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);
}

View file

@ -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");
}
}
}
}

View file

@ -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();

View file

@ -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;

View file

@ -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);
}

View file

@ -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

View file

@ -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;
}

View file

@ -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()`

View file

@ -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");
}
}
}