diff --git a/Bot_Manager.lsl b/Bot_Manager.lsl index 8955a49..d38f651 100644 --- a/Bot_Manager.lsl +++ b/Bot_Manager.lsl @@ -60,6 +60,19 @@ list sentBotMessages = []; // Verbose logging control - toggled by owner integer VERBOSE_LOGGING = FALSE; +// Memory reporting function +reportMemoryUsage(string scriptName) { + integer used = llGetUsedMemory(); + integer free = llGetFreeMemory(); + integer total = used + free; + float percentUsed = ((float)used / (float)total) * 100.0; + + llOwnerSay("๐Ÿง  [" + scriptName + "] Memory: " + + (string)used + " used, " + + (string)free + " free (" + + llGetSubString((string)percentUsed, 0, 4) + "% used)"); +} + // Helper function to check and report memory usage checkMemoryUsage(string context) { integer usedMemory = llGetUsedMemory(); @@ -196,6 +209,8 @@ doBotRoll(string botName, integer diceMax) { default { state_entry() { + reportMemoryUsage("๐Ÿค– Bot Manager"); + initializeChannels(); LISTEN_CHANNEL = BOT_COMMAND_CHANNEL; @@ -215,6 +230,7 @@ default { } on_rez(integer start_param) { + reportMemoryUsage("๐Ÿค– Bot Manager"); llOwnerSay("๐Ÿ”„ Bot Manager rezzed - reinitializing..."); // Re-initialize dynamic channels diff --git a/CHANGELOG.md b/CHANGELOG.md index ee7b85e..ae7ffac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,56 @@ All notable changes to Peril Dice will be documented in this file. +## [2.8.6] - 2025-09-16 + +### ๐Ÿ† **Winner Glow Celebration System** +- **Victory Visual Enhancement**: Added comprehensive winner celebration with green glow effects + - **Scoreboard Glow**: Winner's profile picture and hearts display bright green glow with green tint + - **Floater Glow**: Winner's floating display shows green glow with victory text "โœจ ULTIMATE VICTORY! โœจ" and "๐Ÿ† ULTIMATE SURVIVOR ๐Ÿ†" + - **Visual Priority**: Winner glow overrides peril glow - green takes priority over yellow during victory + - **Clear Recognition**: Eliminated players show red X markers while winner gets celebration spotlight +- **Extended Victory Display**: 24-second timer delay before game reset allows players to appreciate the winner + - **Before**: Winner declared โ†’ Immediate reset โ†’ No time to see who won + - **After**: Winner declared โ†’ Green glow celebration โ†’ 24 seconds to admire โ†’ Then reset + - **User Experience**: Proper victory moment with visual feedback before returning to lobby + +### ๐ŸŽจ **Enhanced Visual Feedback System** +- **Improved Glow Colors**: Enhanced scoreboard glow system with proper color tinting + - **Winner Glow**: Bright green glow + green color tint for unmistakable victory recognition + - **Peril Glow**: Yellow glow + yellow color tint for clear peril player identification + - **Visual Clarity**: Replaced generic white glow with distinct colored glows for better gameplay feedback +- **Floater Victory Detection**: Smart winner detection in floating displays + - **Automatic Detection**: Floaters automatically detect when only one player remains alive + - **Victory Text**: Winner's floater displays celebration text instead of standard game info + - **Consistent Experience**: Both scoreboard and floater systems celebrate the winner simultaneously + +### ๐Ÿ”ง **Timer System Optimization** +- **Victory Delay Timer**: Added dedicated timer mode for victory celebration delays + - **Memory Efficiency**: Removed unused timeout system to free up memory resources + - **Clean Architecture**: Streamlined timer system focuses on essential functionality + - **Reliable Operation**: Victory display timing works consistently without interference from other systems +- **Message Communication Enhancement**: Improved winner glow message handling + - **Scoreboard Integration**: Added proper MSG_UPDATE_WINNER message constant for clean communication + - **Floater Integration**: Enhanced floater update system to trigger victory displays + - **Synchronized Effects**: Both display systems receive winner updates simultaneously + +### ๐ŸŽฎ **User Experience Improvements** +- **Victory Celebration**: Clear winner recognition with proper visual feedback +- **Extended Appreciation Time**: 24 seconds to see and congratulate the winner before reset +- **Visual Hierarchy**: Clean priority system - Winner (green) > Peril (yellow) > Eliminated (red X) +- **Consistent Display**: Winner celebration works on both scoreboard and personal floater displays + +### ๐Ÿ“Š **System Performance** +- **Memory Optimization**: Removed unused timeout system components saving memory +- **Efficient Messaging**: Clean message routing for winner glow effects +- **Timer Efficiency**: Dedicated victory timer prevents conflicts with other timing systems + +### ๐ŸŽฏ **Impact Summary** +- **Before**: Winners were announced but no visual celebration - immediate reset gave no recognition +- **After**: Winners get bright green glow celebration on both scoreboard and floaters with 24 seconds of recognition +- **Player Satisfaction**: Proper victory moment makes winning feel rewarding and celebrated +- **Visual Polish**: Professional-grade visual feedback system enhances the competitive gaming experience + ## [2.8.5] - 2025-09-15 ### ๐Ÿง  **Memory Optimization & Dead Code Cleanup** diff --git a/CHANGELOG_Notecard.txt b/CHANGELOG_Notecard.txt index 6293c23..5c351ba 100644 --- a/CHANGELOG_Notecard.txt +++ b/CHANGELOG_Notecard.txt @@ -3,6 +3,57 @@ PERIL DICE CHANGELOG All notable changes to Peril Dice are documented in this file. +[2.8.6] - 2025-09-16 +==================== + +๐Ÿ† WINNER GLOW CELEBRATION SYSTEM +- Victory Visual Enhancement: Added comprehensive winner celebration with green glow effects + * Scoreboard Glow: Winner's profile picture and hearts display bright green glow with green tint + * Floater Glow: Winner's floating display shows green glow with victory text "โœจ ULTIMATE VICTORY! โœจ" and "๐Ÿ† ULTIMATE SURVIVOR ๐Ÿ†" + * Visual Priority: Winner glow overrides peril glow - green takes priority over yellow during victory + * Clear Recognition: Eliminated players show red X markers while winner gets celebration spotlight +- Extended Victory Display: 24-second timer delay before game reset allows players to appreciate the winner + * Before: Winner declared โ†’ Immediate reset โ†’ No time to see who won + * After: Winner declared โ†’ Green glow celebration โ†’ 24 seconds to admire โ†’ Then reset + * User Experience: Proper victory moment with visual feedback before returning to lobby + +๐ŸŽจ ENHANCED VISUAL FEEDBACK SYSTEM +- Improved Glow Colors: Enhanced scoreboard glow system with proper color tinting + * Winner Glow: Bright green glow + green color tint for unmistakable victory recognition + * Peril Glow: Yellow glow + yellow color tint for clear peril player identification + * Visual Clarity: Replaced generic white glow with distinct colored glows for better gameplay feedback +- Floater Victory Detection: Smart winner detection in floating displays + * Automatic Detection: Floaters automatically detect when only one player remains alive + * Victory Text: Winner's floater displays celebration text instead of standard game info + * Consistent Experience: Both scoreboard and floater systems celebrate the winner simultaneously + +๐Ÿ”ง TIMER SYSTEM OPTIMIZATION +- Victory Delay Timer: Added dedicated timer mode for victory celebration delays + * Memory Efficiency: Removed unused timeout system to free up memory resources + * Clean Architecture: Streamlined timer system focuses on essential functionality + * Reliable Operation: Victory display timing works consistently without interference from other systems +- Message Communication Enhancement: Improved winner glow message handling + * Scoreboard Integration: Added proper MSG_UPDATE_WINNER message constant for clean communication + * Floater Integration: Enhanced floater update system to trigger victory displays + * Synchronized Effects: Both display systems receive winner updates simultaneously + +๐ŸŽฎ USER EXPERIENCE IMPROVEMENTS +- Victory Celebration: Clear winner recognition with proper visual feedback +- Extended Appreciation Time: 24 seconds to see and congratulate the winner before reset +- Visual Hierarchy: Clean priority system - Winner (green) > Peril (yellow) > Eliminated (red X) +- Consistent Display: Winner celebration works on both scoreboard and personal floater displays + +๐Ÿ“Š SYSTEM PERFORMANCE +- Memory Optimization: Removed unused timeout system components saving memory +- Efficient Messaging: Clean message routing for winner glow effects +- Timer Efficiency: Dedicated victory timer prevents conflicts with other timing systems + +๐ŸŽฏ IMPACT SUMMARY +- Before: Winners were announced but no visual celebration - immediate reset gave no recognition +- After: Winners get bright green glow celebration on both scoreboard and floaters with 24 seconds of recognition +- Player Satisfaction: Proper victory moment makes winning feel rewarding and celebrated +- Visual Polish: Professional-grade visual feedback system enhances the competitive gaming experience + [2.8.5] - 2025-09-15 ==================== diff --git a/Controller_MessageHandler.lsl b/Controller_MessageHandler.lsl index 81499af..47faf3a 100644 --- a/Controller_MessageHandler.lsl +++ b/Controller_MessageHandler.lsl @@ -6,6 +6,19 @@ integer VERBOSE_LOGGING = TRUE; // Global flag for verbose debug logs integer MSG_TOGGLE_VERBOSE_LOGS = 9998; // Message to toggle verbose logging +// Memory reporting function +reportMemoryUsage(string scriptName) { + integer used = llGetUsedMemory(); + integer free = llGetFreeMemory(); + integer total = used + free; + float percentUsed = ((float)used / (float)total) * 100.0; + + llOwnerSay("๐Ÿง  [" + scriptName + "] Memory: " + + (string)used + " used, " + + (string)free + " free (" + + llGetSubString((string)percentUsed, 0, 4) + "% used)"); +} + // Message constants for communication with main controller integer MSG_REQUEST_PLAYER_DATA = 8001; integer MSG_REQUEST_PICK_DATA = 8002; @@ -367,6 +380,8 @@ handleDialogRequest(string dialogData) { default { state_entry() { + reportMemoryUsage("๐Ÿ“จ Message Handler"); + llOwnerSay("๐Ÿ“จ [Message Handler] Helper script ready!"); initializeChannels(); } diff --git a/DIRECT_COMMUNICATION_ARCHITECTURE.md b/DIRECT_COMMUNICATION_ARCHITECTURE.md deleted file mode 100644 index 6ad74e8..0000000 --- a/DIRECT_COMMUNICATION_ARCHITECTURE.md +++ /dev/null @@ -1,106 +0,0 @@ -# Direct Communication Architecture - -## Overview -To reduce Main Controller memory pressure (from 92% usage), we've implemented direct system-to-system communication, bypassing the Main Controller for specific operations. - -## Direct Communication Flows - -### โœ… Implemented Direct Connections - -#### 1. **Roll Module โ†’ Scoreboard** (Status & Life Updates) -```lsl -// Life updates during gameplay -llMessageLinked(12, 3002, perilPlayer + "|" + (string)lives + "|NULL_KEY", NULL_KEY); - -// Game status updates -llMessageLinked(12, 3001, "Plot Twist", NULL_KEY); // When player picks rolled number -llMessageLinked(12, 3001, "Direct Hit", NULL_KEY); // When peril player picks their doom -llMessageLinked(12, 3001, "No Shield", NULL_KEY); // When nobody picks rolled number -``` -**Benefit**: Real-time scoreboard updates without Main Controller memory overhead. - -#### 2. **Roll Module โ†’ Dice Display** (Roll Results) -```lsl -// Display roll results directly -llMessageLinked(83, 3020, perilPlayer + "|" + rollResult, NULL_KEY); - -// Clear dice display -llMessageLinked(83, 3021, "", NULL_KEY); -``` -**Benefit**: Eliminates dice message routing through Main Controller. - -#### 3. **Main Controller โ†’ Scoreboard โ†’ Leaderboard** (Win/Loss Tracking) -```lsl -// Elimination tracking (already optimized) -llMessageLinked(SCOREBOARD_LINK, MSG_GAME_LOST, eliminatedPlayer, NULL_KEY); - -// Victory tracking (already optimized) -llMessageLinked(SCOREBOARD_LINK, MSG_GAME_WON, winner, NULL_KEY); -``` -**Benefit**: Scoreboard handles leaderboard updates internally, reducing Main Controller load. - -## Architecture Benefits - -### Memory Savings -- **Main Controller**: Reduced from 92% to estimated ~85% memory usage -- **Eliminated routing logic**: No more message forwarding for status/dice/scoreboard updates -- **Reduced string operations**: Direct messages avoid CSV building/parsing in Main Controller - -### Performance Improvements -- **Real-time updates**: Scoreboard updates immediately when lives change -- **Reduced latency**: No message forwarding delays -- **Better reliability**: Fewer message hops = fewer failure points - -### System Resilience -- **Distributed processing**: Each system handles its own display updates -- **Reduced bottlenecks**: Main Controller no longer central routing point -- **Memory stability**: Less memory pressure on critical controller script - -## Message Flow Comparison - -### Before (Centralized Routing) -``` -Roll Module โ†’ Main Controller โ†’ Scoreboard (Status) -Roll Module โ†’ Main Controller โ†’ Dice Display (Results) - โ†ณ Main Controller โ† High Memory Usage -``` - -### After (Direct Communication) -``` -Roll Module โ†’ Scoreboard (Status & Lives) -Roll Module โ†’ Dice Display (Results) - โ†ณ Main Controller โ† Reduced Memory Usage -``` - -## Link Mapping Reference -- **Link 1**: Main Controller (Root) -- **Link 12**: Scoreboard Manager -- **Link 35**: Leaderboard Communication -- **Link 83**: Dice Display Bridge - -## Message Constants -```lsl -// Scoreboard Messages -integer MSG_GAME_STATUS = 3001; // Status updates (Plot Twist, Direct Hit, etc.) -integer MSG_PLAYER_UPDATE = 3002; // Life updates during gameplay -integer MSG_GAME_WON = 3010; // Victory notifications -integer MSG_GAME_LOST = 3011; // Elimination notifications - -// Dice Display Messages -integer MSG_DICE_ROLL = 3020; // Roll result display -integer MSG_CLEAR_DICE = 3021; // Clear display -``` - -## Testing Validation -- โœ… Roll Module sends status updates directly to scoreboard -- โœ… Roll Module sends life updates directly to scoreboard -- โœ… Roll Module sends dice results directly to dice display -- โœ… Floater cleanup with timing delays for elimination -- โœ… Main Controller routing logic removed for direct paths - -## Future Optimization Opportunities -1. **Bot Manager โ†’ Game Manager**: Direct bot pick notifications -2. **Floater Manager โ†’ Individual Floaters**: Direct floater commands -3. **Dialog Systems**: Direct dialog routing without Main Controller - -This architecture maintains the robust linkset communication while significantly reducing memory pressure on the Main Controller. \ No newline at end of file diff --git a/Floater_Manager.lsl b/Floater_Manager.lsl index 64b2d21..0eadd60 100644 --- a/Floater_Manager.lsl +++ b/Floater_Manager.lsl @@ -8,6 +8,7 @@ integer MSG_CLEANUP_FLOAT = 104; integer MSG_REZ_FLOAT = 105; integer MSG_REGISTER_PLAYER = 106; integer MSG_SYNC_GAME_STATE = 107; +integer MSG_CLEANUP_ALL_FLOATERS = 212; // Debug control message constants integer MSG_DEBUG_PICKS_ON = 7001; @@ -54,12 +55,77 @@ integer DEBUG_PICKS = FALSE; // Verbose logging control - affects general debug messages, toggled by owner integer VERBOSE_LOGGING = FALSE; -list players = []; -list names = []; -list lives = []; -list picksData = []; +// Memory reporting function +reportMemoryUsage(string scriptName) { + integer used = llGetUsedMemory(); + integer free = llGetFreeMemory(); + integer total = used + free; + float percentUsed = ((float)used / (float)total) * 100.0; + + llOwnerSay("๐Ÿง  [" + scriptName + "] Memory: " + + (string)used + " used, " + + (string)free + " free (" + + llGetSubString((string)percentUsed, 0, 4) + "% used)"); +} + +// ALIVE PLAYERS (shrinks during eliminations) +list alivePlayers = []; +list aliveNames = []; +list aliveLives = []; +list alivePicksData = []; + +// ELIMINATED PLAYERS (grows during eliminations) +list eliminatedPlayers = []; +list eliminatedNames = []; +list eliminatedChannels = []; + +// ALL PLAYERS (never shrinks - for channel mapping) +list allPlayerNames = []; // Names in registration order +list allPlayerChannels = []; // Corresponding channels + +// SYNC STATE VARIABLES (received from Main Controller) +list names = []; // Current player names from sync +list players = []; // Current player keys from sync +list lives = []; // Current lives from sync +list picksData = []; // Current picks data from sync +list playerNames = []; // Legacy variable for cleanup compatibility +list playerChannels = []; // Legacy variable for cleanup compatibility + string perilPlayer = ""; +// Move a player from alive to eliminated lists +eliminatePlayer(string playerName) { + integer aliveIdx = llListFindList(aliveNames, [playerName]); + if (aliveIdx == -1) { + llOwnerSay("โš ๏ธ [Floater Manager] Cannot eliminate " + playerName + " - not found in alive list"); + return; + } + + // Get player data before removing from alive lists + key playerKey = llList2Key(alivePlayers, aliveIdx); + string picks = llList2String(alivePicksData, aliveIdx); + + // Find their original channel from permanent mapping + integer channelIdx = llListFindList(allPlayerNames, [playerName]); + integer playerChannel = -1; + if (channelIdx != -1) { + playerChannel = llList2Integer(allPlayerChannels, channelIdx); + } + + // Add to eliminated lists + eliminatedNames += [playerName]; + eliminatedPlayers += [playerKey]; + eliminatedChannels += [playerChannel]; + + // Remove from alive lists + alivePlayers = llDeleteSubList(alivePlayers, aliveIdx, aliveIdx); + aliveNames = llDeleteSubList(aliveNames, aliveIdx, aliveIdx); + aliveLives = llDeleteSubList(aliveLives, aliveIdx, aliveIdx); + alivePicksData = llDeleteSubList(alivePicksData, aliveIdx, aliveIdx); + + llOwnerSay("๐Ÿงน [Floater Manager] Moved " + playerName + " from alive to eliminated lists"); +} + // Returns a list of picks for the given player name, filtering out invalid values list getPicksFor(string nameInput) { // ENHANCED DEBUG: Log the request and current data state @@ -127,37 +193,67 @@ list getPicksFor(string nameInput) { // Converts a player's key into their name (if registered) string getNameFromKey(key id) { - integer i = llListFindList(players, [id]); - if (i != -1) return llList2String(names, i); + integer i = llListFindList(alivePlayers, [id]); + if (i != -1) return llList2String(aliveNames, i); return (string)id; } // Main event handler default { state_entry() { + reportMemoryUsage("๐Ÿ’ฌ Floater Manager"); + initializeChannels(); // Initialize/reset all state variables - players = []; + alivePlayers = []; + aliveNames = []; + aliveLives = []; + alivePicksData = []; + eliminatedPlayers = []; + eliminatedNames = []; + eliminatedChannels = []; + allPlayerNames = []; + allPlayerChannels = []; + + // Initialize sync state variables names = []; + players = []; lives = []; picksData = []; + playerNames = []; + playerChannels = []; perilPlayer = ""; llOwnerSay("๐Ÿ“ฆ Floater Manager ready!"); } on_rez(integer start_param) { + reportMemoryUsage("๐Ÿ’ฌ Floater Manager"); + llOwnerSay("๐Ÿ”„ Floater Manager rezzed - reinitializing..."); // Re-initialize dynamic channels initializeChannels(); // Reset all state variables on rez - players = []; + alivePlayers = []; + aliveNames = []; + aliveLives = []; + alivePicksData = []; + eliminatedPlayers = []; + eliminatedNames = []; + eliminatedChannels = []; + allPlayerNames = []; + allPlayerChannels = []; + + // Initialize sync state variables names = []; + players = []; lives = []; picksData = []; + playerNames = []; + playerChannels = []; perilPlayer = ""; llOwnerSay("โœ… Floater Manager reset complete after rez!"); @@ -167,10 +263,23 @@ default { // Handle full reset from main controller if (num == -99999 && str == "FULL_RESET") { // Reset all game state - players = []; + alivePlayers = []; + aliveNames = []; + aliveLives = []; + alivePicksData = []; + eliminatedPlayers = []; + eliminatedNames = []; + eliminatedChannels = []; + allPlayerNames = []; + allPlayerChannels = []; + + // Reset sync state variables names = []; + players = []; lives = []; picksData = []; + playerNames = []; + playerChannels = []; perilPlayer = ""; llOwnerSay("๐Ÿ“ฆ Floater Manager reset!"); return; @@ -192,7 +301,7 @@ default { if (num == MSG_REGISTER_PLAYER) { // Enforce the maximum number of players - if (llGetListLength(players) >= MAX_PLAYERS) { + if (llGetListLength(alivePlayers) >= MAX_PLAYERS) { llOwnerSay("โš ๏ธ Cannot register new player; the game is full (max " + (string)MAX_PLAYERS + ")."); return; } @@ -202,15 +311,22 @@ default { key avKey = llList2Key(info, 1); // Check if this player is already registered to prevent duplicate floaters - if (llListFindList(players, [avKey]) != -1) { + if (llListFindList(alivePlayers, [avKey]) != -1) { llOwnerSay("โš ๏ธ Player " + name + " is already registered, ignoring duplicate registration"); return; } - names += [name]; - players += [avKey]; - lives += [3]; // Initialize with 3 lives for new players - picksData += [name + "|"]; // Initialize with empty picks + // Add to ALIVE lists + aliveNames += [name]; + alivePlayers += [avKey]; + aliveLives += [3]; // Initialize with 3 lives for new players + alivePicksData += [name + "|"]; // Initialize with empty picks + + // Add to PERMANENT mapping (never changes after registration) + integer playerIdx = llGetListLength(allPlayerNames); + integer ch = FLOATER_BASE_CHANNEL + playerIdx; + allPlayerNames += [name]; + allPlayerChannels += [ch]; llSay(0, "๐Ÿ’€ " + name + " has entered the deadly game! Welcome to your potential doom! ๐Ÿ’€"); // Immediately rez the float after registration. This ensures the @@ -225,9 +341,9 @@ default { } else if (num == MSG_REZ_FLOAT) { string name = str; - integer idx = llListFindList(names, [name]); + integer idx = llListFindList(aliveNames, [name]); if (idx == -1) return; - key avKey = llList2Key(players, idx); + key avKey = llList2Key(alivePlayers, idx); // Determine the rez position. For real avatars, use their current // location plus an offset. For test players (fake keys) or when // OBJECT_POS cannot be obtained, fall back to rezzing relative to @@ -249,7 +365,16 @@ default { // This prevents them from spawning directly on the leaderboard. pos = basePos + <-4.0 - (float)idx * 1.0, 2.0 + (float)idx * 0.8, 1>; } - integer ch = FLOATER_BASE_CHANNEL + idx; + // Use permanent channel mapping instead of alive list index + integer channelIdx = llListFindList(allPlayerNames, [name]); + integer ch; + if (channelIdx != -1) { + ch = llList2Integer(allPlayerChannels, channelIdx); + } else { + // Fallback - this shouldn't happen but use idx as backup + llOwnerSay("โš ๏ธ [Floater Manager] WARNING: " + name + " not in permanent mapping during rez, using fallback"); + ch = FLOATER_BASE_CHANNEL + idx; + } llRezObject("StatFloat", pos, ZERO_VECTOR, ZERO_ROTATION, ch); // Wait a moment then set the description llSleep(0.2); @@ -268,14 +393,39 @@ default { } else if (num == MSG_UPDATE_FLOAT) { string name = str; - integer idx = llListFindList(names, [name]); - if (idx == -1) { - llOwnerSay("โš ๏ธ [Floater Manager] MSG_UPDATE_FLOAT: Player '" + name + "' not found in names list"); + + // FIXED: Use permanent channel mapping that doesn't change during eliminations + integer channelIdx = llListFindList(allPlayerNames, [name]); + integer ch; + if (channelIdx != -1) { + ch = llList2Integer(allPlayerChannels, channelIdx); + } else { + // Fallback - this shouldn't happen if permanent mapping is working + llOwnerSay("โš ๏ธ [Floater Manager] WARNING: Player " + name + " not in permanent mapping"); return; } - key avKey = llList2Key(players, idx); - integer ch = FLOATER_BASE_CHANNEL + idx; - integer lifeCount = llList2Integer(lives, idx); + + // Try to find player in current sync data (names/lives from Main Controller) + // This works for both alive and eliminated players during the elimination sync + integer syncIdx = llListFindList(names, [name]); + key avKey = NULL_KEY; + integer lifeCount = 0; + + if (syncIdx != -1) { + // Player found in current sync data + if (syncIdx < llGetListLength(players)) { + avKey = llList2Key(players, syncIdx); + } + if (syncIdx < llGetListLength(lives)) { + lifeCount = llList2Integer(lives, syncIdx); + } + } else { + // Player not in sync data - they might be eliminated + if (VERBOSE_LOGGING) { + llOwnerSay("๐Ÿ” [Floater Manager] Player " + name + " not in current sync - might be eliminated"); + } + // Continue with lifeCount = 0 to show eliminated status + } if (VERBOSE_LOGGING) llOwnerSay("๐Ÿ”„ [Floater Manager] Updating floater for " + name + " (lives: " + (string)lifeCount + ")"); list picks = getPicksFor(name); @@ -303,50 +453,95 @@ default { } string picksDisplay = llList2CSV(picks); - string txt = "๐ŸŽฒ Peril Dice\n๐Ÿ‘ค " + name + "\nLives: " + (string)lifeCount + "\n" + perilName + "\n๐Ÿ”ข Picks: " + picksDisplay; + + // Check if this player is the winner (only one player with lives > 0) + integer livingPlayers = 0; + integer isWinner = FALSE; + for (i = 0; i < llGetListLength(lives); i++) { + if (llList2Integer(lives, i) > 0) { + livingPlayers++; + } + } + + // If only one player has lives > 0 and this player has lives > 0, they're the winner + if (livingPlayers == 1 && lifeCount > 0) { + isWinner = TRUE; + } + + string txt; + if (isWinner) { + // Winner display with victory text (triggers green glow in PlayerStatus_Float.lsl) + txt = "๐ŸŽฒ Peril Dice\n๐Ÿ‘ค " + name + "\nโœจ ULTIMATE VICTORY! โœจ\n๐Ÿ† ULTIMATE SURVIVOR ๐Ÿ†\nLives: " + (string)lifeCount + "\n๐Ÿ”ข Final Picks: " + picksDisplay; + } else { + // Normal display + txt = "๐ŸŽฒ Peril Dice\n๐Ÿ‘ค " + name + "\nLives: " + (string)lifeCount + "\n" + perilName + "\n๐Ÿ”ข Picks: " + picksDisplay; + } + llRegionSay(ch, "FLOAT:" + (string)avKey + "|" + txt); } else if (num == MSG_CLEANUP_FLOAT) { integer ch = (integer)str; - integer idx = ch - FLOATER_BASE_CHANNEL; // Always send cleanup message to the channel (even for orphaned floaters) llRegionSay(ch, "CLEANUP"); - // IMPROVED: Clean up internal lists more carefully to handle race conditions - // Instead of relying on index calculation, find by channel and remove consistently - if (idx >= 0 && idx < llGetListLength(players)) { - // Store player info before removal for debugging - string removedPlayer = ""; - if (idx < llGetListLength(names)) { - removedPlayer = llList2String(names, idx); + // FIXED: Find player by searching the permanent channel mapping + string removedPlayer = ""; + integer foundIdx = -1; + + // Search for the channel in our permanent mapping + foundIdx = llListFindList(allPlayerChannels, [ch]); + if (foundIdx != -1) { + removedPlayer = llList2String(allPlayerNames, foundIdx); + } + + if (foundIdx != -1 && removedPlayer != "") { + // Find the current index of this player in the active names list + integer currentIdx = llListFindList(names, [removedPlayer]); + llOwnerSay("๐Ÿงน [Floater Manager] Cleaning up floater for: " + removedPlayer + " (channel: " + (string)ch + ")"); + + // Remove from current game lists using the current index + if (currentIdx != -1) { + if (currentIdx < llGetListLength(players)) players = llDeleteSubList(players, currentIdx, currentIdx); + if (currentIdx < llGetListLength(names)) names = llDeleteSubList(names, currentIdx, currentIdx); + if (currentIdx < llGetListLength(lives)) lives = llDeleteSubList(lives, currentIdx, currentIdx); + } else { + llOwnerSay("โš ๏ธ [Floater Manager] WARNING: " + removedPlayer + " not found in current names list for removal"); } - // Remove from all lists at the same index to maintain synchronization - if (idx < llGetListLength(players)) players = llDeleteSubList(players, idx, idx); - if (idx < llGetListLength(names)) names = llDeleteSubList(names, idx, idx); - if (idx < llGetListLength(lives)) lives = llDeleteSubList(lives, idx, idx); - if (idx < llGetListLength(picksData)) picksData = llDeleteSubList(picksData, idx, idx); + // IMPORTANT: Keep permanent mapping intact - never remove from playerNames/playerChannels + // This preserves the original channel assignments - if (removedPlayer != "") { - llOwnerSay("๐Ÿงน [Floater Manager] Cleaned up floater for: " + removedPlayer + " (channel: " + (string)ch + ")"); - - // CRITICAL: Update all remaining floaters to refresh peril status after player removal - // This prevents stale peril player information from persisting in floaters - llSleep(0.2); // Brief delay to ensure cleanup completes - integer i; - for (i = 0; i < llGetListLength(names); i++) { - string remainingPlayerName = llList2String(names, i); - key remainingPlayerKey = NULL_KEY; - if (i < llGetListLength(players)) { - remainingPlayerKey = llList2Key(players, i); - } - llMessageLinked(LINK_SET, MSG_UPDATE_FLOAT, remainingPlayerName, remainingPlayerKey); + // Remove from picksData by searching for the player name + integer pickIdx = -1; + integer p; + for (p = 0; p < llGetListLength(picksData); p++) { + string entry = llList2String(picksData, p); + if (llSubStringIndex(entry, removedPlayer + "|") == 0) { + pickIdx = p; + jump found_pick; } } + @found_pick; + if (pickIdx != -1) { + picksData = llDeleteSubList(picksData, pickIdx, pickIdx); + } + + // CRITICAL: Update all remaining floaters to refresh peril status after player removal + // This prevents stale peril player information from persisting in floaters + llSleep(0.2); // Brief delay to ensure cleanup completes + integer i; + for (i = 0; i < llGetListLength(names); i++) { + string remainingPlayerName = llList2String(names, i); + key remainingPlayerKey = NULL_KEY; + if (i < llGetListLength(players)) { + remainingPlayerKey = llList2Key(players, i); + } + llMessageLinked(LINK_SET, MSG_UPDATE_FLOAT, remainingPlayerName, remainingPlayerKey); + } } else { - // Orphaned floater cleanup - llOwnerSay("๐Ÿงน [Floater Manager] Cleaned up orphaned floater (channel: " + (string)ch + ")"); + // Orphaned floater cleanup - channel not found in our tracking + llOwnerSay("๐Ÿงน [Floater Manager] Cleaned up orphaned floater (channel: " + (string)ch + ") - not in tracking list"); } } else if (num == MSG_SYNC_GAME_STATE) { @@ -393,7 +588,24 @@ default { } // IMPORTANT: Also sync the names list to prevent desynchronization string namesStr = llList2String(parts, 3); - names = llCSV2List(namesStr); + list newNames = llCSV2List(namesStr); + + // CRITICAL: If names list changed size, rebuild permanent mapping if needed + if (llGetListLength(allPlayerNames) == 0 && llGetListLength(newNames) > 0) { + // Initial population of permanent mapping from sync + allPlayerNames = newNames; + allPlayerChannels = []; + integer i; + for (i = 0; i < llGetListLength(newNames); i++) { + integer ch = FLOATER_BASE_CHANNEL + i; + allPlayerChannels += [ch]; + } + if (VERBOSE_LOGGING) { + llOwnerSay("๐Ÿ”„ [Floater Manager] Built permanent mapping: " + llList2CSV(allPlayerNames) + " -> " + llList2CSV(allPlayerChannels)); + } + } + + names = newNames; // Sync players list if available (5th part) if (llGetListLength(parts) >= 5) { @@ -406,18 +618,16 @@ default { llOwnerSay("๐ŸŽฏ Floater Manager: Peril player updated from '" + oldPeril + "' to '" + perilPlayer + "'"); } - // CRITICAL: After synchronizing the game state, ALWAYS update all existing floats - // This ensures they reflect the current peril status and prevents desynchronization - // Force update even if peril player didn't change - other state may have changed + // CRITICAL: After synchronizing the game state, ALWAYS update ALL registered floaters + // This includes both alive and eliminated players so eliminated floaters show proper status integer idx; - for (idx = 0; idx < llGetListLength(names); idx++) { - string n = llList2String(names, idx); - // Use the player's key if it exists; fallback to NULL_KEY for test bots - key k; - if (idx < llGetListLength(players)) { - k = llList2Key(players, idx); - } else { - k = NULL_KEY; + for (idx = 0; idx < llGetListLength(allPlayerNames); idx++) { + string n = llList2String(allPlayerNames, idx); + // Find player key if they're still alive, otherwise use NULL_KEY + key k = NULL_KEY; + integer aliveIdx = llListFindList(names, [n]); + if (aliveIdx != -1 && aliveIdx < llGetListLength(players)) { + k = llList2Key(players, aliveIdx); } llMessageLinked(LINK_SET, MSG_UPDATE_FLOAT, n, k); } @@ -427,19 +637,55 @@ default { if (oldPeril != perilPlayer && perilPlayer != "" && perilPlayer != "NONE") { llOwnerSay("๐Ÿ”„ [Floater Manager] Peril player changed - scheduling secondary update for stability"); llSleep(0.3); // Brief delay to let initial updates complete - for (idx = 0; idx < llGetListLength(names); idx++) { - string n = llList2String(names, idx); - key k; - if (idx < llGetListLength(players)) { - k = llList2Key(players, idx); - } else { - k = NULL_KEY; + for (idx = 0; idx < llGetListLength(allPlayerNames); idx++) { + string n = llList2String(allPlayerNames, idx); + // Find player key if they're still alive, otherwise use NULL_KEY + key k = NULL_KEY; + integer aliveIdx = llListFindList(names, [n]); + if (aliveIdx != -1 && aliveIdx < llGetListLength(players)) { + k = llList2Key(players, aliveIdx); } llMessageLinked(LINK_SET, MSG_UPDATE_FLOAT, n, k); } } } + // Handle aggressive floater cleanup requests + else if (num == MSG_CLEANUP_ALL_FLOATERS) { + string context = str; // "RESET" for conservative, empty for aggressive + + if (context == "RESET") { + // Conservative cleanup during reset - only clean known floaters using permanent mapping + if (llGetListLength(allPlayerChannels) > 0) { + llOwnerSay("๐Ÿงน [Floater Manager] Cleaning up floaters for " + (string)llGetListLength(allPlayerChannels) + " registered channels..."); + integer i; + for (i = 0; i < llGetListLength(allPlayerChannels); i++) { + integer ch = llList2Integer(allPlayerChannels, i); + llRegionSay(ch, "CLEANUP"); + } + } + } else { + // Aggressive cleanup for troubleshooting - scan all possible channels + llOwnerSay("๐Ÿงฝ [Floater Manager] Performing aggressive floater cleanup..."); + integer i; + for (i = 0; i < MAX_PLAYERS; i++) { + integer ch = FLOATER_BASE_CHANNEL + i; + llRegionSay(ch, "CLEANUP"); + llRegionSay(ch + 100, "CLEANUP"); + llRegionSay(ch + 1000, "CLEANUP"); + } + llOwnerSay("โœ… [Floater Manager] Aggressive cleanup complete"); + } + + // Clear our internal tracking + players = []; + names = []; + lives = []; + picksData = []; + perilPlayer = ""; + playerNames = []; + playerChannels = []; + } // Handle debug control messages else if (num == MSG_DEBUG_PICKS_ON) { DEBUG_PICKS = TRUE; diff --git a/GAME_RULES.md b/GAME_RULES.md index 866936c..fa64c37 100644 --- a/GAME_RULES.md +++ b/GAME_RULES.md @@ -77,6 +77,9 @@ Three possible outcomes based on the dice roll: ### **6๏ธโƒฃ Victory** - **Last player standing wins!** ๐Ÿ† +- **Winner Glow Celebration** (NEW in v2.8.6): Winner gets bright green glow on both scoreboard and floater ๐Ÿ’š +- **Extended Victory Display**: 24-second celebration timer before game reset โฑ๏ธ +- **Victory Text**: Winner's floater displays "โœจ ULTIMATE VICTORY! โœจ" and "๐Ÿ† ULTIMATE SURVIVOR ๐Ÿ†" - Victory celebration with confetti effects! โœจ - Winner's name recorded on the leaderboard @@ -169,18 +172,23 @@ The game features thematic messaging for maximum immersion: - **Kick Player** functionality allows owners to remove disruptive players - **Leave Game** system properly cleans up all player data -### **Enhanced Display System (NEW in v2.8.1):** +### **Enhanced Display System (UPDATED in v2.8.6):** +- **Winner Glow Celebration** - Winners get bright green glow on both scoreboard and floaters ๐Ÿ’š +- **Visual Priority System** - Winner glow (green) overrides peril glow (yellow) during victory +- **24-Second Victory Timer** - Extended celebration before game reset for proper recognition - **Real-Time Status Updates** - Peril status updates immediately on floating displays -- **Enhanced Elimination** - Players see 0 hearts โค๏ธ โ†’ ๐Ÿ’”๐Ÿ’”๐Ÿ’” before removal +- **Enhanced Elimination** - Players see 0 hearts โค๏ธ โ†’ ๐Ÿ’”๐Ÿ’”๐Ÿ’” before removal with red X markers โŒ - **Visual Scoreboard** - Shows all players with profile pictures and heart counts - **Floating HUD Displays** - Personal status displays for each player - **Professional Status Actions** - Large status prim shows current game events +- **Color-Coded Glow System** - Green (winner), Yellow (peril), Red X (eliminated) ### **Technical Rules:** - Game supports up to **10 players maximum** -- **2-minute timeout** on dialog responses (improved for faster gameplay) +- **Owner kick functionality** for managing disruptive players - **Automatic game reset** on startup (preserves leaderboard) - **Single linkset architecture** (74 prims total) for reliable operation +- **Professional winner celebration** with 24-second recognition timer --- diff --git a/GAME_RULES_NOTECARD.txt b/GAME_RULES_NOTECARD.txt index d161057..f26c8b5 100644 --- a/GAME_RULES_NOTECARD.txt +++ b/GAME_RULES_NOTECARD.txt @@ -77,6 +77,11 @@ NO SHIELD! 6. Victory - Last player standing wins! +- Winner Glow Celebration (NEW in v2.8.6): Winner gets bright green glow + on both scoreboard and floater +- Extended Victory Display: 24-second celebration timer before game reset +- Victory Text: Winner's floater displays "ULTIMATE VICTORY!" and + "ULTIMATE SURVIVOR" - Victory celebration with confetti effects! - Winner's name recorded on the leaderboard @@ -169,18 +174,26 @@ Game Security (NEW in v2.8.0): - Kick Player functionality allows owners to remove disruptive players - Leave Game system properly cleans up all player data -Enhanced Display System (NEW in v2.8.1): +Enhanced Display System (UPDATED in v2.8.6): +- Winner Glow Celebration - Winners get bright green glow on both + scoreboard and floaters +- Visual Priority System - Winner glow (green) overrides peril glow (yellow) + during victory +- 24-Second Victory Timer - Extended celebration before game reset for + proper recognition - Real-Time Status Updates - Peril status updates immediately on displays -- Enhanced Elimination - Players see 0 hearts before removal +- Enhanced Elimination - Players see 0 hearts before removal with red X markers - Visual Scoreboard - Shows all players with profile pictures and hearts - Floating HUD Displays - Personal status displays for each player - Professional Status Actions - Large status prim shows current events +- Color-Coded Glow System - Green (winner), Yellow (peril), Red X (eliminated) Technical Rules: - Game supports up to 10 players maximum -- 2-minute timeout on dialog responses (improved for faster gameplay) +- Owner kick functionality for managing disruptive players - Automatic game reset on startup (preserves leaderboard) - Single linkset architecture (74 prims total) for reliable operation +- Professional winner celebration with 24-second recognition timer =============================================================================== diff --git a/Game_Calculator.lsl b/Game_Calculator.lsl index 79370ca..e8f4741 100644 --- a/Game_Calculator.lsl +++ b/Game_Calculator.lsl @@ -7,6 +7,19 @@ integer VERBOSE_LOGGING = TRUE; // Global flag for verbose debug logs integer MSG_TOGGLE_VERBOSE_LOGS = 9998; // Message to toggle verbose logging +// Memory reporting function +reportMemoryUsage(string scriptName) { + integer used = llGetUsedMemory(); + integer free = llGetFreeMemory(); + integer total = used + free; + float percentUsed = ((float)used / (float)total) * 100.0; + + llOwnerSay("๐Ÿง  [" + scriptName + "] Memory: " + + (string)used + " used, " + + (string)free + " free (" + + llGetSubString((string)percentUsed, 0, 4) + "% used)"); +} + integer MSG_GET_DICE_TYPE = 1001; integer MSG_DICE_TYPE_RESULT = 1005; integer MSG_GET_PICKS_REQUIRED = 1002; @@ -130,6 +143,8 @@ integer showPickManager(string player, key id) { default { state_entry() { + reportMemoryUsage("๐Ÿงฎ Game Calculator"); + // Initialize/reset all state variables lives = []; picksData = []; @@ -140,6 +155,8 @@ default { } on_rez(integer start_param) { + reportMemoryUsage("๐Ÿงฎ Game Calculator"); + if (VERBOSE_LOGGING) { llOwnerSay("๐Ÿ”„ Game Calculator rezzed - reinitializing..."); } diff --git a/Game_Manager.lsl b/Game_Manager.lsl index 1ecf459..6c17fbc 100644 --- a/Game_Manager.lsl +++ b/Game_Manager.lsl @@ -38,6 +38,46 @@ 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 pick = llList2String(globalPickedNumbers, i); + if (pick != "" && llListFindList(allPicks, [pick]) == -1) { + allPicks += [pick]; + } + } + + return allPicks; +} + // Memory reporting function reportMemoryUsage(string scriptName) { integer used = llGetUsedMemory(); @@ -64,6 +104,7 @@ integer MSG_SYNC_PICKQUEUE = 2001; integer MSG_PLAYER_WON = 551; integer MSG_UPDATE_FLOAT = 2010; integer MSG_DIALOG_FORWARD_REQUEST = 9060; // Request dialog forwarding from Player_RegistrationManager +integer MSG_CONTINUE_ROUND = 998; // Continue round after roll processing continueCurrentRound() { // MEMORY OPTIMIZED: Skip complex validation - trust game state @@ -147,6 +188,10 @@ startNextRound() { 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(12, 3006, winner, NULL_KEY); // MSG_UPDATE_WINNER + llMessageLinked(LINK_SET, MSG_PLAYER_WON, winner, NULL_KEY); llMessageLinked(LINK_SET, 995, "VICTORY_CONFETTI", NULL_KEY); // Don't use hardcoded channel here - let Main Controller handle scoreboard updates @@ -167,6 +212,9 @@ startNextRound() { 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(12, 3005, perilPlayer, NULL_KEY); // MSG_UPDATE_PERIL_PLAYER + // Sync state to floater manager first, then update floaters to show new peril player immediately llOwnerSay("๐Ÿ”„ [Game Manager] Syncing state and updating floaters for new peril player: " + perilPlayer); syncStateToMain(); // Sync the peril player to all modules first @@ -303,8 +351,10 @@ showNextPickerDialog() { integer picksNeeded = 4 - perilLives; // Send proper avoid list to bots so they don't pick human numbers - string globalPicksStr = llList2CSV(globalPickedNumbers); - string botCommand = "BOT_PICK:" + firstName + ":" + (string)picksNeeded + ":" + (string)diceType + ":" + globalPicksStr; + list completeAvoidList = buildCompleteAvoidanceList(); + string avoidListStr = llList2CSV(completeAvoidList); + string botCommand = "BOT_PICK:" + firstName + ":" + (string)picksNeeded + ":" + (string)diceType + ":" + avoidListStr; + llOwnerSay("๐ŸŽฏ [Game Manager] Sending bot command with complete avoid list (" + (string)llGetListLength(completeAvoidList) + " numbers): " + avoidListStr); llMessageLinked(LINK_SET, -9999, botCommand, NULL_KEY); llOwnerSay("๐Ÿค– " + firstName + " is automatically picking " + (string)picksNeeded + " numbers..."); } else { @@ -327,9 +377,10 @@ showNextPickerDialog() { } integer picksNeeded = 4 - perilLives; - // Send dialog payload with current globally picked numbers - string globalPicksStr = llList2CSV(globalPickedNumbers); - string dialogPayload = firstName + "|" + (string)diceType + "|" + (string)picksNeeded + "|" + globalPicksStr; + // Send dialog payload with complete avoid list for humans too + list completeAvoidList = buildCompleteAvoidanceList(); + string avoidListStr = llList2CSV(completeAvoidList); + string dialogPayload = firstName + "|" + (string)diceType + "|" + (string)picksNeeded + "|" + avoidListStr; llOwnerSay("๐ŸŽฏ Showing pick dialog for " + firstName); llSleep(0.5); // Brief delay to prevent spam @@ -617,22 +668,7 @@ default { return; } - // Handle start round requests - if (num == 997) { - if (str == "START_NEXT_ROUND") { - startNextRound(); - } - else if (llSubStringIndex(str, "CONTINUE_ROUND|") == 0) { - string newPeril = llGetSubString(str, 15, -1); - perilPlayer = newPeril; - llOwnerSay("๐ŸŽฏ Game Manager received CONTINUE_ROUND for " + newPeril); - // Reset round state for new round - roundStarted = FALSE; - currentPickerIdx = 0; - continueCurrentRound(); - } - return; - } + // Legacy 997 message handler removed - now using direct MSG_CONTINUE_ROUND (998) communication // Handle human picks if (num == -9998 && llSubStringIndex(str, "HUMAN_PICKED:") == 0) { @@ -661,16 +697,19 @@ default { return; // Ignore duplicate human picks } - // Validate picks against global picked numbers + // 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; + llOwnerSay("๐Ÿ” [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(globalPickedNumbers, [pick]) == -1) { + if (llListFindList(completeAvoidList, [pick]) == -1) { validPicks += [pick]; globalPickedNumbers += [pick]; } else { @@ -705,7 +744,9 @@ default { } integer picksNeeded = 4 - perilLives; - string dialogPayload = playerName + "|" + (string)diceType + "|" + (string)picksNeeded + "|" + llList2CSV(globalPickedNumbers); + // 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); } @@ -849,8 +890,11 @@ default { } integer picksNeeded = 4 - perilLives; - // Send new bot command with updated avoid list - string botCommand = "BOT_PICK:" + playerName + ":" + (string)picksNeeded + ":" + (string)diceType + ":" + llList2CSV(globalPickedNumbers); + // 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; + llOwnerSay("๐ŸŽฏ [Game Manager] Sending retry bot command with complete avoid list (" + (string)llGetListLength(completeAvoidList) + " numbers): " + avoidListStr); llMessageLinked(LINK_SET, -9999, botCommand, NULL_KEY); return; // Don't save this pick } @@ -946,6 +990,39 @@ default { return; } + // Handle continue round requests from Main Controller + if (num == MSG_CONTINUE_ROUND) { + llOwnerSay("๐ŸŽฏ [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; + llOwnerSay("๐ŸŽฏ [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() + llOwnerSay("๐ŸŽฏ [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 verbose logging toggle from Main Controller if (num == 9011 && llSubStringIndex(str, "VERBOSE_LOGGING|") == 0) { list parts = llParseString2List(str, ["|"], []); diff --git a/Game_Scoreboard_Manager_Linkset.lsl b/Game_Scoreboard_Manager_Linkset.lsl index 708d348..4465d30 100644 --- a/Game_Scoreboard_Manager_Linkset.lsl +++ b/Game_Scoreboard_Manager_Linkset.lsl @@ -16,6 +16,8 @@ integer MSG_GAME_STATUS = 3001; integer MSG_PLAYER_UPDATE = 3002; integer MSG_CLEAR_GAME = 3003; integer MSG_REMOVE_PLAYER = 3004; +integer MSG_UPDATE_PERIL_PLAYER = 3005; +integer MSG_UPDATE_WINNER = 3006; // Leaderboard messages (to link 35) integer MSG_GAME_WON = 3010; @@ -63,6 +65,10 @@ list playerNames = []; // Current game players for display list playerLives = []; // Current game player lives list playerProfiles = []; // Maps to profile texture UUIDs +// Peril player tracking +string currentPerilPlayer = ""; // Track current peril player for glow effects +string currentWinner = ""; // Track current winner for green glow effects + // Leaderboard data (persistent - stored in linkset data) list leaderboardNames = []; // Loaded from linkset data at startup list leaderboardWins = []; // Loaded from linkset data at startup @@ -382,18 +388,20 @@ clearAllPlayers() { return; // Don't modify prims outside our range! } - // Reset profile prim to default + // Reset profile prim to default and clear any glow llSetLinkPrimitiveParamsFast(profilePrimIndex, [ PRIM_TEXTURE, ALL_SIDES, TEXTURE_DEFAULT_PROFILE, <1,1,0>, <0,0,0>, 0.0, PRIM_COLOR, ALL_SIDES, <1.0, 1.0, 1.0>, 1.0, - PRIM_TEXT, "", <1,1,1>, 0.0 + PRIM_TEXT, "", <1,1,1>, 0.0, + PRIM_GLOW, ALL_SIDES, 0.0 // Explicitly clear glow ]); - // Reset hearts prim to 3 hearts + // Reset hearts prim to 3 hearts and clear any glow llSetLinkPrimitiveParamsFast(heartsPrimIndex, [ PRIM_TEXTURE, ALL_SIDES, TEXTURE_3_HEARTS, <1,1,0>, <0,0,0>, 0.0, PRIM_COLOR, ALL_SIDES, <1.0, 1.0, 1.0>, 1.0, - PRIM_TEXT, "", <1,1,1>, 0.0 + PRIM_TEXT, "", <1,1,1>, 0.0, + PRIM_GLOW, ALL_SIDES, 0.0 // Explicitly clear glow ]); // Reset overlay prim to transparent @@ -521,6 +529,9 @@ refreshPlayerDisplay() { ]); } } + + // Update player glow effects after refreshing all players + updatePlayerGlowEffects(); } // Check if a player name indicates it's a bot @@ -597,6 +608,84 @@ string getHeartTexture(integer lives) { else return TEXTURE_3_HEARTS; // 3 or more } +// Update all glow effects (peril and winner) +updatePlayerGlowEffects() { + // First, remove glow and reset tint from all players + integer i; + for (i = 0; i < llGetListLength(playerNames); i++) { + integer profilePrimIndex = getProfilePrimLink(i); + integer heartsPrimIndex = getHeartsPrimLink(i); + + // SAFETY CHECK + if (profilePrimIndex >= FIRST_PROFILE_PRIM && profilePrimIndex <= LAST_PROFILE_PRIM) { + llSetLinkPrimitiveParamsFast(profilePrimIndex, [ + PRIM_GLOW, ALL_SIDES, 0.0, + PRIM_COLOR, ALL_SIDES, <1.0, 1.0, 1.0>, 1.0 + ]); + llSetLinkPrimitiveParamsFast(heartsPrimIndex, [ + PRIM_GLOW, ALL_SIDES, 0.0, + PRIM_COLOR, ALL_SIDES, <1.0, 1.0, 1.0>, 1.0 + ]); + } + } + + // Priority 1: Green glow for winner (overrides peril glow) + if (currentWinner != "" && currentWinner != "NONE") { + integer winnerIndex = llListFindList(playerNames, [currentWinner]); + if (winnerIndex != -1) { + integer profilePrimIndex = getProfilePrimLink(winnerIndex); + integer heartsPrimIndex = getHeartsPrimLink(winnerIndex); + + // SAFETY CHECK + if (profilePrimIndex >= FIRST_PROFILE_PRIM && profilePrimIndex <= LAST_PROFILE_PRIM) { + // Add green glow + green tint to winner + llSetLinkPrimitiveParamsFast(profilePrimIndex, [ + PRIM_GLOW, ALL_SIDES, 0.3, + PRIM_COLOR, ALL_SIDES, <0.0, 1.0, 0.0>, 1.0 + ]); + llSetLinkPrimitiveParamsFast(heartsPrimIndex, [ + PRIM_GLOW, ALL_SIDES, 0.3, + PRIM_COLOR, ALL_SIDES, <0.0, 1.0, 0.0>, 1.0 + ]); + + if (VERBOSE_LOGGING) { + llOwnerSay("๐Ÿ† Added green glow to winner: " + currentWinner); + } + } + } + } + // Priority 2: Yellow glow for peril player (only if not the winner) + else if (currentPerilPlayer != "" && currentPerilPlayer != "NONE") { + integer perilIndex = llListFindList(playerNames, [currentPerilPlayer]); + if (perilIndex != -1) { + integer profilePrimIndex = getProfilePrimLink(perilIndex); + integer heartsPrimIndex = getHeartsPrimLink(perilIndex); + + // SAFETY CHECK + if (profilePrimIndex >= FIRST_PROFILE_PRIM && profilePrimIndex <= LAST_PROFILE_PRIM) { + // Add yellow glow + yellow tint to peril player + llSetLinkPrimitiveParamsFast(profilePrimIndex, [ + PRIM_GLOW, ALL_SIDES, 0.2, + PRIM_COLOR, ALL_SIDES, <1.0, 1.0, 0.0>, 1.0 + ]); + llSetLinkPrimitiveParamsFast(heartsPrimIndex, [ + PRIM_GLOW, ALL_SIDES, 0.2, + PRIM_COLOR, ALL_SIDES, <1.0, 1.0, 0.0>, 1.0 + ]); + + if (VERBOSE_LOGGING) { + llOwnerSay("โœจ Added yellow glow to peril player: " + currentPerilPlayer); + } + } + } + } +} + +// Backward compatibility wrapper +updatePerilPlayerGlow() { + updatePlayerGlowEffects(); +} + // Update player display on the grid updatePlayerDisplay(string playerName, integer lives, string profileUUID) { // Find existing player or add new one @@ -791,6 +880,9 @@ updatePlayerDisplay(string playerName, integer lives, string profileUUID) { PRIM_TEXT, "", <1,1,1>, 0.0 ]); } + + // Update player glow effects after display changes + updatePlayerGlowEffects(); } default { @@ -817,6 +909,11 @@ default { // Generate initial leaderboard generateLeaderboardText(); + // Ensure no glow effects remain from previous sessions + currentPerilPlayer = ""; + currentWinner = ""; + updatePlayerGlowEffects(); + if (VERBOSE_LOGGING) { llOwnerSay("โœ… Linkset communication active - no discovery needed!"); llOwnerSay("๐Ÿ“Š Loaded " + (string)llGetListLength(leaderboardNames) + " players from leaderboard data"); @@ -863,6 +960,11 @@ default { clearAllPlayers(); // Clear all current players updateActionsPrim("Title"); // Reset to title + // Clear peril player and winner tracking and remove any remaining glow + currentPerilPlayer = ""; + currentWinner = ""; + updatePlayerGlowEffects(); // This will remove glow from all players + generateLeaderboardText(); // Refresh leaderboard display } else if (num == MSG_GAME_WON) { @@ -877,6 +979,36 @@ default { else if (num == MSG_REMOVE_PLAYER) { removePlayer(str); } + else if (num == MSG_UPDATE_PERIL_PLAYER) { + // Update peril player and refresh glow effects + string oldPeril = currentPerilPlayer; + currentPerilPlayer = str; + if (currentPerilPlayer == "NONE") { + currentPerilPlayer = ""; + } + + if (VERBOSE_LOGGING && oldPeril != currentPerilPlayer) { + llOwnerSay("๐ŸŽฏ Peril player changed from '" + oldPeril + "' to '" + currentPerilPlayer + "'"); + } + + // Update glow effects + updatePlayerGlowEffects(); + } + else if (num == MSG_UPDATE_WINNER) { + // Update winner and refresh glow effects + string oldWinner = currentWinner; + currentWinner = str; + if (currentWinner == "NONE") { + currentWinner = ""; + } + + if (VERBOSE_LOGGING && oldWinner != currentWinner) { + llOwnerSay("๐Ÿ† Winner changed from '" + oldWinner + "' to '" + currentWinner + "'"); + } + + // Update glow effects - winner glow overrides peril glow + updatePlayerGlowEffects(); + } } http_response(key request_id, integer status, list metadata, string body) { diff --git a/INFINITE_LOOP_FIX.md b/INFINITE_LOOP_FIX.md deleted file mode 100644 index 7885f58..0000000 --- a/INFINITE_LOOP_FIX.md +++ /dev/null @@ -1,69 +0,0 @@ -# EMERGENCY FIX: Infinite Loop Resolution - -## Problem -The delegation system created an infinite loop between Main_Controller_Linkset.lsl and Player_RegistrationManager.lsl: - -1. Main Controller's `touch_start` sends `MSG_REGISTER_PLAYER` -2. Player_RegistrationManager receives `MSG_REGISTER_PLAYER` and processes it -3. BUT Main Controller's `link_message` handler ALSO receives `MSG_REGISTER_PLAYER` -4. Main Controller forwards it AGAIN to Player_RegistrationManager -5. INFINITE LOOP ๐Ÿ’€ - -## Root Cause -Both scripts were using the same message constant `MSG_REGISTER_PLAYER = 106`, causing message collision. - -## Solution Applied - -### 1. New Dedicated Message Constant -```lsl -integer MSG_REGISTER_PLAYER_REQUEST = 9050; // Dedicated message to Player_RegistrationManager -``` - -### 2. Updated Message Flow -- **Main Controller `touch_start`**: Sends `MSG_REGISTER_PLAYER_REQUEST` (new) -- **Main Controller `link_message`**: Converts old `MSG_REGISTER_PLAYER` to `MSG_REGISTER_PLAYER_REQUEST` for backward compatibility -- **Player_RegistrationManager**: Only listens for `MSG_REGISTER_PLAYER_REQUEST` - -### 3. Files Modified - -#### Main_Controller_Linkset.lsl -- **Added**: `MSG_REGISTER_PLAYER_REQUEST = 9050` -- **Changed**: `touch_start` uses `MSG_REGISTER_PLAYER_REQUEST` instead of `MSG_REGISTER_PLAYER` -- **Changed**: `link_message` converts old messages to new format -- **Changed**: "Add Test Player" admin function uses new message - -#### Player_RegistrationManager.lsl -- **Changed**: `MSG_REGISTER_PLAYER = 106` โ†’ `MSG_REGISTER_PLAYER_REQUEST = 9050` -- **Changed**: `link_message` handler listens for `MSG_REGISTER_PLAYER_REQUEST` - -## Message Flow Diagram - -### BEFORE (Infinite Loop) -``` -Touch โ†’ Main Controller โ†’ MSG_REGISTER_PLAYER โ†’ Player_RegistrationManager - โ†“ - link_message handler - โ†“ - MSG_REGISTER_PLAYER โ†’ Player_RegistrationManager (LOOP!) -``` - -### AFTER (Fixed) -``` -Touch โ†’ Main Controller โ†’ MSG_REGISTER_PLAYER_REQUEST โ†’ Player_RegistrationManager - โ†“ - link_message handler (converts old messages only) - โ†“ - MSG_REGISTER_PLAYER โ†’ MSG_REGISTER_PLAYER_REQUEST โ†’ Player_RegistrationManager -``` - -## Backward Compatibility -The Main Controller still accepts old `MSG_REGISTER_PLAYER` messages from other scripts and converts them to the new format, ensuring no functionality is lost. - -## Testing -- โœ… Single player registration should work without loops -- โœ… Bot addition via admin menu should work -- โœ… Multiple rapid registrations should not cause loops -- โœ… Existing functionality preserved - -## Status: EMERGENCY FIX APPLIED โšก -The infinite loop has been eliminated while preserving all functionality and maintaining the memory optimization benefits. \ No newline at end of file diff --git a/MARKETPLACE_DESCRIPTION.txt b/MARKETPLACE_DESCRIPTION.txt index 35e16b8..aef73b6 100644 --- a/MARKETPLACE_DESCRIPTION.txt +++ b/MARKETPLACE_DESCRIPTION.txt @@ -2,18 +2,20 @@ PERIL DICE - Professional Single Linkset Game System for Second Life Created by Rebecca Nod and Noose the Bunny -CURRENT VERSION: 2.8.2 - Scoreboard Spam & Display Fixes +CURRENT VERSION: 2.8.6 - Winner Glow Celebration System Thrilling multiplayer elimination game for 2-10 players! Each participant selects numbers before a die is rolled. If the peril player's number comes up, they lose a life. Last player standing wins! -NEW IN v2.8.2: Fixed critical scoreboard spam bug, re-fixed peril status display on floating HUDs, enhanced elimination sequence to show 0 hearts before player removal. +NEW IN v2.8.6: Revolutionary winner recognition with bright green glow effects on both scoreboard and floaters, plus 24-second victory celebration timer giving proper recognition to the Ultimate Survivor before game reset. KEY FEATURES: +- Winner Glow Celebration: Bright green glow effects on scoreboard and floaters with 24-second victory timer - Visual Scoreboard: Real-time player grid with profile pictures and hearts - Enhanced Status Display: Custom textures for Direct Hit, No Shield, Plot Twist, Elimination, Victory +- Color-Coded Glow System: Green (winner), Yellow (peril), Red X (eliminated) - Comprehensive Leaderboard: Persistent win tracking with XyzzyText display - Bot Support: AI players for testing and gameplay variety -- Floating HUD Display: Real-time stats for each player +- Floating HUD Display: Real-time stats for each player with victory text - Intelligent Dice Sizing: d6/d12/d20/d30 based on player count - Security Features: Owner lockout system, kick player functionality - Single Linkset: Complete 74-prim system for easy deployment @@ -52,6 +54,10 @@ EXPERIENCE: None required Perfect for nightclubs, venues, casinos, private parties, group entertainment, community gatherings. VERSION HISTORY: +v2.8.6: Winner Glow Celebration System with 24-second victory recognition +v2.8.5: Memory optimization and bot profile picture fixes +v2.8.4: Disconnect recovery system and verbose logging +v2.8.3: Shield detection fixes and initialization overhaul v2.8.2: Scoreboard spam fix, display improvements v2.8.1: Peril status and elimination heart fixes v2.8.0: Game lockout security, automatic reset @@ -60,6 +66,6 @@ v2.7.0: Single linkset architecture revolution ORIGINAL CONCEPT: Noose the Bunny (djmusica28) AUTOMATED SYSTEM: Enhanced for professional gameplay -Transform your space into an exciting gaming destination! +Transform your space into an exciting gaming destination with professional winner celebration effects! Keywords: dice game, elimination game, multiplayer, party game, club game, casino, entertainment, social game, LSL script, venue game diff --git a/MEMORY_OPTIMIZATION_SUMMARY.md b/MEMORY_OPTIMIZATION_SUMMARY.md deleted file mode 100644 index 11f5a49..0000000 --- a/MEMORY_OPTIMIZATION_SUMMARY.md +++ /dev/null @@ -1,110 +0,0 @@ -# Memory Optimization Summary - Peril Dice Game v2.8.2 - -## Issue Description -The Main Controller script was experiencing stack-heap collisions during player registration, specifically when new players joined the game. This was causing script failures and preventing proper player registration. - -## Root Cause Analysis -The player registration process in `Main_Controller_Linkset.lsl` was performing memory-intensive operations: - -1. **Heavy string concatenations** for status messages and announcements -2. **Large list manipulations** during player addition -3. **Complex bot vs human logic processing** -4. **Immediate floater channel calculations** -5. **Multiple helper update calls** during registration - -All of these operations happened simultaneously in the main controller's memory space, causing the stack and heap to collide. - -## Solution Implemented -**Delegation Pattern**: Moved the memory-intensive player registration logic to a dedicated helper script. - -### Changes Made - -#### 1. Created New Script: `Player_RegistrationManager.lsl` -- **Purpose**: Handle all player registration operations -- **Memory Benefits**: Isolates heavy string operations from main controller -- **Responsibilities**: - - Validate game state restrictions (game started, max players) - - Handle bot vs human player logic - - Manage floater channel assignments - - Send appropriate announcements - - Update main controller with final results - -#### 2. Modified `Main_Controller_Linkset.lsl` -- **Removed**: Entire player registration logic block (lines 617-682 in original) -- **Added**: Simple delegation call that forwards registration requests -- **Added**: Handler for registration updates from the dedicated manager -- **Memory Savings**: ~65 lines of complex string manipulation removed - -### Technical Implementation - -#### Communication Protocol -```lsl -// Main Controller forwards registration requests -if (num == MSG_REGISTER_PLAYER) { - llMessageLinked(LINK_SET, MSG_REGISTER_PLAYER, str, id); - return; -} - -// Registration Manager sends updates back -if (num == 9040) { // MSG_UPDATE_MAIN_LISTS - // Update main controller lists from registration manager - list parts = llParseString2List(str, ["~"], []); - if (llGetListLength(parts) >= 6) { - players = llCSV2List(llList2String(parts, 0)); - names = llCSV2List(llList2String(parts, 1)); - lives = llCSV2List(llList2String(parts, 2)); - readyPlayers = llCSV2List(llList2String(parts, 3)); - // ... process update ... - } -} -``` - -#### Data Flow -1. Player touches object โ†’ Main Controller -2. Main Controller forwards `MSG_REGISTER_PLAYER` โ†’ Registration Manager -3. Registration Manager processes all complex logic in its own memory space -4. Registration Manager sends `MSG_UPDATE_MAIN_LISTS` โ†’ Main Controller -5. Main Controller updates its lists with final results -6. Main Controller calls `updateHelpers()` to sync other scripts - -## Expected Benefits - -### Memory Usage -- **Main Controller**: Reduced peak memory usage during player join by ~30-40% -- **Registration Manager**: Handles heavy operations in isolated memory space -- **Overall System**: Better memory distribution across multiple scripts - -### Performance -- **Faster Registration**: Less memory pressure = faster processing -- **Reduced Failures**: Eliminates stack-heap collision during joins -- **Better Stability**: Main controller maintains lower baseline memory usage - -### Maintainability -- **Separation of Concerns**: Registration logic isolated from main game logic -- **Easier Debugging**: Registration issues can be traced to dedicated script -- **Modular Design**: Can optimize registration logic without affecting main controller - -## Testing Recommendations - -1. **Basic Registration**: Test single player joins -2. **Rapid Registration**: Test multiple players joining quickly -3. **Bot Registration**: Test bot addition via admin menu -4. **Memory Monitoring**: Use "Memory Stats" admin option to verify improvements -5. **Edge Cases**: Test max players, game started restrictions - -## File Changes Summary - -### Modified Files -- `Main_Controller_Linkset.lsl` - Delegated player registration, reduced memory usage - -### New Files -- `Player_RegistrationManager.lsl` - Dedicated player registration handler - -### Architecture Impact -- **Single Linkset Design**: Maintained - no external communication required -- **Link Message Protocol**: Extended with new message type (9040) -- **Script Responsibilities**: Better distributed across helper scripts -- **Memory Architecture**: More balanced memory usage across scripts - -## Version Notes -This optimization maintains full backward compatibility and preserves all existing functionality while significantly reducing memory pressure in the main controller script. \ No newline at end of file diff --git a/Main_Controller_Linkset.lsl b/Main_Controller_Linkset.lsl index 85e0bd9..b9a6c81 100644 --- a/Main_Controller_Linkset.lsl +++ b/Main_Controller_Linkset.lsl @@ -30,6 +30,8 @@ integer MSG_GAME_STATUS = 3001; integer MSG_PLAYER_UPDATE = 3002; integer MSG_CLEAR_GAME = 3003; integer MSG_REMOVE_PLAYER = 3004; +integer MSG_UPDATE_PERIL_PLAYER = 3005; +integer MSG_UPDATE_WINNER = 3006; // Leaderboard messages (to link 35) integer MSG_GAME_WON = 3010; @@ -73,10 +75,10 @@ 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 TIMER_VICTORY_DELAY = 2; // Timer mode for victory glow display integer currentTimerMode = 0; // Track what the timer is currently doing float timerInterval = 1.0; // How often timer() is called for checks +integer victoryDelayTimer = 0; // Track victory delay timing // Debug control - set to TRUE for verbose pick debugging, FALSE for normal operation integer DEBUG_PICKS = FALSE; @@ -151,12 +153,7 @@ string TEXTURE_GAME_ACTIVE = "game_active"; // Optional: different texture duri // Maximum number of players allowed (including test players) integer MAX_PLAYERS = 10; -integer TIMEOUT_SECONDS = 120; // 2 minute timeout -integer timeoutTimer; -integer warning30sec = 30; -integer warning1min = 60; -integer warning90sec = 90; -integer lastWarning = 0; +// Timeout system removed - using owner kick functionality instead key currentPicker; @@ -317,20 +314,14 @@ updateFloatingText() { resetGame() { cleanupListeners(); - // Only clean up channels that actually have active floaters - integer i; - for (i = 0; i < llGetListLength(floaterChannels); i++) { - integer ch = llList2Integer(floaterChannels, i); - llMessageLinked(LINK_SET, MSG_CLEANUP_FLOAT, (string)ch, NULL_KEY); + // Let Floater Manager handle cleanup intelligently + // Only request cleanup if we actually have game data that might have floaters + if (llGetListLength(names) > 0 || llGetListLength(floaterChannels) > 0) { + llMessageLinked(LINK_SET, MSG_CLEANUP_ALL_FLOATERS, "RESET", NULL_KEY); + llSleep(1.0); } - // Also send a few cleanup messages to common channel ranges in case of orphaned floaters - for (i = 0; i < llGetListLength(names); i++) { - integer ch = FLOATER_BASE_CHANNEL + i; - llMessageLinked(LINK_SET, MSG_CLEANUP_FLOAT, (string)ch, NULL_KEY); - } - - llSleep(2.0); + llSleep(0.5); players = names = lives = picksData = globalPickedNumbers = readyPlayers = []; floaterChannels = []; @@ -342,14 +333,14 @@ resetGame() { gameStarting = FALSE; victoryInProgress = FALSE; currentPicker = NULL_KEY; - timeoutTimer = 0; - lastWarning = 0; llMessageLinked(LINK_SET, -99999, "FULL_RESET", NULL_KEY); llSay(syncChannel, "RESET"); // CHANGED: Use link messages instead of llRegionSay for display cleanup llMessageLinked(SCOREBOARD_LINK, MSG_CLEAR_GAME, "", NULL_KEY); + // Clear winner glow from scoreboard (floaters will clear naturally on reset) + llMessageLinked(SCOREBOARD_LINK, MSG_UPDATE_WINNER, "", NULL_KEY); // Clear winner glow llMessageLinked(DICE_LINK, MSG_CLEAR_DICE, "", NULL_KEY); llOwnerSay("๐ŸŽฎ Game reset! All state cleared (including scoreboard)."); @@ -484,6 +475,7 @@ default { } if (toucher == llGetOwner()) { + llOwnerSay("๐Ÿ” [DEBUG] Owner touched - idx: " + (string)idx + ", players list: " + llList2CSV(players)); if (idx == -1) { // Owner is not in game - show join/admin choice if (llListFindList(pendingRegistrations, [toucher]) != -1) { @@ -491,10 +483,14 @@ default { return; } - // Show owner choice menu: join game or access admin functions - 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(toucher, menuText, options, DIALOG_CHANNEL); + // Owner is not in game - determine menu type based on player count + if (llGetListLength(players) >= 2) { + // Enough players for game - show starter menu + llMessageLinked(LINK_SET, MSG_SHOW_MENU, "unregistered_owner_starter|1", toucher); + } else { + // Not enough players - show basic menu + llMessageLinked(LINK_SET, MSG_SHOW_MENU, "unregistered_owner|0", toucher); + } } else { // Owner is already in game - show player menu with appropriate starter status integer isStarter = TRUE; @@ -608,53 +604,112 @@ default { // Send ONLY a direct scoreboard update to show 0 hearts llMessageLinked(SCOREBOARD_LINK, MSG_PLAYER_UPDATE, eliminatedPlayer + "|0|" + (string)llList2Key(players, idx), NULL_KEY); - llSleep(1.5); // Give time for 0 hearts display - // Check if this will cause victory BEFORE removing player + // CRITICAL: Check for victory condition BEFORE assigning new peril player integer willCauseVictory = (llGetListLength(names) <= 2); // After removing one player + string potentialWinner = ""; + + if (willCauseVictory) { + // Find the winner (the remaining non-eliminated player) + integer w; + for (w = 0; w < llGetListLength(names); w++) { + string candidateName = llList2String(names, w); + if (candidateName != eliminatedPlayer) { + potentialWinner = candidateName; + jump found_winner; + } + } + @found_winner; + + if (potentialWinner != "") { + // SEND WINNER GLOW to scoreboard and trigger floater update + llMessageLinked(SCOREBOARD_LINK, MSG_UPDATE_WINNER, potentialWinner, NULL_KEY); + llMessageLinked(LINK_SET, MSG_UPDATE_FLOAT, potentialWinner, NULL_KEY); // Update winner's floater + llOwnerSay("๐Ÿ† [Main Controller] Pre-victory winner glow sent for: " + potentialWinner); + } + } + + // Only assign new peril player if NOT causing victory + if (!willCauseVictory && (perilPlayer == eliminatedPlayer || perilPlayer == "")) { + // Find first remaining alive player to be new peril player (before removing eliminated player) + string newPerilPlayer = ""; + integer k; + for (k = 0; k < llGetListLength(names) && newPerilPlayer == ""; k++) { + string candidateName = llList2String(names, k); + if (candidateName != eliminatedPlayer) { // Skip the player being eliminated + integer candidateIdx = llListFindList(names, [candidateName]); + if (candidateIdx != -1 && candidateIdx < llGetListLength(lives)) { + integer candidateLives = llList2Integer(lives, candidateIdx); + if (candidateLives > 0) { + newPerilPlayer = candidateName; + } + } + } + } + + if (newPerilPlayer != "") { + perilPlayer = newPerilPlayer; + // Send peril player update to scoreboard for glow effect + llMessageLinked(SCOREBOARD_LINK, MSG_UPDATE_PERIL_PLAYER, newPerilPlayer, NULL_KEY); + } else { + perilPlayer = "NONE"; + // Clear peril player glow on scoreboard + llMessageLinked(SCOREBOARD_LINK, MSG_UPDATE_PERIL_PLAYER, "", NULL_KEY); + } + } + + // CRITICAL: Send sync message with 0 lives so floater shows elimination state + // Build sync message BEFORE any list modifications to prevent corruption + string livesStr = llList2CSV(lives); + string picksStr = llDumpList2String(picksData, "^"); + if (picksStr == "") picksStr = "EMPTY"; // Prevent empty string + string perilStr = perilPlayer; + if (perilStr == "") perilStr = "NONE"; // Prevent empty string + string namesStr = llList2CSV(names); + string playersStr = llList2CSV(players); + + string eliminationSync = livesStr + "~" + picksStr + "~" + perilStr + "~" + namesStr + "~" + playersStr; + llMessageLinked(LINK_SET, MSG_SYNC_GAME_STATE, eliminationSync, NULL_KEY); + + // Direct floater update to ensure eliminated status is displayed + llMessageLinked(LINK_SET, MSG_UPDATE_FLOAT, eliminatedPlayer, llList2Key(players, idx)); + llSleep(2.0); // Give floater time to show eliminated status with red display if (willCauseVictory) { // If this elimination will cause victory, give extra time for 0 hearts display llSleep(1.5); // Additional time for victory scenarios } - // THEN: Clean up and remove the player - integer ch = llList2Integer(floaterChannels, idx); - llMessageLinked(LINK_SET, MSG_CLEANUP_FLOAT, (string)ch, NULL_KEY); - llSleep(0.3); // Give floater time to cleanup + // CHANGED: Don't cleanup floaters during elimination - let them show eliminated status + // The floater will show red glow + "ELIMINATED!" and stay visible until game end + // integer ch = llList2Integer(floaterChannels, idx); + // llMessageLinked(LINK_SET, MSG_CLEANUP_FLOAT, (string)ch, NULL_KEY); + // llSleep(0.3); // Give floater time to cleanup + // CRITICAL: Remove eliminated player's picks data by searching for their entry + // NOT by index, since picksData format is "PlayerName|picks" strings + integer pickIdx = -1; + integer p; + for (p = 0; p < llGetListLength(picksData); p++) { + string entry = llList2String(picksData, p); + if (llSubStringIndex(entry, eliminatedPlayer + "|") == 0) { + pickIdx = p; + jump found_pick_entry; + } + } + @found_pick_entry; + if (pickIdx != -1) { + picksData = llDeleteSubList(picksData, pickIdx, pickIdx); + } + + // Now remove from indexed lists (players, names, lives, floaterChannels) 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); - // CRITICAL: Update perilPlayer BEFORE any updateHelpers() calls - // This prevents sending stale sync messages with eliminated players - if (perilPlayer == eliminatedPlayer) { - // Find first remaining alive player to be new peril player - string newPerilPlayer = ""; - integer k; - for (k = 0; k < llGetListLength(names) && newPerilPlayer == ""; k++) { - string candidateName = llList2String(names, k); - integer candidateIdx = llListFindList(names, [candidateName]); - if (candidateIdx != -1 && candidateIdx < llGetListLength(lives)) { - integer candidateLives = llList2Integer(lives, candidateIdx); - if (candidateLives > 0) { - newPerilPlayer = candidateName; - } - } - } - - if (newPerilPlayer != "") { - llOwnerSay("๐ŸŽฏ [Main Controller] Peril player eliminated - assigning new peril player: " + newPerilPlayer); - perilPlayer = newPerilPlayer; - } else { - llOwnerSay("โš ๏ธ [Main Controller] No valid peril player candidates found after elimination!"); - perilPlayer = "NONE"; - } - } + // Peril player assignment already handled before sync message was sent // CHANGED: Send to scoreboard, which will handle leaderboard updates llMessageLinked(SCOREBOARD_LINK, MSG_GAME_LOST, eliminatedPlayer, NULL_KEY); @@ -666,6 +721,11 @@ default { if (llGetListLength(names) == 1) { string winner = llList2String(names, 0); + // ENHANCED: Give eliminated player's floater time to show eliminated status + // before declaring victory and cleaning up + llOwnerSay("โณ [Main Controller] Final elimination detected - allowing display time before victory"); + llSleep(2.0); // Give final eliminated player time to show red eliminated status + // CRITICAL: Set victory flag immediately to prevent sync processing victoryInProgress = TRUE; @@ -687,10 +747,19 @@ default { llMessageLinked(LINK_SET, MSG_PLAYER_WON, winner, NULL_KEY); llMessageLinked(LINK_SET, 995, "VICTORY_CONFETTI", NULL_KEY); + // IMPORTANT: Send winner update to scoreboard and trigger floater update + llMessageLinked(SCOREBOARD_LINK, MSG_UPDATE_WINNER, winner, NULL_KEY); + llMessageLinked(LINK_SET, MSG_UPDATE_FLOAT, winner, NULL_KEY); // Update winner's floater + // CHANGED: Send to scoreboard, which will handle leaderboard updates llMessageLinked(SCOREBOARD_LINK, MSG_GAME_WON, winner, NULL_KEY); - llSleep(STATUS_DISPLAY_TIME * 2.0); // Combined delay + // Use timer instead of llSleep to avoid immediate reset + llOwnerSay("๐Ÿ† [Main Controller] Winner glow applied - starting victory delay timer"); + victoryDelayTimer = llGetUnixTime(); + currentTimerMode = TIMER_VICTORY_DELAY; + llSetTimerEvent(1.0); // Check every second + return; // Exit here - timer will handle reset } else { // CRITICAL: Set victory flag and clear state for no survivors scenario too victoryInProgress = TRUE; @@ -706,10 +775,25 @@ default { currentPicker = NULL_KEY; llSay(0, "๐Ÿ’€ DESPAIR WINS! No Ultimate Survivors remain!"); - llSleep(STATUS_DISPLAY_TIME + 1.0); + + // Use timer for no survivors scenario too + victoryDelayTimer = llGetUnixTime(); + currentTimerMode = TIMER_VICTORY_DELAY; + llSetTimerEvent(1.0); + return; } - resetGame(); - return; + } + + // CRITICAL: After elimination, sync state and continue game with new peril player + if (perilPlayer != "" && perilPlayer != "NONE") { + // Sync the updated game state to all modules (5-part format) + string picksDataStr = "EMPTY"; // Empty picks for new round + string gameSync = llList2CSV(lives) + "~" + picksDataStr + "~" + perilPlayer + "~" + llList2CSV(names) + "~" + llList2CSV(players); + llMessageLinked(LINK_SET, MSG_SYNC_GAME_STATE, gameSync, NULL_KEY); + llSleep(0.5); // Give sync time to propagate + + // Continue the game with the new peril player - delegate to Game Manager + llMessageLinked(LINK_SET, 998, perilPlayer, NULL_KEY); } // MINIMAL: Only send direct scoreboard update for remaining players @@ -770,21 +854,7 @@ default { return; } - // Handle start next round request - delegate to Game Manager - if (num == 997) { - if (sender == llGetLinkNumber()) { - return; - } - - if (roundStarted && str == "START_NEXT_ROUND") { - llOwnerSay("โš ๏ธ [Main Controller] Round already in progress, ignoring START_NEXT_ROUND"); - return; - } - - llOwnerSay("๐Ÿ”„ [Main Controller] Starting new round - Game Manager will handle dice type"); - llMessageLinked(LINK_SET, num, str, id); - return; - } + // Legacy 997 message handler removed - now using direct communication to Game Manager // Handle ready state toggle if (num == MSG_TOGGLE_READY) { @@ -867,18 +937,7 @@ default { 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); - llMessageLinked(LINK_SET, MSG_CLEANUP_FLOAT, (string)(ch + 100), NULL_KEY); - llMessageLinked(LINK_SET, MSG_CLEANUP_FLOAT, (string)(ch + 1000), NULL_KEY); - } - floaterChannels = []; - return; - } + // MSG_CLEANUP_ALL_FLOATERS is handled by Floater Manager only // Handle leave game requests processed by Controller_MessageHandler if (num == 8006) { // MSG_LEAVE_GAME_REQUEST @@ -919,9 +978,13 @@ default { if (newPerilPlayer != "") { llOwnerSay("๐ŸŽฏ [Main Controller] Peril player left - assigning new peril player: " + newPerilPlayer); perilPlayer = newPerilPlayer; + // Send peril player update to scoreboard for glow effect + llMessageLinked(SCOREBOARD_LINK, 3005, newPerilPlayer, NULL_KEY); // MSG_UPDATE_PERIL_PLAYER } else { llOwnerSay("โš ๏ธ [Main Controller] No valid peril player candidates found after player left!"); perilPlayer = "NONE"; + // Clear peril player glow on scoreboard + llMessageLinked(SCOREBOARD_LINK, MSG_UPDATE_PERIL_PLAYER, "", NULL_KEY); } } @@ -1023,7 +1086,7 @@ default { } llSay(0, "โšก THE KILLING GAME CONTINUES! " + perilPlayer + " begins the next deadly round!"); - llMessageLinked(LINK_SET, 997, "START_NEXT_ROUND", NULL_KEY); + llMessageLinked(LINK_SET, 998, perilPlayer, NULL_KEY); return; } } @@ -1087,21 +1150,23 @@ default { } // Create shorter bot names to avoid dialog button length issues - integer botNum = llGetListLength(players) + 1; + // Count existing bots to determine next bot number + integer existingBots = 0; + integer i; + for (i = 0; i < llGetListLength(names); i++) { + string playerName = llList2String(names, i); + if (llSubStringIndex(playerName, "Bot") == 0) { + existingBots++; + } + } + integer botNum = existingBots + 1; string testName = "Bot" + (string)botNum; key fake = llGenerateKey(); llMessageLinked(LINK_SET, MSG_REGISTER_PLAYER_REQUEST, testName + "|" + (string)fake, NULL_KEY); - integer ownerIdx = llListFindList(players, [llGetOwner()]); - integer ownerIsStarter = TRUE; - integer n; - for (n = 0; n < ownerIdx && ownerIsStarter; n++) { - string existingName = llList2String(names, n); - if (llSubStringIndex(existingName, "Bot") != 0) { - ownerIsStarter = FALSE; - } - } - llMessageLinked(LINK_SET, MSG_SHOW_MENU, "owner|" + (string)ownerIsStarter, llGetOwner()); + // Don't show menu immediately - let the player touch the board again + // This avoids timing issues where the menu is shown before registration completes + llOwnerSay("โœ… " + testName + " registration sent - touch the board again to refresh menu"); return; } if (msg == "Force Floaters") { @@ -1115,16 +1180,27 @@ default { } llOwnerSay("โœ… Floater creation requests sent for all players!"); + // After forcing floaters, show appropriate menu based on owner registration status integer ownerIdx = llListFindList(players, [llGetOwner()]); - integer ownerIsStarter = TRUE; - integer n; - for (n = 0; n < ownerIdx && ownerIsStarter; n++) { - string existingName = llList2String(names, n); - if (llSubStringIndex(existingName, "Bot") != 0) { - ownerIsStarter = FALSE; + if (ownerIdx != -1) { + // Owner is registered - show registered owner menu + integer ownerIsStarter = TRUE; + integer n; + for (n = 0; n < ownerIdx && ownerIsStarter; n++) { + string existingName = llList2String(names, n); + if (llSubStringIndex(existingName, "Bot") != 0) { + ownerIsStarter = FALSE; + } + } + llMessageLinked(LINK_SET, MSG_SHOW_MENU, "owner|" + (string)ownerIsStarter, llGetOwner()); + } else { + // Owner is not registered - show unregistered owner menu + if (llGetListLength(players) >= 2) { + llMessageLinked(LINK_SET, MSG_SHOW_MENU, "unregistered_owner_starter|1", llGetOwner()); + } else { + llMessageLinked(LINK_SET, MSG_SHOW_MENU, "unregistered_owner|0", llGetOwner()); } } - llMessageLinked(LINK_SET, MSG_SHOW_MENU, "owner|" + (string)ownerIsStarter, llGetOwner()); return; } } @@ -1176,7 +1252,8 @@ default { llSetText("๐ŸŽฎ GAME IN PROGRESS\n" + (string)actualPlayerCount + " players", <1.0, 0.2, 0.2>, 1.0); sendStatusMessage("Title"); - llMessageLinked(LINK_SET, 997, "START_NEXT_ROUND", NULL_KEY); + // For game start, use empty peril player - Game Manager will select one + llMessageLinked(LINK_SET, 998, "", NULL_KEY); } if (llListFindList(names, [msg]) != -1) { @@ -1197,26 +1274,15 @@ default { llSetTimerEvent(0); } } - else if (currentTimerMode == TIMER_TIMEOUT) { - if (timeoutTimer > 0) { - integer elapsed = llGetUnixTime() - timeoutTimer; - if (elapsed >= TIMEOUT_SECONDS) { - llOwnerSay("โฐ Dialog timeout reached!"); - timeoutTimer = 0; + else if (currentTimerMode == TIMER_VICTORY_DELAY) { + if (victoryDelayTimer > 0) { + integer elapsed = llGetUnixTime() - victoryDelayTimer; + if (elapsed >= (integer)(STATUS_DISPLAY_TIME * 3.0)) { // 24 seconds + llOwnerSay("๐Ÿ† [Main Controller] Victory display time complete - executing game reset"); + victoryDelayTimer = 0; currentTimerMode = TIMER_IDLE; llSetTimerEvent(0); - } else { - integer remaining = TIMEOUT_SECONDS - elapsed; - if (remaining <= warning30sec && lastWarning < warning30sec) { - llOwnerSay("โš ๏ธ 30 seconds remaining for dialog response"); - lastWarning = warning30sec; - } else if (remaining <= warning1min && lastWarning < warning1min) { - llOwnerSay("โš ๏ธ 1 minute remaining for dialog response"); - lastWarning = warning1min; - } else if (remaining <= warning90sec && lastWarning < warning90sec) { - llOwnerSay("โš ๏ธ 1.5 minutes remaining for dialog response"); - lastWarning = warning90sec; - } + resetGame(); // Now reset after the delay } } } diff --git a/NumberPicker_DialogHandler.lsl b/NumberPicker_DialogHandler.lsl index 3fb07c5..f098cfe 100644 --- a/NumberPicker_DialogHandler.lsl +++ b/NumberPicker_DialogHandler.lsl @@ -60,6 +60,19 @@ list globallyPickedNumbers = []; // Numbers already picked by other players integer currentPage = 0; integer NUMBERS_PER_PAGE = 9; // Leave room for navigation buttons +// Memory reporting function +reportMemoryUsage(string scriptName) { + integer used = llGetUsedMemory(); + integer free = llGetFreeMemory(); + integer total = used + free; + float percentUsed = ((float)used / (float)total) * 100.0; + + llOwnerSay("๐Ÿง  [" + scriptName + "] Memory: " + + (string)used + " used, " + + (string)free + " free (" + + llGetSubString((string)percentUsed, 0, 4) + "% used)"); +} + list getNumbersForPage(integer page, integer diceType) { // Build list of available numbers (not picked by others OR by current player) list availableNumbers = []; @@ -176,6 +189,8 @@ showPickDialog(string name, key id, integer diceType, integer picks) { default { state_entry() { + reportMemoryUsage("๐ŸŽฎ NumberPicker Dialog"); + // Initialize dynamic channels initializeChannels(); numberPickChannel = NUMBERPICK_CHANNEL; // Set legacy variable @@ -200,6 +215,8 @@ default { } on_rez(integer start_param) { + reportMemoryUsage("๐ŸŽฎ NumberPicker Dialog"); + if (VERBOSE_LOGGING) { llOwnerSay("๐Ÿ”„ Number Picker rezzed - reinitializing..."); } @@ -290,13 +307,32 @@ default { if (llGetListLength(parts) >= 4) { string globalPicksStr = llList2String(parts, 3); if (globalPicksStr != "") { - // Handle both CSV (comma) and semicolon-separated formats - if (llSubStringIndex(globalPicksStr, ";") != -1) { - // Semicolon-separated format from bots - globallyPickedNumbers = llParseString2List(globalPicksStr, [";"], []); - } else { - // CSV format from humans - globallyPickedNumbers = llCSV2List(globalPicksStr); + // IMPROVED: Handle mixed formats properly + globallyPickedNumbers = []; + + // First split by comma to handle CSV format + list csvParts = llParseString2List(globalPicksStr, [","], []); + integer i; + for (i = 0; i < llGetListLength(csvParts); i++) { + string part = llStringTrim(llList2String(csvParts, i), STRING_TRIM); + + // Check if this part contains semicolons (bot format) + if (llSubStringIndex(part, ";") != -1) { + // Split this part by semicolon and add each number + list semiParts = llParseString2List(part, [";"], []); + integer j; + for (j = 0; j < llGetListLength(semiParts); j++) { + string num = llStringTrim(llList2String(semiParts, j), STRING_TRIM); + if (num != "" && llListFindList(globallyPickedNumbers, [num]) == -1) { + globallyPickedNumbers += [num]; + } + } + } else { + // Single number, add it if not already present + if (part != "" && llListFindList(globallyPickedNumbers, [part]) == -1) { + globallyPickedNumbers += [part]; + } + } } } else { globallyPickedNumbers = []; diff --git a/PlayerStatus_Float.lsl b/PlayerStatus_Float.lsl index 2296d97..4c088ea 100644 --- a/PlayerStatus_Float.lsl +++ b/PlayerStatus_Float.lsl @@ -23,6 +23,19 @@ list picksData; string perilPlayer; list names; +// Memory reporting function +reportMemoryUsage(string scriptName) { + integer used = llGetUsedMemory(); + integer free = llGetFreeMemory(); + integer total = used + free; + float percentUsed = ((float)used / (float)total) * 100.0; + + llOwnerSay("๐Ÿง  [" + scriptName + "] Memory: " + + (string)used + " used, " + + (string)free + " free (" + + llGetSubString((string)percentUsed, 0, 4) + "% used)"); +} + list getPicksFor(string nameInput) { integer i; for (i = 0; i < llGetListLength(picksData); i++) { @@ -47,6 +60,13 @@ list getPicksFor(string nameInput) { default { state_entry() { + // Get player name from object description if available + string playerName = llGetObjectDesc(); + if (playerName == "" || playerName == "(No Description)") { + playerName = "Unknown"; + } + reportMemoryUsage("๐Ÿ“ฑ Player Float (" + playerName + ")"); + llSetText("โณ Waiting...", <1,1,1>, 1.0); llSetTimerEvent(1.0); } @@ -59,12 +79,11 @@ default { // Set up managed listener listenHandle = llListen(start_param, "", NULL_KEY, ""); - // Wait a moment for the description to be set - llSleep(0.1); - myName = llGetObjectDesc(); - if (myName == "" || myName == "(No Description)") { - myName = "UnknownPlayer" + (string)start_param; - } + + // Initialize myName to empty - will be set when SET_NAME message is received + myName = ""; + + // Don't report memory immediately - wait for proper name to be set // PlayerStatus_Float ready and listening } @@ -87,13 +106,53 @@ default { llSetTexture(heartTexture, 2); // Face 2 (back) llSetTexture(heartTexture, 3); // Face 3 (left side) llSetTexture(heartTexture, 4); // Face 4 (front) - // Set white background for all faces to eliminate gaps without tinting - llSetColor(<1.0, 1.0, 1.0>, 1); // White background for right side - llSetColor(<1.0, 1.0, 1.0>, 2); // White background for back - llSetColor(<1.0, 1.0, 1.0>, 3); // White background for left side - llSetColor(<1.0, 1.0, 1.0>, 4); // White background for front - llSetColor(<0.2, 0.2, 0.2>, 0); // Dark gray for top - llSetColor(<0.2, 0.2, 0.2>, 5); // Dark gray for bottom + + // IMPROVED: Special visual treatment for elimination (0 hearts) + if (lifeCount == 0) { + // Make 0 hearts more prominent with red glow + llSetLinkPrimitiveParamsFast(LINK_THIS, [ + PRIM_GLOW, ALL_SIDES, 0.3, + PRIM_COLOR, ALL_SIDES, <1.0, 0.0, 0.0>, 1.0 + ]); + + // Modify display text to show elimination status + displayText = llDumpList2String(llParseString2List(displayText, ["\n"], []), "\n") + "\n๐Ÿ’€ ELIMINATED! ๐Ÿ’€"; + } else { + // Check for winner status first (highest priority) + integer isWinner = FALSE; + if (llSubStringIndex(displayText, "โœจ ULTIMATE VICTORY!") != -1 || + llSubStringIndex(displayText, "ULTIMATE SURVIVOR") != -1) { + isWinner = TRUE; + } + + // Check if this player is in peril for yellow glow + integer isInPeril = FALSE; + if (llSubStringIndex(displayText, "โšก YOU ARE IN PERIL! โšก") != -1) { + isInPeril = TRUE; + } + + if (isWinner) { + // Green glow for winner (overrides peril) + llSetLinkPrimitiveParamsFast(LINK_THIS, [ + PRIM_GLOW, ALL_SIDES, 0.3, + PRIM_COLOR, ALL_SIDES, <0.0, 1.0, 0.0>, 1.0 // Green tint + ]); + } else if (isInPeril) { + // Yellow glow for peril player + llSetLinkPrimitiveParamsFast(LINK_THIS, [ + PRIM_GLOW, ALL_SIDES, 0.2, + PRIM_COLOR, ALL_SIDES, <1.0, 1.0, 0.0>, 1.0 // Yellow tint + ]); + } else { + // Normal colors for living players - remove glow + llSetLinkPrimitiveParamsFast(LINK_THIS, [ + PRIM_GLOW, ALL_SIDES, 0.0, + PRIM_COLOR, ALL_SIDES, <1.0, 1.0, 1.0>, 1.0 + ]); + llSetColor(<0.2, 0.2, 0.2>, 0); // Dark gray for top + llSetColor(<0.2, 0.2, 0.2>, 5); // Dark gray for bottom + } + } // Remove "Lives: X" from display text since hearts show it list lines = llParseString2List(displayText, ["\n"], []); @@ -120,6 +179,9 @@ default { // Name successfully received and set // Update the object description as well for consistency llSetObjectDesc(myName); + + // Now report memory with the proper player name + reportMemoryUsage("๐Ÿ“ฑ " + myName); } } @@ -165,10 +227,57 @@ default { string picksDisplay = llList2CSV(picks); - // Note: Heart texture is set via listen() path to avoid conflicts + // IMPROVED: Set heart texture and handle elimination display + string heartTexture = (string)lifeCount + "_hearts"; + llSetTexture(heartTexture, 1); // Face 1 (right side) + llSetTexture(heartTexture, 2); // Face 2 (back) + llSetTexture(heartTexture, 3); // Face 3 (left side) + llSetTexture(heartTexture, 4); // Face 4 (front) + + string txt; + if (lifeCount == 0) { + // Special treatment for elimination - red glow and elimination message + llSetLinkPrimitiveParamsFast(LINK_THIS, [ + PRIM_GLOW, ALL_SIDES, 0.3, + PRIM_COLOR, ALL_SIDES, <1.0, 0.0, 0.0>, 1.0 + ]); + + txt = "๐ŸŽฒ Peril Dice\n๐Ÿ‘ค " + myName + "\n๐Ÿ’€ ELIMINATED! ๐Ÿ’€\n๐Ÿ”ข Final Picks: " + picksDisplay; + // Set text to red color as well for extra visibility + llSetText(txt, <1.0, 0.2, 0.2>, 1.0); + return; // Skip normal text setting below + } else { + // Check if this is the winner (highest priority) - happens when only 1 player left + integer isWinner = (llGetListLength(names) <= 1 && myName != "" && llListFindList(names, [myName]) != -1); + + // Check if this player is the peril player for yellow glow + integer isInPeril = (myName == perilPlayer && perilPlayer != "" && llSubStringIndex(perilPlayer, ",") == -1); + + if (isWinner) { + // Green glow for winner (overrides peril) + llSetLinkPrimitiveParamsFast(LINK_THIS, [ + PRIM_GLOW, ALL_SIDES, 0.3, + PRIM_COLOR, ALL_SIDES, <0.0, 1.0, 0.0>, 1.0 // Green tint + ]); + } else if (isInPeril) { + // Yellow glow for peril player + llSetLinkPrimitiveParamsFast(LINK_THIS, [ + PRIM_GLOW, ALL_SIDES, 0.2, + PRIM_COLOR, ALL_SIDES, <1.0, 1.0, 0.0>, 1.0 // Yellow tint + ]); + } else { + // Normal colors for living players - remove glow + llSetLinkPrimitiveParamsFast(LINK_THIS, [ + PRIM_GLOW, ALL_SIDES, 0.0, + PRIM_COLOR, ALL_SIDES, <1.0, 1.0, 1.0>, 1.0 + ]); + llSetColor(<0.2, 0.2, 0.2>, 0); // Dark gray for top + llSetColor(<0.2, 0.2, 0.2>, 5); // Dark gray for bottom + } + + txt = "๐ŸŽฒ Peril Dice\n๐Ÿ‘ค " + myName + "\n๐Ÿข Peril: " + perilDisplay + "\n๐Ÿ”ข Picks: " + picksDisplay; + } - // Update text display (lives now shown by heart textures) - string txt = "๐ŸŽฒ Peril Dice\n๐Ÿ‘ค " + myName + "\n๐Ÿข Peril: " + perilDisplay + "\n๐Ÿ”ข Picks: " + picksDisplay; llSetText(txt, <1,1,1>, 1.0); } } diff --git a/Player_DialogHandler.lsl b/Player_DialogHandler.lsl index 064743b..94aaf5d 100644 --- a/Player_DialogHandler.lsl +++ b/Player_DialogHandler.lsl @@ -111,6 +111,19 @@ float MENU_REQUEST_TIMEOUT = 5.0; // 5 second timeout for menu requests integer isLocked = FALSE; key gameOwner; +// Memory reporting function +reportMemoryUsage(string scriptName) { + integer used = llGetUsedMemory(); + integer free = llGetFreeMemory(); + integer total = used + free; + float percentUsed = ((float)used / (float)total) * 100.0; + + llOwnerSay("๐Ÿง  [" + scriptName + "] Memory: " + + (string)used + " used, " + + (string)free + " free (" + + llGetSubString((string)percentUsed, 0, 4) + "% used)"); +} + // Display the main categorized owner menu with lock/unlock controls showOwnerMenu(key id) { // Only show full menu to owner @@ -288,8 +301,32 @@ showKickPlayerMenu(key id, list playerNames) { llDialog(id, "๐Ÿ‘ข Select player to kick from game:", options, DIALOG_CHANNEL); } +// Show menu for unregistered owner +showUnregisteredOwnerMenu(key id, integer canStartGame) { + list options = []; + string menuText = "๐Ÿ‘‘ Owner Options:\n\n"; + + // Always allow owner to join + options += ["Join Game"]; + menuText += "๐ŸŽฎ Join Game - Register as a player\n"; + + // Allow starting game if there are enough players (even if all bots) + if (canStartGame) { + options += ["Start Game"]; + menuText += "โšก Start Game - Begin with current players\n"; + } + + // Always allow owner menu access + options += ["Owner Menu"]; + menuText += "๐Ÿ”ง Owner Menu - Admin functions"; + + llDialog(id, menuText, options, DIALOG_CHANNEL); +} + default { state_entry() { + reportMemoryUsage("๐ŸŽญ Player Dialog"); + // Initialize dynamic channels initializeChannels(); DIALOG_CHANNEL = MAIN_DIALOG_CHANNEL; // Set legacy variable @@ -331,6 +368,8 @@ default { } on_rez(integer start_param) { + reportMemoryUsage("๐ŸŽญ Player Dialog"); + llOwnerSay("๐Ÿ”„ Player Dialog Handler rezzed - reinitializing..."); // Re-initialize dynamic channels @@ -408,6 +447,12 @@ default { } 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) { diff --git a/Player_RegistrationManager.lsl b/Player_RegistrationManager.lsl index 1876e68..8772472 100644 --- a/Player_RegistrationManager.lsl +++ b/Player_RegistrationManager.lsl @@ -153,6 +153,9 @@ handlePlayerRegistration(string regData, key requesterId) { // Send pre-built sync message llMessageLinked(LINK_SET, MSG_SYNC_GAME_STATE, syncMessage, NULL_KEY); + // CRITICAL FIX: Send floater rez message to Floater Manager + llMessageLinked(LINK_SET, 106, newName + "|" + (string)newKey, newKey); + // Send minimal update to Main Controller - just the essential data it needs string updateData = (string)ch + "~" + newName; llMessageLinked(LINK_SET, MSG_UPDATE_MAIN_LISTS, updateData, newKey); diff --git a/README.md b/README.md index 79ff424..b751a8e 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,14 @@ # ๐ŸŽฒ Peril Dice โ€” Professional Single Linkset Game System for Second Life **Created by Rebecca Nod and Noose the Bunny** -**Current Version: 2.8.5 - Memory Optimization & Architecture Cleanup** +**Current Version: 2.8.6 - Winner Glow Celebration System** ## Overview Peril Dice is a multiplayer elimination game where each player selects numbers before a die is rolled. If the peril player's number is rolled, they lose a life. Players are eliminated when they reach zero lives. +**๐Ÿ† NEW in v2.8.6**: **WINNER GLOW CELEBRATION SYSTEM** - Revolutionary winner recognition with bright green glow effects on both scoreboard and floaters, plus 24-second victory celebration timer giving proper recognition to the Ultimate Survivor before game reset. + **๐Ÿšจ NEW in v2.8.5**: **MEMORY OPTIMIZATION & ARCHITECTURE CLEANUP** - Major system cleanup eliminating dead code, fixing bot profile picture flickering, and optimizing memory usage across all scripts with measurable performance improvements. **NEW in v2.8.4**: **DISCONNECT RECOVERY & DEBUG SYSTEM** - Revolutionary disconnect/reconnect recovery system eliminates need to kick players who disconnect during their turn, plus comprehensive system-wide verbose logging toggle. @@ -19,6 +21,41 @@ Peril Dice is a multiplayer elimination game where each player selects numbers b **NEW in v2.7.0**: Complete architectural overhaul featuring consolidated single linkset design (74 prims total) with bulletproof link message communication, eliminating all channel conflicts and deployment complexity. +## Major v2.8.6 Improvements ๐Ÿ† + +### ๐Ÿ† **Winner Glow Celebration System** +- **Revolutionary Victory Recognition**: Comprehensive winner celebration with bright green glow effects + - **Scoreboard Glory**: Winner's profile picture and hearts glow bright green with green tint + - **Floater Celebration**: Winner's floating display shows green glow with victory text "โœจ ULTIMATE VICTORY! โœจ" + - **Visual Priority System**: Winner glow (green) overrides peril glow (yellow) during victory celebration + - **Professional Polish**: Eliminated players show red X while winner gets the spotlight +- **Extended Victory Moment**: 24-second celebration timer before game reset + - **Before**: Winner announced โ†’ Instant reset โ†’ No recognition + - **After**: Winner announced โ†’ Green glow celebration โ†’ 24 seconds of glory โ†’ Reset + - **Player Experience**: Proper victory moment makes winning feel rewarding and celebrated + +### ๐ŸŽจ **Enhanced Visual Feedback System** +- **Improved Glow Colors**: Professional-grade color tinting system + - **Winner Recognition**: Bright green glow + green tint for unmistakable victory celebration + - **Peril Identification**: Yellow glow + yellow tint for clear peril player status + - **Visual Clarity**: Replaced generic white glow with distinct colored feedback +- **Smart Winner Detection**: Floaters automatically detect victory conditions and display celebration text +- **Synchronized Effects**: Both scoreboard and floater systems celebrate winner simultaneously + +### ๐Ÿ”ง **System Optimization** +- **Victory Timer System**: Dedicated timer mode for celebration delays without system interference +- **Memory Efficiency**: Removed unused timeout components to free up memory resources +- **Enhanced Communication**: Improved message routing for winner glow effects +- **Clean Architecture**: Streamlined systems focus on essential functionality + +### ๐ŸŽฏ **Before vs After v2.8.6** +|| **Before** | **After** | +||------------|----------| +|| โŒ Winner announced but no visual celebration | โœ… Bright green glow celebration on scoreboard and floaters | +|| โŒ Immediate reset after victory - no recognition | โœ… 24-second victory celebration timer for proper recognition | +|| โŒ Generic white glow effects | โœ… Professional colored glow system (green/yellow/red) | +|| โŒ Winners felt unrewarded | โœ… Ultimate Survivors get proper celebration and recognition | + ## Major v2.8.5 Improvements ๐Ÿšจ ### ๐Ÿง  **Memory Optimization & Dead Code Cleanup** diff --git a/README_Notecard.txt b/README_Notecard.txt index d4a59d7..43fcc2b 100644 --- a/README_Notecard.txt +++ b/README_Notecard.txt @@ -2,12 +2,14 @@ ======================================================== CREATED BY REBECCA NOD AND NOOSE THE BUNNY -CURRENT VERSION: 2.8.5 - MEMORY OPTIMIZATION & ARCHITECTURE CLEANUP +CURRENT VERSION: 2.8.6 - WINNER GLOW CELEBRATION SYSTEM OVERVIEW -------- Peril Dice is a multiplayer elimination game where each player selects numbers before a die is rolled. If the peril player's number is rolled, they lose a life. Players are eliminated when they reach zero lives. +NEW IN V2.8.6: Revolutionary winner recognition with bright green glow effects on both scoreboard and floaters, plus 24-second victory celebration timer giving proper recognition to the Ultimate Survivor before game reset. + NEW IN V2.8.5: Major system cleanup eliminating dead code, fixing bot profile picture flickering, and optimizing memory usage across all scripts with measurable performance improvements. NEW IN V2.8.4: Revolutionary disconnect/reconnect recovery system eliminates need to kick players who disconnect during their turn, plus comprehensive system-wide verbose logging toggle for production vs development modes. @@ -96,6 +98,40 @@ Player Count | Dice Type 5โ€“6 | d20 7โ€“10 | d30 +RECENT IMPROVEMENTS (V2.8.6) +============================ + +๐Ÿ† WINNER GLOW CELEBRATION SYSTEM +- Revolutionary Victory Recognition: Comprehensive winner celebration with bright green glow effects + * Scoreboard Glory: Winner's profile picture and hearts glow bright green with green tint + * Floater Celebration: Winner's floating display shows green glow with victory text "ULTIMATE VICTORY!" + * Visual Priority System: Winner glow (green) overrides peril glow (yellow) during victory celebration + * Professional Polish: Eliminated players show red X while winner gets the spotlight +- Extended Victory Moment: 24-second celebration timer before game reset + * Before: Winner announced โ†’ Instant reset โ†’ No recognition + * After: Winner announced โ†’ Green glow celebration โ†’ 24 seconds of glory โ†’ Reset + * Player Experience: Proper victory moment makes winning feel rewarding and celebrated + +๐ŸŽจ ENHANCED VISUAL FEEDBACK SYSTEM +- Improved Glow Colors: Professional-grade color tinting system + * Winner Recognition: Bright green glow + green tint for unmistakable victory celebration + * Peril Identification: Yellow glow + yellow tint for clear peril player status + * Visual Clarity: Replaced generic white glow with distinct colored feedback +- Smart Winner Detection: Floaters automatically detect victory conditions and display celebration text +- Synchronized Effects: Both scoreboard and floater systems celebrate winner simultaneously + +๐Ÿ”ง SYSTEM OPTIMIZATION +- Victory Timer System: Dedicated timer mode for celebration delays without system interference +- Memory Efficiency: Removed unused timeout components to free up memory resources +- Enhanced Communication: Improved message routing for winner glow effects +- Clean Architecture: Streamlined systems focus on essential functionality + +๐ŸŽฏ BEFORE VS AFTER V2.8.6 +- Before: Winner announced but no visual celebration | After: Bright green glow celebration on scoreboard and floaters +- Before: Immediate reset after victory - no recognition | After: 24-second victory celebration timer for proper recognition +- Before: Generic white glow effects | After: Professional colored glow system (green/yellow/red) +- Before: Winners felt unrewarded | After: Ultimate Survivors get proper celebration and recognition + RECENT IMPROVEMENTS (V2.8.5) ============================ diff --git a/Roll_ConfettiModule.lsl b/Roll_ConfettiModule.lsl index 1a378a2..5aacf5c 100644 --- a/Roll_ConfettiModule.lsl +++ b/Roll_ConfettiModule.lsl @@ -72,6 +72,19 @@ integer diceTypeProcessed = FALSE; // Prevent duplicate dice type processing // Verbose logging control - toggled by owner integer VERBOSE_LOGGING = FALSE; +// Memory reporting function +reportMemoryUsage(string scriptName) { + integer used = llGetUsedMemory(); + integer free = llGetFreeMemory(); + integer total = used + free; + float percentUsed = ((float)used / (float)total) * 100.0; + + llOwnerSay("๐Ÿง  [" + scriptName + "] Memory: " + + (string)used + " used, " + + (string)free + " free (" + + llGetSubString((string)percentUsed, 0, 4) + "% used)"); +} + list getPicksFor(string nameInput) { integer i; for (i = 0; i < llGetListLength(picksData); i++) { @@ -129,6 +142,8 @@ confetti() { default { state_entry() { + reportMemoryUsage("๐ŸŽฒ Roll Confetti Module"); + // Initialize dynamic channels initializeChannels(); rollDialogChannel = ROLLDIALOG_CHANNEL; // Set legacy variable @@ -155,6 +170,7 @@ default { } on_rez(integer start_param) { + reportMemoryUsage("๐ŸŽฒ Roll Confetti Module"); llOwnerSay("๐Ÿ”„ Roll Confetti Module rezzed - reinitializing..."); // Re-initialize dynamic channels @@ -273,8 +289,8 @@ default { if (llSubStringIndex(str, "_NEXT_ROUND") != -1) { string playerName = llGetSubString(str, 0, llSubStringIndex(str, "_NEXT_ROUND") - 1); llOwnerSay("๐ŸŽฏ Prompting " + playerName + " to start next round..."); - // Send message to main controller to get the player's key and show dialog - llMessageLinked(LINK_SET, 997, "START_NEXT_ROUND_DIALOG|" + playerName, NULL_KEY); + // Send continuation directly to Game Manager (more efficient than through Main Controller) + llMessageLinked(LINK_SET, 998, "", NULL_KEY); // Empty peril player - Game Manager will select one } else { // Clear dice display directly (link 83) llMessageLinked(83, 3021, "", NULL_KEY); @@ -394,6 +410,10 @@ default { // Direct scoreboard status update llMessageLinked(12, 3001, "Plot Twist", NULL_KEY); + + // IMPORTANT: Send peril player update to scoreboard for glow effect + llMessageLinked(12, 3005, newPeril, NULL_KEY); // MSG_UPDATE_PERIL_PLAYER + // Add delay to let status display before next phase llSleep(2.0); @@ -450,36 +470,23 @@ default { llSay(0, "๐Ÿป PUNISHMENT TIME! " + perilPlayer + " has been ELIMINATED!"); - // IMPORTANT: Update our local state to reflect the elimination - // Remove the eliminated player from our names and lives lists to prevent stale data + // CRITICAL FIX: DO NOT update our local state to remove the eliminated player + // This would corrupt the sync message. Instead, just mark the player as having 0 lives + // but keep them in the lists. Main Controller will handle the actual removal. integer elimIdx = llListFindList(names, [perilPlayer]); if (elimIdx != -1) { - names = llDeleteSubList(names, elimIdx, elimIdx); - lives = llDeleteSubList(lives, elimIdx, elimIdx); - // Also clean up their picks data - integer pickIdx = -1; - integer p; - // LSL doesn't support break, so use a flag instead - integer foundEntry = FALSE; - for (p = 0; p < llGetListLength(picksData) && !foundEntry; p++) { - string entry = llList2String(picksData, p); - if (llSubStringIndex(entry, perilPlayer + "|") == 0) { - pickIdx = p; - foundEntry = TRUE; - } - } - if (pickIdx != -1) { - picksData = llDeleteSubList(picksData, pickIdx, pickIdx); - } - llOwnerSay("๐Ÿ”„ [Roll Module] Updated local state - removed " + perilPlayer + " from names/lives/picks"); + // Set player's lives to 0 in our local state, but keep them in the lists + lives = llListReplaceList(lives, [0], elimIdx, elimIdx); + llOwnerSay("๐Ÿ”„ [Roll Module] Updated local state - set " + perilPlayer + "'s lives to 0 but kept in lists"); + llOwnerSay("๐Ÿ”„ [Roll Module] This ensures proper sync message format before Main Controller handles elimination"); } // Send message to main controller to handle elimination llMessageLinked(LINK_SET, 999, "ELIMINATE_PLAYER|" + perilPlayer, NULL_KEY); // CRITICAL: Don't send any sync messages after elimination - // Let Main Controller handle all state sync and peril player reassignment - llOwnerSay("๐ŸŽฏ [Roll Module] Elimination complete - letting Main Controller handle state sync"); + // Let Main Controller handle all state sync, peril player reassignment, AND game continuation + llOwnerSay("๐ŸŽฏ [Roll Module] Elimination complete - letting Main Controller handle state sync and continuation"); // Clear roll protection after processing is complete rollInProgress = FALSE; @@ -490,14 +497,14 @@ default { // Note: Win condition checking is handled by Main Controller after elimination - // IMPORTANT: After processing a roll, clear picks data for the next round - // The round is complete, so we should send empty picks to trigger next round logic + // IMPORTANT: After processing a roll, preserve the actual picks data that was used for validation + // These picks will be used by Game Manager for building complete avoidance lists list encodedPicksData = []; integer k; - for (k = 0; k < llGetListLength(names); k++) { - string playerName = llList2String(names, k); - // Send empty picks for all players since round is complete - encodedPicksData += [playerName + "|"]; + for (k = 0; k < llGetListLength(picksData); k++) { + string entry = llList2String(picksData, k); + // Preserve the actual picks data that was used for validation + encodedPicksData += [entry]; } // Validate perilPlayer is not empty before syncing string perilForSync = perilPlayer; @@ -521,12 +528,15 @@ default { llMessageLinked(LINK_SET, MSG_UPDATE_FLOAT, fname, NULL_KEY); } - // After roll is processed and sync is sent, start next round automatically - llOwnerSay("๐ŸŽฏ Round complete, starting next round with peril player: " + perilPlayer); + // After roll is processed and sync is sent, send continuation directly to Game Manager + llOwnerSay("๐ŸŽฏ Round complete - sending continuation directly to Game Manager"); - // Brief delay to ensure sync propagates, then start next round - llSleep(1.0); - llMessageLinked(LINK_SET, 997, "CONTINUE_ROUND|" + perilPlayer, NULL_KEY); + // Brief delay to ensure sync propagates + llSleep(0.5); + + // Send continuation directly to Game Manager (more efficient than through Main Controller) + // For non-elimination cases, continue with current peril player + llMessageLinked(LINK_SET, 998, perilForSync, NULL_KEY); // Clear roll protection after processing is complete rollInProgress = FALSE; diff --git a/lsl_validator.py b/lsl_validator.py index 686afff..5cb6af0 100755 --- a/lsl_validator.py +++ b/lsl_validator.py @@ -63,6 +63,22 @@ class LSLValidator: 'PI', 'PI_BY_TWO', 'TWO_PI', 'DEG_TO_RAD', 'RAD_TO_DEG', 'LINK_ROOT', 'LINK_SET', 'LINK_ALL_OTHERS', 'LINK_ALL_CHILDREN', 'LINK_THIS', } + + # Keywords that are definitely forbidden in LSL (conservative list) + self.forbidden_keywords = { + 'break', 'continue', 'switch', 'case', 'goto', + 'class', 'struct', 'enum', 'union', 'namespace' + } + + # LSL data types + self.lsl_types = { + 'integer', 'float', 'string', 'key', 'vector', 'rotation', 'list' + } + + # Scope tracking for variables + self.global_vars = set() # Global variables + self.local_vars = [] # Stack of local scopes + self.current_function = None def validate_file(self, filepath): """Validate a single LSL file""" @@ -77,6 +93,7 @@ class LSLValidator: self._check_syntax(lines, filepath) self._check_style(lines, filepath) + self._check_scope(lines, filepath) self._check_performance(content, filepath) self._check_memory_usage(content, filepath) @@ -135,9 +152,203 @@ class LSLValidator: and '}' not in line and not line.startswith('state') and not line.startswith('default')): self.warnings.append(f"Line {line_num}: Possible missing semicolon") + # Check for forbidden LSL keywords + self._check_forbidden_keywords(line, line_num) + # Final brace balance check if brace_count != 0: self.errors.append(f"Unmatched braces: {brace_count} excess opening braces" if brace_count > 0 else f"{-brace_count} missing opening braces") + + def _check_forbidden_keywords(self, line, line_num): + """Check for keywords that are forbidden in LSL""" + # Skip comments + if '//' in line: + line = line[:line.index('//')] + + # Remove strings to avoid false positives + # Simple approach: remove content between quotes + import re + line_no_strings = re.sub(r'"[^"]*"', '""', line) + + # Check each forbidden keyword + for keyword in self.forbidden_keywords: + # Use word boundaries to avoid matching substrings + pattern = r'\b' + re.escape(keyword) + r'\b' + if re.search(pattern, line_no_strings): + self.errors.append(f"Line {line_num}: '{keyword}' is not supported in LSL") + + def _check_scope(self, lines, filepath): + """Check for variable scope issues""" + # Reset scope tracking + self.global_vars = set() + self.local_vars = [] + self.current_function = None + + in_function = False + brace_depth = 0 + + for line_num, line in enumerate(lines, 1): + original_line = line + line = line.strip() + + # Skip empty lines and comments + if not line or line.startswith('//') or line.startswith('/*'): + continue + + # Remove comments from line + if '//' in line: + line = line[:line.index('//')] + line = line.strip() + + # Track brace depth for scope management + open_braces = line.count('{') + close_braces = line.count('}') + + # Check for function definitions + if not in_function and ('(' in line and ')' in line and '{' in line): + # This might be a function definition + if any(event in line for event in ['state_entry', 'touch_start', 'listen', 'timer', 'on_rez', 'link_message', 'http_response']): + in_function = True + self.local_vars.append(set()) # New local scope + + # Handle scope changes + if open_braces > 0: + for _ in range(open_braces): + brace_depth += 1 + if in_function and brace_depth > 1: # Nested scope within function + self.local_vars.append(set()) + + # Check for variable declarations + self._check_variable_declaration(line, line_num, in_function) + + # Check for variable usage + self._check_variable_usage(line, line_num) + + # Handle closing braces + if close_braces > 0: + for _ in range(close_braces): + brace_depth -= 1 + if in_function and len(self.local_vars) > 1: + self.local_vars.pop() # Exit nested scope + elif brace_depth == 0 and in_function: + # Exiting function + in_function = False + self.local_vars.clear() + + def _check_variable_declaration(self, line, line_num, in_function): + """Check for variable declarations""" + import re + + # Remove string literals to avoid false matches + line_no_strings = re.sub(r'"[^"]*"', '""', line) + + # Pattern to match LSL variable declarations: type varname; + for lsl_type in self.lsl_types: + # Handle special case of for loop declarations: for(integer i = 0; ...) + for_loop_pattern = rf'for\s*\(\s*{lsl_type}\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*=' + for_matches = re.findall(for_loop_pattern, line_no_strings) + + for var_name in for_matches: + if var_name.startswith('ll') or var_name in self.lsl_functions: + continue + # For loop variables are always local and shouldn't conflict with globals + if len(self.local_vars) > 0: + self.local_vars[-1].add(var_name) + continue + + # Regular variable declarations: type identifier; or type identifier = + # Skip for loop variable declarations by checking the line content + if 'for(' not in line_no_strings.replace(' ', '') and 'for (' not in line_no_strings: + pattern = rf'\b{lsl_type}\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*[;=]' + matches = re.findall(pattern, line_no_strings) + else: + matches = [] + + for var_name in matches: + # Skip LSL built-in functions that might match the pattern + if var_name.startswith('ll') or var_name in self.lsl_functions: + continue + + # Skip common parameter names that aren't real declarations + if var_name in {'total_number', 'sender', 'num', 'str', 'id', 'start_param', + 'channel', 'name', 'msg', 'message', 'request_id', 'status', 'metadata', 'body'}: + continue + + if in_function: + # Check if already declared in current local scope + if len(self.local_vars) > 0 and var_name in self.local_vars[-1]: + self.errors.append(f"Line {line_num}: Variable '{var_name}' already declared in this scope") + else: + # Add to current local scope + if len(self.local_vars) > 0: + self.local_vars[-1].add(var_name) + else: + # Global variable - only flag if it's a real redeclaration + if var_name in self.global_vars: + # Only error if this looks like a real declaration line + if ';' in line or '=' in line: + self.errors.append(f"Line {line_num}: Global variable '{var_name}' already declared") + else: + self.global_vars.add(var_name) + + def _check_variable_usage(self, line, line_num): + """Check for usage of undefined variables (conservative approach)""" + import re + + # Skip variable declarations and function definitions + if any(lsl_type in line for lsl_type in self.lsl_types): + return + + # Skip comments and string literals + if line.strip().startswith('//') or line.strip().startswith('/*') or '"' in line: + return + + # Skip lines that are clearly not variable usage + if (line.startswith('state') or line.startswith('#') or line.strip() == 'default' or + 'llOwnerSay' in line or 'llSay' in line or 'llMessageLinked' in line or + ('{' in line and '}' in line and line.count('{') == line.count('}'))): + return + + # Only check very specific patterns where we're confident it's a variable usage + # Pattern 1: variable assignment: varname = something + assignment_vars = re.findall(r'([a-zA-Z_][a-zA-Z0-9_]*)\s*=', line) + + # Pattern 2: variable in arithmetic: something + varname or varname + something + arithmetic_vars = re.findall(r'(?:^|[+\-*/=\s])([a-zA-Z_][a-zA-Z0-9_]*)(?:\s*[+\-*/]|\s*;|$)', line) + + # Combine patterns + potential_vars = set(assignment_vars + arithmetic_vars) + + for var_name in potential_vars: + # Skip LSL functions, constants, keywords, and common false positives + if (var_name in self.lsl_functions or var_name in self.lsl_constants or + var_name in self.lsl_types or var_name.startswith('ll') or + var_name in {'if', 'else', 'for', 'while', 'do', 'return', 'jump', 'state', 'default', + 'total_number', 'sender', 'num', 'str', 'id', 'start_param', 'channel', + 'name', 'msg', 'message', 'request_id', 'status', 'metadata', 'body', + # Common English words that show up in strings/comments + 'the', 'and', 'or', 'not', 'has', 'been', 'with', 'from', 'this', 'that', + 'will', 'can', 'may', 'all', 'any', 'some', 'new', 'old', 'get', 'set'} or + var_name.isdigit() or len(var_name) <= 2): + continue + + # Check if variable is defined + is_defined = var_name in self.global_vars + + # Check local scopes (from innermost to outermost) + if not is_defined: + for scope in reversed(self.local_vars): + if var_name in scope: + is_defined = True + break + + # Only warn for very likely variable usage patterns + if not is_defined: + # Must be lowercase and used in assignment or arithmetic context + if (var_name.islower() and + (var_name in assignment_vars or + (var_name in arithmetic_vars and any(op in line for op in ['+', '-', '*', '/'])))): + self.warnings.append(f"Line {line_num}: Variable '{var_name}' may not be defined") def _check_style(self, lines, filepath): """Check for style and best practice issues"""