// === Controller Message Handler Helper Script === // 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 // Memory reporting function reportMemoryUsage(string scriptName) { integer used = llGetUsedMemory(); integer free = llGetFreeMemory(); integer total = used + free; float percentUsed = ((float)used / (float)total) * 100.0; llOwnerSay("🧠 [" + scriptName + "] Memory: " + (string)used + " used, " + (string)free + " free (" + llGetSubString((string)percentUsed, 0, 4) + "% used)"); } // Message constants for communication with main controller integer MSG_REQUEST_PLAYER_DATA = 8001; integer MSG_REQUEST_PICK_DATA = 8002; integer MSG_REQUEST_LIFE_DATA = 8003; integer MSG_PICK_ACTION = 8004; integer MSG_DICE_TYPE_REQUEST = 8005; integer MSG_LEAVE_GAME_REQUEST = 8006; integer MSG_OWNER_MESSAGE = 9030; // Send formatted owner message integer MSG_PUBLIC_MESSAGE = 9031; // Send formatted public message integer MSG_REGION_MESSAGE = 9032; // Send formatted region message integer MSG_DIALOG_REQUEST = 9033; // Send formatted dialog // Game state data synchronized from Main Controller list players = []; list names = []; list lives = []; list picksData = []; list readyPlayers = []; integer diceType = 6; string perilPlayer = ""; // Track who the peril player is to determine if game is active integer roundStarted = FALSE; // Track if the game round has started // Message constants from Main Controller (synchronized) integer MSG_SYNC_GAME_STATE = 107; integer MSG_SYNC_PICKQUEUE = 2001; // Channel constants for scoreboard updates integer SCOREBOARD_CHANNEL_1; integer SCOREBOARD_CHANNEL_2; integer FLOATER_BASE_CHANNEL; integer MSG_CLEANUP_FLOAT = 104; integer MSG_UPDATE_FLOAT = 103; // Dynamic channel calculation (matches Main Controller) integer CHANNEL_BASE = -77000; integer calculateChannel(integer offset) { // Use BOTH owner's key AND object's key to make channels unique per game instance string ownerStr = (string)llGetOwner(); string objectStr = (string)llGetKey(); string combinedStr = ownerStr + objectStr; // Create a more unique hash using both keys string hashStr = llMD5String(combinedStr, 0); integer hash1 = llSubStringIndex("0123456789abcdef", llGetSubString(hashStr, 0, 0)); integer hash2 = llSubStringIndex("0123456789abcdef", llGetSubString(hashStr, 1, 1)); integer combinedHash = hash1 * 16 + hash2; // Creates 0-255 range return CHANNEL_BASE - (offset * 1000) - combinedHash; } initializeChannels() { SCOREBOARD_CHANNEL_1 = calculateChannel(6); // ~-77600 range SCOREBOARD_CHANNEL_2 = calculateChannel(7); // ~-77700 range FLOATER_BASE_CHANNEL = calculateChannel(9); // ~-77900 range } // Handle player list requests handlePlayerListRequest(key requester) { string namesCSV = llList2CSV(names); llMessageLinked(LINK_SET, 203, namesCSV, requester); } // Handle pick data requests handlePickDataRequest(string targetName, key requester) { integer j; for (j = 0; j < llGetListLength(picksData); j++) { string rawEntry = llList2String(picksData, j); list pickParts = llParseString2List(rawEntry, ["|"], []); if (llList2String(pickParts, 0) == targetName) { llMessageLinked(LINK_SET, 205, targetName + "|" + llList2String(pickParts, 1), requester); return; } } llMessageLinked(LINK_SET, 205, targetName + "|", requester); } // Handle life data requests handleLifeDataRequest(string playerName, key requester) { integer lifeIdx = llListFindList(names, [playerName]); if (lifeIdx != -1) { string lifeVal = llList2String(lives, lifeIdx); llMessageLinked(LINK_SET, 208, playerName + "|" + lifeVal, requester); } } // Handle pick actions (ADD_PICK/REMOVE_PICK) handlePickAction(string actionData) { list actionParts = llParseString2List(actionData, ["~"], []); string pickAction = llList2String(actionParts, 0); list args = llParseString2List(llList2String(actionParts, 1), ["|"], []); string name = llList2String(args, 0); string pick = llList2String(args, 1); integer k; for (k = 0; k < llGetListLength(picksData); k++) { string entry = llList2String(picksData, k); list pdParts = llParseString2List(entry, ["|"], []); if (llList2String(pdParts, 0) == name) { list pickList = []; string rawPicks = llList2String(pdParts, 1); if (rawPicks != "") { pickList = llParseString2List(rawPicks, [","], []); } if (pickAction == "ADD_PICK") { if (llListFindList(pickList, [pick]) == -1) { pickList += [pick]; if (VERBOSE_LOGGING) { llOwnerSay("➕ [Message Handler] Added " + pick + " to " + name); } } } else if (pickAction == "REMOVE_PICK") { integer removeIdx = llListFindList(pickList, [pick]); if (removeIdx != -1) { pickList = llDeleteSubList(pickList, removeIdx, removeIdx); if (VERBOSE_LOGGING) { llOwnerSay("➖ [Message Handler] Removed " + pick + " from " + name); } } } // Update local picks data picksData = llListReplaceList(picksData, [name + "|" + llList2CSV(pickList)], k, k); // Notify Main Controller of the change llMessageLinked(LINK_SET, 204, actionData, NULL_KEY); return; } } } // Handle dice type requests handleDiceTypeRequest(key requester) { if (VERBOSE_LOGGING) { llOwnerSay("🎲 [Message Handler] Sending dice type: " + (string)diceType); } llMessageLinked(LINK_SET, 102, (string)diceType, requester); // MSG_ROLL_RESULT } // Handle leave game and kick player requests handleLeaveGameRequest(string requestData) { list leaveParts = llParseString2List(requestData, ["|"], []); string leaveAction = llList2String(leaveParts, 0); if (leaveAction == "LEAVE_GAME" || leaveAction == "KICK_PLAYER") { string leavingName = llList2String(leaveParts, 1); key requestKey = (key)llList2String(leaveParts, 2); // This is the requester's key, not the player's key integer leaveIdx = llListFindList(names, [leavingName]); // Find by name instead of key if (leaveIdx != -1) { // Update local lists FIRST to prevent race conditions players = llDeleteSubList(players, leaveIdx, leaveIdx); names = llDeleteSubList(names, leaveIdx, leaveIdx); lives = llDeleteSubList(lives, leaveIdx, leaveIdx); picksData = llDeleteSubList(picksData, leaveIdx, leaveIdx); // 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) { readyPlayers = llDeleteSubList(readyPlayers, readyIdx, readyIdx); } // Different messages for voluntary leave vs kick if (leaveAction == "LEAVE_GAME") { llOwnerSay("👋 [Message Handler] " + leavingName + " left the game"); llSay(0, "👋 " + leavingName + " has left the deadly game! 👋"); } else if (leaveAction == "KICK_PLAYER") { llOwnerSay("👢 [Message Handler] " + leavingName + " was kicked from the game"); llSay(0, "👢 " + leavingName + " has been kicked from the deadly game by the owner! 👢"); } // Notify Main Controller of the removal llMessageLinked(LINK_SET, MSG_LEAVE_GAME_REQUEST, requestData, NULL_KEY); // Check if game should end (0 players = reset, 1 player = victory) if (llGetListLength(names) == 0) { 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) string winner = llList2String(names, 0); llSay(0, "✨ ULTIMATE VICTORY! " + winner + " is the Ultimate Survivor!"); // Notify Main Controller of victory llMessageLinked(LINK_SET, 998, "GAME_WON|" + winner, NULL_KEY); } else if (llGetListLength(names) == 1) { // Game not started yet, just inform but don't declare victory string remainingPlayer = llList2String(names, 0); if (VERBOSE_LOGGING) { llOwnerSay("📝 [Message Handler] Only " + remainingPlayer + " remains, but game hasn't started yet"); } } } } } // Synchronize game state from Main Controller syncGameState(string stateData) { list syncParts = llParseString2List(stateData, ["~"], []); // Handle special RESET sync message if (llGetListLength(syncParts) >= 5 && llList2String(syncParts, 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(syncParts) >= 4) { lives = llCSV2List(llList2String(syncParts, 0)); // Decode picks data string encodedPicksDataStr = llList2String(syncParts, 1); picksData = []; if (encodedPicksDataStr != "" && encodedPicksDataStr != "EMPTY") { list encodedEntries = llParseString2List(encodedPicksDataStr, ["^"], []); integer m; for (m = 0; m < llGetListLength(encodedEntries); m++) { string syncEntry = llList2String(encodedEntries, m); list entryParts = llParseString2List(syncEntry, ["|"], []); if (llGetListLength(entryParts) >= 2) { string syncPlayerName = llList2String(entryParts, 0); string picks = llList2String(entryParts, 1); // Convert semicolons back to commas picks = llDumpList2String(llParseString2List(picks, [";"], []), ","); picksData += [syncPlayerName + "|" + picks]; } else { picksData += [syncEntry]; } } } // Track peril player and round state for victory detection string receivedPeril = llList2String(syncParts, 2); if (receivedPeril == "NONE" || receivedPeril == "") { perilPlayer = ""; roundStarted = FALSE; } else { perilPlayer = receivedPeril; roundStarted = TRUE; // Game is active if there's a peril player } names = llCSV2List(llList2String(syncParts, 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; } } } } // Memory-efficient message handling functions // Handle owner messages handleOwnerMessage(string messageData) { // Format: "simple_message" or "type|param1|param2" list ownerParts = llParseString2List(messageData, ["|"], []); if (llGetListLength(ownerParts) == 1) { // Simple message llOwnerSay(messageData); } else { // Formatted message string ownerMsgType = llList2String(ownerParts, 0); if (ownerMsgType == "JOIN") { string ownerPlayerName = llList2String(ownerParts, 1); llOwnerSay("👋 " + ownerPlayerName + " joined the game!"); llSay(0, "🔔 Added player: " + ownerPlayerName); } else if (ownerMsgType == "LEAVE") { string leavePlayerName = llList2String(ownerParts, 1); llOwnerSay("👋 " + leavePlayerName + " left the game."); } else if (ownerMsgType == "ERROR") { string error = llList2String(ownerParts, 1); llOwnerSay("⚠️ " + error); } else if (ownerMsgType == "SUCCESS") { string msg = llList2String(ownerParts, 1); llOwnerSay("✅ " + msg); } else if (ownerMsgType == "DEBUG") { string component = llList2String(ownerParts, 1); string msg = llList2String(ownerParts, 2); llOwnerSay("🔧 [" + component + "] " + msg); } } } // Handle public messages handlePublicMessage(string messageData) { list publicParts = llParseString2List(messageData, ["|"], []); if (llGetListLength(publicParts) == 1) { llSay(0, messageData); } else { string publicMsgType = llList2String(publicParts, 0); if (publicMsgType == "GAME") { string stateVal = llList2String(publicParts, 1); llSay(0, "🎮 " + stateVal); } } } // Handle region messages to specific players handleRegionMessage(string messageData) { // Format: "player_key|message" or "player_key|type|param1|param2" list regionParts = llParseString2List(messageData, ["|"], []); if (llGetListLength(regionParts) >= 2) { key regionPlayerKey = llList2Key(regionParts, 0); string message = llList2String(regionParts, 1); if (llGetListLength(regionParts) == 2) { // Simple message - use llSay since we can't use llRegionSayTo on channel 0 llSay(0, "[To " + llKey2Name(regionPlayerKey) + "] " + message); } else { // Formatted message string regionMsgType = llList2String(regionParts, 1); if (regionMsgType == "WELCOME") { string msg = llList2String(regionParts, 2); llSay(0, "[To " + llKey2Name(regionPlayerKey) + "] 🔄 " + msg); } } } } // Handle dialog requests handleDialogRequest(string dialogData) { // Format: "player_key|dialog_text|option1,option2,option3|channel" list dialogParts = llParseString2List(dialogData, ["|"], []); if (llGetListLength(dialogParts) >= 4) { key dialogPlayerKey = llList2Key(dialogParts, 0); string dialogText = llList2String(dialogParts, 1); string optionsStr = llList2String(dialogParts, 2); integer channel = llList2Integer(dialogParts, 3); list options = llParseString2List(optionsStr, [","], []); llDialog(dialogPlayerKey, dialogText, options, channel); } } default { state_entry() { reportMemoryUsage("📨 Message Handler"); llOwnerSay("📨 [Message Handler] Helper script ready!"); initializeChannels(); } 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); return; } // Handle kick player list requests (separate message ID to avoid conflicts) if (num == 8009 && str == "REQUEST_PLAYER_LIST_KICK") { // Send back kick-specific message ID to avoid conflict with pick management string namesCSV = llList2CSV(names); 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; } // Handle pick data requests if (num == 206) { handlePickDataRequest(str, id); return; } // Handle life data requests if (num == 208) { handleLifeDataRequest(str, id); return; } // Handle pick actions if (num == 204) { handlePickAction(str); return; } // Handle dice type requests if (num == 996) { if (str == "GET_DICE_TYPE") { handleDiceTypeRequest(id); } return; } // Handle leave game requests (use proper message ID, not 107 which is for sync) if (num == 8007) { // Different from MSG_SYNC_GAME_STATE = 107 handleLeaveGameRequest(str); return; } // Sync game state from Main Controller if (num == MSG_SYNC_GAME_STATE) { syncGameState(str); return; } // Sync player list for reference if (num == MSG_SYNC_PICKQUEUE) { // Update players list reference (if needed for future functionality) return; } // Handle memory-heavy message operations if (num == MSG_OWNER_MESSAGE) { handleOwnerMessage(str); return; } if (num == MSG_PUBLIC_MESSAGE) { handlePublicMessage(str); return; } if (num == MSG_REGION_MESSAGE) { handleRegionMessage(str); return; } if (num == MSG_DIALOG_REQUEST) { handleDialogRequest(str); return; } // Handle full reset if (num == -99999 && str == "FULL_RESET") { players = []; names = []; lives = []; picksData = []; readyPlayers = []; diceType = 6; perilPlayer = ""; roundStarted = FALSE; llOwnerSay("📨 [Message Handler] Reset complete"); } } }