Peril_Dice/Player_DialogHandler.lsl

803 lines
36 KiB
Text
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "Peril_Constants.lsl"
// === 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 must 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();
// CRITICAL: Always use root prim (1) for channel calculation so scripts in child prims match
string objectStr = (string)llGetLinkKey(1);
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;
// 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", "⬅️ 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;
// 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() {
REPORT_MEMORY();
// 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) {
REPORT_MEMORY();
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 == MSG_RESET_ALL && 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) {
dbg("🕵️ [Dialog Handler] Received MSG_SHOW_MENU (" + str + ") from script link/sender: " + (string)sender + ", ID: " + (string)id);
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 == MSG_REQUEST_PLAYER_LIST_KICK) {
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_REQUEST, currentPickTarget, id);
} else {
dbg("❌ Name mismatch: got " + returnedName + " but expected " + currentPickTarget);
}
}
else if (num == MSG_LIFE_LOOKUP_REQUEST) {
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, MSG_LOCK_GAME, "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, MSG_UNLOCK_GAME, "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_REQUEST, pname + "|" + (string)id, id);
// 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, MSG_LEAVE_GAME, "LEAVE_GAME|" + pname + "|" + (string)id, id);
}
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, MSG_REQUEST_PLAYER_LIST_KICK, "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 == "Manage Picks") {
llMessageLinked(LINK_THIS, MSG_REQUEST_PLAYER_LIST_PICK, "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, MSG_OWNER_PICK_MANAGER, currentPickTarget, id);
}
else if (msg == "Add Pick") {
askForNewPick(id);
}
else if (msg == "⬅️ Back") {
llMessageLinked(LINK_THIS, MSG_REQUEST_PLAYER_LIST_PICK, "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(DELAY_DIALOG_REFRESH);
llMessageLinked(LINK_THIS, MSG_OWNER_PICK_MANAGER, 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, MSG_LEAVE_GAME, "KICK_PLAYER|" + playerToKick + "|" + (string)id, id);
} 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(DELAY_DIALOG_REFRESH);
llMessageLinked(LINK_THIS, MSG_OWNER_PICK_MANAGER, 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.");
}
}
}