Peril_Dice/Main_Controller.lsl

1128 lines
52 KiB
Text

// === Main Controller - Peril Dice Game (Refactored with Modular Architecture) ===
// 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;
}
//
// This version of the main game controller includes support for players (including
// the owner) joining the game at runtime via a MSG_REGISTER_PLAYER message. When
// a new player registers, they are added to the internal lists (players, names,
// lives and picksData), a floating display is rezzed for them, and helpers are
// updated. This mirrors the behaviour used when adding a test player, but now
// applies to any avatar joining the game via the dialog handler.
// =============================================================================
// DYNAMIC CHANNEL CONFIGURATION (replaces hardcoded channels)
// =============================================================================
// Base channel offset - change this to avoid conflicts with other objects
integer 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 SYNC_CHANNEL;
integer NUMBERPICK_CHANNEL;
integer ROLLDIALOG_CHANNEL;
integer MAIN_DIALOG_CHANNEL;
integer BOT_COMMAND_CHANNEL;
integer SCOREBOARD_CHANNEL_1;
integer SCOREBOARD_CHANNEL_2;
integer SCOREBOARD_CHANNEL_3;
integer FLOATER_BASE_CHANNEL;
// Legacy channel variables (for backward compatibility during transition)
integer syncChannel;
integer numberPickChannel;
integer rollDialogChannel;
integer DIALOG_CHANNEL;
// Channel initialization function
initializeChannels() {
SYNC_CHANNEL = calculateChannel(1); // ~-77100 range
NUMBERPICK_CHANNEL = calculateChannel(2); // ~-77200 range
ROLLDIALOG_CHANNEL = calculateChannel(3); // ~-77300 range
MAIN_DIALOG_CHANNEL = calculateChannel(4); // ~-77400 range
BOT_COMMAND_CHANNEL = calculateChannel(5); // ~-77500 range
SCOREBOARD_CHANNEL_1 = calculateChannel(6); // ~-77600 range
SCOREBOARD_CHANNEL_2 = calculateChannel(7); // ~-77700 range
SCOREBOARD_CHANNEL_3 = calculateChannel(8); // ~-77800 range
FLOATER_BASE_CHANNEL = calculateChannel(9); // ~-77900 range
// Set legacy variables for backward compatibility
syncChannel = SYNC_CHANNEL;
numberPickChannel = NUMBERPICK_CHANNEL;
rollDialogChannel = ROLLDIALOG_CHANNEL;
DIALOG_CHANNEL = MAIN_DIALOG_CHANNEL;
// Report channels to owner for debugging
llOwnerSay("🔧 [Main Controller] Dynamic channels initialized:");
llOwnerSay(" Sync: " + (string)SYNC_CHANNEL);
llOwnerSay(" Dialog: " + (string)MAIN_DIALOG_CHANNEL);
llOwnerSay(" Roll: " + (string)ROLLDIALOG_CHANNEL);
llOwnerSay(" Scoreboard: " + (string)SCOREBOARD_CHANNEL_1);
llOwnerSay(" Floater Base: " + (string)FLOATER_BASE_CHANNEL);
}
// Listen handle management
integer dialogHandle = -1;
integer botHandle = -1;
integer rollHandle = -1;
// Game timing settings to prevent dialog system overload
float BOT_PICK_DELAY = 2.0; // Delay after bot picks before next action
float HUMAN_PICK_DELAY = 1.0; // Delay after human picks before next action
float DIALOG_DELAY = 1.5; // Delay before showing dialogs
integer gameTimer = 0; // Timer for game flow delays
// Memory monitoring now handled by Controller_Memory.lsl helper script
integer MSG_MEMORY_CHECK = 6001;
integer MSG_MEMORY_STATS = 6002;
integer MSG_MEMORY_CLEANUP = 6003;
integer MSG_MEMORY_REPORT = 6004;
integer MSG_EMERGENCY_CLEANUP = 6005;
integer MSG_MEMORY_STATS_REQUEST = 6006;
// Unified Timer System - prevents conflicts between multiple timer needs
integer TIMER_IDLE = 0;
integer TIMER_STATUS = 1;
integer TIMER_TIMEOUT = 2;
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
// 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
string lastStatus = ""; // Track last status sent
// Discovery system now handled by Controller_Discovery.lsl helper script
list players = [];
list names = [];
list lives = [];
string perilPlayer = "";
list globalPickedNumbers = [];
list picksData = [];
list readyPlayers = [];
list floaterChannels = []; // Track actual floater channels for cleanup
list pickQueue = [];
integer currentPickerIdx = 0;
integer diceType = 6;
integer MSG_SHOW_DIALOG = 101;
integer MSG_ROLL_RESULT = 102;
integer MSG_UPDATE_FLOAT = 103;
integer MSG_CLEANUP_FLOAT = 104;
integer MSG_REZ_FLOAT = 105;
integer MSG_REGISTER_PLAYER = 106;
integer MSG_SYNC_GAME_STATE = 107;
integer MSG_SHOW_MENU = 201;
integer MSG_TOGGLE_READY = 202;
integer MSG_QUERY_READY_STATE = 210;
integer MSG_READY_STATE_RESULT = 211;
integer MSG_CLEANUP_ALL_FLOATERS = 212;
integer MSG_SHOW_ROLL_DIALOG = 301;
integer MSG_GET_CURRENT_DIALOG = 302;
integer MSG_PLAYER_WON = 551;
integer MSG_RESET_LEADERBOARD = 553; // Custom message to reset leaderboard
// Display configuration
integer CONTROLLER_FACE = 1; // Face to display start image on (front face)
string TEXTURE_START = "title_start"; // Start image texture
string TEXTURE_GAME_ACTIVE = "game_active"; // Optional: different texture during game
integer MSG_GET_DICE_TYPE = 1001;
integer MSG_GET_PICKS_REQUIRED = 1002;
integer MSG_GET_PICKER_INDEX = 1003;
integer MSG_DICE_TYPE_RESULT = 1005;
integer MSG_SERIALIZE_GAME_STATE = 1004;
integer MSG_SYNC_PICKQUEUE = 2001;
// Maximum number of players allowed (including test players). This should
// mirror the value used in Floater Manager to avoid inconsistencies.
integer MAX_PLAYERS = 10;
integer TIMEOUT_SECONDS = 600;
integer timeoutTimer;
integer warning2min = 120;
integer warning5min = 300;
integer warning9min = 540;
integer lastWarning = 0;
key currentPicker;
// Memory monitoring functions now delegated to Controller_Memory.lsl helper script
checkMemoryUsage(string context) {
// Delegate to memory monitor helper
llMessageLinked(LINK_SET, MSG_MEMORY_CHECK, context, NULL_KEY);
}
emergencyMemoryCleanup() {
llOwnerSay("🎆 [Main Controller] Emergency memory cleanup initiated!");
// Clean up temporary variables and optimize lists
globalPickedNumbers = llListSort(globalPickedNumbers, 1, TRUE); // Remove duplicates
// Force garbage collection by clearing and rebuilding critical lists
list tempPlayers = players;
list tempNames = names;
list tempLives = lives;
players = [];
names = [];
lives = [];
players = tempPlayers;
names = tempNames;
lives = tempLives;
llOwnerSay("🎆 [Main Controller] Emergency cleanup complete - memory: " +
(string)llGetUsedMemory() + " bytes");
}
reportMemoryStats() {
// Request memory stats report from helper, provide our stats as well
string statsData = (string)llGetUsedMemory() + "|" +
(string)llGetListLength(players) + "|" +
(string)llGetListLength(names) + "|" +
(string)llGetListLength(lives) + "|" +
(string)llGetListLength(picksData) + "|" +
(string)llGetListLength(readyPlayers) + "|" +
(string)llGetListLength(pickQueue) + "|" +
(string)llGetListLength(globalPickedNumbers) + "|" +
(string)llGetListLength(floaterChannels);
// Send to memory monitor for comprehensive reporting
llMessageLinked(LINK_SET, MSG_MEMORY_STATS_REQUEST, statsData, NULL_KEY);
// Also request the helper to show its own stats
llMessageLinked(LINK_SET, MSG_MEMORY_STATS, "REQUEST_REPORT", NULL_KEY);
}
// Send status message to scoreboard with timing
sendStatusMessage(string status) {
statusTimer = llGetUnixTime();
lastStatus = status;
llRegionSay(SCOREBOARD_CHANNEL_1, "GAME_STATUS|" + status);
llOwnerSay("📢 Status: " + status + " (showing for " + (string)STATUS_DISPLAY_TIME + "s)");
// Start unified timer for status message clearing
currentTimerMode = TIMER_STATUS;
llSetTimerEvent(STATUS_DISPLAY_TIME + 1.0); // Add 1 second buffer
}
// Forward game state to helpers when it changes - MEMORY OPTIMIZED
updateHelpers() {
checkMemoryUsage("updateHelpers_start");
string perilForSync = "NONE";
if (roundStarted) perilForSync = perilPlayer;
// Simplified picks data processing
string picksDataStr = "EMPTY";
integer dataCount = llGetListLength(picksData);
if (dataCount > 0) {
picksDataStr = llDumpList2String(picksData, "^");
checkMemoryUsage("updateHelpers_after_picks_processing");
}
// Send core game state (now includes players list for dialog targeting)
llMessageLinked(LINK_SET, MSG_SYNC_GAME_STATE,
llList2CSV(lives) + "~" + picksDataStr + "~" + perilForSync + "~" + llList2CSV(names) + "~" + llList2CSV(players), NULL_KEY);
// Don't sync pickQueue from Main to Game Manager - Game Manager manages its own pickQueue
// Simplified scoreboard updates
integer i;
for (i = 0; i < llGetListLength(names); i++) {
llRegionSay(SCOREBOARD_CHANNEL_2, "PLAYER_UPDATE|" + llList2String(names, i) + "|" +
(string)llList2Integer(lives, i) + "|" + (string)llList2Key(players, i));
}
}
// Request helper-calculated values
requestDiceType() {
llMessageLinked(LINK_SET, MSG_GET_DICE_TYPE, (string)llGetListLength(names), NULL_KEY);
}
requestPicksRequired(integer idx) {
llMessageLinked(LINK_SET, MSG_GET_PICKS_REQUIRED, llList2String(names, idx), NULL_KEY);
}
requestPickerIndex(string name) {
llMessageLinked(LINK_SET, MSG_GET_PICKER_INDEX, name, NULL_KEY);
}
string generateSerializedState() {
return llList2CSV(lives) + "~" + llList2CSV(picksData) + "~" + perilPlayer + "~" + llList2CSV(names);
}
// Clean up listen handles
cleanupListeners() {
if (dialogHandle != -1) {
llListenRemove(dialogHandle);
dialogHandle = -1;
}
if (botHandle != -1) {
llListenRemove(botHandle);
botHandle = -1;
}
if (rollHandle != -1) {
llListenRemove(rollHandle);
rollHandle = -1;
}
}
// Re-initialize listen handles
initListeners() {
dialogHandle = llListen(DIALOG_CHANNEL, "", NULL_KEY, "");
botHandle = llListen(BOT_COMMAND_CHANNEL, "", NULL_KEY, ""); // Listen for bot responses
rollHandle = llListen(rollDialogChannel, "", NULL_KEY, ""); // Listen for roll dialog responses
}
resetGame() {
llOwnerSay("🧼 Debug - Starting resetGame(), players: " + (string)llGetListLength(players) + ", tracked channels: " + llList2CSV(floaterChannels));
// Clean up listeners first
cleanupListeners();
// UNIVERSAL CLEANUP: Always clean ALL possible floater channels regardless of tracked state
// This ensures cleanup works even after script resets destroy tracking data
llOwnerSay("🧼 Debug - Universal floater cleanup (0-" + (string)(MAX_PLAYERS-1) + ")");
integer i;
for (i = 0; i < MAX_PLAYERS; i++) {
integer ch = FLOATER_BASE_CHANNEL + i;
llOwnerSay("🧼 Debug - Cleaning universal channel " + (string)i + ": " + (string)ch);
llMessageLinked(LINK_SET, MSG_CLEANUP_FLOAT, (string)ch, NULL_KEY);
}
llOwnerSay("🧼 Debug - Universal cleanup complete, waiting for floater processing...");
// Give floater cleanup time to process BEFORE clearing state
llSleep(2.0);
// Reset all game state variables
players = names = lives = picksData = globalPickedNumbers = readyPlayers = [];
floaterChannels = []; // Clear the tracked channels
pendingRegistrations = []; // Clear pending registrations
perilPlayer = "";
pickQueue = [];
currentPickerIdx = 0;
roundStarted = FALSE; // Reset round flag
gameStarting = FALSE; // Reset game starting flag
diceTypeProcessed = FALSE; // Reset dice type processing flag
currentPicker = NULL_KEY; // Reset current picker
timeoutTimer = 0; // Reset timeout timer
lastWarning = 0; // Reset warning timer
diceType = 6; // Reset dice type to default
// Send reset message to all other scripts to clear their state
llMessageLinked(LINK_SET, -99999, "FULL_RESET", NULL_KEY);
llSay(syncChannel, "RESET");
// Send CLEAR_GAME message to scoreboard to reset display
llRegionSay(SCOREBOARD_CHANNEL_1, "CLEAR_GAME");
// Clear dice display
llRegionSay(SCOREBOARD_CHANNEL_3, "CLEAR_DICE");
llOwnerSay("🎮 Game reset! All state cleared (including scoreboard).");
// Clear status tracking
statusTimer = 0;
lastStatus = "";
// Reset controller display to start image
llSetTexture(TEXTURE_START, CONTROLLER_FACE);
llSetText("🎮 PERIL DICE GAME\nTouch to play!", <1.0, 1.0, 0.0>, 1.0);
llSleep(0.5); // Give other scripts time to process reset
llSetTimerEvent(0);
// Re-initialize listeners after reset
initListeners();
updateHelpers();
llOwnerSay("🧼 Debug - resetGame() complete");
}
// continueCurrentRound() removed - handled by Game Manager
// startNextRound() removed - handled by Game Manager
// showNextPickerDialog() removed - handled by Game Manager
integer roundStarted = FALSE;
integer gameStarting = FALSE; // Track when game is in startup sequence
integer diceTypeProcessed = FALSE; // Track if we've already processed initial dice type
// CONTINUE_ROUND logic now handled entirely by Game Manager
// Prevent duplicate registration by tracking pending registrations
list pendingRegistrations = []; // Keys of players who have registration in progress
integer REGISTRATION_TIMEOUT = 10; // Seconds to keep pending registration
default {
state_entry() {
llOwnerSay("🎮 Main Controller ready!");
llOwnerSay("🎮 Main Controller key: " + (string)llGetKey());
llOwnerSay("🎮 Main Controller position: " + (string)llGetPos());
// Initialize dynamic channel configuration
initializeChannels();
// Set the start image on the controller prim
llSetTexture(TEXTURE_START, CONTROLLER_FACE);
llSetText("🎮 PERIL DICE GAME\nTouch to play!", <1.0, 1.0, 0.0>, 1.0);
// Clean up any existing listeners
if (dialogHandle != -1) llListenRemove(dialogHandle);
if (botHandle != -1) llListenRemove(botHandle);
if (rollHandle != -1) llListenRemove(rollHandle);
// Set up managed listeners with dynamic channels
dialogHandle = llListen(MAIN_DIALOG_CHANNEL, "", NULL_KEY, "");
botHandle = llListen(BOT_COMMAND_CHANNEL, "", NULL_KEY, ""); // Listen for bot responses
rollHandle = llListen(ROLLDIALOG_CHANNEL, "", NULL_KEY, ""); // Listen for roll dialog responses
// Discovery system now handled by Controller_Discovery.lsl helper script
}
touch_start(integer total_number) {
key toucher = llDetectedKey(0);
integer idx = llListFindList(players, [toucher]);
// Check if player has a current dialog they can recover
if (idx != -1 && toucher != llGetOwner() && roundStarted) {
string playerName = llList2String(names, idx);
// Check if this player is the current picker waiting for a dialog
if (currentPicker == toucher) {
llRegionSayTo(toucher, 0, "🔄 Restoring your number picking dialog...");
llMessageLinked(LINK_SET, MSG_GET_CURRENT_DIALOG, playerName, toucher);
return;
}
// Check if this player is the peril player who might need a roll dialog
else if (playerName == perilPlayer && currentPickerIdx >= llGetListLength(pickQueue)) {
llRegionSayTo(toucher, 0, "🔄 Restoring your roll dialog...");
llMessageLinked(LINK_SET, MSG_SHOW_ROLL_DIALOG, perilPlayer, toucher);
return;
}
}
// Special handling for owner during gameplay
if (toucher == llGetOwner() && roundStarted) {
string ownerName = getPlayerName(toucher);
list options = [];
string menuText = "Game in progress. What would you like to do?";
// Check if owner has active dialogs they can recover
integer hasActiveDialog = FALSE;
if (currentPicker == toucher) {
options += ["🔄 Recover Pick Dialog"];
hasActiveDialog = TRUE;
}
if (ownerName == perilPlayer && currentPickerIdx >= llGetListLength(pickQueue)) {
options += ["🔄 Recover Roll Dialog"];
hasActiveDialog = TRUE;
}
// Always offer admin menu
options += ["🔧 Admin Menu"];
// If no active dialogs, just show admin menu directly
if (!hasActiveDialog) {
llMessageLinked(LINK_SET, MSG_SHOW_MENU, "owner|0", toucher);
return;
}
// Show choice dialog
llDialog(toucher, menuText, options, DIALOG_CHANNEL);
return;
}
if (toucher == llGetOwner()) {
// Determine if this will be the first registered player (starter) before registration
integer isStarter = FALSE;
if (idx == -1) {
// Check if registration is already pending for this player
if (llListFindList(pendingRegistrations, [toucher]) != -1) {
llOwnerSay("⏳ Registration already in progress for " + getPlayerName(toucher));
return;
}
// No index found; the owner is not yet registered.
// Owner becomes starter if there are no existing human players (excluding bots)
isStarter = TRUE; // Default to TRUE for owner
integer j;
for (j = 0; j < llGetListLength(names) && isStarter; j++) {
string existingName = llList2String(names, j);
// If there's already a human player (not a bot), owner is not starter
if (llSubStringIndex(existingName, "TestBot") != 0) {
isStarter = FALSE;
}
}
string ownerName = getPlayerName(toucher);
// Add to pending registrations
pendingRegistrations += [toucher];
// Send a registration request; Main.lsl will handle adding them and rezzing a float
llMessageLinked(LINK_SET, MSG_REGISTER_PLAYER, ownerName + "|" + (string)toucher, NULL_KEY);
} else {
// Already registered; check if owner is the first human player
// Owner is starter if they are at index 0 OR if all players before them are bots
isStarter = TRUE; // Default to TRUE for owner
integer k;
for (k = 0; k < idx && isStarter; k++) {
string existingName = llList2String(names, k);
// If there's a human player before the owner, owner is not starter
if (llSubStringIndex(existingName, "TestBot") != 0) {
isStarter = FALSE;
}
}
// Special case: if owner is at index 0, they are definitely the starter
if (idx == 0) {
isStarter = TRUE;
}
}
// Show the owner menu with the appropriate starter flag
llMessageLinked(LINK_SET, MSG_SHOW_MENU, "owner|" + (string)isStarter, toucher);
} else if (idx != -1) {
// For non-owner players, check if they're the first human player
integer isStarter = TRUE; // Default to TRUE
integer m;
for (m = 0; m < idx && isStarter; m++) {
string existingName = llList2String(names, m);
// If there's a human player before this player, they're not starter
if (llSubStringIndex(existingName, "TestBot") != 0) {
isStarter = FALSE;
}
}
llMessageLinked(LINK_SET, MSG_SHOW_MENU, "player|" + (string)isStarter, toucher);
} else {
// Unregistered non-owner player - register them and show menu
string playerName = getPlayerName(toucher);
if (playerName != "") {
// Check if registration is already pending for this player
if (llListFindList(pendingRegistrations, [toucher]) != -1) {
llOwnerSay("⏳ Registration already in progress for " + playerName);
return;
}
// Add to pending registrations
pendingRegistrations += [toucher];
// Send a registration request; Main.lsl will handle adding them and rezzing a float
llMessageLinked(LINK_SET, MSG_REGISTER_PLAYER, playerName + "|" + (string)toucher, NULL_KEY);
// Determine if this will be the first human player (starter)
integer isStarter = TRUE; // Default to TRUE
integer n;
for (n = 0; n < llGetListLength(names) && isStarter; n++) {
string existingName = llList2String(names, n);
// If there's already a human player (not a bot), this player is not starter
if (llSubStringIndex(existingName, "TestBot") != 0) {
isStarter = FALSE;
}
}
// Show the player menu with the appropriate starter flag
llMessageLinked(LINK_SET, MSG_SHOW_MENU, "player|" + (string)isStarter, toucher);
} else {
llOwnerSay("⚠️ Could not get name for toucher: " + (string)toucher);
}
}
}
link_message(integer sender, integer num, string str, key id) {
// Player list and pick data handling now delegated to Controller_MessageHandler.lsl
// Handle dice type result from helper - forward to Game Manager once
if (num == MSG_DICE_TYPE_RESULT) {
// Prevent duplicate processing of the same dice type result
if (diceTypeProcessed) {
return;
}
diceType = (integer)str;
diceTypeProcessed = TRUE;
// Set round started flag and update helpers
roundStarted = TRUE;
updateHelpers();
// Forward to Game Manager ONCE
llMessageLinked(LINK_SET, MSG_DICE_TYPE_RESULT, (string)diceType, NULL_KEY);
return;
}
if (num == MSG_GET_PICKS_REQUIRED) {
integer picksRequired = (integer)str;
llOwnerSay(" Picks required: " + str);
return;
}
if (num == MSG_GET_PICKER_INDEX) {
integer pickIndex = (integer)str;
llOwnerSay(" Picker index: " + str);
return;
}
if (num == MSG_SERIALIZE_GAME_STATE) {
string serialized = str;
llOwnerSay(" Serialized game state: " + serialized);
return;
}
// Pick actions now delegated to Controller_MessageHandler.lsl
// New: handle dynamic registration of players (owner or players) via MSG_REGISTER_PLAYER
if (num == MSG_REGISTER_PLAYER) {
checkMemoryUsage("player_registration_start");
// str is in the format "Name|<key>"
list parts = llParseString2List(str, ["|"], []);
string newName = llList2String(parts, 0);
key newKey = (key)llList2String(parts, 1);
// Prevent joining game in progress or during startup (except owner accessing menu)
if ((roundStarted || gameStarting) && newKey != llGetOwner()) {
llOwnerSay("🚫 " + newName + " cannot join - the killing game has begun!");
llRegionSayTo(newKey, 0, "🚫 The killing game has already begun! Wait for the current game to end.");
return;
}
// Do not register if already present
integer existingIdx = llListFindList(players, [newKey]);
if (existingIdx == -1) {
// Add to local lists
players += [newKey];
names += [newName];
lives += [3];
// Don't create empty picks entries - they will be added when players actually pick
checkMemoryUsage("player_registration_after_list_additions");
// Track the floater channel for this player
integer newPlayerIdx = llGetListLength(names) - 1;
integer ch = FLOATER_BASE_CHANNEL + newPlayerIdx;
floaterChannels += [ch];
// Auto-mark bots as ready, leave humans as not ready
if (llSubStringIndex(newName, "TestBot") == 0) {
readyPlayers += [newName];
llSay(0, "🤖 " + newName + " boots up with deadly precision - ready to play! 🤖");
} else {
// Check if this is the first human player (starter - automatically ready only if owner)
integer humanCount = 0;
integer i;
for (i = 0; i < llGetListLength(names); i++) {
string playerName = llList2String(names, i);
if (llSubStringIndex(playerName, "TestBot") != 0) {
humanCount++;
}
}
if (humanCount == 1) { // This is the first human player
if (newKey == llGetOwner()) {
// Owner is auto-ready as starter
readyPlayers += [newName];
llSay(0, "👑 " + newName + " steps forward as the game master - automatically ready for the deadly challenge! 👑");
} else {
// Non-owner starter can toggle ready state
llSay(0, "👑 " + newName + " steps forward as the game master! Touch to set your ready status.");
}
}
}
// The float will be rezzed by the Floater Manager after it processes
// this registration. Avoid sending MSG_REZ_FLOAT here to prevent
// race conditions; instead, request the float from Floater Manager
// after it has updated its own lists. Immediately propagate
// helper updates now.
updateHelpers();
// Notify owner of registration
llOwnerSay("🔔 Added player: " + newName);
}
// Remove from pending registrations whether successful or not
integer pendingIdx = llListFindList(pendingRegistrations, [newKey]);
if (pendingIdx != -1) {
pendingRegistrations = llDeleteSubList(pendingRegistrations, pendingIdx, pendingIdx);
}
return;
}
// Handle elimination requests
if (num == 999) {
list parts = llParseString2List(str, ["|"], []);
if (llList2String(parts, 0) == "ELIMINATE_PLAYER") {
string eliminatedPlayer = llList2String(parts, 1);
integer idx = llListFindList(names, [eliminatedPlayer]);
if (idx != -1) {
// Remove player's float using the tracked channel
integer ch = llList2Integer(floaterChannels, idx);
llMessageLinked(LINK_SET, MSG_CLEANUP_FLOAT, (string)ch, NULL_KEY);
// Remove from all lists
players = llDeleteSubList(players, idx, idx);
names = llDeleteSubList(names, idx, idx);
lives = llDeleteSubList(lives, idx, idx);
picksData = llDeleteSubList(picksData, idx, idx);
floaterChannels = llDeleteSubList(floaterChannels, idx, idx);
llOwnerSay("🗑️ Eliminated " + eliminatedPlayer + ". Remaining players: " + (string)llGetListLength(names));
// Record the loss in the leaderboard
llRegionSay(SCOREBOARD_CHANNEL_1, "GAME_LOST|" + eliminatedPlayer);
// Show elimination status when someone is actually eliminated
sendStatusMessage("Elimination");
// Update all scripts with the new player lists after elimination
updateHelpers();
// Check if game should end (1 or fewer players remaining)
if (llGetListLength(names) <= 1) {
if (llGetListLength(names) == 1) {
string winner = llList2String(names, 0);
llSay(0, "✨ ULTIMATE VICTORY! " + winner + " is the Ultimate Survivor!");
// Trigger victory confetti and record win
llMessageLinked(LINK_SET, MSG_PLAYER_WON, winner, NULL_KEY);
llMessageLinked(LINK_SET, 995, "VICTORY_CONFETTI", NULL_KEY);
llRegionSay(SCOREBOARD_CHANNEL_1, "GAME_WON|" + winner); // Send winner info to scoreboard
// Wait for elimination status to display before showing victory
llSleep(STATUS_DISPLAY_TIME * 0.8); // Wait 80% of status display time (~6.4 seconds)
sendStatusMessage("Victory"); // Show victory status
// Wait for victory status to display before reset
llSleep(STATUS_DISPLAY_TIME + 1.0);
} else {
llSay(0, "💀 DESPAIR WINS! No Ultimate Survivors remain!");
// Wait for elimination status to display before reset (elimination already sent above)
llSleep(STATUS_DISPLAY_TIME + 1.0);
}
resetGame();
return;
}
// If the eliminated player was the peril player, assign a new one and continue game
if (eliminatedPlayer == perilPlayer) {
// Find first remaining alive player to be new peril player
perilPlayer = "";
integer k;
for (k = 0; k < llGetListLength(names) && perilPlayer == ""; k++) {
string candidateName = llList2String(names, k);
if (llList2Integer(lives, k) > 0) {
perilPlayer = candidateName;
llOwnerSay("🔄 After elimination, new peril player: " + perilPlayer);
}
}
// Continue the game with the new peril player (don't reset roundStarted)
if (perilPlayer != "") {
llOwnerSay("🎯 Continuing game with " + perilPlayer + " as new peril player");
// Reset picks and queue for new round, but keep roundStarted = TRUE
picksData = [];
globalPickedNumbers = [];
pickQueue = [perilPlayer];
integer i;
for (i = 0; i < llGetListLength(names); i++) {
string playerName = llList2String(names, i);
if (playerName != perilPlayer) {
pickQueue += [playerName];
}
}
currentPickerIdx = 0;
updateHelpers();
requestDiceType();
}
} else {
// Continue with existing peril player
updateHelpers();
}
}
}
return;
}
// Handle game won
if (num == 998) {
list parts = llParseString2List(str, ["|"], []);
if (llList2String(parts, 0) == "GAME_WON") {
string winner = llList2String(parts, 1);
llRegionSay(SCOREBOARD_CHANNEL_3, "GAME_WON|" + winner); // Send winner info to scoreboard
llSleep(2.0); // Let celebration message display
resetGame();
}
return;
}
// Handle incoming sync updates from Roll Confetti Module (only after rolls)
if (num == MSG_SYNC_GAME_STATE) {
// Only accept sync if it contains different data than what we have
list parts = llParseString2List(str, ["~"], []);
if (llGetListLength(parts) >= 4) {
list newLives = llCSV2List(llList2String(parts, 0));
string currentLivesStr = llList2CSV(lives);
string newLivesStr = llList2CSV(newLives);
// Check if we need to update (lives changed OR peril player changed)
string newPerilPlayerCheck = llList2String(parts, 2);
// Don't accept "NONE" as a valid peril player if we already have a real one,
// UNLESS the eliminated player is trying to be set as peril player
if (newPerilPlayerCheck == "NONE" && perilPlayer != "" && perilPlayer != "NONE") {
// Check if current peril player is still in the game
integer perilStillInGame = llListFindList(names, [perilPlayer]) != -1;
if (perilStillInGame) {
return; // Don't process this sync update
}
}
integer perilChanged = (perilPlayer != newPerilPlayerCheck);
integer livesChanged = (newLivesStr != currentLivesStr);
if (livesChanged || perilChanged) {
// Decode the pickData from the encoded format
string encodedPicksDataStr = llList2String(parts, 1);
list newPicksData = [];
if (encodedPicksDataStr != "") {
list encodedEntries = llParseString2List(encodedPicksDataStr, ["^"], []);
integer i;
for (i = 0; i < llGetListLength(encodedEntries); i++) {
string entry = llList2String(encodedEntries, i);
list entryParts = llParseString2List(entry, ["|"], []);
if (llGetListLength(entryParts) >= 2) {
string playerName = llList2String(entryParts, 0);
string picks = llList2String(entryParts, 1);
// Convert semicolons back to commas
picks = llDumpList2String(llParseString2List(picks, [";"], []), ",");
newPicksData += [playerName + "|" + picks];
} else {
newPicksData += [entry];
}
}
}
string newPerilPlayer = llList2String(parts, 2);
list newNames = llCSV2List(llList2String(parts, 3));
// Update our game state with the new data
lives = newLives;
picksData = newPicksData;
perilPlayer = newPerilPlayer;
names = newNames;
// Check if this is a post-roll state update and we need to start a new picking round
// Only trigger if lives changed (indicating a roll occurred) and we have a valid peril player
// Also check that all picks are empty (indicating this is after a roll, not during picking)
integer allPicksEmpty = TRUE;
integer i;
for (i = 0; i < llGetListLength(newPicksData) && allPicksEmpty; i++) {
string entry = llList2String(newPicksData, i);
list parts = llParseString2List(entry, ["|"], []);
if (llGetListLength(parts) >= 2 && llList2String(parts, 1) != "") {
allPicksEmpty = FALSE;
}
}
// Main Controller no longer sends CONTINUE_ROUND messages
// Game Manager handles all round continuation logic internally
if (livesChanged && newPerilPlayer != "" && newPerilPlayer != "NONE" && roundStarted && allPicksEmpty) {
llOwnerSay("🎯 Post-roll state update detected - Game Manager will handle round continuation");
}
// Don't call updateHelpers() here to avoid loop - other modules already have the data
}
}
return;
}
// Handle start next round request - delegate to Game Manager
if (num == 997) {
// Additional protection against round start spam
if (roundStarted && str == "START_NEXT_ROUND") {
llOwnerSay("⚠️ [Main Controller] Round already in progress, ignoring START_NEXT_ROUND");
return;
}
// Forward the request to Game Manager
llMessageLinked(LINK_SET, num, str, id);
// Don't request dice type here - it's handled in the "Start Game" listener below
return;
}
// Dice type requests and leave game handling now delegated to Controller_MessageHandler.lsl
// Handle ready state toggle
if (num == MSG_TOGGLE_READY) {
string playerName = str;
integer idx = llListFindList(names, [playerName]);
if (idx != -1) {
// Don't allow bots to toggle ready state (they're always ready)
if (llSubStringIndex(playerName, "TestBot") == 0) {
llOwnerSay("🤖 Bots are always ready and cannot change state");
return;
}
integer readyIdx = llListFindList(readyPlayers, [playerName]);
if (readyIdx == -1) {
// Player is not ready, make them ready
readyPlayers += [playerName];
llSay(0, "⚔️ " + playerName + " steels themselves for the deadly challenge ahead! ⚔️");
} else {
// Player is ready, make them not ready
readyPlayers = llDeleteSubList(readyPlayers, readyIdx, readyIdx);
llSay(0, "🏃 " + playerName + " loses their nerve and backs away from the challenge! 🏃");
}
}
return;
}
// Handle ready state queries
if (num == MSG_QUERY_READY_STATE) {
// Parse player name and optional request ID
list queryParts = llParseString2List(str, ["|"], []);
string playerName;
integer requestID = 0;
if (llGetListLength(queryParts) >= 2) {
// New format with request ID
playerName = llList2String(queryParts, 0);
requestID = (integer)llList2String(queryParts, 1);
} else {
// Legacy format - just player name
playerName = str;
}
integer isReady = llListFindList(readyPlayers, [playerName]) != -1;
integer isBot = llSubStringIndex(playerName, "TestBot") == 0;
string result = playerName + "|" + (string)isReady + "|" + (string)isBot + "|" + (string)requestID;
llMessageLinked(LINK_SET, MSG_READY_STATE_RESULT, result, id);
return;
}
// Handle aggressive floater cleanup
if (num == MSG_CLEANUP_ALL_FLOATERS) {
integer i;
for (i = 0; i < MAX_PLAYERS; i++) {
integer ch = FLOATER_BASE_CHANNEL + i;
llMessageLinked(LINK_SET, MSG_CLEANUP_FLOAT, (string)ch, NULL_KEY);
// Also try some potential duplicate channels
llMessageLinked(LINK_SET, MSG_CLEANUP_FLOAT, (string)(ch + 100), NULL_KEY);
llMessageLinked(LINK_SET, MSG_CLEANUP_FLOAT, (string)(ch + 1000), NULL_KEY);
}
// Reset our tracked channels list
floaterChannels = [];
return;
}
// Handle HUMAN_PICKED messages - no forwarding needed since NumberPicker sends to LINK_SET
if (num == -9998 && llSubStringIndex(str, "HUMAN_PICKED:") == 0) {
// No need to forward - Game Manager receives directly from NumberPicker
// Forwarding here creates an infinite loop!
return;
}
// Handle BOT_PICKED messages - no forwarding needed since Bot Manager sends to LINK_SET
if (num == -9997 && llSubStringIndex(str, "BOT_PICKED:") == 0) {
// No need to forward - Game Manager receives directly from Bot Manager
// Forwarding here creates an infinite loop!
return;
}
// Handle memory monitor messages
if (num == MSG_EMERGENCY_CLEANUP) {
// Memory monitor is requesting emergency cleanup
emergencyMemoryCleanup();
return;
}
}
listen(integer channel, string name, key id, string msg) {
// Discovery system now handled by Controller_Discovery.lsl helper script
if (channel == rollDialogChannel) {
if (msg == "Start Next Round" || msg == "BEGIN KILLING GAME") {
// Prevent duplicate round starts
if (roundStarted) {
llOwnerSay("⚠️ Round already in progress, ignoring duplicate round start from roll dialog");
return;
}
llSay(0, "⚡ THE KILLING GAME CONTINUES! " + perilPlayer + " begins the next deadly round!");
// Delegate to Game Manager
llMessageLinked(LINK_SET, 997, "START_NEXT_ROUND", NULL_KEY);
return;
}
}
if (channel == DIALOG_CHANNEL) {
// Handle owner choice dialog responses during gameplay
if (id == llGetOwner() && roundStarted) {
if (msg == "🔄 Recover Pick Dialog") {
string ownerName = getPlayerName(id);
llRegionSayTo(id, 0, "🔄 Restoring your number picking dialog...");
llMessageLinked(LINK_SET, MSG_GET_CURRENT_DIALOG, ownerName, id);
return;
}
else if (msg == "🔄 Recover Roll Dialog") {
llRegionSayTo(id, 0, "🔄 Restoring your roll dialog...");
llMessageLinked(LINK_SET, MSG_SHOW_ROLL_DIALOG, perilPlayer, id);
return;
}
else if (msg == "🔧 Admin Menu") {
llMessageLinked(LINK_SET, MSG_SHOW_MENU, "owner|0", id);
return;
}
}
// Owner-specific commands: only owner messages in the dialog should be processed here.
if (id == llGetOwner()) {
if (msg == "Reset Game") {
resetGame(); // Only reset current game, keep leaderboard
return;
}
if (msg == "Reset Leaderboard") {
llRegionSay(SCOREBOARD_CHANNEL_1, "RESET_LEADERBOARD"); // Reset only leaderboard scores
llOwnerSay("🏆 Leaderboard scores reset - game wins cleared!");
return;
}
if (msg == "Reset All") {
resetGame(); // Reset current game
llRegionSay(SCOREBOARD_CHANNEL_1, "RESET_LEADERBOARD"); // Reset leaderboard scores
llOwnerSay("🔄 Complete reset - game and leaderboard cleared!");
return;
}
if (msg == "Memory Stats") {
reportMemoryStats();
return;
}
// Note: do not handle "Start Game" here; allow the generic start logic below to apply,
// so that minimum player checks are enforced and non-owner starters can initiate the game.
if (msg == "Add Test Player") {
// Ensure we do not exceed the maximum allowed players
if (llGetListLength(players) >= MAX_PLAYERS) {
llOwnerSay("⚠️ Cannot add test player; the game is full (max " + (string)MAX_PLAYERS + ").");
return;
}
// Generate a unique name and key for the test player
string testName = "TestBot" + (string)llGetUnixTime();
key fake = llGenerateKey();
// Delegate registration to the MSG_REGISTER_PLAYER handler; it will add to lists and rez a float
llMessageLinked(LINK_SET, MSG_REGISTER_PLAYER, testName + "|" + (string)fake, NULL_KEY);
// Show the owner menu again - owner should still be starter after adding bots
integer ownerIdx = llListFindList(players, [llGetOwner()]);
integer ownerIsStarter = TRUE; // Default to TRUE for owner
integer n;
for (n = 0; n < ownerIdx && ownerIsStarter; n++) {
string existingName = llList2String(names, n);
// If there's a human player before the owner, owner is not starter
if (llSubStringIndex(existingName, "TestBot") != 0) {
ownerIsStarter = FALSE;
}
}
llMessageLinked(LINK_SET, MSG_SHOW_MENU, "owner|" + (string)ownerIsStarter, llGetOwner());
return;
}
}
// Allow the first registered player (index 0) or the owner to start the game.
// When the starter clicks "Start Game", begin the round.
if (msg == "Start Game") {
// Enforce a minimum of 2 players before starting
if (llGetListLength(players) < 2) {
llOwnerSay("⚠️ Need at least 2 players to start the game.");
return;
}
// Check if all non-starter players are ready
string starterName = "";
if (id == llGetOwner()) {
starterName = getPlayerName(llGetOwner());
} else if (llGetListLength(players) > 0) {
key firstPlayer = llList2Key(players, 0);
if (id == firstPlayer) {
starterName = getPlayerName(firstPlayer);
}
}
if (starterName == "") {
llOwnerSay("⚠️ Only the game starter can start the game.");
return;
}
// Check if all non-starter human players are ready
list notReadyPlayers = [];
integer i;
for (i = 0; i < llGetListLength(names); i++) {
string playerName = llList2String(names, i);
// Skip the starter and bots (bots are auto-ready)
if (playerName != starterName && llSubStringIndex(playerName, "TestBot") != 0) {
if (llListFindList(readyPlayers, [playerName]) == -1) {
notReadyPlayers += [playerName];
}
}
}
if (llGetListLength(notReadyPlayers) > 0) {
llOwnerSay("⚠️ Cannot start game. These players are not ready: " + llList2CSV(notReadyPlayers));
return;
}
// All checks passed, start the game
gameStarting = TRUE; // Prevent new players from joining during startup
llSay(0, "⚡ ALL PARTICIPANTS READY! THE DEADLY PERIL DICE GAME BEGINS! ⚡");
// Change controller display to show game is active
llSetText("🎮 GAME IN PROGRESS\nRound " + (string)(llGetListLength(names)) + " players", <1.0, 0.2, 0.2>, 1.0);
sendStatusMessage("Title"); // Show title status at game start
// Delegate to Game Manager
llMessageLinked(LINK_SET, 997, "START_NEXT_ROUND", NULL_KEY);
// Don't request dice type here - let Game Manager handle it to avoid duplicate requests
}
// If the message text matches a player name, request their pick list
if (llListFindList(names, [msg]) != -1) {
llMessageLinked(LINK_SET, 206, msg, id);
}
}
}
timer() {
if (currentTimerMode == TIMER_STATUS) {
// Handle status message clearing
if (statusTimer > 0 && (llGetUnixTime() - statusTimer) >= STATUS_DISPLAY_TIME) {
statusTimer = 0;
lastStatus = "";
llRegionSay(SCOREBOARD_CHANNEL_1, "GAME_STATUS|Title");
llOwnerSay("📢 Status cleared - reverted to Title");
currentTimerMode = TIMER_IDLE;
llSetTimerEvent(0);
}
}
else if (currentTimerMode == TIMER_TIMEOUT) {
// Handle dialog timeout warnings
if (timeoutTimer > 0) {
integer elapsed = llGetUnixTime() - timeoutTimer;
if (elapsed >= TIMEOUT_SECONDS) {
llOwnerSay("⏰ Dialog timeout reached!");
timeoutTimer = 0;
currentTimerMode = TIMER_IDLE;
llSetTimerEvent(0);
} else {
integer remaining = TIMEOUT_SECONDS - elapsed;
if (remaining <= warning2min && lastWarning < warning2min) {
llOwnerSay("⚠️ " + (string)(remaining / 60) + " minutes remaining for dialog response");
lastWarning = warning2min;
} else if (remaining <= warning5min && lastWarning < warning5min) {
llOwnerSay("⚠️ " + (string)(remaining / 60) + " minutes remaining for dialog response");
lastWarning = warning5min;
} else if (remaining <= warning9min && lastWarning < warning9min) {
llOwnerSay("⚠️ " + (string)(remaining / 60) + " minutes remaining for dialog response");
lastWarning = warning9min;
}
}
}
}
// Discovery timer handling now delegated to Controller_Discovery.lsl helper script
}
}