#define DEBUG_LOGS 0 // Set to 1 to enable logs, 0 to STRIP from memory completely #if DEBUG_LOGS #define dbg(msg) dbg(msg) #else #define dbg(msg) #endif // === Dialog Handler (Owner & Player) with unified Ready/Leave menu and join support === // Helper function to get display name with fallback to username string getPlayerName(key id) { string displayName = llGetDisplayName(id); if (displayName == "") { // Fallback to legacy username if display name is unavailable displayName = llKey2Name(id); } return displayName; } // Owners and players share the same Ready/Leave dialog. // Owners also get an "Owner" button to access advanced options. // Owners can join the game and receive a floating display. // ============================================================================= // DYNAMIC CHANNEL CONFIGURATION // ============================================================================= // Base channel offset - should match Main.lsl #define CHANNEL_BASE -77000 // Calculate channels dynamically to avoid hardcoded conflicts integer calculateChannel(integer offset) { // Use BOTH owner's key AND object's key to make channels unique per game instance // This prevents interference when same owner has multiple game tables 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; } // Dynamic channel variables integer MAIN_DIALOG_CHANNEL; integer SCOREBOARD_DATA_CHANNEL; integer LEADERBOARD_DATA_CHANNEL; integer DICE_DATA_CHANNEL; // Channel initialization function initializeChannels() { MAIN_DIALOG_CHANNEL = calculateChannel(4); // ~-77400 range to match Main.lsl SCOREBOARD_DATA_CHANNEL = calculateChannel(6); // ~-83000 range LEADERBOARD_DATA_CHANNEL = calculateChannel(7); // ~-84000 range DICE_DATA_CHANNEL = calculateChannel(8); // ~-85000 range // Report channel to owner for debugging dbg("šŸ”§ [Owner/Player Dialog] Dynamic channels initialized:"); dbg(" Main Dialog: " + (string)MAIN_DIALOG_CHANNEL); dbg(" Scoreboard: " + (string)SCOREBOARD_DATA_CHANNEL); dbg(" Leaderboard: " + (string)LEADERBOARD_DATA_CHANNEL); dbg(" Dice: " + (string)DICE_DATA_CHANNEL); } integer DIALOG_CHANNEL; // Legacy variable, will be set dynamically // Listen handle management integer listenHandle = -1; integer scoreboardHandle = -1; integer leaderboardHandle = -1; integer diceHandle = -1; // Position reset state variables key controller_key; vector controller_pos; rotation controller_rot; list foundDisplays; // Store found display info: [type, key, offset, rotation, ...] integer scanInProgress = FALSE; #define MSG_SHOW_MENU 201 #define MSG_PICK_ACTION 204 #define MSG_PLAYER_LIST_RESULT 203 #define MSG_PICK_LIST_RESULT 205 #define MSG_LIFE_LOOKUP 207 #define MSG_REGISTER_PLAYER 106 #define MSG_REZ_FLOAT 105 #define MSG_TOGGLE_READY 202 #define MSG_QUERY_READY_STATE 210 #define MSG_READY_STATE_RESULT 211 #define MSG_CLEANUP_ALL_FLOATERS 212 #define MSG_QUERY_OWNER_STATUS 213 #define MSG_OWNER_STATUS_RESULT 214 #define MSG_GET_CURRENT_DIALOG 302 // Categorized owner menu system list mainOwnerOptions = ["šŸ‘„ Player Management", "šŸ”„ Reset Options", "šŸ› ļø Troubleshooting", "ā¬…ļø Back to Game"]; list mainOwnerOptionsLocked = ["šŸ‘„ Player Management", "šŸ”„ Reset Options", "šŸ› ļø Troubleshooting", "šŸ”“ Unlock Game", "ā¬…ļø Back to Game"]; list mainOwnerOptionsUnlocked = ["šŸ‘„ Player Management", "šŸ”„ Reset Options", "šŸ› ļø Troubleshooting", "šŸ”’ Lock Game", "ā¬…ļø Back to Game"]; // 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", "šŸ” Toggle Verbose Logs", "šŸ”„ Check for Updates", "ā¬…ļø Back to Main"]; // State tracking for dynamic ready menu with race condition protection key pendingMenuPlayer = NULL_KEY; integer pendingMenuIsStarter = FALSE; integer pendingMenuIsOwner = FALSE; integer pendingMenuRequestID = 0; // Unique ID for each menu request integer currentRequestID = 0; // Counter for generating unique request IDs float pendingMenuTimestamp = 0.0; // Timeout for pending requests #define MENU_REQUEST_TIMEOUT 5.0 // 5 second timeout for menu requests // Lockout system - when TRUE, only owner can access menus integer isLocked = FALSE; key gameOwner; // 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)"); } // Display the main categorized owner menu with lock/unlock controls showOwnerMenu(key id) { // Only show full menu to owner if (id != gameOwner) { llRegionSayTo(id, 0, "āš ļø Access denied. Only the game owner can access this menu."); return; } list menuOptions; string menuTitle = "šŸ”§ Owner Menu - Select Category"; if (isLocked) { menuOptions = mainOwnerOptionsLocked; menuTitle += "\nšŸ”’ GAME IS LOCKED - Only owner can access menus"; } else { menuOptions = mainOwnerOptionsUnlocked; menuTitle += "\nšŸ”“ GAME IS UNLOCKED - All players can access menus"; } llDialog(id, menuTitle, menuOptions, DIALOG_CHANNEL); } // Display category sub-menus showPlayerManagementMenu(key id) { llDialog(id, "šŸ‘„ Player Management", playerManagementOptions, DIALOG_CHANNEL); } showResetOptionsMenu(key id) { llDialog(id, "šŸ”„ Reset Options", resetOptions, DIALOG_CHANNEL); } showTroubleshootingMenu(key id) { llDialog(id, "šŸ› ļø Troubleshooting", troubleshootingOptions, DIALOG_CHANNEL); } // Display the combined Ready/Leave menu for both players and owners // If the caller is the starter, the first button is "Start"; otherwise shows dynamic ready state. // Owners receive an extra "Owner" button to access the advanced owner menu. showReadyLeaveMenu(key id, integer isStarter, integer isOwner) { // Check for timeout of previous request if (pendingMenuPlayer != NULL_KEY && (llGetTime() - pendingMenuTimestamp) > MENU_REQUEST_TIMEOUT) { dbg("ā° Previous menu request timed out, clearing pending state"); pendingMenuPlayer = NULL_KEY; } // Generate unique request ID and store pending menu state // New requests always override previous ones for the same player currentRequestID++; if (currentRequestID > 1000000) currentRequestID = 1; // Prevent overflow // If there's a pending request for a different player, warn about it but proceed if (pendingMenuPlayer != NULL_KEY && pendingMenuPlayer != id) { dbg("āš ļø [Debug] Overriding pending request for " + (string)pendingMenuPlayer + " with new request for " + (string)id); } pendingMenuPlayer = id; pendingMenuIsStarter = isStarter; pendingMenuIsOwner = isOwner; pendingMenuRequestID = currentRequestID; pendingMenuTimestamp = llGetTime(); string playerName = getPlayerName(id); // Include request ID in the query to track responses string queryData = playerName + "|" + (string)currentRequestID; llMessageLinked(LINK_SET, MSG_QUERY_READY_STATE, queryData, id); } // Internal function to show the menu once we have ready state showReadyLeaveMenuWithState(key id, integer isStarter, integer isOwner, integer isReady, integer isBot) { // Bots don't get dialogs - they're managed by the owner if (isBot) { return; } // Lockout system: Block dialog generation for non-owners when locked if (isLocked && id != gameOwner) { llRegionSayTo(id, 0, "šŸ”’ Game is LOCKED. Only the owner can access any features. Contact " + getPlayerName(gameOwner) + " to unlock."); return; } list options; string menuText = "Select an option:"; if (isStarter) { options = ["Start Game", "Leave Game"]; menuText = "You are the game starter. You can start the game when all other players are ready."; } else { // Dynamic ready button based on current state if (isReady) { options = ["Not Ready", "Leave Game"]; menuText = "You are READY to play. Click 'Not Ready' to change your status."; } else { options = ["Ready", "Leave Game"]; menuText = "You are NOT READY to play. Click 'Ready' when you want to participate."; } } if (isOwner) { options += ["Owner"]; } // Display the menu with contextual information llDialog(id, menuText, options, DIALOG_CHANNEL); } // Manage picks UI helpers remain unchanged list currentPickList; string currentPickTarget; integer currentPickLimit = 3; // Track mapping between truncated display names and original names for kick functionality list kickDisplayNames = []; // What the user sees in the dialog list kickOriginalNames = []; // The actual player names showPickManageMenu(key id, list playerNames) { list pickOptions = []; integer j; for (j = 0; j < llGetListLength(playerNames); j++) { string pname = llList2String(playerNames, j); pickOptions += ["šŸ›  " + pname]; } pickOptions += ["ā¬…ļø Back"]; llDialog(id, "šŸ“‹ Manage Picks for: ", pickOptions, DIALOG_CHANNEL); } showPickListMenu(key id) { list listOptions = []; integer k; for (k = 0; k < llGetListLength(currentPickList); k++) { string pick = llList2String(currentPickList, k); if (llStringLength(pick) > 0) { listOptions += ["REMOVE: " + pick]; } } listOptions += ["Add Pick", "ā¬…ļø Back"]; llDialog(id, "šŸ›  Picks for: " + currentPickTarget, listOptions, DIALOG_CHANNEL); } askForNewPick(key id) { llTextBox(id, "šŸ”¢ Enter a number to add for " + currentPickTarget + ":", DIALOG_CHANNEL); } // Show kick player menu - displays all registered players for selection showKickPlayerMenu(key id, list playerNames) { if (llGetListLength(playerNames) == 0) { llRegionSayTo(id, 0, "āš ļø No players are currently registered to kick."); showPlayerManagementMenu(id); return; } list kickOptions = []; kickDisplayNames = []; // Reset mapping kickOriginalNames = []; integer m; for (m = 0; m < llGetListLength(playerNames); m++) { string originalName = llList2String(playerNames, m); string kickDisplayName = originalName; // Truncate long names to fit within 24 character button limit ("šŸ‘¢ " = 3 chars) if (llStringLength(kickDisplayName) > 21) { kickDisplayName = llGetSubString(kickDisplayName, 0, 18) + "..."; } string buttonLabel = "šŸ‘¢ " + kickDisplayName; // Debug: Check button length dbg("šŸ” Debug kick button: '" + buttonLabel + "' = " + (string)llStringLength(buttonLabel) + " chars"); kickOptions += [buttonLabel]; kickDisplayNames += [buttonLabel]; // Store the full button label kickOriginalNames += [originalName]; // Store the original name } kickOptions += ["ā¬…ļø Back"]; llDialog(id, "šŸ‘¢ Select player to kick from game:", kickOptions, DIALOG_CHANNEL); } // Show menu for unregistered owner showUnregisteredOwnerMenu(key id, integer canStartGame) { list ownerOptions = []; string ownerMenuText = "šŸ‘‘ Owner Options:\n\n"; // Always allow owner to join ownerOptions += ["Join Game"]; ownerMenuText += "šŸŽ® Join Game - Register as a player\n"; // Allow starting game if there are enough players (even if all bots) if (canStartGame) { ownerOptions += ["Start Game"]; ownerMenuText += "⚔ Start Game - Begin with current players\n"; } // Always allow owner menu access ownerOptions += ["Owner Menu"]; ownerMenuText += "šŸ”§ Owner Menu - Admin functions"; llDialog(id, ownerMenuText, ownerOptions, DIALOG_CHANNEL); } default { state_entry() { reportMemoryUsage("šŸŽ­ Player Dialog"); // Initialize dynamic channels initializeChannels(); DIALOG_CHANNEL = MAIN_DIALOG_CHANNEL; // Set legacy variable // Initialize lockout system gameOwner = llGetOwner(); isLocked = FALSE; // Default to unlocked // Initialize position reset variables controller_key = llGetKey(); // Initialize/reset dialog state variables pendingMenuPlayer = NULL_KEY; pendingMenuIsStarter = FALSE; pendingMenuIsOwner = FALSE; pendingMenuRequestID = 0; currentRequestID = 0; pendingMenuTimestamp = 0.0; currentPickList = []; currentPickTarget = ""; currentPickLimit = 3; kickDisplayNames = []; kickOriginalNames = []; // Clean up any existing listeners if (listenHandle != -1) llListenRemove(listenHandle); if (scoreboardHandle != -1) llListenRemove(scoreboardHandle); if (leaderboardHandle != -1) llListenRemove(leaderboardHandle); if (diceHandle != -1) llListenRemove(diceHandle); // Set up managed listeners with dynamic channels listenHandle = llListen(DIALOG_CHANNEL, "", NULL_KEY, ""); scoreboardHandle = llListen(SCOREBOARD_DATA_CHANNEL, "", "", ""); leaderboardHandle = llListen(LEADERBOARD_DATA_CHANNEL, "", "", ""); diceHandle = llListen(DICE_DATA_CHANNEL, "", "", ""); dbg("šŸŽ­ Owner and Player Dialog Handler ready with lockout system!"); dbg("šŸ”“ Game is UNLOCKED - All players can access menus"); } on_rez(integer start_param) { reportMemoryUsage("šŸŽ­ Player Dialog"); dbg("šŸ”„ Player Dialog Handler rezzed - reinitializing..."); // Re-initialize dynamic channels initializeChannels(); DIALOG_CHANNEL = MAIN_DIALOG_CHANNEL; // Re-initialize lockout system gameOwner = llGetOwner(); isLocked = FALSE; // Reset position variables controller_key = llGetKey(); // Reset all dialog state variables pendingMenuPlayer = NULL_KEY; pendingMenuIsStarter = FALSE; pendingMenuIsOwner = FALSE; pendingMenuRequestID = 0; currentRequestID = 0; pendingMenuTimestamp = 0.0; currentPickList = []; currentPickTarget = ""; currentPickLimit = 3; kickDisplayNames = []; kickOriginalNames = []; // Clean up any existing listeners if (listenHandle != -1) llListenRemove(listenHandle); if (scoreboardHandle != -1) llListenRemove(scoreboardHandle); if (leaderboardHandle != -1) llListenRemove(leaderboardHandle); if (diceHandle != -1) llListenRemove(diceHandle); // Set up managed listeners with dynamic channels listenHandle = llListen(DIALOG_CHANNEL, "", NULL_KEY, ""); scoreboardHandle = llListen(SCOREBOARD_DATA_CHANNEL, "", "", ""); leaderboardHandle = llListen(LEADERBOARD_DATA_CHANNEL, "", "", ""); diceHandle = llListen(DICE_DATA_CHANNEL, "", "", ""); dbg("āœ… Player Dialog Handler reset complete after rez!"); } link_message(integer sender, integer num, string str, key id) { // Handle full reset from main controller if (num == -99999 && str == "FULL_RESET") { // Reset dialog state including race condition protection pendingMenuPlayer = NULL_KEY; pendingMenuIsStarter = FALSE; pendingMenuIsOwner = FALSE; pendingMenuRequestID = 0; currentRequestID = 0; pendingMenuTimestamp = 0.0; currentPickList = []; currentPickTarget = ""; currentPickLimit = 3; dbg("šŸŽ­ Owner and Player Dialog Handler reset!"); return; } if (num == MSG_SHOW_MENU) { list args = llParseString2List(str, ["|"], []); if (llGetListLength(args) < 2) { dbg("āš ļø Invalid message format: " + str); return; } string targetType = llList2String(args, 0); integer isStarter = (integer)llList2String(args, 1); if (targetType == "owner") { // Both starter and non-starter owners get ready/leave menu with Owner button showReadyLeaveMenu(id, isStarter, TRUE); } else if (targetType == "registered_owner") { // Registered owner gets ready/leave menu with Owner button showReadyLeaveMenu(id, isStarter, TRUE); } else if (targetType == "player") { showReadyLeaveMenu(id, isStarter, FALSE); } else if (targetType == "admin") { // Show admin category menu directly showOwnerMenu(id); } else if (targetType == "unregistered_owner") { // Unregistered owner - basic menu (join, owner menu) showUnregisteredOwnerMenu(id, FALSE); } else if (targetType == "unregistered_owner_starter") { // Unregistered owner with enough players to start game showUnregisteredOwnerMenu(id, TRUE); } } else if (num == MSG_READY_STATE_RESULT) { // Parse ready state response and show appropriate menu list parts = llParseString2List(str, ["|"], []); if (llGetListLength(parts) >= 4) { string playerName = llList2String(parts, 0); integer isReady = (integer)llList2String(parts, 1); integer isBot = (integer)llList2String(parts, 2); integer responseRequestID = (integer)llList2String(parts, 3); // Validate that this response matches our pending request if (pendingMenuPlayer == id && responseRequestID == pendingMenuRequestID) { // Check for timeout if ((llGetTime() - pendingMenuTimestamp) > MENU_REQUEST_TIMEOUT) { dbg("ā° Menu request timed out, ignoring response"); pendingMenuPlayer = NULL_KEY; return; } showReadyLeaveMenuWithState(pendingMenuPlayer, pendingMenuIsStarter, pendingMenuIsOwner, isReady, isBot); // Reset pending state ONLY after successfully showing the dialog pendingMenuPlayer = NULL_KEY; pendingMenuRequestID = 0; } } else { dbg("āš ļø Invalid ready state result format - expected 4 parts, got " + (string)llGetListLength(parts)); } return; } else if (num == MSG_PLAYER_LIST_RESULT) { list playerNames = llParseString2List(str, [","], []); dbg("šŸ“‹ Fetching list of players for pick management..."); showPickManageMenu(id, playerNames); } // Handle player list result for kick functionality else if (num == 8009) { dbg("šŸ” [Dialog Handler] Received kick player list string: '" + str + "'"); list rawNames = llParseString2List(str, [","], []); dbg("šŸ” [Dialog Handler] Raw parsed into " + (string)llGetListLength(rawNames) + " names: " + llList2CSV(rawNames)); // Filter out any command strings that might have gotten mixed in list playerNames = []; integer i; for (i = 0; i < llGetListLength(rawNames); i++) { string name = llStringTrim(llList2String(rawNames, i), STRING_TRIM); // Skip empty names and command strings if (name != "" && llSubStringIndex(name, "REQUEST_") != 0 && llSubStringIndex(name, "COMMAND_") != 0 && llSubStringIndex(name, "MSG_") != 0) { playerNames += [name]; } else { dbg("āš ļø [Dialog Handler] Filtered out invalid name: '" + name + "'"); } } dbg("šŸ” [Dialog Handler] Filtered to " + (string)llGetListLength(playerNames) + " valid names: " + llList2CSV(playerNames)); showKickPlayerMenu(id, playerNames); } else if (num == MSG_PICK_LIST_RESULT) { list parts = llParseString2List(str, ["|"], []); string returnedName = llList2String(parts, 0); string picks = llList2String(parts, 1); if (llStringTrim(returnedName, STRING_TRIM) == llStringTrim(currentPickTarget, STRING_TRIM)) { dbg("āœ… Matched pick list for: " + returnedName); currentPickList = llCSV2List(picks); llMessageLinked(LINK_THIS, MSG_LIFE_LOOKUP, currentPickTarget, id); } else { dbg("āŒ Name mismatch: got " + returnedName + " but expected " + currentPickTarget); } } else if (num == MSG_LIFE_LOOKUP) { list parts = llParseString2List(str, ["|"], []); if (llList2String(parts, 0) == currentPickTarget) { currentPickLimit = (integer)llList2String(parts, 1); showPickListMenu(id); } } else if (num == MSG_OWNER_STATUS_RESULT) { // Parse owner status response and show appropriate menu list parts = llParseString2List(str, ["|"], []); if (llGetListLength(parts) >= 5) { string ownerName = llList2String(parts, 0); integer isRegistered = (integer)llList2String(parts, 1); integer isPending = (integer)llList2String(parts, 2); integer isStarter = (integer)llList2String(parts, 3); integer responseRequestID = (integer)llList2String(parts, 4); // Validate that this response matches our pending request if (pendingMenuPlayer == id && responseRequestID == pendingMenuRequestID) { // Check for timeout if ((llGetTime() - pendingMenuTimestamp) > MENU_REQUEST_TIMEOUT) { dbg("ā° Owner status query timed out, ignoring response"); pendingMenuPlayer = NULL_KEY; return; } // Decide which menu to show based on registration status if (isRegistered) { // Owner is registered - show ready/leave menu with Owner button dbg("šŸ” [Debug] Owner is registered, isStarter=" + (string)isStarter + ", showing ready/leave menu"); dbg("šŸ” [Debug] About to call showReadyLeaveMenu with id=" + (string)id + ", isStarter=" + (string)isStarter + ", isOwner=TRUE"); // DON'T reset pending state here - showReadyLeaveMenu needs it! // The ready state handler will reset it when the dialog is shown showReadyLeaveMenu(id, isStarter, TRUE); dbg("šŸ” [Debug] showReadyLeaveMenu call completed"); } else if (isPending) { llRegionSayTo(id, 0, "ā³ Registration already in progress for " + ownerName); // Reset pending state for non-registered responses pendingMenuPlayer = NULL_KEY; pendingMenuRequestID = 0; } else { // Owner is not registered - show join/admin choice menu list options = ["Join Game", "Owner Menu"]; string menuText = "šŸ‘‘ Owner Options:\n\nšŸŽ® Join Game - Register as a player\nšŸ”§ Owner Menu - Admin functions without joining"; llDialog(id, menuText, options, DIALOG_CHANNEL); // Reset pending state for non-registered responses pendingMenuPlayer = NULL_KEY; pendingMenuRequestID = 0; } } else { dbg("āš ļø Owner status response mismatch - ignoring"); } } else { dbg("āš ļø Invalid owner status result format - expected 5 parts, got " + (string)llGetListLength(parts)); } return; } } listen(integer channel, string name, key id, string msg) { // Lockout system: Check if game is locked and user is not owner if (isLocked && id != gameOwner) { // Block ALL access for non-owners when locked llRegionSayTo(id, 0, "šŸ”’ Game is LOCKED. Only the owner can access any features. Contact " + getPlayerName(gameOwner) + " to unlock."); return; } // Handle "Owner" button to show owner options if (msg == "Owner") { showOwnerMenu(id); } // Handle lock/unlock controls else if (msg == "šŸ”’ Lock Game") { if (id == gameOwner) { isLocked = TRUE; // Notify Main Controller to update floating text llMessageLinked(LINK_SET, 9001, "LOCK_GAME", id); dbg("šŸ”’ Game has been LOCKED - Only owner can access any features"); llSay(0, "šŸ”’ Game has been locked by the owner. Only the owner can access any features."); showOwnerMenu(id); // Refresh the owner menu } else { llRegionSayTo(id, 0, "āš ļø Only the game owner can lock the game."); } } else if (msg == "šŸ”“ Unlock Game") { if (id == gameOwner) { isLocked = FALSE; // Notify Main Controller to update floating text llMessageLinked(LINK_SET, 9002, "UNLOCK_GAME", id); dbg("šŸ”“ Game has been UNLOCKED - All players can access menus"); llSay(0, "šŸ”“ Game has been unlocked by the owner. All features are now available."); showOwnerMenu(id); // Refresh the owner menu } else { llRegionSayTo(id, 0, "āš ļø Only the game owner can unlock the game."); } } // Handle category menu navigation else if (msg == "šŸ‘„ Player Management") { showPlayerManagementMenu(id); } else if (msg == "šŸ”„ Reset Options") { showResetOptionsMenu(id); } else if (msg == "šŸ› ļø Troubleshooting") { showTroubleshootingMenu(id); } else if (msg == "ā¬…ļø Back to Main") { showOwnerMenu(id); } else if (msg == "ā¬…ļø Back to Game") { // Intelligently determine what menu to show based on owner's registration status if (id == gameOwner) { // Query the Main Controller about the owner's registration status string ownerName = getPlayerName(id); // Generate unique request ID for tracking currentRequestID++; if (currentRequestID > 1000000) currentRequestID = 1; // Store pending owner query state pendingMenuPlayer = id; pendingMenuRequestID = currentRequestID; pendingMenuTimestamp = llGetTime(); string queryData = ownerName + "|" + (string)currentRequestID; llMessageLinked(LINK_SET, MSG_QUERY_OWNER_STATUS, queryData, id); } } // Owner joins the game: register and rez a float for them else if (msg == "Join Game") { // Use the avatar's key as the identifier string pname = getPlayerName(id); llMessageLinked(LINK_SET, MSG_REGISTER_PLAYER, pname + "|" + (string)id, NULL_KEY); // Floater Manager will automatically rez the floater during registration } else if (msg == "Leave Game") { // Send leave game message to Controller_MessageHandler (not sync channel 107!) string pname = getPlayerName(id); llMessageLinked(LINK_SET, 8007, "LEAVE_GAME|" + pname + "|" + (string)id, NULL_KEY); } else if (msg == "Ready" || msg == "Not Ready") { // Toggle ready state (both buttons do the same thing - toggle) string pname = getPlayerName(id); llMessageLinked(LINK_SET, MSG_TOGGLE_READY, pname, NULL_KEY); } else if (msg == "Kick Player") { // Show kick player menu - request player list first llMessageLinked(LINK_SET, 8009, "REQUEST_PLAYER_LIST_KICK", id); } else if (msg == "Cleanup Floaters") { // Universal floater cleanup that works even after script resets dbg("🧩 Force cleaning ALL possible floater channels..."); llMessageLinked(LINK_SET, MSG_CLEANUP_ALL_FLOATERS, "", NULL_KEY); } else if (msg == "Force Floaters") { // Force creation of floaters for all registered players - send as dialog response to Main Controller dbg("šŸ”§ Requesting floater creation for all registered players..."); // 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 dbg("šŸ” Toggling verbose logging system-wide..."); llMessageLinked(LINK_SET, 9010, "TOGGLE_VERBOSE_LOGS", id); // Return to troubleshooting menu after toggling showTroubleshootingMenu(id); } else if (msg == "šŸ”„ Check for Updates") { // Trigger update check via Update_Receiver script dbg("šŸ”„ Checking GitHub for Peril Dice updates..."); llMessageLinked(LINK_SET, 2100, "CHECK_UPDATES_REQUEST", id); // Return to troubleshooting menu showTroubleshootingMenu(id); } else if (msg == "Manage Picks") { llMessageLinked(LINK_THIS, 202, "REQUEST_PLAYER_LIST", id); } else if (llSubStringIndex(msg, "šŸ›  ") == 0) { currentPickTarget = llStringTrim(llDeleteSubString(msg, 0, 1), STRING_TRIM); dbg("šŸŽÆ Pick target set to: " + currentPickTarget); currentPickList = []; llMessageLinked(LINK_THIS, 206, currentPickTarget, id); } else if (msg == "Add Pick") { askForNewPick(id); } else if (msg == "ā¬…ļø Back") { llMessageLinked(LINK_THIS, 202, "REQUEST_PLAYER_LIST", id); } else if (llSubStringIndex(msg, "REMOVE: ") == 0) { string pick = llGetSubString(msg, 8, -1); string payload = "REMOVE_PICK~" + currentPickTarget + "|" + pick; llMessageLinked(LINK_THIS, MSG_PICK_ACTION, payload, id); llSleep(0.2); llMessageLinked(LINK_THIS, 206, currentPickTarget, id); } // Handle kick player selection else if (llSubStringIndex(msg, "šŸ‘¢ ") == 0) { // Find the original name using the display name mapping integer idx = llListFindList(kickDisplayNames, [msg]); if (idx != -1) { string playerToKick = llList2String(kickOriginalNames, idx); llRegionSayTo(id, 0, "šŸ‘¢ Kicking player: " + playerToKick + " from the game..."); // Send kick message to Controller_MessageHandler (not sync channel 107!) llMessageLinked(LINK_SET, 8007, "KICK_PLAYER|" + playerToKick + "|" + (string)id, NULL_KEY); } else { dbg("āš ļø Could not find original name for kick selection: " + msg); } // Return to player management menu showPlayerManagementMenu(id); } // Forward numeric picks to the game logic else if ((integer)msg > 0) { if (llGetListLength(currentPickList) >= currentPickLimit) { dbg("āš ļø Reached pick limit of " + (string)currentPickLimit + " for " + currentPickTarget); showPickListMenu(id); return; } if (llListFindList(currentPickList, [msg]) != -1) { dbg("āš ļø Pick already exists: " + msg); showPickListMenu(id); return; } integer i; for (i = 0; i < llGetListLength(currentPickList); i++) { if (llList2String(currentPickList, i) == msg) { dbg("āš ļø Duplicate pick detected."); return; } } string payload = "ADD_PICK~" + currentPickTarget + "|" + msg; llMessageLinked(LINK_THIS, MSG_PICK_ACTION, payload, id); llSleep(0.2); llMessageLinked(LINK_THIS, 206, currentPickTarget, id); } // Handle position scan responses from follower displays else if ((channel == SCOREBOARD_DATA_CHANNEL || channel == LEADERBOARD_DATA_CHANNEL || channel == DICE_DATA_CHANNEL) && llSubStringIndex(msg, "POSITION_RESPONSE|") == 0) { if (!scanInProgress) return; // Ignore if not scanning list parts = llParseString2List(msg, ["|"], []); if (llGetListLength(parts) >= 4) { string object_type = llList2String(parts, 1); vector object_pos = (vector)llList2String(parts, 2); rotation object_rot = (rotation)llList2String(parts, 3); // Calculate offset and relative rotation vector offset = (object_pos - controller_pos) / controller_rot; rotation rel_rot = object_rot / controller_rot; // Store display info: [type, offset, rotation, ...] foundDisplays += [object_type, offset, rel_rot]; dbg("āœ… Found " + object_type + " display at position " + (string)object_pos); dbg(" Config: " + object_type + "_offset=" + (string)offset); dbg(" Rotation: " + object_type + "_rotation=" + (string)rel_rot); } } // Note: Automatic auto-config requests are now handled by Controller_Discovery.lsl // This script only handles manual position reset dialogs } timer() { llSetTimerEvent(0.0); scanInProgress = FALSE; integer numDisplays = llGetListLength(foundDisplays) / 3; // 3 items per display (type, offset, rotation) if (numDisplays == 0) { dbg("\nāš ļø No follower displays found within 20 meters."); dbg("šŸ“ Make sure your displays have the follower script and are close enough."); dbg("šŸ”„ Check that they use the same dynamic channel system as this controller."); } else { dbg("\nšŸŽ‰ Position scan complete! Found " + (string)numDisplays + " display(s)."); dbg("\nšŸ“„ CONFIG UPDATE METHODS:"); dbg("1ļøāƒ£ MANUAL: Copy the config lines above to each display's 'config' notecard"); dbg("2ļøāƒ£ AUTOMATIC: The displays should move to their new positions automatically!"); dbg("\nšŸ“† The new positions are now saved and will be remembered when objects are rezzed."); // Send position update commands to displays (future enhancement) dbg("šŸ”„ Broadcasting position updates to all found displays..."); // Broadcast updates to each found display integer i; for (i = 0; i < llGetListLength(foundDisplays); i += 3) { string displayType = llList2String(foundDisplays, i); vector displayOffset = (vector)llList2String(foundDisplays, i + 1); rotation displayRotation = (rotation)llList2String(foundDisplays, i + 2); // Send update to appropriate channel integer updateChannel; if (displayType == "scoreboard") updateChannel = SCOREBOARD_DATA_CHANNEL; else if (displayType == "leaderboard") updateChannel = LEADERBOARD_DATA_CHANNEL; else if (displayType == "dice") updateChannel = DICE_DATA_CHANNEL; else { // Skip unknown display types dbg("āš ļø Unknown display type: " + displayType); } // Only send update if we found a valid channel if (displayType == "scoreboard" || displayType == "leaderboard" || displayType == "dice") { string updateMsg = "POSITION_UPDATE|" + displayType + "|" + (string)displayOffset + "|" + (string)displayRotation; llRegionSay(updateChannel, updateMsg); dbg("āœ… Sent position update to " + displayType + " display"); } } dbg("\n✨ Position reset complete! Your displays should now be in the correct positions."); } } }