Peril_Dice/Game_Manager.lsl

1053 lines
51 KiB
Text

#include "Peril_Constants.lsl"
// === Game Manager - Game Flow Logic ===
// 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;
}
// Game timing settings
#define BOT_PICK_DELAY 2.0
#define HUMAN_PICK_DELAY 1.0
#define DIALOG_DELAY 1.5
#define STATUS_DISPLAY_TIME 8.0
// Game state - synced from Main Controller
list players = [];
list names = [];
list lives = [];
string perilPlayer = "";
list globalPickedNumbers = [];
list picksData = [];
list pickQueue = [];
integer currentPickerIdx = 0;
integer diceType = 6;
key currentPicker;
integer roundStarted = FALSE;
integer diceTypeProcessed = FALSE; // Track if we've already processed dice type for this round
integer ignorePicksSync = FALSE; // Temporarily ignore picks data sync during round initialization
integer roundContinueInProgress = FALSE; // Prevent multiple rapid calls to continueCurrentRound
integer lastSyncProcessTime = 0; // Track when we last processed a sync to prevent rapid duplicates
// Duplicate request prevention
string lastHumanPickMessage = "";
string lastBotPickMessage = "";
integer lastProcessTime = 0;
// Build complete avoidance list from all current picks
list buildCompleteAvoidanceList() {
list allPicks = [];
integer i;
// Add all picks from picksData (current round)
for (i = 0; i < llGetListLength(picksData); i++) {
string entry = llList2String(picksData, i);
list entryParts = llParseString2List(entry, ["|"], []);
if (llGetListLength(entryParts) >= 2) {
string picks = llList2String(entryParts, 1);
if (picks != "") {
// Split multiple picks (comma or semicolon separated)
list playerPicks = llParseString2List(picks, [","], []);
if (llGetListLength(playerPicks) == 1) {
// Also try semicolon separator
playerPicks = llParseString2List(picks, [";"], []);
}
integer j;
for (j = 0; j < llGetListLength(playerPicks); j++) {
string pick = llStringTrim(llList2String(playerPicks, j), STRING_TRIM);
if (pick != "" && llListFindList(allPicks, [pick]) == -1) {
allPicks += [pick];
}
}
}
}
}
// Also add any picks from globalPickedNumbers as backup
for (i = 0; i < llGetListLength(globalPickedNumbers); i++) {
string globalPick = llList2String(globalPickedNumbers, i);
if (globalPick != "" && llListFindList(allPicks, [globalPick]) == -1) {
allPicks += [globalPick];
}
}
return allPicks;
}
// Memory reporting function
integer reportMemoryUsage(string scriptName) {
integer used = llGetUsedMemory();
integer free = llGetFreeMemory();
integer total = used + free;
float percentUsed = ((float)used / (float)total) * 100.0;
dbg("🧠 [" + scriptName + "] Memory: " +
(string)used + " used, " +
(string)free + " free (" +
llGetSubString((string)percentUsed, 0, 4) + "% used)");
return 0;
}
// Build complete avoidance list from all current picks
integer continueCurrentRound() {
// MEMORY OPTIMIZED: Skip complex validation - trust game state
// Main Controller handles all game ending validation
// Check for eliminated players (0 lives) - don't continue if any exist
integer continueIdx;
for (continueIdx = 0; continueIdx < llGetListLength(names); continueIdx++) {
string continueName = llList2String(names, continueIdx);
integer continuePlayerIdx = llListFindList(names, [continueName]);
if (continuePlayerIdx != -1 && continuePlayerIdx < llGetListLength(lives)) {
integer continuePlayerLives = llList2Integer(lives, continuePlayerIdx);
if (continuePlayerLives <= 0) {
dbg("🛑 [Game Manager] continueCurrentRound: Found eliminated player " + continueName + " with " + (string)continuePlayerLives + " lives - aborting round start");
return 0;
}
}
}
if (perilPlayer == "" || perilPlayer == "NONE") {
dbg("🛑 [Game Manager] continueCurrentRound: Invalid peril player (" + perilPlayer + ") - aborting round start");
return 0;
}
// Verify peril player exists and has lives
integer perilIdx = llListFindList(names, [perilPlayer]);
if (perilIdx == -1) {
dbg("🛑 [Game Manager] continueCurrentRound: Peril player " + perilPlayer + " not found in names list - aborting round start");
return 0;
}
if (perilIdx >= llGetListLength(lives)) {
dbg("🛑 [Game Manager] continueCurrentRound: Peril player " + perilPlayer + " index out of bounds - aborting round start");
return 0;
}
integer perilLives = llList2Integer(lives, perilIdx);
if (perilLives <= 0) {
dbg("🛑 [Game Manager] continueCurrentRound: Peril player " + perilPlayer + " has " + (string)perilLives + " lives - aborting round start");
return 0;
}
// Start fresh for a new round - clear picks data
// globalPickedNumbers will be cleared when first player picks
picksData = [];
dbg("🔄 [Game Manager] continueCurrentRound: All validation passed, starting fresh round with " + (string)llGetListLength(names) + " players");
// Set round as started for the new round
roundStarted = TRUE;
diceTypeProcessed = FALSE; // Reset dice type processing for new round
// Create new pick queue with peril player first
pickQueue = [perilPlayer];
for (continueIdx = 0; continueIdx < llGetListLength(names); continueIdx++) {
string continuePlayerName = llList2String(names, continueIdx);
if (continuePlayerName != perilPlayer) {
pickQueue += [continuePlayerName];
}
}
currentPickerIdx = 0;
dbg("🎯 [Game Manager] Pick queue created: " + llList2CSV(pickQueue) + ", requesting dice type");
// Request dice type directly from Calculator for the new round
llMessageLinked(LINK_SET, MSG_GET_DICE_TYPE, (string)llGetListLength(names), NULL_KEY);
// Note: showNextPickerDialog() will be called when dice type result is received
return 0;
}
integer startNextRound() {
// Prevent multiple calls to start round
if (roundStarted) {
dbg("⚠️ Round already started, ignoring duplicate start request");
return 0;
}
if (llGetListLength(names) < 2) {
dbg("⚠️ Need at least 2 players to start the game.");
return 0;
}
if (llGetListLength(names) == 1) {
string winner = llList2String(names, 0);
llSay(0, "✨ ULTIMATE VICTORY! " + winner + " is the Ultimate Survivor!");
// Send winner glow update to scoreboard
llMessageLinked(LINK_SCOREBOARD, MSG_UPDATE_WINNER, winner, NULL_KEY); // MSG_UPDATE_WINNER
llMessageLinked(LINK_SET, MSG_PLAYER_WON, winner, NULL_KEY);
llMessageLinked(LINK_SET, MSG_EFFECT_CONFETTI, "VICTORY_CONFETTI", NULL_KEY);
// Don't use hardcoded channel here - let Main Controller handle scoreboard updates
// Main Controller will send the proper GAME_WON message to scoreboard
// Let main controller handle reset
return 0;
}
dbg("🎯 Game Manager starting new round...");
// Set roundStarted immediately to prevent duplicate calls
roundStarted = TRUE;
diceTypeProcessed = FALSE; // Reset for new round
// Select random peril player if none is set
if (perilPlayer == "" || perilPlayer == "NONE") {
integer randomIdx = (integer)llFrand(llGetListLength(names));
perilPlayer = llList2String(names, randomIdx);
llSay(0, "🎯 " + perilPlayer + " has been randomly selected and is now in peril!");
// Send peril player update to scoreboard for glow effect
llMessageLinked(LINK_SCOREBOARD, MSG_UPDATE_PERIL_PLAYER, perilPlayer, NULL_KEY); // MSG_UPDATE_PERIL_PLAYER
// Sync state to floater manager first, then update floaters to show new peril player immediately
dbg("🔄 [Game Manager] Syncing state and updating floaters for new peril player: " + perilPlayer);
syncStateToMain(); // Sync the peril player to all modules first
llSleep(0.1); // Brief delay to ensure sync propagates
integer startJ;
for (startJ = 0; startJ < llGetListLength(names); startJ++) {
string startPlayerName = llList2String(names, startJ);
llMessageLinked(LINK_SET, MSG_UPDATE_FLOAT, startPlayerName, NULL_KEY);
}
}
picksData = [];
// globalPickedNumbers will be cleared when first player picks
// Create pick queue with peril player first
dbg("🎯 Debug - Creating pickQueue. names: " + llList2CSV(names) + ", perilPlayer: " + perilPlayer);
pickQueue = [perilPlayer];
integer k;
for (k = 0; k < llGetListLength(names); k++) {
string startName = llList2String(names, k);
if (startName != perilPlayer) {
pickQueue += [startName];
}
}
currentPickerIdx = 0;
dbg("🎯 Debug - Created pickQueue: " + llList2CSV(pickQueue) + ", currentPickerIdx: " + (string)currentPickerIdx);
// Request dice type for this round
llMessageLinked(LINK_SET, MSG_GET_DICE_TYPE, (string)llGetListLength(names), NULL_KEY);
dbg("🎯 Game Manager round setup complete, requesting dice type...");
return 0;
}
integer showNextPickerDialog() {
// MEMORY OPTIMIZED: Skip complex validation - trust that game is active
// Main Controller handles all game ending logic
// Clear globalPickedNumbers if this is the first picker of a new round
if (currentPickerIdx == 0 && llGetListLength(picksData) == 0) {
globalPickedNumbers = [];
dbg("🔄 [Game Manager] Cleared globalPickedNumbers at start of new round");
}
if (diceType <= 0) {
dbg("❌ Cannot show picker dialog: diceType not set (" + (string)diceType + ")");
return 0;
}
if (currentPickerIdx >= llGetListLength(pickQueue)) {
dbg("❌ Cannot show picker dialog: currentPickerIdx (" + (string)currentPickerIdx + ") >= pickQueue length (" + (string)llGetListLength(pickQueue) + ")");
return 0;
}
string firstName = llList2String(pickQueue, currentPickerIdx);
if (firstName == "") {
dbg("❌ Cannot show picker dialog: empty player name at index " + (string)currentPickerIdx);
return 0;
}
// CRITICAL: Verify this player is still alive and in the game
integer showPlayerIdx = llListFindList(names, [firstName]);
if (showPlayerIdx == -1) {
dbg("❌ Cannot show picker dialog: player " + firstName + " not found in current game");
return 0;
}
if (showPlayerIdx >= llGetListLength(lives)) {
dbg("❌ Cannot show picker dialog: player " + firstName + " index out of bounds for lives list");
return 0;
}
integer showPlayerLives = llList2Integer(lives, showPlayerIdx);
if (showPlayerLives <= 0) {
dbg("❌ Cannot show picker dialog: player " + firstName + " has been eliminated (" + (string)showPlayerLives + " lives)");
return 0;
}
// CHECK: Don't show dialog if this player already has picks to prevent loops
integer alreadyHasPicks = FALSE;
integer showIdx;
for (showIdx = 0; showIdx < llGetListLength(picksData) && !alreadyHasPicks; showIdx++) {
if (llSubStringIndex(llList2String(picksData, showIdx), firstName + "|") == 0) {
string existingPicks = llGetSubString(llList2String(picksData, showIdx), llStringLength(firstName) + 1, -1);
if (existingPicks != "") {
alreadyHasPicks = TRUE;
dbg("🎯 Game Manager: " + firstName + " already has picks (" + existingPicks + "), advancing to next player");
}
}
}
if (alreadyHasPicks) {
// Skip to next player to prevent infinite loops
currentPickerIdx++;
if (currentPickerIdx < llGetListLength(pickQueue)) {
showNextPickerDialog();
} else {
// All picked, show roll dialog through Player_RegistrationManager
string firstRollRequest = "SHOW_ROLL_DIALOG|" + perilPlayer + "|" + perilPlayer;
llMessageLinked(LINK_SET, MSG_DIALOG_FORWARD_REQUEST, firstRollRequest, NULL_KEY);
}
return 0;
}
integer nameIdx = llListFindList(names, [firstName]);
if (nameIdx == -1) {
dbg("❌ Cannot show picker dialog: player " + firstName + " not found in names list");
return 0;
}
// SIMPLE FIX: Send dialog request to Main Controller with player name
// Main Controller has the player keys and can forward to NumberPicker with correct key
llSleep(DIALOG_DELAY);
if (llSubStringIndex(firstName, "Bot") == 0) {
// Bot picking - but first double-check this bot doesn't already have picks
integer botAlreadyHasPicks = FALSE;
integer showJ;
for (showJ = 0; showJ < llGetListLength(picksData) && !botAlreadyHasPicks; showJ++) {
if (llSubStringIndex(llList2String(picksData, showJ), firstName + "|") == 0) {
string existingBotPicks = llGetSubString(llList2String(picksData, showJ), llStringLength(firstName) + 1, -1);
if (existingBotPicks != "") {
botAlreadyHasPicks = TRUE;
dbg("⚠️ [Game Manager] Bot " + firstName + " already has picks, skipping bot command: " + existingBotPicks);
}
}
}
if (!botAlreadyHasPicks) {
integer showPerilIdx = llListFindList(names, [perilPlayer]);
integer showPerilLives = 3;
if (showPerilIdx != -1) {
showPerilLives = llList2Integer(lives, showPerilIdx);
}
integer showPicksNeeded = 4 - showPerilLives;
// Send proper avoid list to bots so they don't pick human numbers
list showCompleteAvoidList = buildCompleteAvoidanceList();
string showAvoidListStr = llList2CSV(showCompleteAvoidList);
string botCommand = "BOT_PICK:" + firstName + ":" + (string)showPicksNeeded + ":" + (string)diceType + ":" + showAvoidListStr;
dbg("🎯 [Game Manager] Sending bot command with complete avoid list (" + (string)llGetListLength(showCompleteAvoidList) + " numbers): " + showAvoidListStr);
llMessageLinked(LINK_SET, MSG_BOT_COMMAND, botCommand, NULL_KEY);
dbg("🤖 " + firstName + " is automatically picking " + (string)showPicksNeeded + " numbers...");
} else {
// Bot already has picks, advance to next player immediately
currentPickerIdx++;
if (currentPickerIdx < llGetListLength(pickQueue)) {
showNextPickerDialog();
} else {
// All picked, show roll dialog through Player_RegistrationManager
string humanRollRequest = "SHOW_ROLL_DIALOG|" + perilPlayer + "|" + perilPlayer;
llMessageLinked(LINK_SET, MSG_DIALOG_FORWARD_REQUEST, humanRollRequest, NULL_KEY);
}
}
} else {
// Human picking
integer humanPerilIdx = llListFindList(names, [perilPlayer]);
integer humanPerilLives = 3;
if (humanPerilIdx != -1) {
humanPerilLives = llList2Integer(lives, humanPerilIdx);
}
integer humanPicksNeeded = 4 - humanPerilLives;
// Send dialog payload with complete avoid list for humans too
list humanCompleteAvoidList = buildCompleteAvoidanceList();
string humanAvoidListStr = llList2CSV(humanCompleteAvoidList);
string dialogPayload = firstName + "|" + (string)diceType + "|" + (string)humanPicksNeeded + "|" + humanAvoidListStr;
dbg("🎯 Showing pick dialog for " + firstName);
llSleep(0.5); // Brief delay to prevent spam
// Send dialog request through Player_RegistrationManager (it has the correct player keys)
string dialogRequest = "SHOW_DIALOG|" + firstName + "|" + dialogPayload;
llMessageLinked(LINK_SET, MSG_DIALOG_FORWARD_REQUEST, dialogRequest, NULL_KEY);
}
return 0;
}
// requestDiceType() removed - Game Manager no longer requests dice type directly
// This prevents loops. Main Controller handles all dice type requests.
integer syncStateToMain() {
// MEMORY OPTIMIZED: Skip ALL sync operations to prevent corrupting Main Controller's data
// Main Controller is the master - Game Manager should not send sync messages
return 0;
}
default {
state_entry() {
DISCOVER_CORE_LINKS();
reportMemoryUsage("Game Manager");
dbg("🎯 Game Manager ready - discovery complete! Scoreboard: " + (string)LINK_SCOREBOARD);
// Initialize/reset all game state variables
players = [];
names = [];
lives = [];
perilPlayer = "";
globalPickedNumbers = [];
picksData = [];
pickQueue = [];
currentPickerIdx = 0;
diceType = 6;
currentPicker = NULL_KEY;
roundStarted = FALSE;
diceTypeProcessed = FALSE;
ignorePicksSync = FALSE;
roundContinueInProgress = FALSE;
lastHumanPickMessage = "";
lastBotPickMessage = "";
lastProcessTime = 0;
dbg("🎯 Game Manager ready!");
}
on_rez(integer start_param) {
DISCOVER_CORE_LINKS();
reportMemoryUsage("Game Manager");
dbg("🔄 Game Manager reset via rez...");
// Reset all game state variables on rez
players = [];
names = [];
lives = [];
perilPlayer = "";
globalPickedNumbers = [];
picksData = [];
pickQueue = [];
currentPickerIdx = 0;
diceType = 6;
currentPicker = NULL_KEY;
roundStarted = FALSE;
diceTypeProcessed = FALSE;
ignorePicksSync = FALSE;
roundContinueInProgress = FALSE;
lastHumanPickMessage = "";
lastBotPickMessage = "";
lastProcessTime = 0;
dbg("✅ Game Manager reset complete after rez!");
}
link_message(integer sender, integer num, string str, key id) {
// Handle incoming sync updates from Roll Confetti Module and other sources
if (num == MSG_SYNC_GAME_STATE) {
// Only skip sync messages if we're explicitly ignoring them
// Allow player registration syncs even when game is not active
list parts = llParseString2List(str, ["~"], []);
if (llGetListLength(parts) >= 4) {
list newLives = llCSV2List(llList2String(parts, 0));
string currentLivesStr = llList2CSV(lives);
string newLivesStr = llList2CSV(newLives);
string newPerilPlayerCheck = llList2String(parts, 2);
list newNames = llCSV2List(llList2String(parts, 3));
// MEMORY OPTIMIZED: Skip complex validation that causes circular sync issues
// Trust that sender sends valid data
if (newPerilPlayerCheck == "NONE" && perilPlayer != "" && perilPlayer != "NONE") {
integer perilStillInGame = llListFindList(names, [perilPlayer]) != -1;
if (perilStillInGame) {
return;
}
}
integer perilChanged = (perilPlayer != newPerilPlayerCheck);
integer livesChanged = (newLivesStr != currentLivesStr);
if (livesChanged || perilChanged) {
string encodedPicksDataStr = llList2String(parts, 1);
list newPicksData = [];
if (encodedPicksDataStr != "") {
list encodedEntries = llParseString2List(encodedPicksDataStr, ["^"], []);
integer syncI;
for (syncI = 0; syncI < llGetListLength(encodedEntries); syncI++) {
string syncEntry = llList2String(encodedEntries, syncI);
list entryParts = llParseString2List(syncEntry, ["|"], []);
if (llGetListLength(entryParts) >= 2) {
string syncPlayerName = llList2String(entryParts, 0);
string picks = llList2String(entryParts, 1);
picks = llDumpList2String(llParseString2List(picks, [";"], []), ",");
newPicksData += [syncPlayerName + "|" + picks];
} else {
newPicksData += [syncEntry];
}
}
}
string newPerilPlayer = llList2String(parts, 2);
list newNames = llCSV2List(llList2String(parts, 3));
lives = newLives;
picksData = newPicksData;
perilPlayer = newPerilPlayer;
names = newNames;
// Send lightweight update to Main Controller for floating text (only if substantial change)
// Use the NEW names list count (after elimination has been processed)
if (perilChanged && perilPlayer != "" && perilPlayer != "NONE") {
integer currentPlayerCount = llGetListLength(newNames); // Use the updated count
string lightUpdate = "PERIL_UPDATE|" + perilPlayer + "|" + (string)currentPlayerCount;
llMessageLinked(LINK_SET, MSG_SYNC_LIGHTWEIGHT, lightUpdate, NULL_KEY);
}
integer allPicksEmpty = TRUE;
integer checkI;
for (checkI = 0; checkI < llGetListLength(newPicksData) && allPicksEmpty; checkI++) {
string checkEntry = llList2String(newPicksData, checkI);
list checkParts = llParseString2List(checkEntry, ["|"], []);
if (llGetListLength(checkParts) >= 2 && llList2String(checkParts, 1) != "") {
allPicksEmpty = FALSE;
}
}
if (livesChanged && newPerilPlayer != "" && newPerilPlayer != "NONE" && roundStarted && allPicksEmpty) {
dbg("🎯 Post-roll state update detected - continuing round");
continueCurrentRound();
}
}
}
return;
}
// Receive legacy game state updates from main controller (simplified version)
if (num == MSG_SYNC_LEGACY) {
dbg("🔧 [Game Manager] Received sync: " + str);
list parts = llParseString2List(str, ["~"], []);
dbg("🔧 [Game Manager] Parsed into " + (string)llGetListLength(parts) + " parts");
// MEMORY OPTIMIZED: Skip heavy validation that causes sync rejections
// Trust that Main Controller sends valid data
// Handle special RESET sync message
if (llGetListLength(parts) >= 5 && llList2String(parts, 0) == "RESET") {
dbg("🔄 [Game Manager] Received reset sync - ignoring during reset");
return;
}
if (llGetListLength(parts) < 4) {
dbg("⚠️ [Game Manager] Incomplete sync message received, parts: " + (string)llGetListLength(parts) + " - IGNORING");
return;
}
{
// MEMORY OPTIMIZED: Handle compatible format efficiently
if (llGetListLength(parts) >= 4) {
// Parse names from part 3 (compatible with standard format)
string namesStr = llList2String(parts, 3);
// CRITICAL: Must use real player names for dialog system
if (llSubStringIndex(namesStr, ",") != -1) {
// Multiple players - extract real names and trim spaces
integer commaPos = llSubStringIndex(namesStr, ",");
string firstName = llStringTrim(llGetSubString(namesStr, 0, commaPos - 1), STRING_TRIM);
string secondName = llStringTrim(llGetSubString(namesStr, commaPos + 1, -1), STRING_TRIM);
names = [firstName, secondName];
// Parse lives from sync message instead of hardcoding
string multiLivesStr = llList2String(parts, 0);
if (llSubStringIndex(multiLivesStr, ",") != -1) {
lives = llCSV2List(multiLivesStr);
} else {
lives = [3, 3]; // Fallback for single player
}
} else {
// Single player - use real name and trim spaces
names = [llStringTrim(namesStr, STRING_TRIM)];
// Parse lives from sync message instead of hardcoding
string livesStr = llList2String(parts, 0);
if (livesStr != "") {
lives = llCSV2List(livesStr);
} else {
lives = [3]; // Fallback
}
}
dbg("🎯 [Game Manager] Updated: " + (string)llGetListLength(names) + " players (minimal parsing)");
}
// Update peril player
string receivedPerilPlayer = llList2String(parts, 2);
if (receivedPerilPlayer != "NONE" && receivedPerilPlayer != "") {
perilPlayer = receivedPerilPlayer;
dbg("🎯 [Game Manager] Peril player updated to: " + perilPlayer);
}
// MEMORY OPTIMIZED: Skip all other complex processing
}
return;
}
// Receive pick queue updates
if (num == MSG_SYNC_PICKQUEUE) {
list newPickQueue = llCSV2List(str);
string senderStr = (string)sender;
string queueLenStr = (string)llGetListLength(newPickQueue);
dbg("🔍 [Game Manager] Received pickQueue sync from sender " + senderStr + ": '" + str + "' (" + queueLenStr + " items)");
// STRONGEST PROTECTION: Never accept empty pickQueues during any active round
if (roundStarted && llGetListLength(pickQueue) > 0 && (str == "" || llGetListLength(newPickQueue) == 0)) {
dbg("🔍 [Game Manager] REJECTING empty/invalid pickQueue sync during active round - keeping: " + llList2CSV(pickQueue));
return;
}
// Additional protection: Don't accept pickQueue syncs if we just created a valid one
if (llGetListLength(pickQueue) > 0 && (str == "" || llGetListLength(newPickQueue) == 0)) {
dbg("🔍 [Game Manager] REJECTING empty pickQueue sync - keeping valid queue: " + llList2CSV(pickQueue));
return;
}
// Only accept valid non-empty pickQueues
if (llGetListLength(newPickQueue) > 0) {
pickQueue = newPickQueue;
dbg("🔍 [Game Manager] pickQueue updated to: " + llList2CSV(pickQueue));
} else {
dbg("🔍 [Game Manager] Ignoring invalid pickQueue sync - keeping current: " + llList2CSV(pickQueue));
}
return;
}
// Handle dice type result from Calculator
if (num == MSG_DICE_TYPE_RESULT) {
// Prevent duplicate processing of dice type results
if (diceTypeProcessed) {
dbg("🎲 Ignoring duplicate dice type result: " + str);
return;
}
diceType = (integer)str;
diceTypeProcessed = TRUE;
dbg("🎲 Game Manager received dice type: d" + str + " from Calculator");
llSleep(0.3); // Brief delay
// Start dialog if round has been started and we have a queue
if (roundStarted && currentPickerIdx < llGetListLength(pickQueue) && llGetListLength(pickQueue) > 0 && diceType > 0) {
// Check if current picker already has picks to avoid duplicate dialogs
string currentPlayerName = llList2String(pickQueue, currentPickerIdx);
integer alreadyHasPicks = FALSE;
integer i;
for (i = 0; i < llGetListLength(picksData); i++) {
if (llSubStringIndex(llList2String(picksData, i), currentPlayerName + "|") == 0) {
string picks = llGetSubString(llList2String(picksData, i), llStringLength(currentPlayerName) + 1, -1);
if (picks != "") {
alreadyHasPicks = TRUE;
}
}
}
if (!alreadyHasPicks) {
dbg("🎯 Starting picks for " + currentPlayerName);
llSleep(0.5);
showNextPickerDialog();
}
}
return;
}
// Legacy 997 message handler removed - now using direct MSG_CONTINUE_ROUND (998) communication
// Handle human picks
if (num == MSG_HUMAN_PICKED && llSubStringIndex(str, "HUMAN_PICKED:") == 0) {
list parts = llParseString2List(str, [":"], []);
if (llGetListLength(parts) >= 3) {
string playerName = llList2String(parts, 1);
string picksStr = llList2String(parts, 2);
dbg("👤 HUMAN: " + playerName + " picking " + picksStr + " (global has: " + llList2CSV(globalPickedNumbers) + ")");
// Check if this player already has picks to prevent duplicate processing
integer alreadyHasPicks = FALSE;
integer existingPicksIdx = -1;
integer m;
for (m = 0; m < llGetListLength(picksData) && !alreadyHasPicks; m++) {
if (llSubStringIndex(llList2String(picksData, m), playerName + "|") == 0) {
string existingPicks = llGetSubString(llList2String(picksData, m), llStringLength(playerName) + 1, -1);
if (existingPicks != "") {
dbg("⚠️ [Game Manager] " + playerName + " already has picks: " + existingPicks + ", ignoring duplicate HUMAN_PICKED");
alreadyHasPicks = TRUE;
}
}
}
if (alreadyHasPicks) {
return; // Ignore duplicate human picks
}
// Validate picks against complete avoidance list (all current picks from all players)
list completeAvoidList = buildCompleteAvoidanceList();
list newPicks = llParseString2List(picksStr, [","], []);
list validPicks = [];
list duplicatePicks = [];
integer i;
dbg("🔍 [Game Manager] Validating " + playerName + "'s picks against complete avoid list (" + (string)llGetListLength(completeAvoidList) + " numbers): " + llList2CSV(completeAvoidList));
for (i = 0; i < llGetListLength(newPicks); i++) {
string pick = llStringTrim(llList2String(newPicks, i), STRING_TRIM);
if (pick != "") {
if (llListFindList(completeAvoidList, [pick]) == -1) {
validPicks += [pick];
globalPickedNumbers += [pick];
} else {
duplicatePicks += [pick];
}
}
}
if (llGetListLength(duplicatePicks) > 0) {
dbg("❌ DUPLICATE PICKS REJECTED: " + playerName + " tried to pick: " + llList2CSV(duplicatePicks));
integer idx = llListFindList(names, [playerName]);
if (idx != -1) {
llRegionSayTo(llList2Key(players, idx), 0, "❌ Some picks were already taken. Please pick again.");
// Remove valid picks we added
integer j;
for (j = 0; j < llGetListLength(validPicks); j++) {
string validPick = llList2String(validPicks, j);
integer globalIdx = llListFindList(globalPickedNumbers, [validPick]);
if (globalIdx != -1) {
globalPickedNumbers = llDeleteSubList(globalPickedNumbers, globalIdx, globalIdx);
}
}
// Re-show the dialog with updated globally picked numbers
dbg("⚠️ [Game Manager] Re-showing pick dialog for " + playerName + " with updated picks");
// Calculate picks needed based on peril player's lives
integer perilIdx = llListFindList(names, [perilPlayer]);
integer perilLives = 3;
if (perilIdx != -1) {
perilLives = llList2Integer(lives, perilIdx);
}
integer picksNeeded = 4 - perilLives;
// Use complete avoid list for re-shown dialog too
list updatedCompleteAvoidList = buildCompleteAvoidanceList();
string dialogPayload = playerName + "|" + (string)diceType + "|" + (string)picksNeeded + "|" + llList2CSV(updatedCompleteAvoidList);
string dialogRequest = "SHOW_DIALOG|" + playerName + "|" + dialogPayload;
llMessageLinked(LINK_SET, MSG_DIALOG_FORWARD_REQUEST, dialogRequest, NULL_KEY);
}
return;
}
// Update picks data
integer idx = llListFindList(names, [playerName]);
if (idx != -1) {
integer picksIdx = -1;
integer k;
for (k = 0; k < llGetListLength(picksData); k++) {
if (llSubStringIndex(llList2String(picksData, k), playerName + "|") == 0) {
picksIdx = k;
}
}
string newEntry = playerName + "|" + picksStr;
if (picksIdx == -1) {
picksData += [newEntry];
} else {
picksData = llListReplaceList(picksData, [newEntry], picksIdx, picksIdx);
}
dbg("🟢 " + playerName + " picks saved: " + picksStr);
llSay(0, "🎯 " + playerName + " stakes their life on numbers: " + picksStr + " 🎲");
// IMMEDIATE FLOATER UPDATE: Update this player's floater right after their pick is saved
// First sync the picks data so Floater Manager has the latest picks, then update the floater
// Send just the updated picks data to Floater Manager (lightweight sync)
// MEMORY OPTIMIZED: Direct sync message construction
llMessageLinked(LINK_SET, MSG_SYNC_GAME_STATE,
llList2CSV(lives) + "~" +
llDumpList2String(picksData, "^") + "~" +
perilPlayer + "~" +
llList2CSV(names), NULL_KEY);
// Brief delay to ensure sync reaches Floater Manager before update request
llSleep(0.1);
// Now update the floater
llMessageLinked(LINK_SET, MSG_UPDATE_FLOAT, playerName, llList2Key(players, idx));
llSleep(HUMAN_PICK_DELAY);
// Move to next picker
currentPickerIdx++;
if (currentPickerIdx < llGetListLength(pickQueue)) {
dbg("🔍 [Game Manager] More players in queue, showing next dialog");
showNextPickerDialog();
} else {
// All picked, show roll dialog
dbg("🎯 [Game Manager] All players have picked! Showing roll dialog to " + perilPlayer);
// CRITICAL: Sync the updated picks data to all modules before roll phase
dbg("🔄 [Game Manager] Syncing final picks data before roll phase...");
syncStateToMain();
llSleep(1.0); // Longer delay to ensure sync propagates to all modules
// Send roll dialog through Player_RegistrationManager
dbg("🎯 [Game Manager] Sending roll dialog request for: " + perilPlayer);
string rollRequest = "SHOW_ROLL_DIALOG|" + perilPlayer + "|" + perilPlayer;
llMessageLinked(LINK_SET, MSG_DIALOG_FORWARD_REQUEST, rollRequest, NULL_KEY);
}
}
}
return;
}
// Handle bot picks
if (num == MSG_BOT_PICKED && llSubStringIndex(str, "BOT_PICKED:") == 0) {
dbg("🔍 [Game Manager] BOT_PICKED message received: " + str);
list parts = llParseString2List(str, [":"], []);
if (llGetListLength(parts) >= 3) {
string playerName = llList2String(parts, 1);
string picksStr = llList2String(parts, 2);
dbg("🤖 BOT: " + playerName + " picking " + picksStr + " (global had: " + llList2CSV(globalPickedNumbers) + ")");
// Check if this bot has already made picks this round to prevent duplicates
integer existingPicksIdx = -1;
integer m;
for (m = 0; m < llGetListLength(picksData); m++) {
if (llSubStringIndex(llList2String(picksData, m), playerName + "|") == 0) {
string existingPicks = llGetSubString(llList2String(picksData, m), llStringLength(playerName) + 1, -1);
if (existingPicks != "") {
dbg("⚠️ [Game Manager] Ignoring duplicate BOT_PICKED for " + playerName + " (already has picks: " + existingPicks + ")");
// IMPORTANT: Even though this is a duplicate, check if all players have now picked
// Count how many players have picks
integer playersWithPicks = 0;
integer p;
for (p = 0; p < llGetListLength(names); p++) {
string checkPlayerName = llList2String(names, p);
integer q;
for (q = 0; q < llGetListLength(picksData); q++) {
if (llSubStringIndex(llList2String(picksData, q), checkPlayerName + "|") == 0) {
string checkPicks = llGetSubString(llList2String(picksData, q), llStringLength(checkPlayerName) + 1, -1);
if (checkPicks != "") {
playersWithPicks++;
// Don't break - continue to avoid double counting
}
}
}
}
// If all players have picks, advance to roll phase
if (playersWithPicks >= llGetListLength(names)) {
dbg("🎯 [Game Manager] All players have picks! Moving to roll phase...");
// CRITICAL: Sync the updated picks data to all modules before roll phase
dbg("🔄 [Game Manager] Syncing final picks data before roll phase...");
syncStateToMain();
llSleep(1.0); // Longer delay to ensure sync propagates to all modules
// Force close any active number picker dialogs
dbg("🚫 [Game Manager] Sending CLOSE_ALL_DIALOGS command");
llMessageLinked(LINK_SET, MSG_BOT_COMMAND, "CLOSE_ALL_DIALOGS", NULL_KEY);
// Send roll dialog through Player_RegistrationManager
string rollRequest = "SHOW_ROLL_DIALOG|" + perilPlayer + "|" + perilPlayer;
llMessageLinked(LINK_SET, MSG_DIALOG_FORWARD_REQUEST, rollRequest, NULL_KEY);
}
return; // Ignore duplicate - bot already has real picks
}
existingPicksIdx = m;
// Don't break - continue to make sure there are no other entries with picks
}
}
// VALIDATE: Check for duplicate picks before saving
if (picksStr != "" && llListFindList(globalPickedNumbers, [picksStr]) != -1) {
dbg("❌ [Game Manager] Bot " + playerName + " tried to pick duplicate number: " + picksStr + " - REJECTING");
dbg("🙄 [Game Manager] Sending new pick command to Bot Manager for " + playerName);
// Calculate picks needed for bot retry
integer perilIdx = llListFindList(names, [perilPlayer]);
integer perilLives = 3;
if (perilIdx != -1) {
perilLives = llList2Integer(lives, perilIdx);
}
integer picksNeeded = 4 - perilLives;
// Send new bot command with complete updated avoid list
list completeAvoidList = buildCompleteAvoidanceList();
string avoidListStr = llList2CSV(completeAvoidList);
string botCommand = "BOT_PICK:" + playerName + ":" + (string)picksNeeded + ":" + (string)diceType + ":" + avoidListStr;
dbg("🎯 [Game Manager] Sending retry bot command with complete avoid list (" + (string)llGetListLength(completeAvoidList) + " numbers): " + avoidListStr);
llMessageLinked(LINK_SET, MSG_BOT_COMMAND, botCommand, NULL_KEY);
return; // Don't save this pick
}
// Update picks data
integer idx = llListFindList(names, [playerName]);
dbg("🔧 [Game Manager] Looking for bot " + playerName + " in names list: " + llList2CSV(names) + " (idx: " + (string)idx + ")");
if (idx != -1) {
integer picksIdx = -1;
integer k;
for (k = 0; k < llGetListLength(picksData); k++) {
if (llSubStringIndex(llList2String(picksData, k), playerName + "|") == 0) {
picksIdx = k;
}
}
string newEntry = playerName + "|" + picksStr;
if (picksIdx == -1) {
picksData += [newEntry];
} else {
picksData = llListReplaceList(picksData, [newEntry], picksIdx, picksIdx);
}
// Update global picked numbers - bot picks are single numbers (already validated above)
globalPickedNumbers += [picksStr];
llSay(0, "🎯 " + playerName + " (bot) stakes their digital life on numbers: " + picksStr + " 🎲");
// IMMEDIATE FLOATER UPDATE: Update this bot's floater right after their pick is saved
// First sync the picks data so Floater Manager has the latest picks, then update the floater
// Send just the updated picks data to Floater Manager (lightweight sync)
// MEMORY OPTIMIZED: Direct sync message construction
llMessageLinked(LINK_SET, MSG_SYNC_GAME_STATE,
llList2CSV(lives) + "~" +
llDumpList2String(picksData, "^") + "~" +
perilPlayer + "~" +
llList2CSV(names), NULL_KEY);
// Brief delay to ensure sync reaches Floater Manager before update request
llSleep(0.1);
// Now update the floater
llMessageLinked(LINK_SET, MSG_UPDATE_FLOAT, playerName, NULL_KEY);
// Don't do full sync to Main here - it creates loops, but targeted floater update is safe
llSleep(BOT_PICK_DELAY);
// Check if all players now have picks (more reliable than picker index)
integer playersWithPicks = 0;
integer p;
for (p = 0; p < llGetListLength(names); p++) {
string checkPlayerName = llList2String(names, p);
integer playerHasPicks = FALSE;
integer q;
for (q = 0; q < llGetListLength(picksData) && !playerHasPicks; q++) {
if (llSubStringIndex(llList2String(picksData, q), checkPlayerName + "|") == 0) {
string checkPicks = llGetSubString(llList2String(picksData, q), llStringLength(checkPlayerName) + 1, -1);
if (checkPicks != "") {
playerHasPicks = TRUE;
}
}
}
if (playerHasPicks) {
playersWithPicks++;
}
}
dbg("🔍 [Game Manager] Bot pick complete - players with picks: " + (string)playersWithPicks + "/" + (string)llGetListLength(names));
// If all players have picks, advance to roll phase
if (playersWithPicks >= llGetListLength(names)) {
dbg("🎯 [Game Manager] All players have picks! Moving to roll phase...");
// CRITICAL: Sync the updated picks data to all modules before roll phase
dbg("🔄 [Game Manager] Syncing final picks data before roll phase...");
syncStateToMain();
llSleep(0.2); // Brief delay to ensure sync propagates
// Force close any active number picker dialogs
dbg("🚫 [Game Manager] Sending CLOSE_ALL_DIALOGS command");
llMessageLinked(LINK_SET, MSG_BOT_COMMAND, "CLOSE_ALL_DIALOGS", NULL_KEY);
// Send roll dialog through Player_RegistrationManager
string rollRequest = "SHOW_ROLL_DIALOG|" + perilPlayer + "|" + perilPlayer;
llMessageLinked(LINK_SET, MSG_DIALOG_FORWARD_REQUEST, rollRequest, NULL_KEY);
} else {
// Move to next picker
currentPickerIdx++;
if (currentPickerIdx < llGetListLength(pickQueue)) {
showNextPickerDialog();
}
}
}
}
return;
}
// Handle continue round requests from Main Controller
if (num == MSG_CONTINUE_ROUND) {
dbg("🎯 [Game Manager] Received continue round request with peril player: '" + str + "'");
// Update peril player from the continue message if provided
if (str != "" && str != "NONE") {
perilPlayer = str;
dbg("🎯 [Game Manager] Updated peril player to: " + perilPlayer);
// Reset round state and continue with existing peril player
roundStarted = FALSE;
currentPickerIdx = 0;
diceTypeProcessed = FALSE;
roundContinueInProgress = FALSE;
// Continue current round with assigned peril player
continueCurrentRound();
} else {
// Empty peril player means this is initial game start - use startNextRound()
dbg("🎯 [Game Manager] Empty peril player - starting initial game round");
// Reset round state
roundStarted = FALSE;
currentPickerIdx = 0;
diceTypeProcessed = FALSE;
roundContinueInProgress = FALSE;
// Start initial round (will select random peril player)
startNextRound();
}
return;
}
// Handle reset
if (num == MSG_RESET_ALL && str == "FULL_RESET") {
players = names = lives = picksData = globalPickedNumbers = pickQueue = [];
perilPlayer = "";
currentPickerIdx = 0;
diceType = 6;
currentPicker = NULL_KEY;
roundStarted = FALSE;
diceTypeProcessed = FALSE; // Reset for new game
roundContinueInProgress = FALSE; // Reset protection flag
lastSyncProcessTime = 0; // Reset sync timing
dbg("🎯 Game Manager reset - ready for new game!");
return;
}
// Handle emergency state reset (for when game gets stuck)
if (num == MSG_EMERGENCY_RESET && str == "EMERGENCY_RESET") {
dbg("🚨 [Game Manager] Emergency reset triggered!");
roundStarted = FALSE;
perilPlayer = "";
currentPickerIdx = 0;
pickQueue = [];
diceTypeProcessed = FALSE;
roundContinueInProgress = FALSE;
lastSyncProcessTime = 0; // Reset sync timing
dbg("🔒 [Game Manager] Emergency state reset complete");
return;
}
}
}