chore: v3.1.0 Peril Dice - Linkset Stability Update & FURWARE Text Mesh Migration

This commit is contained in:
RebeccaNod1 2026-04-04 03:38:41 -06:00
parent 8931c88c35
commit 915c079a7a
35 changed files with 2308 additions and 4202 deletions

View file

@ -1,11 +1,12 @@
// Bot Manager for Peril Dice
#include "Peril_Constants.lsl"
// Handles logic for test players (bots): picking numbers and rolling dice
// =============================================================================
// DYNAMIC CHANNEL CONFIGURATION
// =============================================================================
// Base channel offset - should match Main.lsl
// Base channel offset must match Main.lsl
#define CHANNEL_BASE -77000
// Calculate channels dynamically to avoid hardcoded conflicts
@ -35,7 +36,6 @@ initializeChannels() {
// Configuration
integer LISTEN_CHANNEL; // Will be set dynamically
#define MSG_SYNC_GAME_STATE 107
// Listen handle management
integer listenHandle = -1;
@ -57,9 +57,6 @@ list processedBotCommands = [];
// Track sent BOT_PICKED messages to prevent duplicate sending
list sentBotMessages = [];
// Verbose logging control - toggled by owner
integer VERBOSE_LOGGING = FALSE;
// Memory reporting function
reportMemoryUsage(string scriptName) {
integer used = llGetUsedMemory();
@ -67,7 +64,7 @@ reportMemoryUsage(string scriptName) {
integer total = used + free;
float percentUsed = ((float)used / (float)total) * 100.0;
llOwnerSay("🧠 [" + scriptName + "] Memory: " +
dbg("🧠 [" + scriptName + "] Memory: " +
(string)used + " used, " +
(string)free + " free (" +
llGetSubString((string)percentUsed, 0, 4) + "% used)");
@ -79,7 +76,7 @@ checkMemoryUsage(string context) {
float memoryPercentage = (float)usedMemory / 65536.0; // LSL scripts have 64KB limit
if (memoryPercentage > MEMORY_WARNING_THRESHOLD) {
llOwnerSay("[Bot Manager] ⚠️ High memory usage in " + context + ": " +
dbg("🤖 [Bot Manager] ⚠️ High memory usage in " + context + ": " +
(string)usedMemory + " bytes (" +
(string)llRound(memoryPercentage * 100.0) + "% of 64KB limit)");
}
@ -87,23 +84,20 @@ checkMemoryUsage(string context) {
// Helper to parse and respond to pick commands
doBotPick(string botName, integer count, integer diceMax, list avoidNumbers) {
if (VERBOSE_LOGGING) llOwnerSay("[Bot Manager] 🔧 doBotPick ENTRY - botName:" + botName + " count:" + (string)count + " diceMax:" + (string)diceMax);
checkMemoryUsage("doBotPick start");
// Validate bot exists and is still alive
integer botIdx = llListFindList(names, [botName]);
if (botIdx == -1) {
llOwnerSay("[Bot Manager] ❌ Bot '" + botName + "' not found in game - ignoring command");
dbg("🤖 [Bot Manager] ❌ Bot '" + botName + "' not found in game - ignoring command");
return;
}
if (VERBOSE_LOGGING) llOwnerSay("[Bot Manager] 🔧 Bot found at index:" + (string)botIdx);
integer botLives = llList2Integer(lives, botIdx);
if (botLives <= 0) {
llOwnerSay("[Bot Manager] ❌ Bot '" + botName + "' is eliminated (lives=" + (string)botLives + ") - ignoring command");
dbg("🤖 [Bot Manager] ❌ Bot '" + botName + "' is eliminated (lives=" + (string)botLives + ") - ignoring command");
return;
}
if (VERBOSE_LOGGING) llOwnerSay("[Bot Manager] 🔧 Bot lives check passed:" + (string)botLives);
// IMPROVED ALGORITHM: Calculate available numbers first
list availableNumbers = [];
@ -121,13 +115,13 @@ doBotPick(string botName, integer count, integer diceMax, list avoidNumbers) {
// Graceful degradation: adjust target if not enough numbers available
if (availableCount < count) {
if (availableCount > 0) {
llOwnerSay("[Bot Manager] ⚠️ " + botName + " requested " + (string)count + " picks, but only " + (string)availableCount + " numbers available - picking all available");
dbg("🤖 [Bot Manager] ⚠️ " + botName + " requested " + (string)count + " picks, but only " + (string)availableCount + " numbers available - picking all available");
targetCount = availableCount;
} else {
llOwnerSay("[Bot Manager] ❌ " + botName + " cannot pick any numbers - all " + (string)diceMax + " numbers are taken!");
dbg("🤖 [Bot Manager] ❌ " + botName + " cannot pick any numbers - all " + (string)diceMax + " numbers are taken!");
// Still send empty response to prevent game hanging
string emptyResponse = "BOT_PICKED:" + botName + ":";
llMessageLinked(LINK_SET, -9997, emptyResponse, NULL_KEY);
llMessageLinked(LINK_SET, MSG_BOT_PICKED, emptyResponse, NULL_KEY);
return;
}
}
@ -150,32 +144,29 @@ doBotPick(string botName, integer count, integer diceMax, list avoidNumbers) {
// Final validation
if (llGetListLength(picks) < count) {
llOwnerSay("[Bot Manager] ℹ️ " + botName + " picked " + (string)llGetListLength(picks) + "/" + (string)count + " numbers (limited by availability)");
dbg("🤖 [Bot Manager] ℹ️ " + botName + " picked " + (string)llGetListLength(picks) + "/" + (string)count + " numbers (limited by availability)");
} else {
llOwnerSay("[Bot Manager] ✅ " + botName + " successfully picked " + (string)llGetListLength(picks) + " numbers");
dbg("🤖 [Bot Manager] ✅ " + botName + " successfully picked " + (string)llGetListLength(picks) + " numbers");
}
string pickString = llDumpList2String(picks, ";");
string response = "BOT_PICKED:" + botName + ":" + pickString;
// Check if we've already sent this exact message to prevent duplicates
if (VERBOSE_LOGGING) llOwnerSay("[Bot Manager] 🔍 DEBUG - Checking if message already sent: " + response);
if (VERBOSE_LOGGING) llOwnerSay("[Bot Manager] 🔍 DEBUG - sentBotMessages list: " + llDumpList2String(sentBotMessages, "|"));
if (llListFindList(sentBotMessages, [response]) != -1) {
llOwnerSay("[Bot Manager] ⚠️ DUPLICATE SEND - Already sent message: " + response);
dbg("🤖 [Bot Manager] ⚠️ DUPLICATE SEND - Already sent message: " + response);
return;
}
// Mark this message as sent BEFORE sending to prevent race conditions
sentBotMessages += [response];
if (VERBOSE_LOGGING) llOwnerSay("[Bot Manager] 🔍 DEBUG - Added to sentBotMessages, new list: " + llDumpList2String(sentBotMessages, "|"));
// Prepend to list for more efficient check later
sentBotMessages = [response] + llList2List(sentBotMessages, 0, 19); // Keep last 20
llOwnerSay("[Bot Manager] 🤖 " + botName + " picked: " + llList2CSV(picks));
llOwnerSay("[Bot Manager] 📬 SENDING BOT_PICKED: " + response);
dbg("🤖 [Bot Manager] 🤖 " + botName + " picked: " + llList2CSV(picks));
dbg("🤖 [Bot Manager] 📬 SENDING BOT_PICKED: " + response);
// Send message immediately to avoid timing issues
llMessageLinked(LINK_SET, -9997, response, NULL_KEY);
llOwnerSay("[Bot Manager] 📬 BOT_PICKED message sent via llMessageLinked(LINK_SET, -9997, ...)");
llMessageLinked(LINK_SET, MSG_BOT_PICKED, response, NULL_KEY);
dbg("🤖 [Bot Manager] 📬 BOT_PICKED message sent via llMessageLinked(LINK_SET, MSG_BOT_PICKED, ...)");
// Do NOT sleep after sending messages - this causes LSL message delivery loops!
}
@ -185,18 +176,18 @@ doBotRoll(string botName, integer diceMax) {
// Validate bot exists and is still alive
integer rollBotIdx = llListFindList(names, [botName]);
if (rollBotIdx == -1) {
llOwnerSay("[Bot Manager] ❌ Bot '" + botName + "' not found in game - ignoring roll command");
dbg("🤖 [Bot Manager] ❌ Bot '" + botName + "' not found in game - ignoring roll command");
return;
}
integer rollBotLives = llList2Integer(lives, rollBotIdx);
if (rollBotLives <= 0) {
llOwnerSay("[Bot Manager] ❌ Bot '" + botName + "' is eliminated (lives=" + (string)rollBotLives + ") - ignoring roll command");
dbg("🤖 [Bot Manager] ❌ Bot '" + botName + "' is eliminated (lives=" + (string)rollBotLives + ") - ignoring roll command");
return;
}
if (botName != perilPlayer) {
llOwnerSay("[Bot Manager] ❌ Bot '" + botName + "' is not the peril player (current peril: '" + perilPlayer + "') - ignoring roll command");
dbg("🤖 [Bot Manager] ❌ Bot '" + botName + "' is not the peril player (current peril: '" + perilPlayer + "') - ignoring roll command");
return;
}
@ -205,6 +196,7 @@ doBotRoll(string botName, integer diceMax) {
integer roll = 1 + (integer)(llFrand((float)diceMax));
llRegionSay(LISTEN_CHANNEL, "BOT_ROLL:" + botName + ":" + (string)roll);
dbg("🤖 [Bot Manager] 🎲 " + botName + " rolled: " + (string)roll);
}
default {
@ -226,12 +218,12 @@ default {
sentBotMessages = [];
listenHandle = llListen(LISTEN_CHANNEL, "", NULL_KEY, "");
llOwnerSay("🤖 Bot Manager ready!");
dbg("🤖 [Bot Manager] Bot Manager ready!");
}
on_rez(integer start_param) {
reportMemoryUsage("🤖 Bot Manager");
llOwnerSay("🔄 Bot Manager rezzed - reinitializing...");
dbg("🤖 [Bot Manager] 🔄 Bot Manager rezzed - reinitializing...");
// Re-initialize dynamic channels
initializeChannels();
@ -249,33 +241,19 @@ default {
sentBotMessages = [];
listenHandle = llListen(LISTEN_CHANNEL, "", NULL_KEY, "");
llOwnerSay("✅ Bot Manager reset complete after rez!");
dbg("🤖 [Bot Manager] ✅ Bot Manager reset complete after rez!");
}
link_message(integer sender, integer num, string str, key id) {
// Handle verbose logging toggle from Main Controller
if (num == 9011 && llSubStringIndex(str, "VERBOSE_LOGGING|") == 0) {
list parts = llParseString2List(str, ["|"], []);
if (llGetListLength(parts) >= 2) {
VERBOSE_LOGGING = (integer)llList2String(parts, 1);
if (VERBOSE_LOGGING) {
llOwnerSay("🔍 [Bot Manager] Verbose logging ON");
} else {
llOwnerSay("🔍 [Bot Manager] Verbose logging OFF");
}
}
return;
}
// Handle full reset from main controller
if (num == -99999 && str == "FULL_RESET") {
if (num == MSG_RESET_ALL && str == "FULL_RESET") {
// Reset bot manager state
names = [];
lives = [];
perilPlayer = "";
processedBotCommands = []; // Clear processed commands for new game
sentBotMessages = []; // Clear sent messages for new game
llOwnerSay("[Bot Manager] Reset complete!");
dbg("🤖 [Bot Manager] Reset complete!");
return;
}
@ -306,7 +284,7 @@ default {
if (oldPerilPlayer != perilPlayer && perilPlayer != "") {
processedBotCommands = [];
sentBotMessages = [];
llOwnerSay("[Bot Manager] New round detected (peril change), cleared processed commands and sent messages");
dbg("🤖 [Bot Manager] New round detected (peril change), cleared processed commands and sent messages");
} else {
// Only clear if we detect truly empty picks data during an active game
// Don't clear during initial registration (when picks = "EMPTY")
@ -314,17 +292,17 @@ default {
if (picksStr != "EMPTY" && (picksStr == "" || llSubStringIndex(picksStr, "|") == -1) && perilPlayer != "") {
processedBotCommands = [];
sentBotMessages = [];
llOwnerSay("[Bot Manager] New round detected (picks cleared), cleared processed commands and sent messages");
dbg("🤖 [Bot Manager] New round detected (picks cleared), cleared processed commands and sent messages");
}
}
}
return;
}
if (num == -9999) {
if (num == MSG_BOT_COMMAND) {
if (llSubStringIndex(str, "BOT_PICK:") == 0) {
// Format: BOT_PICK:bot_1:3:20:1,2,3 (last part is optional already picked numbers)
llOwnerSay("[Bot Manager] 📬 RECEIVED BOT_PICK: " + str);
dbg("🤖 [Bot Manager] 📬 RECEIVED BOT_PICK: " + str);
list parts = llParseStringKeepNulls(str, [":"], []);
if (llGetListLength(parts) >= 4) {
string botName = llList2String(parts, 1);
@ -336,13 +314,13 @@ default {
string commandSignature = "PICK:" + botName + ":" + (string)count + ":" + (string)diceMax + ":" + perilPlayer;
if (llListFindList(processedBotCommands, [commandSignature]) != -1) {
llOwnerSay("[Bot Manager] ⚠️ DUPLICATE - Ignoring identical command for " + botName + " (already processed this exact pick request)");
dbg("🤖 [Bot Manager] ⚠️ DUPLICATE - Ignoring identical command for " + botName + " (already processed this exact pick request)");
return;
}
// Mark this command as processed BEFORE doing the work to prevent race conditions
processedBotCommands += [commandSignature];
llOwnerSay("[Bot Manager] ✅ Processing " + botName + " pick command");
dbg("🤖 [Bot Manager] ✅ Processing " + botName + " pick command");
list avoidNumbers = [];
if (llGetListLength(parts) >= 5) {
string avoidStr = llList2String(parts, 4);
@ -359,8 +337,6 @@ default {
}
}
}
// Debug: Show the game state before calling doBotPick
if (VERBOSE_LOGGING) llOwnerSay("[Bot Manager] 🔧 Game state - names:" + llList2CSV(names) + " lives:" + llList2CSV(lives));
doBotPick(botName, count, diceMax, avoidNumbers);
}
}

View file

@ -4,6 +4,27 @@
All notable changes to Peril Dice will be documented in this file.
## [3.1.0] - 2026-04-04
### 🔍 **Dynamic Link Discovery System**
- **Architecture Decentralization**: Eliminated all hardcoded link indices across 15+ scripts. Systems now perform an automated "Link Discovery" scan on startup and `on_rez`.
- **Name-Based Mapping**: Implemented a robust pattern-matching system (`Scoreboard:0:0`, `profile:R:C`) to identify and bind to links regardless of their link-order in the set.
- **Improved Resilience**: Resolved persistent "Previously declared within scope" errors and stabilized inter-script communication during region crossings and teleports.
### 🧩 **Unified Constants & Centralized Logic**
- **Centralized `Peril_Constants.lsl`**: Shifted all system settings, message IDs, and macros to a single, shared constants file for 100% project consistency.
- **Pre-Processor Debugging**: Implemented a global `dbg()` macro system. Debug logging can now be toggled board-wide with a single switch, significantly reducing production bytecode size.
### 🎭 **FURWARE Text Mesh Migration**
- **Visual Display Overhaul**: Fully migrated the Dice Display and Leaderboard from legacy prim-based XYText to modern mesh-integrated **FURWARE Text**.
- **Sync Reliability**: New bridge scripts handle real-time data distribution to the FURWARE master, ensuring 0ms delay in visual updates.
- **Performance Optimization**: Reduced prim texture-flipping overhead by switching to mesh-offset mapping.
### 🧠 **Compiler & Execution Stability**
- **Scope Refactoring**: Moved all loop variable declarations (`refreshI`, `glowI`, etc.) to the top-level of their respective functions to ensure compatibility with strict LSL compiler versions.
- **Logic Robustness**: Replaced `continue` and range-check based validation with explicit "link-found" `if` blocks, preventing infinite loops and scope errors.
- **Mismatched Variable Fixes**: Resolved inconsistent renaming of local variables in the Scoreboard Manager's refresh logic.
## [3.0.0] - 2026-03-24
### 📦 **Commercial CasperVend Integration**
@ -16,6 +37,10 @@ All notable changes to Peril Dice will be documented in this file.
- **Bytecode Reduction**: Converted over 170 verbose `llOwnerSay` debug statements to a conditionally compiled `dbg()` macro, reducing compiled bytecode size.
- **10-Player Load Stability**: Freed critical script memory limits within `Main_Controller_Linkset.lsl`, `Game_Manager.lsl`, and `Player_DialogHandler.lsl` to ensure rock-solid stability during maximum load.
### 🏆 **Experience-Based Leaderboard**
- **Persistent Storage**: Leaderboard data is now stored in Second Life Experience Keys (KVP), ensuring lifetime persistence of player statistics.
- **Region Independence**: Player wins and losses are tracked globally, surviving object resets and region changes.
### 🎨 **Z-Fighting & Alignment Fixes**
- **Initialization Stability**: Patched `Game_Scoreboard_Manager_Linkset.lsl`, `Leaderboard_Communication_Linkset.lsl`, and `XyzzyText_Dice_Bridge_Linkset.lsl` with `on_rez` auto-resets to guarantee synchronized texture deployment.
- **Microscopic Position Offsets**: Resolved persistent Z-fighting flickering on custom UI elements by applying meticulous microscopic local axis offsets (`+0.005m`, `+0.008m`).

View file

@ -3,6 +3,28 @@ PERIL DICE CHANGELOG
All notable changes to Peril Dice are documented in this file.
[3.1.0] - 2026-04-04
====================
🔍 DYNAMIC LINK DISCOVERY SYSTEM
- Architecture Decentralization: Eliminated all hardcoded link indices across 15+ scripts. Systems now perform an automated "Link Discovery" scan on startup and on_rez.
- Name-Based Mapping: Implemented a robust pattern-matching system (Scoreboard:0:0, profile:R:C) to identify and bind to links regardless of their link-order in the set.
- Improved Resilience: Resolved persistent "Previously declared within scope" errors and stabilized inter-script communication during region crossings and teleports.
🧩 UNIFIED CONSTANTS & CENTRALIZED LOGIC
- Centralized Peril_Constants.lsl: Shifted all system settings, message IDs, and macros to a single, shared constants file for 100% project consistency.
- Pre-Processor Debugging: Implemented a global dbg() macro system. Debug logging can now be toggled board-wide with a single switch, significantly reducing production bytecode size.
🎭 FURWARE TEXT MESH MIGRATION
- Visual Display Overhaul: Fully migrated the Dice Display and Leaderboard from legacy prim-based XYText to modern mesh-integrated FURWARE Text.
- Sync Reliability: New bridge scripts handle real-time data distribution to the FURWARE master, ensuring 0ms delay in visual updates.
- Performance Optimization: Reduced prim texture-flipping overhead by switching to mesh-offset mapping.
🧠 COMPILER & EXECUTION STABILITY
- Scope Refactoring: Moved all loop variable declarations (refreshI, glowI, etc.) to the top-level of their respective functions to ensure compatibility with strict LSL compiler versions.
- Logic Robustness: Replaced continue and range-check based validation with explicit "link-found" if blocks, preventing infinite loops and scope errors.
- Mismatched Variable Fixes: Resolved inconsistent renaming of local variables in the Scoreboard Manager's refresh logic.
[3.0.0] - 2026-03-24
====================
@ -16,6 +38,10 @@ All notable changes to Peril Dice are documented in this file.
- Bytecode Reduction: Converted over 170 verbose llOwnerSay debug statements to a macro, reducing compiled bytecode size.
- 10-Player Load Stability: Freed critical script memory within Main Controller, Game Manager, and Dialog Handler to ensure rock-solid stability during maximum load.
🏆 EXPERIENCE-BASED LEADERBOARD
- Persistent Storage: Leaderboard data is now stored in Second Life Experience Keys (KVP), ensuring lifetime persistence of player statistics.
- Region Independence: Player wins and losses are tracked globally, surviving object resets and region changes.
🎨 Z-FIGHTING & ALIGNMENT FIXES
- Initialization Stability: Patched Scoreboard Manager, Leaderboard Communication, and Dice Bridge with on_rez auto-resets to guarantee synchronized texture deployment.
- Microscopic Position Offsets: Resolved persistent Z-fighting flickering on custom UI elements by applying meticulous microscopic local axis offsets (+0.005m).

View file

@ -1,25 +1,15 @@
#include "Peril_Constants.lsl"
// === Controller Memory Monitor ===
// Handles memory monitoring and reporting for the Main Controller
// Communicates with other scripts via link messages
// Verbose logging control
integer VERBOSE_LOGGING = TRUE; // Global flag for verbose debug logs
#define MSG_TOGGLE_VERBOSE_LOGS 9998 // Message to toggle verbose logging
// Memory monitoring system
#define MEMORY_WARNING_THRESHOLD 0.8 // Warn at 80% usage
#define MEMORY_CRITICAL_THRESHOLD 0.9 // Critical at 90% usage
integer lastMemoryCheck = 0; // Track last memory check time
#define MEMORY_CHECK_INTERVAL 60 // Check every 60 seconds
// Message constants for inter-script communication
#define MSG_MEMORY_CHECK 6001
#define MSG_MEMORY_STATS 6002
#define MSG_MEMORY_CLEANUP 6003
#define MSG_MEMORY_REPORT 6004
#define MSG_EMERGENCY_CLEANUP 6005
#define MSG_MEMORY_STATS_REQUEST 6006
// Track main controller stats for comprehensive reporting
list mainControllerStats = [];
@ -29,7 +19,7 @@ checkMemoryUsage(string context) {
float memoryPercentage = (float)usedMemory / 65536.0; // 64KB = 65536 bytes
if (memoryPercentage > MEMORY_CRITICAL_THRESHOLD) {
llOwnerSay("🎆 [Memory Monitor] CRITICAL MEMORY: " + context + " - " +
dbg("📊 [Memory Monitor] 🎆 CRITICAL MEMORY: " + context + " - " +
(string)usedMemory + " bytes (" +
(string)llRound(memoryPercentage * 100.0) + "% of 64KB limit)");
// Trigger emergency cleanup in main controller
@ -37,11 +27,9 @@ checkMemoryUsage(string context) {
} else if (memoryPercentage > MEMORY_WARNING_THRESHOLD) {
// Only warn once per interval to avoid spam
if ((currentTime - lastMemoryCheck) >= MEMORY_CHECK_INTERVAL) {
if (VERBOSE_LOGGING) {
llOwnerSay("⚠️ [Memory Monitor] High memory in " + context + ": " +
(string)usedMemory + " bytes (" +
(string)llRound(memoryPercentage * 100.0) + "% of 64KB limit)");
}
dbg("📊 [Memory Monitor] ⚠️ High memory in " + context + ": " +
(string)usedMemory + " bytes (" +
(string)llRound(memoryPercentage * 100.0) + "% of 64KB limit)");
lastMemoryCheck = currentTime;
}
}
@ -53,7 +41,7 @@ checkMemoryUsage(string context) {
// Emergency memory cleanup function
emergencyMemoryCleanup() {
llOwnerSay("🎆 [Memory Monitor] Emergency memory cleanup initiated!");
dbg("📊 [Memory Monitor] 🎆 Emergency memory cleanup initiated!");
// Clean up temporary variables and optimize lists
mainControllerStats = [];
@ -63,7 +51,7 @@ emergencyMemoryCleanup() {
mainControllerStats = [];
mainControllerStats = tempStats;
llOwnerSay("🎆 [Memory Monitor] Emergency cleanup complete - memory: " +
dbg("📊 [Memory Monitor] 🎆 Emergency cleanup complete - memory: " +
(string)llGetUsedMemory() + " bytes");
}
@ -71,8 +59,8 @@ reportMemoryStats() {
integer statsUsedMemory = llGetUsedMemory();
float statsMemoryPercentage = (float)statsUsedMemory / 65536.0;
llOwnerSay("📊 [Memory Monitor] Memory Monitor Stats:");
llOwnerSay(" Helper Memory Used: " + (string)statsUsedMemory + " bytes (" +
dbg("📊 [Memory Monitor] Memory Monitor Stats:");
dbg("📊 [Memory Monitor] Helper Memory Used: " + (string)statsUsedMemory + " bytes (" +
(string)llRound(statsMemoryPercentage * 100.0) + "% of 64KB)");
string memoryStatus;
if (statsMemoryPercentage > MEMORY_CRITICAL_THRESHOLD) {
@ -82,7 +70,7 @@ reportMemoryStats() {
} else {
memoryStatus = "NORMAL";
}
llOwnerSay(" Memory Status: " + memoryStatus);
dbg("📊 [Memory Monitor] Memory Status: " + memoryStatus);
// Request main controller stats
llMessageLinked(LINK_SET, MSG_MEMORY_STATS_REQUEST, "REQUEST_MAIN_STATS", NULL_KEY);
@ -107,25 +95,25 @@ processMainControllerStats(string statsData) {
float mainMemoryPercentage = (float)mainMemory / 65536.0;
llOwnerSay("📊 [Memory Monitor] Main Controller Stats:");
llOwnerSay(" Main Memory Used: " + (string)mainMemory + " bytes (" +
dbg("📊 [Memory Monitor] Main Controller Stats:");
dbg("📊 [Memory Monitor] Main Memory Used: " + (string)mainMemory + " bytes (" +
(string)llRound(mainMemoryPercentage * 100.0) + "% of 64KB)");
llOwnerSay(" Players: " + (string)players);
llOwnerSay(" Names: " + (string)names);
llOwnerSay(" Lives: " + (string)lives);
llOwnerSay(" Picks Data: " + (string)picksData);
llOwnerSay(" Ready Players: " + (string)readyPlayers);
llOwnerSay(" Pick Queue: " + (string)pickQueue);
llOwnerSay(" Global Picked: " + (string)globalPicked);
dbg("📊 [Memory Monitor] Players: " + (string)players);
dbg("📊 [Memory Monitor] Names: " + (string)names);
dbg("📊 [Memory Monitor] Lives: " + (string)lives);
dbg("📊 [Memory Monitor] Picks Data: " + (string)picksData);
dbg("📊 [Memory Monitor] Ready Players: " + (string)readyPlayers);
dbg("📊 [Memory Monitor] Pick Queue: " + (string)pickQueue);
dbg("📊 [Memory Monitor] Global Picked: " + (string)globalPicked);
if (floaterChannels > 0) {
llOwnerSay(" Floater Channels: " + (string)floaterChannels);
dbg("📊 [Memory Monitor] Floater Channels: " + (string)floaterChannels);
}
}
}
default {
state_entry() {
llOwnerSay("📊 [Memory] Memory Monitor ready!");
dbg("📊 [Memory Monitor] Memory Monitor ready!");
// Start periodic memory monitoring
llSetTimerEvent(MEMORY_CHECK_INTERVAL);
@ -137,18 +125,8 @@ default {
}
link_message(integer sender, integer num, string str, key id) {
// Handle verbose logging toggle
if (num == MSG_TOGGLE_VERBOSE_LOGS) {
VERBOSE_LOGGING = !VERBOSE_LOGGING;
if (VERBOSE_LOGGING) {
llOwnerSay("🔊 [Memory Monitor] Verbose logging ENABLED");
} else {
llOwnerSay("🔊 [Memory Monitor] Verbose logging DISABLED");
}
return;
}
if (num == MSG_MEMORY_CHECK) {
// Manual memory check request
checkMemoryUsage(str);
}
@ -169,11 +147,11 @@ default {
// Perform emergency cleanup on this helper script
emergencyMemoryCleanup();
}
else if (num == -99999 && str == "FULL_RESET") {
else if (num == MSG_RESET_ALL && str == "FULL_RESET") {
// Handle full game reset
lastMemoryCheck = 0;
mainControllerStats = [];
llOwnerSay("🔄 [Memory Monitor] Reset complete");
dbg("📊 [Memory Monitor] 🔄 Reset complete");
}
}
}

View file

@ -1,36 +1,9 @@
#include "Peril_Constants.lsl"
// === Controller Message Handler Helper Script ===
// Handles non-critical message processing for the Main Controller
// Reduces Main Controller size and improves modularity
// Verbose logging control
integer VERBOSE_LOGGING = TRUE; // Global flag for verbose debug logs
#define 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
#define MSG_REQUEST_PLAYER_DATA 8001
#define MSG_REQUEST_PICK_DATA 8002
#define MSG_REQUEST_LIFE_DATA 8003
#define MSG_PICK_ACTION 8004
#define MSG_DICE_TYPE_REQUEST 8005
#define MSG_LEAVE_GAME_REQUEST 8006
#define MSG_OWNER_MESSAGE 9030 // Send formatted owner message
#define MSG_PUBLIC_MESSAGE 9031 // Send formatted public message
#define MSG_REGION_MESSAGE 9032 // Send formatted region message
#define MSG_DIALOG_REQUEST 9033 // Send formatted dialog
// Game state data synchronized from Main Controller
list players = [];
list names = [];
@ -42,15 +15,12 @@ string perilPlayer = ""; // Track who the peril player is to determine if game
integer roundStarted = FALSE; // Track if the game round has started
// Message constants from Main Controller (synchronized)
integer MSG_SYNC_GAME_STATE = 107;
#define MSG_SYNC_PICKQUEUE 2001
// Channel constants for scoreboard updates
integer SCOREBOARD_CHANNEL_1;
integer SCOREBOARD_CHANNEL_2;
integer FLOATER_BASE_CHANNEL;
#define MSG_CLEANUP_FLOAT 104
#define MSG_UPDATE_FLOAT 103
// Dynamic channel calculation (matches Main Controller)
#define CHANNEL_BASE -77000
@ -70,6 +40,19 @@ integer calculateChannel(integer offset) {
return CHANNEL_BASE - (offset * 1000) - combinedHash;
}
// Memory reporting function
reportMemoryUsage(string scriptName) {
integer used = llGetUsedMemory();
integer free = llGetFreeMemory();
integer total = used + free;
float percentUsed = ((float)used / (float)total) * 100.0;
dbg("🧠 [" + scriptName + "] Memory: " +
(string)used + " used, " +
(string)free + " free (" +
llGetSubString((string)percentUsed, 0, 4) + "% used)");
}
initializeChannels() {
SCOREBOARD_CHANNEL_1 = calculateChannel(6); // ~-77600 range
SCOREBOARD_CHANNEL_2 = calculateChannel(7); // ~-77700 range
@ -79,7 +62,7 @@ initializeChannels() {
// Handle player list requests
handlePlayerListRequest(key requester) {
string namesCSV = llList2CSV(names);
llMessageLinked(LINK_SET, 203, namesCSV, requester);
llMessageLinked(LINK_SET, MSG_PLAYER_LIST_PICK_RESULT, namesCSV, requester);
}
// Handle pick data requests
@ -89,11 +72,11 @@ handlePickDataRequest(string targetName, key requester) {
string rawEntry = llList2String(picksData, j);
list pickParts = llParseString2List(rawEntry, ["|"], []);
if (llList2String(pickParts, 0) == targetName) {
llMessageLinked(LINK_SET, 205, targetName + "|" + llList2String(pickParts, 1), requester);
llMessageLinked(LINK_SET, MSG_PICK_LIST_RESULT, targetName + "|" + llList2String(pickParts, 1), requester);
return;
}
}
llMessageLinked(LINK_SET, 205, targetName + "|", requester);
llMessageLinked(LINK_SET, MSG_PICK_LIST_RESULT, targetName + "|", requester);
}
// Handle life data requests
@ -101,7 +84,7 @@ handleLifeDataRequest(string playerName, key requester) {
integer lifeIdx = llListFindList(names, [playerName]);
if (lifeIdx != -1) {
string lifeVal = llList2String(lives, lifeIdx);
llMessageLinked(LINK_SET, 208, playerName + "|" + lifeVal, requester);
llMessageLinked(LINK_SET, MSG_LIFE_LOOKUP_RESULT, playerName + "|" + lifeVal, requester);
}
}
@ -127,17 +110,11 @@ handlePickAction(string actionData) {
if (pickAction == "ADD_PICK") {
if (llListFindList(pickList, [pick]) == -1) {
pickList += [pick];
if (VERBOSE_LOGGING) {
llOwnerSay("➕ [Message Handler] Added " + pick + " to " + name);
}
}
} else if (pickAction == "REMOVE_PICK") {
integer removeIdx = llListFindList(pickList, [pick]);
if (removeIdx != -1) {
pickList = llDeleteSubList(pickList, removeIdx, removeIdx);
if (VERBOSE_LOGGING) {
llOwnerSay("➖ [Message Handler] Removed " + pick + " from " + name);
}
}
}
@ -145,7 +122,7 @@ handlePickAction(string actionData) {
picksData = llListReplaceList(picksData, [name + "|" + llList2CSV(pickList)], k, k);
// Notify Main Controller of the change
llMessageLinked(LINK_SET, 204, actionData, NULL_KEY);
llMessageLinked(LINK_SET, MSG_PICK_ACTION, actionData, NULL_KEY);
return;
}
}
@ -153,10 +130,7 @@ handlePickAction(string actionData) {
// Handle dice type requests
handleDiceTypeRequest(key requester) {
if (VERBOSE_LOGGING) {
llOwnerSay("🎲 [Message Handler] Sending dice type: " + (string)diceType);
}
llMessageLinked(LINK_SET, 102, (string)diceType, requester); // MSG_ROLL_RESULT
llMessageLinked(LINK_SET, MSG_DICE_TYPE_RESULT, (string)diceType, requester);
}
// Handle leave game and kick player requests
@ -175,13 +149,6 @@ handleLeaveGameRequest(string requestData) {
lives = llDeleteSubList(lives, leaveIdx, leaveIdx);
picksData = llDeleteSubList(picksData, leaveIdx, leaveIdx);
// THEN send floater cleanup using the previously calculated channel
// Note: We DON'T send MSG_CLEANUP_FLOAT here anymore to avoid double-cleanup
// The Main Controller will handle floater cleanup after list synchronization
if (VERBOSE_LOGGING) {
llOwnerSay("📝 [Message Handler] Updated local lists for " + leavingName + ", deferring floater cleanup to Main Controller");
}
// Remove from ready list if present
integer readyIdx = llListFindList(readyPlayers, [leavingName]);
if (readyIdx != -1) {
@ -190,10 +157,8 @@ handleLeaveGameRequest(string requestData) {
// Different messages for voluntary leave vs kick
if (leaveAction == "LEAVE_GAME") {
llOwnerSay("👋 [Message Handler] " + leavingName + " left the game");
llSay(0, "👋 " + leavingName + " has left the deadly game! 👋");
} else if (leaveAction == "KICK_PLAYER") {
llOwnerSay("👢 [Message Handler] " + leavingName + " was kicked from the game");
llSay(0, "👢 " + leavingName + " has been kicked from the deadly game by the owner! 👢");
}
@ -202,22 +167,16 @@ handleLeaveGameRequest(string requestData) {
// Check if game should end (0 players = reset, 1 player = victory)
if (llGetListLength(names) == 0) {
if (VERBOSE_LOGGING) {
llOwnerSay("🔄 [Message Handler] All players left - game will reset");
}
// Main Controller will handle the reset
} else if (llGetListLength(names) == 1 && roundStarted && perilPlayer != "" && perilPlayer != "NONE") {
// Only declare victory if the game has actually started (round active and peril player assigned)
string winner = llList2String(names, 0);
llSay(0, "✨ ULTIMATE VICTORY! " + winner + " is the Ultimate Survivor!");
// Notify Main Controller of victory
llMessageLinked(LINK_SET, 998, "GAME_WON|" + winner, NULL_KEY);
llMessageLinked(LINK_SET, MSG_CONTINUE_GAME, "GAME_WON|" + winner, NULL_KEY);
} else if (llGetListLength(names) == 1) {
// Game not started yet, just inform but don't declare victory
string remainingPlayer = llList2String(names, 0);
if (VERBOSE_LOGGING) {
llOwnerSay("📝 [Message Handler] Only " + remainingPlayer + " remains, but game hasn't started yet");
}
}
}
}
@ -229,9 +188,6 @@ syncGameState(string stateData) {
// Handle special RESET sync message
if (llGetListLength(syncParts) >= 5 && llList2String(syncParts, 0) == "RESET") {
if (VERBOSE_LOGGING) {
llOwnerSay("🔄 [Message Handler] Received reset sync - clearing all state");
}
// Clear all state during reset
players = [];
names = [];
@ -283,10 +239,6 @@ syncGameState(string stateData) {
if (perilPlayer != "" && perilPlayer != "NONE") {
integer perilIdx = llListFindList(names, [perilPlayer]);
if (perilIdx == -1) {
// Only warn if we actually have players - during reset this is expected
if (llGetListLength(names) > 0 && VERBOSE_LOGGING) {
llOwnerSay("⚠️ [Message Handler] WARNING: Peril player '" + perilPlayer + "' not found in names list - clearing peril status");
}
perilPlayer = "";
roundStarted = FALSE;
}
@ -302,27 +254,21 @@ handleOwnerMessage(string messageData) {
list ownerParts = llParseString2List(messageData, ["|"], []);
if (llGetListLength(ownerParts) == 1) {
// Simple message
llOwnerSay(messageData);
} else {
// Formatted message
string ownerMsgType = llList2String(ownerParts, 0);
if (ownerMsgType == "JOIN") {
string ownerPlayerName = llList2String(ownerParts, 1);
llOwnerSay("👋 " + ownerPlayerName + " joined the game!");
llSay(0, "🔔 Added player: " + ownerPlayerName);
} else if (ownerMsgType == "LEAVE") {
string leavePlayerName = llList2String(ownerParts, 1);
llOwnerSay("👋 " + leavePlayerName + " left the game.");
} else if (ownerMsgType == "ERROR") {
string error = llList2String(ownerParts, 1);
llOwnerSay("⚠️ " + error);
} else if (ownerMsgType == "SUCCESS") {
string msg = llList2String(ownerParts, 1);
llOwnerSay("✅ " + msg);
} else if (ownerMsgType == "DEBUG") {
string component = llList2String(ownerParts, 1);
string msg = llList2String(ownerParts, 2);
llOwnerSay("🔧 [" + component + "] " + msg);
}
}
}
@ -382,61 +328,59 @@ default {
state_entry() {
reportMemoryUsage("📨 Message Handler");
llOwnerSay("📨 [Message Handler] Helper script ready!");
initializeChannels();
}
on_rez(integer start_param) {
reportMemoryUsage("📨 Message Handler");
initializeChannels();
// Reset local state if needed (synced from Main)
players = [];
names = [];
lives = [];
picksData = [];
readyPlayers = [];
}
link_message(integer sender, integer num, string str, key id) {
// Handle verbose logging toggle
if (num == MSG_TOGGLE_VERBOSE_LOGS) {
VERBOSE_LOGGING = !VERBOSE_LOGGING;
if (VERBOSE_LOGGING) {
llOwnerSay("🔊 [Message Handler] Verbose logging ENABLED");
} else {
llOwnerSay("🔊 [Message Handler] Verbose logging DISABLED");
}
return;
}
// Handle player list requests (for pick management only)
if (num == 202 && str == "REQUEST_PLAYER_LIST") {
if (num == MSG_REQUEST_PLAYER_LIST_PICK && str == "REQUEST_PLAYER_LIST") {
handlePlayerListRequest(id);
return;
}
// Handle kick player list requests (separate message ID to avoid conflicts)
if (num == 8009 && str == "REQUEST_PLAYER_LIST_KICK") {
if (num == MSG_REQUEST_PLAYER_LIST_KICK && str == "REQUEST_PLAYER_LIST_KICK") {
// Send back kick-specific message ID to avoid conflict with pick management
string namesCSV = llList2CSV(names);
if (VERBOSE_LOGGING) {
llOwnerSay("🔍 [Message Handler] Received request: '" + str + "'");
llOwnerSay("🔍 [Message Handler] Names list: " + llDumpList2String(names, ", "));
llOwnerSay("🔍 [Message Handler] Sending kick player list: '" + namesCSV + "'");
}
llMessageLinked(LINK_SET, 8009, namesCSV, id);
dbg("🧠 [Controller Message Handler] 🔍 Received request: '" + str + "'");
dbg("🧠 [Controller Message Handler] 🔍 Names list: " + llDumpList2String(names, ", "));
dbg("🧠 [Controller Message Handler] 🔍 Sending kick player list: '" + namesCSV + "'");
llMessageLinked(LINK_SET, MSG_REQUEST_PLAYER_LIST_KICK, namesCSV, id);
return;
}
// Handle pick data requests
if (num == 206) {
if (num == MSG_OWNER_PICK_MANAGER) {
handlePickDataRequest(str, id);
return;
}
// Handle life data requests
if (num == 208) {
if (num == MSG_LIFE_LOOKUP_REQUEST) {
handleLifeDataRequest(str, id);
return;
}
// Handle pick actions
if (num == 204) {
if (num == MSG_PICK_ACTION) {
handlePickAction(str);
return;
}
// Handle dice type requests
if (num == 996) {
if (num == MSG_GET_DICE_TYPE) {
if (str == "GET_DICE_TYPE") {
handleDiceTypeRequest(id);
}
@ -444,7 +388,7 @@ default {
}
// Handle leave game requests (use proper message ID, not 107 which is for sync)
if (num == 8007) { // Different from MSG_SYNC_GAME_STATE = 107
if (num == MSG_LEAVE_GAME) { // Different from MSG_SYNC_GAME_STATE = 107
handleLeaveGameRequest(str);
return;
}
@ -483,7 +427,7 @@ default {
}
// Handle full reset
if (num == -99999 && str == "FULL_RESET") {
if (num == MSG_RESET_ALL && str == "FULL_RESET") {
players = [];
names = [];
lives = [];
@ -492,7 +436,7 @@ default {
diceType = 6;
perilPlayer = "";
roundStarted = FALSE;
llOwnerSay("📨 [Message Handler] Reset complete");
dbg("🧠 [Controller Message Handler] Reset complete");
}
}
}

View file

@ -1,11 +1,10 @@
// ====================================================================
// Enhanced Link Scanner for Peril Dice Update System
// Enhanced Link Scanner for Peril Dice Linkset
// ====================================================================
// Extended version of Temp_LinkScanner.lsl that identifies:
// 1. Current linkset structure
// 2. Which scripts belong on which links
// 3. Missing scripts or misplaced scripts
// 4. Generates accurate placement info for Update_Checker.lsl
//
// USAGE: Place this script on any prim in the linkset, touch to scan
// ====================================================================
@ -83,7 +82,6 @@ performFullScan() {
string scriptName = llList2String(LINK1_SCRIPTS, j);
llOwnerSay(" • " + scriptName);
}
llOwnerSay(" + Update_Checker.lsl (NEW - replaces eliminated UpdateHelper)");
llOwnerSay("");
// Analyze scoreboard
@ -119,15 +117,7 @@ performFullScan() {
}
llOwnerSay("");
// Update checker guide
llOwnerSay("=== 🎯 UPDATE CHECKER PLACEMENT GUIDE ===");
llOwnerSay("📥 UPDATE CHECKER DEPLOYMENT:");
llOwnerSay(" ✅ Place Update_Checker.lsl on Link 1 (Root Prim)");
llOwnerSay(" ✅ Replaces eliminated UpdateHelper.lsl from v2.8.5");
llOwnerSay(" ✅ Integrates with existing admin menu system");
llOwnerSay("");
llOwnerSay("🔄 ESSENTIAL FILES FOR UPDATES:");
llOwnerSay("🔄 ESSENTIAL FILES FOR GAMEPLAY:");
llOwnerSay(" 📂 Link 1 Scripts (" + (string)llGetListLength(LINK1_SCRIPTS) + " files)");
llOwnerSay(" 📂 Link-Specific Scripts (3 files)");
llOwnerSay(" 📂 XyzzyText Script (1 file × 48 prims)");
@ -147,7 +137,6 @@ performFullScan() {
default {
state_entry() {
llOwnerSay("🔍 Enhanced Link Scanner ready for Peril Dice linkset");
llOwnerSay("📊 This will map script locations for the Update Checker");
llOwnerSay("👆 Touch to perform full linkset analysis");
}

View file

@ -0,0 +1,49 @@
#include "Peril_Constants.lsl"
string leftText = "";
// Distribute text to the FURWARE "Leaderboard" box
distributeFullText(string text) {
// Send directly to FURWARE. It handles multiple prims and newlines automatically.
llMessageLinked(LINK_SET, 0, text, (key)(FW_DATA + ":Leaderboard"));
}
default {
on_rez(integer start_param) {
DISCOVER_CORE_LINKS();
llResetScript();
}
state_entry() {
DISCOVER_CORE_LINKS();
dbg("📋 [Leaderboard] Leaderboard Communication Script ready! (FURWARE Version)");
dbg("📋 [Leaderboard] Managing 'Leaderboard' FURWARE box");
dbg("📋 [Leaderboard] ✅ Discovery complete! Scoreboard: " + (string)LINK_SCOREBOARD);
// Start with blank display
distributeFullText("");
}
link_message(integer sender, integer num, string str, key id) {
// Only listen to messages from the scoreboard
if (sender != LINK_SCOREBOARD) {
return;
}
if (num == MSG_RESET_LEADERBOARD) {
// FURWARE handles the entire leaderboard as a single text block
if (llGetSubString(str, 0, 14) == "FORMATTED_TEXT|") {
distributeFullText(llGetSubString(str, 15, -1));
}
// For individual bank updates
else if (llGetSubString(str, 0, 9) == "LEFT_TEXT|") {
leftText = llGetSubString(str, 10, -1);
distributeFullText(leftText);
}
}
else if (llSubStringIndex(str, "LEADERBOARD") == 0) {
dbg("📋 [Leaderboard] ⚠️ Received legacy LEADERBOARD format - ignoring");
}
}
}

View file

@ -0,0 +1,66 @@
#include "Peril_Constants.lsl"
// =============================================================================
// LINKSET COMMUNICATION - NO DISCOVERY NEEDED
// =============================================================================
// Function to display dice text across the "Dice" box
displayDiceText(string text) {
// Send to FURWARE Text engine
llMessageLinked(LINK_SET, 0, text, (key)(FW_DATA + ":Dice"));
dbg("🎲 [Dice Bridge] Dice display updated: '" + text + "'");
}
default {
on_rez(integer start_param) {
DISCOVER_CORE_LINKS();
llResetScript();
}
state_entry() {
DISCOVER_CORE_LINKS();
dbg("🎲 [Dice Bridge] Dice Display Bridge ready! (FURWARE Version)");
dbg("🎲 [Dice Bridge] ✅ Discovery complete! Controller: " + (string)LINK_CONTROLLER);
// Initialize dice display with blank text
displayDiceText("");
}
link_message(integer sender, integer num, string str, key id) {
// Only listen to messages from the main controller or scoreboard
if (sender != LINK_CONTROLLER && sender != LINK_SCOREBOARD) return;
if (num == MSG_DICE_ROLL) {
// Handle different message formats
list parts = llParseString2List(str, ["|"], []);
if (llGetListLength(parts) >= 3) {
// Format: "player|result|dicetype" - single die roll
string player = llList2String(parts, 0);
string result = llList2String(parts, 1);
string diceType = llList2String(parts, 2);
// Create dice display: "Player: rolled 4"
// FURWARE handles alignment and wrapping, so we don't need strict padding
string displayText = player + ": rolled " + result;
displayDiceText(displayText);
}
else {
// Treat as direct display text
displayDiceText(str);
}
}
else if (num == MSG_DICE_CLEAR) {
displayDiceText("");
dbg("🎲 [Dice Bridge] Dice display cleared");
}
}
timer() {
llSetTimerEvent(0.0);
displayDiceText("");
dbg("🎲 [Dice Bridge] Test display cleared");
}
}

View file

@ -1,24 +1,13 @@
#include "Peril_Constants.lsl"
// === FloatManager (Consolidated) ===
// This version enforces a maximum of 10 players.
#define MSG_SHOW_DIALOG 101
#define MSG_ROLL_RESULT 102
#define MSG_UPDATE_FLOAT 103
#define MSG_CLEANUP_FLOAT 104
#define MSG_REZ_FLOAT 105
#define MSG_REGISTER_PLAYER 106
#define MSG_SYNC_GAME_STATE 107
#define MSG_CLEANUP_ALL_FLOATERS 212
// Debug control message constants
#define MSG_DEBUG_PICKS_ON 7001
#define MSG_DEBUG_PICKS_OFF 7002
// =============================================================================
// DYNAMIC CHANNEL CONFIGURATION FOR FLOATERS
// =============================================================================
// Base channel offset - should match Main.lsl
// Base channel offset must match Main.lsl
#define CHANNEL_BASE -77000
// Calculate channels dynamically to avoid hardcoded conflicts
@ -52,9 +41,6 @@ initializeChannels() {
// Debug control - set to TRUE for verbose pick debugging, FALSE for normal operation
integer DEBUG_PICKS = FALSE;
// Verbose logging control - affects general debug messages, toggled by owner
integer VERBOSE_LOGGING = FALSE;
// Memory reporting function
reportMemoryUsage(string scriptName) {
integer used = llGetUsedMemory();
@ -62,142 +48,45 @@ reportMemoryUsage(string scriptName) {
integer total = used + free;
float percentUsed = ((float)used / (float)total) * 100.0;
llOwnerSay("🧠 [" + scriptName + "] Memory: " +
dbg("🧠 [" + 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)
// ALL REGISTERED PLAYERS (for channel mapping - order never changes)
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
// CURRENT GAME STATE (Synced from Main Controller)
list names = []; // Current player names in game
list players = []; // Current player keys in game
list lives = []; // Current health 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
// Helper to get picks for a player, handles both comma (human) and semicolon (bot)
list getPicksFor(string nameInput) {
// ENHANCED DEBUG: Log the request and current data state
if (DEBUG_PICKS) llOwnerSay("🔍 [Floater Manager] getPicksFor(" + nameInput + "), picksData entries: " + (string)llGetListLength(picksData));
integer i;
for (i = 0; i < llGetListLength(picksData); i++) {
string entry = llList2String(picksData, i);
if (DEBUG_PICKS) llOwnerSay("🔍 [Floater Manager] Examining entry [" + (string)i + "]: " + entry);
if (llSubStringIndex(entry, "|") == -1) {
if (entry != "") llOwnerSay("⚠️ Malformed picks entry (missing pipe): " + entry);
} else {
list parts = llParseString2List(entry, ["|"], []);
// Handle entries ending with "|" (empty picks) - LSL drops trailing empty elements
if (llGetListLength(parts) == 1 && llGetSubString(entry, -1, -1) == "|") {
parts += [""]; // Add the empty picks part back
}
if (llGetListLength(parts) < 2) {
if (entry != "") llOwnerSay("⚠️ Malformed picks entry (too few parts): " + entry);
}
else if (llList2String(parts, 0) == nameInput) {
string pickString = llList2String(parts, 1);
if (DEBUG_PICKS) llOwnerSay("🔍 [Floater Manager] Found match for " + nameInput + ", pickString: '" + pickString + "'");
if (pickString == "") {
// Empty picks are normal during initialization, don't warn
if (DEBUG_PICKS) llOwnerSay("🔍 [Floater Manager] Empty picks for " + nameInput + ", returning []");
return [];
} else {
// IMPROVED PARSING: Handle both comma AND semicolon delimited picks
// First, try parsing as comma-separated (most common format from Game Manager)
list pickList = llParseString2List(pickString, [","], []);
// If that gives us only one item, try semicolon-separated
if (llGetListLength(pickList) == 1 && llSubStringIndex(pickString, ";") != -1) {
pickList = llParseString2List(pickString, [";"], []);
if (DEBUG_PICKS) llOwnerSay("🔍 [Floater Manager] Using semicolon parsing for: " + pickString);
} else {
if (DEBUG_PICKS) llOwnerSay("🔍 [Floater Manager] Using comma parsing for: " + pickString + ", got " + (string)llGetListLength(pickList) + " picks");
}
list filtered = [];
integer j;
for (j = 0; j < llGetListLength(pickList); j++) {
string val = llStringTrim(llList2String(pickList, j), STRING_TRIM);
if (val != "" && (string)((integer)val) == val) {
filtered += [val];
if (DEBUG_PICKS) llOwnerSay("🔍 [Floater Manager] Added valid pick: " + val);
} else if (val != "") {
if (DEBUG_PICKS) llOwnerSay("⚠️ [Floater Manager] Filtered out invalid pick: '" + val + "'");
}
}
if (DEBUG_PICKS) llOwnerSay("🔍 [Floater Manager] Final filtered picks for " + nameInput + ": " + llList2CSV(filtered));
return filtered;
}
integer pipeIdx = llSubStringIndex(entry, "|");
if (pipeIdx != -1 && llGetSubString(entry, 0, pipeIdx - 1) == nameInput) {
string pickString = llGetSubString(entry, pipeIdx + 1, -1);
if (pickString == "") return [];
list pickList = llParseString2List(pickString, [",", ";"], []); // Parse both at once
list filtered = [];
integer j;
for (j = 0; j < llGetListLength(pickList); j++) {
string val = llStringTrim(llList2String(pickList, j), STRING_TRIM);
if (val != "") filtered += [val];
}
return filtered;
}
}
if (DEBUG_PICKS) llOwnerSay("🔍 [Floater Manager] No picks found for " + nameInput + ", returning []");
return [];
}
// Converts a player's key into their name (if registered)
string getNameFromKey(key id) {
integer idx = llListFindList(alivePlayers, [id]);
if (idx != -1) return llList2String(aliveNames, idx);
return (string)id;
}
// Main event handler
default {
state_entry() {
@ -206,498 +95,209 @@ default {
initializeChannels();
// Initialize/reset all state variables
alivePlayers = [];
aliveNames = [];
aliveLives = [];
alivePicksData = [];
eliminatedPlayers = [];
eliminatedNames = [];
eliminatedChannels = [];
allPlayerNames = [];
allPlayerChannels = [];
// Initialize sync state variables
names = [];
players = [];
lives = [];
picksData = [];
playerNames = [];
playerChannels = [];
perilPlayer = "";
llOwnerSay("📦 Floater Manager ready!");
dbg("💬 [Floater Manager] 📦 Floater Manager ready!");
}
on_rez(integer start_param) {
reportMemoryUsage("💬 Floater Manager");
dbg("💬 [Floater Manager] 🔄 Floater Manager rezzed - reinitializing...");
llOwnerSay("🔄 Floater Manager rezzed - reinitializing...");
// Re-initialize dynamic channels
initializeChannels();
// Reset all state variables on rez
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!");
dbg("💬 [Floater Manager] ✅ Floater Manager reset complete after rez!");
}
link_message(integer sender, integer num, string str, key id) {
// Handle full reset from main controller
if (num == -99999 && str == "FULL_RESET") {
// Reset all game state
alivePlayers = [];
aliveNames = [];
aliveLives = [];
alivePicksData = [];
eliminatedPlayers = [];
eliminatedNames = [];
eliminatedChannels = [];
if (num == MSG_RESET_ALL && str == "FULL_RESET") {
allPlayerNames = [];
allPlayerChannels = [];
// Reset sync state variables
names = [];
players = [];
lives = [];
picksData = [];
playerNames = [];
playerChannels = [];
perilPlayer = "";
llOwnerSay("📦 Floater Manager reset!");
return;
}
// Handle verbose logging toggle from Main Controller
if (num == 9011 && llSubStringIndex(str, "VERBOSE_LOGGING|") == 0) {
list parts = llParseString2List(str, ["|"], []);
if (llGetListLength(parts) >= 2) {
VERBOSE_LOGGING = (integer)llList2String(parts, 1);
if (VERBOSE_LOGGING) {
llOwnerSay("🔍 [Floater Manager] Verbose logging ON");
} else {
llOwnerSay("🔍 [Floater Manager] Verbose logging OFF");
}
}
dbg("💬 [Floater Manager] 📦 Floater Manager reset!");
return;
}
if (num == MSG_REGISTER_PLAYER) {
// Enforce the maximum number of players
if (llGetListLength(alivePlayers) >= MAX_PLAYERS) {
llOwnerSay("⚠️ Cannot register new player; the game is full (max " + (string)MAX_PLAYERS + ").");
if (llGetListLength(allPlayerNames) >= MAX_PLAYERS) {
dbg("💬 [Floater Manager] ⚠️ Game is full (max " + (string)MAX_PLAYERS + ").");
return;
}
// Register a new player: store their name and avatar key
list info = llParseString2List(str, ["|"], []);
string name = llList2String(info, 0);
key avKey = llList2Key(info, 1);
// Check if this player is already registered to prevent duplicate floaters
if (llListFindList(alivePlayers, [avKey]) != -1) {
llOwnerSay("⚠️ Player " + name + " is already registered, ignoring duplicate registration");
return;
}
if (llListFindList(allPlayerNames, [name]) != -1) return;
// 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
// float manager has updated its internal lists before attempting
// to look up the player's index. Without this, other scripts
// sending MSG_REZ_FLOAT in parallel could cause a race condition
// where the float manager has not yet seen the new name, so it
// cannot resolve the index and fails to rez the float. By
// initiating the rezzing here, we guarantee the float is created
// only after the registration is complete.
allPlayerNames += [name]; // Permanent map
llSay(0, "💀 " + name + " has entered the deadly game! 💀");
llMessageLinked(LINK_SET, MSG_REZ_FLOAT, name, avKey);
}
else if (num == MSG_REZ_FLOAT) {
string name = str;
integer idx = llListFindList(aliveNames, [name]);
if (idx == -1) return;
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
// the object's own position to ensure a float is created.
integer channelIdx = llListFindList(allPlayerNames, [name]);
if (channelIdx == -1) return;
key avKey = id; // Passed from MSG_REGISTER_PLAYER or MSG_REZ_FLOAT
list details = llGetObjectDetails(avKey, [OBJECT_POS]);
vector basePos;
if (llGetListLength(details) > 0) {
basePos = llList2Vector(details, 0);
} else {
basePos = llGetPos();
}
vector basePos = llGetPos();
if (llGetListLength(details) > 0) basePos = llList2Vector(details, 0);
vector pos;
if (llGetListLength(details) > 0) {
// Real avatars: rez next to the avatar with the standard offset
pos = basePos + <1, 0, 1>;
pos = basePos + <1, 0, 1>; // Avatar relative
} else {
// Test players: arrange in a compact 2x5 grid to stay within parcel boundaries
// Grid layout: 2 rows of 5 floaters each, spaced 1.5m apart
integer row = idx / 5; // Row 0 or 1
integer col = idx % 5; // Column 0-4
float xOffset = -3.0 - (float)col * 1.5; // -3.0 to -9.0
float yOffset = 2.0 + (float)row * 2.0; // 2.0 or 4.0
pos = basePos + <xOffset, yOffset, 1>;
}
// 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;
// Test players: grid layout
integer row = channelIdx / 5;
integer col = channelIdx % 5;
pos = basePos + <-3.0 - (float)col * 1.5, 2.0 + (float)row * 2.0, 1>;
}
integer ch = FLOATER_BASE_CHANNEL + channelIdx;
llRezObject("StatFloat", pos, ZERO_VECTOR, ZERO_ROTATION, ch);
// Wait a moment then set the description
llSleep(0.2);
// Find the rezzed object and set its description
list nearby = llGetObjectDetails(llGetKey(), [OBJECT_POS]);
if (llGetListLength(nearby) > 0) {
// Use llRegionSay to tell the StatFloat its name
llRegionSay(ch, "SET_NAME:" + name);
}
// After rezzing, immediately update the float so it displays the
// correct lives, picks and peril status. Use the avatar key as
// the id to keep the float tied to the player. This ensures that
// test players also receive an update even if they don’t have a
// valid avatar key.
llRegionSay(ch, "SET_NAME:" + name);
llMessageLinked(LINK_SET, MSG_UPDATE_FLOAT, name, avKey);
}
else if (num == MSG_UPDATE_FLOAT) {
string name = str;
// 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;
}
if (channelIdx == -1) return;
integer ch = FLOATER_BASE_CHANNEL + channelIdx;
// 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;
key avKey = id;
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 (syncIdx < llGetListLength(players)) avKey = llList2Key(players, syncIdx);
if (syncIdx < llGetListLength(lives)) lifeCount = llList2Integer(lives, syncIdx);
}
if (VERBOSE_LOGGING) llOwnerSay("🔄 [Floater Manager] Updating floater for " + name + " (lives: " + (string)lifeCount + ")");
list picks = getPicksFor(name);
if (VERBOSE_LOGGING) llOwnerSay("🔄 [Floater Manager] Retrieved picks for " + name + ": " + llList2CSV(picks));
string perilName;
// Show proper peril status based on current game state
// Check if we have any players with lives > 0 to determine if game is active
integer gameActive = FALSE;
integer i;
for (i = 0; i < llGetListLength(lives); i++) {
if (llList2Integer(lives, i) > 0) {
gameActive = TRUE;
}
}
if (!gameActive || perilPlayer == "" || perilPlayer == "NONE" || llSubStringIndex(perilPlayer, ",") != -1) {
perilName = "🧑 Status: Waiting for game to start...";
} else if (name == perilPlayer) {
// This player is currently in peril
perilName = "⚡ YOU ARE IN PERIL! ⚡";
} else {
// Show who is currently in peril
perilName = "🧑 Peril Player: " + perilPlayer;
}
string picksDisplay = llList2CSV(picks);
// Check if this player is the winner (only one player with lives > 0 AND game is active)
// Visual Settings
float glow = 0.0;
vector color = <1,1,1>;
string heartTexture = (string)lifeCount + "_hearts";
// Status String Logic
string perilStatus;
if (perilPlayer == "" || perilPlayer == "NONE") perilStatus = "🧑 Status: Waiting...";
else if (name == perilPlayer) perilStatus = "⚡ YOU ARE IN PERIL! ⚡";
else perilStatus = "🧑 Peril Player: " + perilPlayer;
// Winner detection
integer livingPlayers = 0;
integer isWinner = FALSE;
integer i;
for (i = 0; i < llGetListLength(lives); i++) {
if (llList2Integer(lives, i) > 0) {
livingPlayers++;
}
if (llList2Integer(lives, i) > 0) livingPlayers++;
}
// Winner detection: only trigger if game is active, has multiple total players, and only 1 survivor
// This prevents false winner detection during initial player registration
if (gameActive && livingPlayers == 1 && lifeCount > 0 && llGetListLength(names) >= 2) {
isWinner = TRUE;
string txt = "🎲 Peril Dice\n👤 " + name + "\n" + perilStatus + "\n🔢 Picks: " + picksDisplay;
if (lifeCount <= 0) {
glow = 0.3;
color = <1,0,0>; // Red
txt = "🎲 Peril Dice\n👤 " + name + "\n🔢 Picks: " + picksDisplay + "\n💀 ELIMINATED! 💀";
} else if (livingPlayers == 1 && llGetListLength(names) >= 2) {
glow = 0.3;
color = <0,1,0>; // Green
txt = "🎲 Peril Dice\n👤 " + name + "\n✨ ULTIMATE VICTORY! ✨\n🏆 ULTIMATE SURVIVOR 🏆\n🔢 Final Picks: " + picksDisplay;
} else if (name == perilPlayer) {
glow = 0.2;
color = <1,1,0>; // Yellow
}
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);
// Send Universal API command
llRegionSay(ch, "CMD:" + (string)avKey +
"|T=" + txt +
"|G=" + (string)glow +
"|C=" + (string)color +
"|H=" + heartTexture);
}
else if (num == MSG_CLEANUP_FLOAT) {
integer ch = (integer)str;
// Always send cleanup message to the channel (even for orphaned floaters)
llRegionSay(ch, "CLEANUP");
// 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 foundIdx = ch - FLOATER_BASE_CHANNEL;
if (foundIdx >= 0 && foundIdx < llGetListLength(allPlayerNames)) {
string removedPlayer = llList2String(allPlayerNames, foundIdx);
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");
}
// IMPORTANT: Keep permanent mapping intact - never remove from playerNames/playerChannels
// This preserves the original channel assignments
// 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) {
if (llSubStringIndex(llList2String(picksData, p), removedPlayer + "|") == 0) {
pickIdx = p;
jump found_pick;
p = llGetListLength(picksData);
}
}
@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 - channel not found in our tracking
llOwnerSay("🧹 [Floater Manager] Cleaned up orphaned floater (channel: " + (string)ch + ") - not in tracking list");
if (pickIdx != -1) picksData = llDeleteSubList(picksData, pickIdx, pickIdx);
}
}
else if (num == MSG_SYNC_GAME_STATE) {
// Synchronize the lists for lives and picksData when receiving a new game state
list parts = llParseString2List(str, ["~"], []);
if (llGetListLength(parts) < 4) return;
// Handle special RESET sync message
if (llGetListLength(parts) >= 5 && llList2String(parts, 0) == "RESET") {
llOwnerSay("🔄 [Floater Manager] Received reset sync - ignoring during reset");
return;
}
if (llGetListLength(parts) < 4) {
llOwnerSay("⚠️ Floater Manager: Incomplete sync message received, parts: " + (string)llGetListLength(parts));
return;
}
string livesStr = llList2String(parts, 0);
lives = llCSV2List(livesStr);
// ENHANCED DEBUG: Log the received picks data
lives = llCSV2List(llList2String(parts, 0));
string picksDataStr = llList2String(parts, 1);
if (VERBOSE_LOGGING) llOwnerSay("📥 [Floater Manager] Received picks data: '" + picksDataStr + "'");
// Use ^ delimiter for picksData to avoid comma conflicts
if (picksDataStr == "" || picksDataStr == "EMPTY") {
picksData = [];
if (VERBOSE_LOGGING) llOwnerSay("📥 [Floater Manager] Picks data empty, cleared picksData list");
} else {
picksData = llParseString2List(picksDataStr, ["^"], []);
if (VERBOSE_LOGGING) {
llOwnerSay("📥 [Floater Manager] Parsed " + (string)llGetListLength(picksData) + " picks data entries:");
integer debugIdx;
for (debugIdx = 0; debugIdx < llGetListLength(picksData); debugIdx++) {
llOwnerSay(" [" + (string)debugIdx + "]: " + llList2String(picksData, debugIdx));
}
}
}
string receivedPeril = llList2String(parts, 2);
string oldPeril = perilPlayer;
if (receivedPeril == "NONE") {
perilPlayer = ""; // Convert placeholder back to empty
perilPlayer = "";
} else {
perilPlayer = receivedPeril;
}
// IMPORTANT: Also sync the names list to prevent desynchronization
string namesStr = llList2String(parts, 3);
list newNames = llCSV2List(namesStr);
names = llCSV2List(llList2String(parts, 3));
// 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) {
string playersStr = llList2String(parts, 4);
players = llCSV2List(playersStr);
}
// Debug the peril player status change
if (oldPeril != perilPlayer) {
llOwnerSay("🎯 Floater Manager: Peril player updated from '" + oldPeril + "' to '" + perilPlayer + "'");
}
if (llGetListLength(allPlayerNames) == 0 && llGetListLength(names) > 0) allPlayerNames = names;
if (llGetListLength(parts) >= 5) players = llCSV2List(llList2String(parts, 4));
// 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(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);
}
if (aliveIdx != -1 && aliveIdx < llGetListLength(players)) k = llList2Key(players, aliveIdx);
llMessageLinked(LINK_SET, MSG_UPDATE_FLOAT, n, k);
}
// Additional safety: If there was a peril player change, add a brief delay and update again
// This handles edge cases where floater updates might race with other state changes
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(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");
integer i;
for (i = 0; i < llGetListLength(allPlayerNames); i++) {
llRegionSay(FLOATER_BASE_CHANNEL + i, "CLEANUP");
}
// 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;
llOwnerSay("🔍 [Floater Manager] Pick debugging ENABLED");
}
else if (num == MSG_DEBUG_PICKS_OFF) {
DEBUG_PICKS = FALSE;
llOwnerSay("🔇 [Floater Manager] Pick debugging DISABLED");
players = []; names = []; lives = []; picksData = []; perilPlayer = "";
}
else if (num == MSG_DEBUG_PICKS_ON) DEBUG_PICKS = TRUE;
else if (num == MSG_DEBUG_PICKS_OFF) DEBUG_PICKS = FALSE;
}
}

1055
Furware_Text_Engine.lsl Normal file

File diff suppressed because it is too large Load diff

View file

@ -183,8 +183,11 @@ The game features thematic messaging for maximum immersion:
- **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**
### **Technical Rules (UPDATED in v3.1.0):**
- **Dynamic Link Discovery** - Scripts find their targets by name; reordering prims won't break the game 🔍
- **FURWARE Text Mesh Integration** - Professional mesh-based displays for dice and leaderboard 🎨
- **Centralized Constants** - System-wide settings managed in `Peril_Constants.lsl` 🧩
- **Game supports up to 10 players maximum**
- **Owner kick functionality** for managing disruptive players
- **Automatic game reset** on startup (preserves leaderboard)
- **Single linkset architecture** (74 prims total) for reliable operation
@ -293,7 +296,7 @@ While the core rules remain the same, consider these optional variations:
---
**Version:** 2.8.1
**Last Updated:** August 14, 2025
**Game System:** Production Ready - Display & Elimination Fixes
**Version:** 3.1.0
**Last Updated:** April 04, 2026
**Game System:** Production Ready - Linkset Stability Release
**Credits:** Original concept by Noose the Bunny (djmusica28), Automated system implementation by Rebecca Nod

View file

@ -188,7 +188,10 @@ Enhanced Display System (UPDATED in v2.8.6):
- Professional Status Actions - Large status prim shows current events
- Color-Coded Glow System - Green (winner), Yellow (peril), Red X (eliminated)
Technical Rules:
Technical Rules (UPDATED in v3.1.0):
- Dynamic Link Discovery - Scripts find their targets by name; reordering prims won't break the game 🔍
- FURWARE Text Mesh Integration - Professional mesh-based displays for dice and leaderboard 🎨
- Centralized Constants - System-wide settings managed in Peril_Constants.lsl 🧩
- Game supports up to 10 players maximum
- Owner kick functionality for managing disruptive players
- Automatic game reset on startup (preserves leaderboard)
@ -299,9 +302,9 @@ Dice! May luck and strategy guide you to Ultimate Victory!
===============================================================================
Version: 2.8.1
Last Updated: August 14, 2025
Game System: Production Ready - Display & Elimination Fixes
Version: 3.1.0
Last Updated: April 04, 2026
Game System: Production Ready - Linkset Stability Release
Credits: Original concept by Noose the Bunny (djmusica28),
Automated system implementation by Rebecca Nod

View file

@ -1,12 +1,10 @@
#include "Peril_Constants.lsl"
// === Game Calculator Module (Max 10 players) ===
// Calculates dice types, pick requirements, and other game mechanics
// This version chooses a dice size based on the number of players,
// ensuring at least three numbers are available per player.
// Verbose logging control
integer VERBOSE_LOGGING = TRUE; // Global flag for verbose debug logs
#define MSG_TOGGLE_VERBOSE_LOGS 9998 // Message to toggle verbose logging
// Memory reporting function
reportMemoryUsage(string scriptName) {
integer used = llGetUsedMemory();
@ -14,20 +12,12 @@ reportMemoryUsage(string scriptName) {
integer total = used + free;
float percentUsed = ((float)used / (float)total) * 100.0;
llOwnerSay("🧠 [" + scriptName + "] Memory: " +
dbg("🧠 [" + scriptName + "] Memory: " +
(string)used + " used, " +
(string)free + " free (" +
llGetSubString((string)percentUsed, 0, 4) + "% used)");
}
#define MSG_GET_DICE_TYPE 1001
#define MSG_DICE_TYPE_RESULT 1005
#define MSG_GET_PICKS_REQUIRED 1002
#define MSG_GET_PICKER_INDEX 1003
#define MSG_SERIALIZE_GAME_STATE 1004
#define MSG_SYNC_GAME_STATE 107
#define MSG_SYNC_PICKQUEUE 2001
list lives;
list picksData;
string perilPlayer;
@ -97,16 +87,10 @@ integer showPickManager(string player, key id) {
string matchName = llList2String(names, k);
if (llSubStringIndex(matchName, searchName) != -1) {
searchIdx = k;
if (VERBOSE_LOGGING) {
llOwnerSay("✅ Matched pick list for: " + matchName);
}
k = llGetListLength(names);
}
}
if (searchIdx == -1) {
if (VERBOSE_LOGGING) {
llOwnerSay("⚠️ Could not find player: " + player);
}
return 0;
}
}
@ -131,13 +115,6 @@ integer showPickManager(string player, key id) {
}
}
// Don't show hardcoded dialog - let the Owner Dialog Handler manage this
if (VERBOSE_LOGGING) {
llOwnerSay("🧮 Calculator delegating pick management to dialog handler for: " + (string)id);
}
// Return without showing dialog - this should be handled by the proper dialog handler
// Return dummy value since LSL does not have a void return type
return 0;
}
@ -151,49 +128,31 @@ default {
perilPlayer = "";
names = [];
pickQueue = [];
llOwnerSay("🧮 Game Calculator ready!");
dbg("🧮 [Calculator] Game Calculator ready!");
}
on_rez(integer start_param) {
reportMemoryUsage("🧮 Game Calculator");
if (VERBOSE_LOGGING) {
llOwnerSay("🔄 Game Calculator rezzed - reinitializing...");
}
// Reset all state variables on rez
lives = [];
picksData = [];
perilPlayer = "";
names = [];
pickQueue = [];
if (VERBOSE_LOGGING) {
llOwnerSay("✅ Game Calculator reset complete after rez!");
}
}
link_message(integer sender, integer num, string str, key id) {
// Handle verbose logging toggle
if (num == MSG_TOGGLE_VERBOSE_LOGS) {
VERBOSE_LOGGING = !VERBOSE_LOGGING;
if (VERBOSE_LOGGING) {
llOwnerSay("🔊 [Game Calculator] Verbose logging ENABLED");
} else {
llOwnerSay("🔊 [Game Calculator] Verbose logging DISABLED");
}
return;
}
// Handle full reset from main controller
if (num == -99999 && str == "FULL_RESET") {
if (num == MSG_RESET_ALL && str == "FULL_RESET") {
// Reset helper state
lives = [];
picksData = [];
perilPlayer = "";
names = [];
pickQueue = [];
llOwnerSay("🧮 Calculator Module reset!");
dbg("🧮 [Calculator] Calculator Module reset!");
return;
}
@ -202,17 +161,13 @@ default {
// Handle special RESET sync message
if (llGetListLength(parts) >= 5 && llList2String(parts, 0) == "RESET") {
if (VERBOSE_LOGGING) {
llOwnerSay("🔄 [Game Calculator] Received reset sync - ignoring during reset");
}
dbg("🧮 [Calculator] 🔄 Received reset sync - ignoring during reset");
return;
}
if (llGetListLength(parts) < 4) {
if (VERBOSE_LOGGING) {
llOwnerSay("⚠️ Game Calculator: Incomplete sync message received, parts: " + (string)llGetListLength(parts));
llOwnerSay("⚠️ Incomplete game state received.");
}
dbg("🧮 [Calculator] ⚠️ Incomplete sync message received, parts: " + (string)llGetListLength(parts));
dbg("🧮 [Calculator] ⚠️ Incomplete game state received.");
return;
}
lives = llCSV2List(llList2String(parts, 0));
@ -237,9 +192,7 @@ default {
}
else if (num == MSG_GET_DICE_TYPE) {
integer playerCount = (integer)str;
if (VERBOSE_LOGGING) {
llOwnerSay("📈 Calculator: Calculating dice type for " + (string)playerCount + " players");
}
dbg("🧮 [Calculator] 📈 Calculating dice type for " + (string)playerCount + " players");
integer result = getDiceType(playerCount);
// Send result only to the requester (sender), not broadcast to everyone
llMessageLinked(sender, MSG_DICE_TYPE_RESULT, (string)result, NULL_KEY);
@ -255,8 +208,8 @@ default {
else if (num == MSG_SERIALIZE_GAME_STATE) {
llMessageLinked(LINK_THIS, MSG_SERIALIZE_GAME_STATE, serializeGameState(), NULL_KEY);
}
else if (num == 206) {
llOwnerSay("📨 MSG 206 received for: " + str + " from: " + (string)id);
else if (num == MSG_OWNER_PICK_MANAGER) {
dbg("🧮 [Calculator] 📨 MSG 206 received for: " + str + " from: " + (string)id);
showPickManager(str, id);
}
}

View file

@ -1,3 +1,5 @@
#include "Peril_Constants.lsl"
// === Game Manager - Game Flow Logic ===
// Helper function to get display name with fallback to username
@ -15,7 +17,6 @@ string getPlayerName(key id) {
#define HUMAN_PICK_DELAY 1.0
#define DIALOG_DELAY 1.5
#define STATUS_DISPLAY_TIME 8.0
// Game state - synced from Main Controller
list players = [];
list names = [];
@ -79,42 +80,22 @@ list buildCompleteAvoidanceList() {
}
// Memory reporting function
reportMemoryUsage(string scriptName) {
integer reportMemoryUsage(string scriptName) {
integer used = llGetUsedMemory();
integer free = llGetFreeMemory();
integer total = used + free;
float percentUsed = ((float)used / (float)total) * 100.0;
llOwnerSay("🧠 [" + scriptName + "] Memory: " +
dbg("🧠 [" + scriptName + "] Memory: " +
(string)used + " used, " +
(string)free + " free (" +
llGetSubString((string)percentUsed, 0, 4) + "% used)");
return 0;
}
// Verbose logging control - toggled by owner
#define DEBUG_LOGS 0 // Set to 1 to enable logs, 0 to STRIP from memory completely
#if DEBUG_LOGS
#define dbg(msg) dbg(msg)
integer VERBOSE_LOGGING = FALSE;
#else
#define dbg(msg)
#define VERBOSE_LOGGING 0
#endif
// Original VERBOSE_LOGGING definition removed
// Build complete avoidance list from all current picks
// Message constants
#define MSG_SHOW_DIALOG 101
#define MSG_SHOW_ROLL_DIALOG 301
#define MSG_GET_DICE_TYPE 1001
#define MSG_DICE_TYPE_RESULT 1005
#define MSG_SYNC_GAME_STATE 107
#define MSG_SYNC_PICKQUEUE 2001
#define MSG_PLAYER_WON 551
#define MSG_UPDATE_FLOAT 2010
#define MSG_DIALOG_FORWARD_REQUEST 9060 // Request dialog forwarding from Player_RegistrationManager
#define MSG_CONTINUE_ROUND 998 // Continue round after roll processing
continueCurrentRound() {
integer continueCurrentRound() {
// MEMORY OPTIMIZED: Skip complex validation - trust game state
// Main Controller handles all game ending validation
@ -127,30 +108,30 @@ continueCurrentRound() {
integer continuePlayerLives = llList2Integer(lives, continuePlayerIdx);
if (continuePlayerLives <= 0) {
dbg("🛑 [Game Manager] continueCurrentRound: Found eliminated player " + continueName + " with " + (string)continuePlayerLives + " lives - aborting round start");
return;
return 0;
}
}
}
if (perilPlayer == "" || perilPlayer == "NONE") {
dbg("🛑 [Game Manager] continueCurrentRound: Invalid peril player (" + perilPlayer + ") - aborting round start");
return;
return 0;
}
// Verify peril player exists and has lives
integer perilIdx = llListFindList(names, [perilPlayer]);
if (perilIdx == -1) {
dbg("🛑 [Game Manager] continueCurrentRound: Peril player " + perilPlayer + " not found in names list - aborting round start");
return;
return 0;
}
if (perilIdx >= llGetListLength(lives)) {
dbg("🛑 [Game Manager] continueCurrentRound: Peril player " + perilPlayer + " index out of bounds - aborting round start");
return;
return 0;
}
integer perilLives = llList2Integer(lives, perilIdx);
if (perilLives <= 0) {
dbg("🛑 [Game Manager] continueCurrentRound: Peril player " + perilPlayer + " has " + (string)perilLives + " lives - aborting round start");
return;
return 0;
}
// Start fresh for a new round - clear picks data
@ -179,18 +160,19 @@ continueCurrentRound() {
llMessageLinked(LINK_SET, MSG_GET_DICE_TYPE, (string)llGetListLength(names), NULL_KEY);
// Note: showNextPickerDialog() will be called when dice type result is received
return 0;
}
startNextRound() {
integer startNextRound() {
// Prevent multiple calls to start round
if (roundStarted) {
dbg("⚠️ Round already started, ignoring duplicate start request");
return;
return 0;
}
if (llGetListLength(names) < 2) {
dbg("⚠️ Need at least 2 players to start the game.");
return;
return 0;
}
if (llGetListLength(names) == 1) {
@ -198,14 +180,14 @@ startNextRound() {
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_SCOREBOARD, MSG_UPDATE_WINNER, winner, NULL_KEY); // MSG_UPDATE_WINNER
llMessageLinked(LINK_SET, MSG_PLAYER_WON, winner, NULL_KEY);
llMessageLinked(LINK_SET, 995, "VICTORY_CONFETTI", NULL_KEY);
llMessageLinked(LINK_SET, MSG_EFFECT_CONFETTI, "VICTORY_CONFETTI", NULL_KEY);
// Don't use hardcoded channel here - let Main Controller handle scoreboard updates
// Main Controller will send the proper GAME_WON message to scoreboard
// Let main controller handle reset
return;
return 0;
}
dbg("🎯 Game Manager starting new round...");
@ -221,7 +203,7 @@ startNextRound() {
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
llMessageLinked(LINK_SCOREBOARD, MSG_UPDATE_PERIL_PLAYER, perilPlayer, NULL_KEY); // MSG_UPDATE_PERIL_PLAYER
// Sync state to floater manager first, then update floaters to show new peril player immediately
dbg("🔄 [Game Manager] Syncing state and updating floaters for new peril player: " + perilPlayer);
@ -253,9 +235,10 @@ startNextRound() {
// Request dice type for this round
llMessageLinked(LINK_SET, MSG_GET_DICE_TYPE, (string)llGetListLength(names), NULL_KEY);
dbg("🎯 Game Manager round setup complete, requesting dice type...");
return 0;
}
showNextPickerDialog() {
integer showNextPickerDialog() {
// MEMORY OPTIMIZED: Skip complex validation - trust that game is active
// Main Controller handles all game ending logic
@ -267,36 +250,36 @@ showNextPickerDialog() {
if (diceType <= 0) {
dbg("❌ Cannot show picker dialog: diceType not set (" + (string)diceType + ")");
return;
return 0;
}
if (currentPickerIdx >= llGetListLength(pickQueue)) {
dbg("❌ Cannot show picker dialog: currentPickerIdx (" + (string)currentPickerIdx + ") >= pickQueue length (" + (string)llGetListLength(pickQueue) + ")");
return;
return 0;
}
string firstName = llList2String(pickQueue, currentPickerIdx);
if (firstName == "") {
dbg("❌ Cannot show picker dialog: empty player name at index " + (string)currentPickerIdx);
return;
return 0;
}
// CRITICAL: Verify this player is still alive and in the game
integer showPlayerIdx = llListFindList(names, [firstName]);
if (showPlayerIdx == -1) {
dbg("❌ Cannot show picker dialog: player " + firstName + " not found in current game");
return;
return 0;
}
if (showPlayerIdx >= llGetListLength(lives)) {
dbg("❌ Cannot show picker dialog: player " + firstName + " index out of bounds for lives list");
return;
return 0;
}
integer showPlayerLives = llList2Integer(lives, showPlayerIdx);
if (showPlayerLives <= 0) {
dbg("❌ Cannot show picker dialog: player " + firstName + " has been eliminated (" + (string)showPlayerLives + " lives)");
return;
return 0;
}
// CHECK: Don't show dialog if this player already has picks to prevent loops
@ -312,23 +295,23 @@ showNextPickerDialog() {
}
}
if (alreadyHasPicks) {
// Skip to next player to prevent infinite loops
currentPickerIdx++;
if (currentPickerIdx < llGetListLength(pickQueue)) {
showNextPickerDialog();
} else {
// All picked, show roll dialog through Player_RegistrationManager
string firstRollRequest = "SHOW_ROLL_DIALOG|" + perilPlayer + "|" + perilPlayer;
llMessageLinked(LINK_SET, MSG_DIALOG_FORWARD_REQUEST, firstRollRequest, NULL_KEY);
if (alreadyHasPicks) {
// Skip to next player to prevent infinite loops
currentPickerIdx++;
if (currentPickerIdx < llGetListLength(pickQueue)) {
showNextPickerDialog();
} else {
// All picked, show roll dialog through Player_RegistrationManager
string firstRollRequest = "SHOW_ROLL_DIALOG|" + perilPlayer + "|" + perilPlayer;
llMessageLinked(LINK_SET, MSG_DIALOG_FORWARD_REQUEST, firstRollRequest, NULL_KEY);
}
return 0;
}
return;
}
integer nameIdx = llListFindList(names, [firstName]);
if (nameIdx == -1) {
dbg("❌ Cannot show picker dialog: player " + firstName + " not found in names list");
return;
return 0;
}
// SIMPLE FIX: Send dialog request to Main Controller with player name
@ -363,7 +346,7 @@ showNextPickerDialog() {
string showAvoidListStr = llList2CSV(showCompleteAvoidList);
string botCommand = "BOT_PICK:" + firstName + ":" + (string)showPicksNeeded + ":" + (string)diceType + ":" + showAvoidListStr;
dbg("🎯 [Game Manager] Sending bot command with complete avoid list (" + (string)llGetListLength(showCompleteAvoidList) + " numbers): " + showAvoidListStr);
llMessageLinked(LINK_SET, -9999, botCommand, NULL_KEY);
llMessageLinked(LINK_SET, MSG_BOT_COMMAND, botCommand, NULL_KEY);
dbg("🤖 " + firstName + " is automatically picking " + (string)showPicksNeeded + " numbers...");
} else {
// Bot already has picks, advance to next player immediately
@ -397,21 +380,23 @@ showNextPickerDialog() {
string dialogRequest = "SHOW_DIALOG|" + firstName + "|" + dialogPayload;
llMessageLinked(LINK_SET, MSG_DIALOG_FORWARD_REQUEST, dialogRequest, NULL_KEY);
}
return 0;
}
// requestDiceType() removed - Game Manager no longer requests dice type directly
// This prevents loops. Main Controller handles all dice type requests.
syncStateToMain() {
integer syncStateToMain() {
// MEMORY OPTIMIZED: Skip ALL sync operations to prevent corrupting Main Controller's data
// Main Controller is the master - Game Manager should not send sync messages
return;
return 0;
}
default {
state_entry() {
DISCOVER_CORE_LINKS();
reportMemoryUsage("Game Manager");
dbg("🎯 Game Manager initializing...");
dbg("🎯 Game Manager ready - discovery complete! Scoreboard: " + (string)LINK_SCOREBOARD);
// Initialize/reset all game state variables
players = [];
@ -436,8 +421,9 @@ default {
}
on_rez(integer start_param) {
DISCOVER_CORE_LINKS();
reportMemoryUsage("Game Manager");
dbg("🔄 Game Manager rezzed - reinitializing...");
dbg("🔄 Game Manager reset via rez...");
// Reset all game state variables on rez
players = [];
@ -522,7 +508,7 @@ default {
if (perilChanged && perilPlayer != "" && perilPlayer != "NONE") {
integer currentPlayerCount = llGetListLength(newNames); // Use the updated count
string lightUpdate = "PERIL_UPDATE|" + perilPlayer + "|" + (string)currentPlayerCount;
llMessageLinked(LINK_SET, 9070, lightUpdate, NULL_KEY);
llMessageLinked(LINK_SET, MSG_SYNC_LIGHTWEIGHT, lightUpdate, NULL_KEY);
}
integer allPicksEmpty = TRUE;
@ -545,7 +531,7 @@ default {
}
// Receive legacy game state updates from main controller (simplified version)
if (num == 9071) {
if (num == MSG_SYNC_LEGACY) {
dbg("🔧 [Game Manager] Received sync: " + str);
list parts = llParseString2List(str, ["~"], []);
dbg("🔧 [Game Manager] Parsed into " + (string)llGetListLength(parts) + " parts");
@ -681,7 +667,7 @@ default {
// Legacy 997 message handler removed - now using direct MSG_CONTINUE_ROUND (998) communication
// Handle human picks
if (num == -9998 && llSubStringIndex(str, "HUMAN_PICKED:") == 0) {
if (num == MSG_HUMAN_PICKED && llSubStringIndex(str, "HUMAN_PICKED:") == 0) {
list parts = llParseString2List(str, [":"], []);
if (llGetListLength(parts) >= 3) {
string playerName = llList2String(parts, 1);
@ -827,7 +813,7 @@ default {
}
// Handle bot picks
if (num == -9997 && llSubStringIndex(str, "BOT_PICKED:") == 0) {
if (num == MSG_BOT_PICKED && llSubStringIndex(str, "BOT_PICKED:") == 0) {
dbg("🔍 [Game Manager] BOT_PICKED message received: " + str);
list parts = llParseString2List(str, [":"], []);
if (llGetListLength(parts) >= 3) {
@ -874,7 +860,7 @@ default {
// Force close any active number picker dialogs
dbg("🚫 [Game Manager] Sending CLOSE_ALL_DIALOGS command");
llMessageLinked(LINK_SET, -9999, "CLOSE_ALL_DIALOGS", NULL_KEY);
llMessageLinked(LINK_SET, MSG_BOT_COMMAND, "CLOSE_ALL_DIALOGS", NULL_KEY);
// Send roll dialog through Player_RegistrationManager
string rollRequest = "SHOW_ROLL_DIALOG|" + perilPlayer + "|" + perilPlayer;
@ -905,7 +891,7 @@ default {
string avoidListStr = llList2CSV(completeAvoidList);
string botCommand = "BOT_PICK:" + playerName + ":" + (string)picksNeeded + ":" + (string)diceType + ":" + avoidListStr;
dbg("🎯 [Game Manager] Sending retry bot command with complete avoid list (" + (string)llGetListLength(completeAvoidList) + " numbers): " + avoidListStr);
llMessageLinked(LINK_SET, -9999, botCommand, NULL_KEY);
llMessageLinked(LINK_SET, MSG_BOT_COMMAND, botCommand, NULL_KEY);
return; // Don't save this pick
}
@ -984,7 +970,7 @@ default {
// Force close any active number picker dialogs
dbg("🚫 [Game Manager] Sending CLOSE_ALL_DIALOGS command");
llMessageLinked(LINK_SET, -9999, "CLOSE_ALL_DIALOGS", NULL_KEY);
llMessageLinked(LINK_SET, MSG_BOT_COMMAND, "CLOSE_ALL_DIALOGS", NULL_KEY);
// Send roll dialog through Player_RegistrationManager
string rollRequest = "SHOW_ROLL_DIALOG|" + perilPlayer + "|" + perilPlayer;
@ -1034,24 +1020,9 @@ default {
return;
}
// Handle verbose logging toggle from Main Controller
if (num == 9011 && llSubStringIndex(str, "VERBOSE_LOGGING|") == 0) {
list parts = llParseString2List(str, ["|"], []);
if (llGetListLength(parts) >= 2) {
#if DEBUG_LOGS
VERBOSE_LOGGING = (integer)llList2String(parts, 1);
#endif
if (VERBOSE_LOGGING) {
dbg("🔍 [Game Manager] Verbose logging ON");
} else {
dbg("🔍 [Game Manager] Verbose logging OFF");
}
}
return;
}
// Handle reset
if (num == -99999 && str == "FULL_RESET") {
if (num == MSG_RESET_ALL && str == "FULL_RESET") {
players = names = lives = picksData = globalPickedNumbers = pickQueue = [];
perilPlayer = "";
currentPickerIdx = 0;
@ -1066,7 +1037,7 @@ default {
}
// Handle emergency state reset (for when game gets stuck)
if (num == -99998 && str == "EMERGENCY_RESET") {
if (num == MSG_EMERGENCY_RESET && str == "EMERGENCY_RESET") {
dbg("🚨 [Game Manager] Emergency reset triggered!");
roundStarted = FALSE;
perilPlayer = "";

File diff suppressed because it is too large Load diff

View file

@ -1,229 +0,0 @@
////////////////////////////////////////////
// Leaderboard Communication Script - Linkset Version
// Based on actual link verification results
////////////////////////////////////////////
// =============================================================================
// LINKSET COMMUNICATION - NO DISCOVERY NEEDED
// =============================================================================
// Verbose logging control
integer VERBOSE_LOGGING = TRUE; // Global flag for verbose debug logs
#define MSG_TOGGLE_VERBOSE_LOGS 9998 // Message to toggle verbose logging
// Message constants for link communication
// Leaderboard messages (from link 12 - scoreboard)
#define MSG_GAME_WON 3010
#define MSG_GAME_LOST 3011
#define MSG_RESET_LEADERBOARD 3012
#define DISPLAY_STRING 204000
// Bank assignments based on actual link testing - UPDATED FOR LINKSET:
// Original links were 1-48, now they are 35-82 (offset by +34)
list leftmostLinks = [35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46]; // Links 35-46 = Leftmost bank (was 1-12)
list middleLeftLinks = [47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58]; // Links 47-58 = Middle-left bank (was 13-24)
list middleRightLinks = [59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70]; // Links 59-70 = Middle-right bank (was 25-36)
list rightmostLinks = [71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82]; // Links 71-82 = Rightmost bank (was 37-48)
string leftText = "";
string middleLeftText = "";
string middleRightText = "";
string rightText = "";
// Distribute text across all 4 banks properly - each line can be 40 chars total
distributeFullText(string text) {
list lines = llParseString2List(text, ["\n"], []);
list allLinks = leftmostLinks + middleLeftLinks + middleRightLinks + rightmostLinks;
integer lineIndex = 0;
integer maxLines = 12; // Each bank has 12 prims
// Process each line
while (lineIndex < maxLines) {
string currentLine;
if (lineIndex < llGetListLength(lines)) {
currentLine = llList2String(lines, lineIndex);
} else {
currentLine = ""; // Empty line
}
// Pad line to 40 characters (4 banks × 10 chars each)
while (llStringLength(currentLine) < 40) {
currentLine += " ";
}
// Truncate if too long
if (llStringLength(currentLine) > 40) {
currentLine = llGetSubString(currentLine, 0, 39);
}
// Distribute this line across the 4 banks (10 chars each)
integer bank;
for (bank = 0; bank < 4; bank++) {
string bankText = llGetSubString(currentLine, bank * 10, (bank * 10) + 9);
integer linkNum;
if (bank == 0) linkNum = llList2Integer(leftmostLinks, lineIndex);
else if (bank == 1) linkNum = llList2Integer(middleLeftLinks, lineIndex);
else if (bank == 2) linkNum = llList2Integer(middleRightLinks, lineIndex);
else linkNum = llList2Integer(rightmostLinks, lineIndex);
llMessageLinked(linkNum, DISPLAY_STRING, bankText, "");
}
lineIndex++;
}
}
// Legacy function for single bank distribution
distributeToBank(string text, list linkNumbers) {
list bankLines;
// If text is empty, create a list of empty strings to clear all prims
if (text == "") {
integer numPrims = llGetListLength(linkNumbers);
integer j;
for (j = 0; j < numPrims; j++) {
bankLines += [" "]; // 10 spaces to clear each line
}
} else {
bankLines = llParseString2List(text, ["\n"], []);
}
// Send text to each prim in the bank
integer k;
for (k = 0; k < llGetListLength(linkNumbers); k++) {
string line;
if (k < llGetListLength(bankLines)) {
line = llList2String(bankLines, k);
} else {
line = " "; // 10 spaces for empty lines
}
// Ensure exactly 10 characters
if (llStringLength(line) > 10) {
line = llGetSubString(line, 0, 9);
} else if (llStringLength(line) < 10) {
line = line + llGetSubString(" ", 0, 9 - llStringLength(line));
}
integer primLink = llList2Integer(linkNumbers, k);
llMessageLinked(primLink, DISPLAY_STRING, line, "");
}
}
default {
on_rez(integer start_param) {
llResetScript(); // Fixes Z-fighting and alignment glitches on rez
}
state_entry() {
if (VERBOSE_LOGGING) {
llOwnerSay("📋 Leaderboard Communication Script ready! (Linkset Version)");
llOwnerSay("📋 This is link " + (string)llGetLinkNumber() + " - should be link 35");
llOwnerSay("📋 Managing XyzzyText links 35-82 (48 prims total)");
llOwnerSay("✅ Linkset communication active - listening for messages from link 12!");
}
// Start with blank display - real data will come from scoreboard script
distributeFullText("");
}
link_message(integer sender, integer num, string str, key id) {
// Handle verbose logging toggle
if (num == MSG_TOGGLE_VERBOSE_LOGS) {
VERBOSE_LOGGING = !VERBOSE_LOGGING;
if (VERBOSE_LOGGING) {
llOwnerSay("🔊 [Leaderboard] Verbose logging ENABLED");
} else {
llOwnerSay("🔊 [Leaderboard] Verbose logging DISABLED");
}
return;
}
// Only listen to messages from the scoreboard (link 12)
if (sender != 12) {
return;
}
if (num == MSG_RESET_LEADERBOARD) {
// Handle the different text section messages
if (llGetSubString(str, 0, 9) == "LEFT_TEXT|") {
leftText = llGetSubString(str, 10, -1);
distributeToBank(leftText, leftmostLinks);
} else if (llGetSubString(str, 0, 16) == "MIDDLE_LEFT_TEXT|") {
middleLeftText = llGetSubString(str, 17, -1);
distributeToBank(middleLeftText, middleLeftLinks);
} else if (llGetSubString(str, 0, 17) == "MIDDLE_RIGHT_TEXT|") {
middleRightText = llGetSubString(str, 18, -1);
distributeToBank(middleRightText, middleRightLinks);
} else if (llGetSubString(str, 0, 10) == "RIGHT_TEXT|") {
rightText = llGetSubString(str, 11, -1);
distributeToBank(rightText, rightmostLinks);
}
else if (llGetSubString(str, 0, 14) == "FORMATTED_TEXT|") {
// This is pre-formatted text from the scoreboard - just display it
string formattedText = llGetSubString(str, 15, -1);
leftText = formattedText;
distributeFullText(leftText);
}
else if (llSubStringIndex(str, "LEADERBOARD") == 0) {
// Legacy leaderboard format - not used with linkset version
// Scoreboard now sends pre-formatted text via FORMATTED_TEXT
if (VERBOSE_LOGGING) {
llOwnerSay("⚠️ [Leaderboard] Received legacy LEADERBOARD format - ignoring");
}
}
}
}
touch_start(integer total_number) {
// Owner can get debug info by touching leaderboard
if (llDetectedKey(0) == llGetOwner()) {
llOwnerSay("📋 Leaderboard Status:");
llOwnerSay(" Link Number: " + (string)llGetLinkNumber());
llOwnerSay(" Total prims in linkset: " + (string)llGetNumberOfPrims());
llOwnerSay(" Managing links 35-82 (48 XyzzyText prims)");
llOwnerSay(" Listening for messages from link 12 (scoreboard)");
llOwnerSay(" Current text sections:");
string leftStatus;
if (leftText != "") {
leftStatus = "SET (" + (string)llStringLength(leftText) + " chars)";
} else {
leftStatus = "EMPTY";
}
llOwnerSay(" Left: " + leftStatus);
string middleLeftStatus;
if (middleLeftText != "") {
middleLeftStatus = "SET";
} else {
middleLeftStatus = "EMPTY";
}
llOwnerSay(" Middle-Left: " + middleLeftStatus);
string middleRightStatus;
if (middleRightText != "") {
middleRightStatus = "SET";
} else {
middleRightStatus = "EMPTY";
}
llOwnerSay(" Middle-Right: " + middleRightStatus);
string rightStatus;
if (rightText != "") {
rightStatus = "SET";
} else {
rightStatus = "EMPTY";
}
llOwnerSay(" Right: " + rightStatus);
} else {
// Non-owner touch - show friendly message
llSay(0, "🏆 Leaderboard shows the top players! Keep playing to climb the ranks!");
}
}
}

View file

@ -2,23 +2,25 @@ PERIL DICE - Professional Single Linkset Game System for Second Life
Created by Rebecca Nod and Noose the Bunny
CURRENT VERSION: 3.0.0 - Commercial CasperVend Release
CURRENT VERSION: 3.1.0 - The "Linkset Stability" Update
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 v3.0.0: Transitioned to commercial CasperVend system! Massive memory optimizations utilizing pre-processor macros for 10-player load stability. Resolved all Z-fighting and alignment glitches on rez to ensure perfectly synchronized graphics.
NEW IN v3.1.0: **LINKSET STABILITY & DYNAMIC DISCOVERY!** Complete architectural decentralization—scripts now find their linked prims by name, making the game board immune to link-order changes or region restarts. Full migration to modern mesh-based **FURWARE Text** for dice and leaderboards. Centralized system constants in `Peril_Constants.lsl` for 100% project consistency.
KEY FEATURES:
- Dynamic Link Discovery: "Bulletproof" linkset architecture that auto-configures by prim name
- FURWARE Text Mesh: Professional, mesh-based displays for dice and leaderboard results
- 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
- Experience Leaderboard: Persistent win tracking via Experience KVP with XyzzyText display
- Bot Support: AI players for testing and gameplay variety
- 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
- Single Linkset: Complete consolidated system for instant deployment
GAME RULES:
1. 2-10 players join and mark ready
@ -49,11 +51,12 @@ INCLUDED:
PERMISSIONS: Copy/Modify/No Transfer
LAND IMPACT: 74 prims
EXPERIENCE: None required
EXPERIENCE: Required for KVP Leaderboard functionality
Perfect for nightclubs, venues, casinos, private parties, group entertainment, community gatherings.
VERSION HISTORY:
v3.1.0: Dynamic Link Discovery, FURWARE Text Mesh, and Centralized Constants
v3.0.0: Commercial CasperVend Release, memory optimizations, and Z-fighting fixes
v2.8.7: Linkset Analysis Tools and structure verification
v2.8.6: Winner Glow Celebration System with 24-second victory recognition

View file

@ -1,3 +1,5 @@
#include "Peril_Constants.lsl"
// === Main Controller - Peril Dice Game (Linkset Version) ===
// Helper function to get display name with fallback to username
@ -10,75 +12,10 @@ string getPlayerName(key id) {
return displayName;
}
//
// LINKSET VERSION - Uses llMessageLinked() instead of llRegionSay()
// All discovery and channel management code removed
//
// =============================================================================
// LINKSET COMMUNICATION CONSTANTS
// DEBUG & LOCAL CONSTANTS
// =============================================================================
#define DEBUG_LOGS 0 // Set to 1 to enable logs, 0 to STRIP them from bytecode and recover memory
#if DEBUG_LOGS
#define dbg(msg) dbg(msg)
#else
#define dbg(msg)
#endif
// Target link numbers (UPDATED after overlay prim insertion)
#define SCOREBOARD_LINK 12 // Scoreboard manager cube
#define LEADERBOARD_LINK 35 // Leaderboard manager (first XyzzyText prim)
#define DICE_LINK 83 // Dice display manager (first dice prim)
// Message constants for link communication
// Scoreboard messages (to link 12)
#define MSG_GAME_STATUS 3001
#define MSG_PLAYER_UPDATE 3002
#define MSG_CLEAR_GAME 3003
#define MSG_REMOVE_PLAYER 3004
#define MSG_UPDATE_PERIL_PLAYER 3005
#define MSG_UPDATE_WINNER 3006
// Leaderboard messages (to link 35)
#define MSG_GAME_WON 3010
#define MSG_GAME_LOST 3011
#define MSG_RESET_LEADERBOARD 3012
// Dice messages (to link 83)
#define MSG_DICE_ROLL 3020
#define MSG_CLEAR_DICE 3021
// Legacy message constants (keep for internal controller communication)
#define MSG_SHOW_DIALOG 101
#define MSG_ROLL_RESULT 102
#define MSG_UPDATE_FLOAT 103
#define MSG_CLEANUP_FLOAT 104
#define MSG_REZ_FLOAT 105
#define MSG_REGISTER_PLAYER 106
#define MSG_REGISTER_PLAYER_REQUEST 9050 // New dedicated message to Player_RegistrationManager
#define MSG_SYNC_GAME_STATE 107
#define MSG_SHOW_MENU 201
#define MSG_TOGGLE_READY 202
#define MSG_QUERY_READY_STATE 210
#define MSG_READY_STATE_RESULT 211
#define MSG_CLEANUP_ALL_FLOATERS 212
#define MSG_QUERY_OWNER_STATUS 213
#define MSG_OWNER_STATUS_RESULT 214
#define MSG_SHOW_ROLL_DIALOG 301
#define MSG_GET_CURRENT_DIALOG 302
#define MSG_PLAYER_WON 551
#define MSG_GET_PICKS_REQUIRED 1002
#define MSG_GET_PICKER_INDEX 1003
// Memory monitoring handled by Controller_Memory.lsl helper script
#define MSG_MEMORY_CHECK 6001
#define MSG_MEMORY_STATS 6002
#define MSG_MEMORY_CLEANUP 6003
#define MSG_MEMORY_REPORT 6004
#define MSG_EMERGENCY_CLEANUP 6005
#define MSG_MEMORY_STATS_REQUEST 6006
// Unified Timer System - prevents conflicts between multiple timer needs
#define TIMER_IDLE 0
#define TIMER_STATUS 1
@ -90,20 +27,9 @@ integer victoryDelayTimer = 0; // Track victory delay timing
// Debug control - set to TRUE for verbose pick debugging, FALSE for normal operation
#define DEBUG_PICKS FALSE
// Verbose logging moved to dedicated Verbose_Logger.lsl script to save memory
// Message constants for verbose logging
#define MSG_VERBOSE_LOG 9020
#define MSG_VERBOSE_TOGGLE 9010
// Script ID for Main Controller (used by Verbose_Logger for prefixes)
#define SCRIPT_ID_MAIN 0
// Message handling constants (delegate to Controller_MessageHandler)
#define MSG_OWNER_MESSAGE 9030
#define MSG_PUBLIC_MESSAGE 9031
#define MSG_REGION_MESSAGE 9032
#define MSG_DIALOG_REQUEST 9033
// Memory reporting function
reportMemoryUsage(string scriptName) {
integer used = llGetUsedMemory();
@ -175,7 +101,8 @@ integer gameTimer = 0;
integer calculateChannel(integer offset) {
string ownerStr = (string)llGetOwner();
string objectStr = (string)llGetKey();
// CRITICAL: Always use root prim (1) for channel calculation so scripts in child prims match
string objectStr = (string)llGetLinkKey(1);
string combinedStr = ownerStr + objectStr;
string hashStr = llMD5String(combinedStr, 0);
@ -259,7 +186,7 @@ sendStatusMessage(string status) {
lastStatus = status;
// CHANGED: Use link message instead of llRegionSay
llMessageLinked(SCOREBOARD_LINK, MSG_GAME_STATUS, status, NULL_KEY);
llMessageLinked(LINK_SCOREBOARD, MSG_GAME_STATUS, status, NULL_KEY);
// Removed ownerMsg call to save memory
@ -283,7 +210,7 @@ updateHelpers() {
if (nameCount > 0) {
integer lastIdx = nameCount - 1;
// Minimal string operations - direct message
llMessageLinked(SCOREBOARD_LINK, MSG_PLAYER_UPDATE,
llMessageLinked(LINK_SCOREBOARD, MSG_PLAYER_UPDATE,
llList2String(names, lastIdx) + "|" +
(string)llList2Integer(lives, lastIdx) + "|" +
(string)llList2Key(players, lastIdx), NULL_KEY);
@ -341,14 +268,14 @@ resetGame() {
victoryInProgress = FALSE;
currentPicker = NULL_KEY;
llMessageLinked(LINK_SET, -99999, "FULL_RESET", NULL_KEY);
llMessageLinked(LINK_SET, MSG_RESET_ALL, "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);
llMessageLinked(LINK_SCOREBOARD, 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);
llMessageLinked(LINK_SCOREBOARD, MSG_UPDATE_WINNER, "", NULL_KEY); // Clear winner glow
llMessageLinked(LINK_DICE_BRIDGE, MSG_CLEAR_DICE, "", NULL_KEY);
dbg("🎮 Game reset! All state cleared (including scoreboard).");
@ -371,8 +298,9 @@ resetGame() {
default {
state_entry() {
DISCOVER_CORE_LINKS();
reportMemoryUsage("Main Controller");
dbg("🎮 Main Controller ready! (Linkset Version)");
dbg("🎮 Main Controller ready! (Discovery Mode)");
// Initialize lockout system
gameOwner = llGetOwner();
@ -388,8 +316,9 @@ default {
}
on_rez(integer start_param) {
DISCOVER_CORE_LINKS();
reportMemoryUsage("Main Controller");
dbg("🔄 Main Controller rezzed - resetting game state...");
dbg("🔄 Main Controller reset via rez...");
// Re-initialize lockout system
gameOwner = llGetOwner();
@ -413,29 +342,22 @@ default {
string playerName = llList2String(names, idx);
// ENHANCED: Check if this player is supposed to be the current picker based on game state
// This handles cases where currentPicker got corrupted due to disconnect
integer shouldBePicking = FALSE;
if (currentPickerIdx < llGetListLength(pickQueue)) {
string expectedPickerName = llList2String(pickQueue, currentPickerIdx);
// Removed debugMsg call to save memory
if (playerName == expectedPickerName) {
shouldBePicking = TRUE;
// Fix corrupted currentPicker state
if (currentPicker != toucher) {
// Removed ownerMsg call to save memory
currentPicker = toucher;
}
}
} else {
// Removed debugMsg call to save memory
}
// FALLBACK: If game is stuck and no one is set as current picker, but we're in pick phase,
// let any registered player attempt to resume (with owner confirmation)
if (!shouldBePicking && currentPicker == NULL_KEY && currentPickerIdx < llGetListLength(pickQueue)) {
if (toucher == llGetOwner()) {
// Removed ownerMsg calls to save memory
// Game stuck error and recovery logging removed
currentPicker = toucher;
shouldBePicking = TRUE;
}
@ -544,8 +466,6 @@ default {
}
}
llMessageLinked(LINK_SET, MSG_SHOW_MENU, "player|" + (string)isStarter, toucher);
} else {
dbg("⚠️ Could not get name for toucher: " + (string)toucher);
}
}
}
@ -562,7 +482,7 @@ default {
}
// Handle registration updates from Player_RegistrationManager
if (num == 9040) { // MSG_UPDATE_MAIN_LISTS
if (num == MSG_UPDATE_MAIN_LISTS) { // MSG_UPDATE_MAIN_LISTS
// ULTRA-OPTIMIZED: Player_RegistrationManager now handles all heavy processing
// Main Controller just adds the player to its master lists
list parts = llParseString2List(str, ["~"], []);
@ -599,7 +519,7 @@ default {
}
// Handle elimination requests
if (num == 999) {
if (num == MSG_ELIMINATE_PLAYER) {
list parts = llParseString2List(str, ["|"], []);
if (llList2String(parts, 0) == "ELIMINATE_PLAYER") {
string eliminatedPlayer = llList2String(parts, 1);
@ -609,7 +529,7 @@ default {
lives = llListReplaceList(lives, [0], idx, idx);
// Send ONLY a direct scoreboard update to show 0 hearts
llMessageLinked(SCOREBOARD_LINK, MSG_PLAYER_UPDATE,
llMessageLinked(LINK_SCOREBOARD, MSG_PLAYER_UPDATE,
eliminatedPlayer + "|0|" + (string)llList2Key(players, idx), NULL_KEY);
// CRITICAL: Check for victory condition BEFORE assigning new peril player
@ -630,7 +550,7 @@ default {
if (potentialWinner != "") {
// SEND WINNER GLOW to scoreboard and trigger floater update
llMessageLinked(SCOREBOARD_LINK, MSG_UPDATE_WINNER, potentialWinner, NULL_KEY);
llMessageLinked(LINK_SCOREBOARD, MSG_UPDATE_WINNER, potentialWinner, NULL_KEY);
llMessageLinked(LINK_SET, MSG_UPDATE_FLOAT, potentialWinner, NULL_KEY); // Update winner's floater
dbg("🏆 [Main Controller] Pre-victory winner glow sent for: " + potentialWinner);
}
@ -657,11 +577,11 @@ default {
if (newPerilPlayer != "") {
perilPlayer = newPerilPlayer;
// Send peril player update to scoreboard for glow effect
llMessageLinked(SCOREBOARD_LINK, MSG_UPDATE_PERIL_PLAYER, newPerilPlayer, NULL_KEY);
llMessageLinked(LINK_SCOREBOARD, 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);
llMessageLinked(LINK_SCOREBOARD, MSG_UPDATE_PERIL_PLAYER, "", NULL_KEY);
}
}
@ -714,7 +634,7 @@ default {
// 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);
llMessageLinked(LINK_SCOREBOARD, MSG_GAME_LOST, eliminatedPlayer, NULL_KEY);
// Status message now sent directly by Roll Module
@ -747,14 +667,14 @@ default {
llSay(0, "✨ ULTIMATE VICTORY! " + winner + " is the Ultimate Survivor!");
llMessageLinked(LINK_SET, MSG_PLAYER_WON, winner, NULL_KEY);
llMessageLinked(LINK_SET, 995, "VICTORY_CONFETTI", NULL_KEY);
llMessageLinked(LINK_SET, MSG_EFFECT_CONFETTI, "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_SCOREBOARD, 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);
llMessageLinked(LINK_SCOREBOARD, MSG_GAME_WON, winner, NULL_KEY);
// Use timer instead of llSleep to avoid immediate reset
dbg("🏆 [Main Controller] Winner glow applied - starting victory delay timer");
@ -806,10 +726,10 @@ default {
}
// Handle emergency reset for stuck games
if (num == -99998 && str == "EMERGENCY_RESET") {
if (num == MSG_EMERGENCY_RESET && str == "EMERGENCY_RESET") {
dbg("🚨 [Main Controller] Emergency reset triggered - sending emergency reset to all scripts");
// Signal all scripts to emergency reset
llMessageLinked(LINK_SET, -99998, "EMERGENCY_RESET", NULL_KEY);
llMessageLinked(LINK_SET, MSG_EMERGENCY_RESET, "EMERGENCY_RESET", NULL_KEY);
llSleep(0.5);
// Then do full reset
resetGame();
@ -822,7 +742,7 @@ default {
if (llList2String(parts, 0) == "GAME_WON") {
string winner = llList2String(parts, 1);
// CHANGED: Use link message instead of llRegionSay
llMessageLinked(DICE_LINK, MSG_DICE_ROLL, winner + "|WON", NULL_KEY);
llMessageLinked(LINK_DICE_BRIDGE, MSG_DICE_ROLL, winner + "|WON", NULL_KEY);
llSleep(2.0);
resetGame();
}
@ -942,7 +862,7 @@ default {
// 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
if (num == MSG_LEAVE_GAME_REQUEST) { // MSG_LEAVE_GAME_REQUEST
// This message comes FROM the Controller_MessageHandler after it has processed the removal
// We need to update the main controller's lists to match
list parts = llParseString2List(str, ["|"], []);
@ -953,7 +873,7 @@ default {
integer idx = llListFindList(names, [leavingName]);
if (idx != -1) {
// CHANGED: Remove player from scoreboard BEFORE updating lists
llMessageLinked(SCOREBOARD_LINK, MSG_REMOVE_PLAYER, leavingName, NULL_KEY);
llMessageLinked(LINK_SCOREBOARD, MSG_REMOVE_PLAYER, leavingName, NULL_KEY);
// Clean up the leaving player's floater using their current channel
integer ch = FLOATER_BASE_CHANNEL + idx;
@ -981,12 +901,12 @@ default {
dbg("🎯 [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
llMessageLinked(LINK_SCOREBOARD, MSG_UPDATE_PERIL_PLAYER, newPerilPlayer, NULL_KEY); // MSG_UPDATE_PERIL_PLAYER
} else {
dbg("⚠️ [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);
llMessageLinked(LINK_SCOREBOARD, MSG_UPDATE_PERIL_PLAYER, "", NULL_KEY);
}
}
@ -1032,7 +952,7 @@ default {
}
// Handle lock/unlock messages from Dialog Handler
if (num == 9001) { // Lock game message
if (num == MSG_LOCK_GAME) { // Lock game message
if (id == gameOwner && str == "LOCK_GAME") {
isLocked = TRUE;
updateFloatingText();
@ -1041,7 +961,7 @@ default {
return;
}
if (num == 9002) { // Unlock game message
if (num == MSG_UNLOCK_GAME) { // Unlock game message
if (id == gameOwner && str == "UNLOCK_GAME") {
isLocked = FALSE;
updateFloatingText();
@ -1051,17 +971,6 @@ default {
}
// Handle verbose logging toggle - redirect to dedicated Verbose_Logger script
if (num == MSG_VERBOSE_TOGGLE && str == "TOGGLE_VERBOSE_LOGS") {
// Forward to dedicated verbose logger script, but exclude ourselves from LINK_SET
// Send to each script individually to avoid the forwarding loop
integer i;
integer linkCount = llGetNumberOfPrims();
for (i = 2; i <= linkCount; i++) { // Start from 2 to skip ourselves (link 1)
llMessageLinked(i, MSG_VERBOSE_TOGGLE, "TOGGLE_VERBOSE_LOGS", id);
}
return;
}
// Handle memory monitor messages
if (num == MSG_EMERGENCY_CLEANUP) {
@ -1071,7 +980,7 @@ default {
}
// Handle reset requests from other scripts (like Game_Manager)
if (num == -99998 && str == "REQUEST_GAME_RESET") {
if (num == MSG_EMERGENCY_RESET && str == "REQUEST_GAME_RESET") {
dbg("🔄 [Main Controller] Reset requested by " + (string)sender + ", executing...");
resetGame();
return;
@ -1130,13 +1039,13 @@ default {
}
if (msg == "Reset Leaderboard") {
// CHANGED: Send to scoreboard, which will handle leaderboard reset
llMessageLinked(SCOREBOARD_LINK, MSG_RESET_LEADERBOARD, "", NULL_KEY);
llMessageLinked(LINK_SCOREBOARD, MSG_RESET_LEADERBOARD, "", NULL_KEY);
dbg("🏆 Leaderboard scores reset - game wins cleared!");
return;
}
if (msg == "Reset All") {
resetGame();
llMessageLinked(SCOREBOARD_LINK, MSG_RESET_LEADERBOARD, "", NULL_KEY);
llMessageLinked(LINK_SCOREBOARD, MSG_RESET_LEADERBOARD, "", NULL_KEY);
dbg("🔄 Complete reset - game and leaderboard cleared!");
return;
}
@ -1275,7 +1184,7 @@ default {
statusTimer = 0;
lastStatus = "";
// CHANGED: Use link message instead of llRegionSay
llMessageLinked(SCOREBOARD_LINK, MSG_GAME_STATUS, "Title", NULL_KEY);
llMessageLinked(LINK_SCOREBOARD, MSG_GAME_STATUS, "Title", NULL_KEY);
dbg("📢 Status cleared - reverted to Title");
currentTimerMode = TIMER_IDLE;
llSetTimerEvent(0);

View file

@ -1,17 +1,15 @@
#include "Peril_Constants.lsl"
// === Dialog Handler (Updated for Peril Dice - Paginated Multiple Picks) ===
#define MSG_SHOW_DIALOG 101
#define MSG_GET_CURRENT_DIALOG 302
// Verbose logging control
integer VERBOSE_LOGGING = TRUE; // Global flag for verbose debug logs
#define MSG_TOGGLE_VERBOSE_LOGS 9998 // Message to toggle verbose logging
// =============================================================================
// DYNAMIC CHANNEL CONFIGURATION
// =============================================================================
// Base channel offset - should match Main.lsl
// Base channel offset must match Main.lsl
#define CHANNEL_BASE -77000
// Calculate channels dynamically to avoid hardcoded conflicts
@ -39,10 +37,8 @@ initializeChannels() {
NUMBERPICK_CHANNEL = calculateChannel(2); // ~-77200 range to match Main.lsl
// Report channel to owner for debugging
if (VERBOSE_LOGGING) {
llOwnerSay("🔧 [Number Picker Dialog] Dynamic channel initialized:");
llOwnerSay(" Number Pick: " + (string)NUMBERPICK_CHANNEL);
}
dbg("🎮 [NumberPicker] 🔧 Dynamic channel initialized:");
dbg(" Number Pick: " + (string)NUMBERPICK_CHANNEL);
}
integer numberPickChannel; // Legacy variable, will be set dynamically
@ -67,7 +63,7 @@ reportMemoryUsage(string scriptName) {
integer total = used + free;
float percentUsed = ((float)used / (float)total) * 100.0;
llOwnerSay("🧠 [" + scriptName + "] Memory: " +
dbg("🧠 [" + scriptName + "] Memory: " +
(string)used + " used, " +
(string)free + " free (" +
llGetSubString((string)percentUsed, 0, 4) + "% used)");
@ -101,20 +97,16 @@ list getNumbersForPage(integer page, integer diceType) {
}
showPickDialog(string name, key id, integer diceType, integer picks) {
if (VERBOSE_LOGGING) {
llOwnerSay("📋 [NumberPicker] showPickDialog called for " + name + ", dice:" + (string)diceType + ", picks:" + (string)picks);
}
dbg("🎮 [NumberPicker] 📋 showPickDialog called for " + name + ", dice:" + (string)diceType + ", picks:" + (string)picks);
if (diceType <= 0) {
llOwnerSay("⚠️ Cannot show dialog: diceType is 0 or invalid.");
dbg("🎮 [Number Picker Dialog] ⚠️ Cannot show dialog: diceType is 0 or invalid.");
return;
}
// Initialize or update picking session
if (currentPlayer != name) {
if (VERBOSE_LOGGING) {
llOwnerSay("📋 [NumberPicker] Starting new session for " + name);
}
dbg("🎮 [NumberPicker] 📋 Starting new session for " + name);
currentPlayer = name;
currentPlayerKey = id;
currentDiceType = diceType;
@ -122,9 +114,7 @@ showPickDialog(string name, key id, integer diceType, integer picks) {
currentPicks = [];
currentPage = 0;
} else {
if (VERBOSE_LOGGING) {
llOwnerSay("📋 [NumberPicker] Updating existing session for " + name);
}
dbg("🎮 [NumberPicker] 📋 Updating existing session for " + name);
}
list options;
@ -176,13 +166,11 @@ showPickDialog(string name, key id, integer diceType, integer picks) {
dialogText = "Pick " + pickText + " (1-" + (string)diceType + ")" + pageInfo + currentPicksText;
}
if (VERBOSE_LOGGING) {
llOwnerSay("📋 [NumberPicker] About to show dialog to " + name + " (" + (string)id + ")");
llOwnerSay("📋 [NumberPicker] Dialog text: " + dialogText);
llOwnerSay("📋 [NumberPicker] Options (" + (string)llGetListLength(options) + "): " + llList2CSV(options));
llOwnerSay("📋 [NumberPicker] Channel: " + (string)numberPickChannel);
llOwnerSay("📋 [NumberPicker] globallyPickedNumbers: " + llList2CSV(globallyPickedNumbers));
}
dbg("🎮 [NumberPicker] 📋 About to show dialog to " + name + " (" + (string)id + ")");
dbg("🎮 [NumberPicker] 📋 Dialog text: " + dialogText);
dbg("🎮 [NumberPicker] 📋 Options (" + (string)llGetListLength(options) + "): " + llList2CSV(options));
dbg("🎮 [NumberPicker] 📋 Channel: " + (string)numberPickChannel);
dbg("🎮 [NumberPicker] 📋 globallyPickedNumbers: " + llList2CSV(globallyPickedNumbers));
llDialog(id, dialogText, options, numberPickChannel);
}
@ -211,15 +199,13 @@ default {
// Set up managed listener with dynamic channel
listenHandle = llListen(numberPickChannel, "", NULL_KEY, "");
llOwnerSay("🎮 Number Picker Dialog Handler ready!");
dbg("🎮 [NumberPicker] Number Picker Dialog Handler ready!");
}
on_rez(integer start_param) {
reportMemoryUsage("🎮 NumberPicker Dialog");
if (VERBOSE_LOGGING) {
llOwnerSay("🔄 Number Picker rezzed - reinitializing...");
}
dbg("🎮 [NumberPicker] 🔄 Number Picker rezzed - reinitializing...");
// Re-initialize dynamic channels
initializeChannels();
@ -241,45 +227,28 @@ default {
// Set up managed listener with dynamic channel
listenHandle = llListen(numberPickChannel, "", NULL_KEY, "");
if (VERBOSE_LOGGING) {
llOwnerSay("✅ Number Picker reset complete after rez!");
}
dbg("🎮 [NumberPicker] ✅ Number Picker reset complete after rez!");
}
link_message(integer sender, integer num, string str, key id) {
// Handle verbose logging toggle
if (num == MSG_TOGGLE_VERBOSE_LOGS) {
VERBOSE_LOGGING = !VERBOSE_LOGGING;
if (VERBOSE_LOGGING) {
llOwnerSay("🔊 [NumberPicker] Verbose logging ENABLED");
} else {
llOwnerSay("🔊 [NumberPicker] Verbose logging DISABLED");
}
return;
}
// Handle dialog close command
if (num == -9999 && str == "CLOSE_ALL_DIALOGS") {
if (VERBOSE_LOGGING) {
llOwnerSay("🚫 [NumberPicker] CLOSE_ALL_DIALOGS received! currentPlayer: " + currentPlayer + ", key: " + (string)currentPlayerKey);
}
if (num == MSG_CLOSE_ALL_DIALOGS && str == "CLOSE_ALL_DIALOGS") {
dbg("🎮 [NumberPicker] 🚫 CLOSE_ALL_DIALOGS received! currentPlayer: " + currentPlayer + ", key: " + (string)currentPlayerKey);
if (currentPlayerKey != NULL_KEY) {
if (VERBOSE_LOGGING) {
llOwnerSay("🚫 [NumberPicker] Forced dialog close for " + currentPlayer);
}
dbg("🎮 [NumberPicker] 🚫 Forced dialog close for " + currentPlayer);
currentPlayer = "";
currentPlayerKey = NULL_KEY;
currentPicks = [];
} else {
if (VERBOSE_LOGGING) {
llOwnerSay("🚫 [NumberPicker] No active dialog to close");
}
dbg("🎮 [NumberPicker] 🚫 No active dialog to close");
}
return;
}
// Handle full reset from main controller
if (num == -99999 && str == "FULL_RESET") {
if (num == MSG_RESET_ALL && str == "FULL_RESET") {
// Reset all dialog state
currentPlayer = "";
currentPlayerKey = NULL_KEY;
@ -288,14 +257,14 @@ default {
currentPicks = [];
globallyPickedNumbers = [];
currentPage = 0;
llOwnerSay("🎮 Number Picker Dialog Handler reset!");
dbg("🎮 [NumberPicker] Number Picker Dialog Handler reset!");
return;
}
if (num == MSG_SHOW_DIALOG) {
list parts = llParseString2List(str, ["|"], []);
if (llGetListLength(parts) < 3) {
llOwnerSay("⚠️ Malformed MSG_SHOW_DIALOG input: " + str);
dbg("🎮 [NumberPicker] ⚠️ Malformed MSG_SHOW_DIALOG input: " + str);
return;
}
@ -349,9 +318,7 @@ default {
string playerName = str;
// Only show dialog if this player is the current picker
if (currentPlayer == playerName && currentPlayerKey == id) {
if (VERBOSE_LOGGING) {
llOwnerSay("🔄 Restoring dialog for " + playerName);
}
dbg("🎮 [NumberPicker] 🔄 Restoring dialog for " + playerName);
showPickDialog(currentPlayer, currentPlayerKey, currentDiceType, picksNeeded);
} else {
llRegionSayTo(id, 0, "❌ You don't have an active dialog to restore.");
@ -362,17 +329,13 @@ default {
listen(integer channel, string name, key id, string message) {
// Ignore any responses if no active dialog session
if (currentPlayerKey == NULL_KEY || currentPlayer == "") {
if (VERBOSE_LOGGING) {
llOwnerSay("🚫 [NumberPicker] Ignoring stale dialog response (no active session). Player: " + name + ", Key: " + (string)id + ", Message: " + message);
}
dbg("🎮 [NumberPicker] 🚫 Ignoring stale dialog response (no active session). Player: " + name + ", Key: " + (string)id + ", Message: " + message);
return;
}
// Ignore responses from wrong player
if (id != currentPlayerKey) {
if (VERBOSE_LOGGING) {
llOwnerSay("🚫 [NumberPicker] Ignoring dialog response from wrong player. Expected: " + (string)currentPlayerKey + ", Got: " + (string)id + ", Message: " + message);
}
dbg("🎮 [NumberPicker] 🚫 Ignoring dialog response from wrong player. Expected: " + (string)currentPlayerKey + ", Got: " + (string)id + ", Message: " + message);
return;
}
@ -381,17 +344,13 @@ default {
if (messageNum > 0 && messageNum <= currentDiceType) {
// This looks like a number selection - verify it's not already globally picked
if (llListFindList(globallyPickedNumbers, [message]) != -1) {
if (VERBOSE_LOGGING) {
llOwnerSay("🚫 [NumberPicker] Ignoring stale dialog response - number " + message + " already globally picked. Player: " + name);
}
dbg("🎮 [NumberPicker] 🚫 Ignoring stale dialog response - number " + message + " already globally picked. Player: " + name);
return;
}
// Also verify it's not already in current picks
if (llListFindList(currentPicks, [message]) != -1) {
if (VERBOSE_LOGGING) {
llOwnerSay("🚫 [NumberPicker] Ignoring stale dialog response - number " + message + " already in current picks. Player: " + name);
}
dbg("🎮 [NumberPicker] 🚫 Ignoring stale dialog response - number " + message + " already in current picks. Player: " + name);
return;
}
}
@ -419,27 +378,19 @@ default {
// Handle completion
if (message == "✅ Done") {
if (VERBOSE_LOGGING) {
llOwnerSay("📋 [NumberPicker] Done button clicked by " + name + ", currentPicks: " + llList2CSV(currentPicks) + ", needed: " + (string)picksNeeded);
}
dbg("📋 [NumberPicker] Done button clicked by " + name + ", currentPicks: " + llList2CSV(currentPicks) + ", needed: " + (string)picksNeeded);
if (llGetListLength(currentPicks) >= picksNeeded) {
string picksStr = llList2CSV(currentPicks);
string response = "HUMAN_PICKED:" + currentPlayer + ":" + picksStr;
if (VERBOSE_LOGGING) {
llOwnerSay("📤 [NumberPicker] SENDING HUMAN_PICKED: " + response);
}
llMessageLinked(LINK_SET, -9998, response, NULL_KEY);
dbg("📤 [NumberPicker] SENDING HUMAN_PICKED: " + response);
llMessageLinked(LINK_SET, MSG_HUMAN_PICKED, response, NULL_KEY);
// Reset state
if (VERBOSE_LOGGING) {
llOwnerSay("📋 [NumberPicker] Resetting session state for " + currentPlayer);
}
dbg("🎮 [NumberPicker] 📋 Resetting session state for " + currentPlayer);
currentPlayer = "";
currentPlayerKey = NULL_KEY;
currentPicks = [];
} else {
if (VERBOSE_LOGGING) {
llOwnerSay("⚠️ " + currentPlayer + " needs " + (string)(picksNeeded - llGetListLength(currentPicks)) + " more picks");
}
dbg("🎮 [NumberPicker] ⚠️ " + currentPlayer + " needs " + (string)(picksNeeded - llGetListLength(currentPicks)) + " more picks");
showPickDialog(currentPlayer, currentPlayerKey, currentDiceType, picksNeeded);
}
return;
@ -452,7 +403,7 @@ default {
// Add to picks (numbers already picked won't be shown in dialog)
currentPicks += [message];
} else {
llOwnerSay("⚠️ " + currentPlayer + " has already picked enough numbers");
dbg("🎮 [NumberPicker] ⚠️ " + currentPlayer + " has already picked enough numbers");
}
// Show updated dialog

131
Peril_Constants.lsl Normal file
View file

@ -0,0 +1,131 @@
// =============================================================================
// PERIL DICE - CENTRAL CONSTANTS (Preprocessor Version)
// =============================================================================
// --- Linkset Layout (Dynamic Discovery) ---
integer LINK_CONTROLLER = 1; // Usually Root (link 1)
integer LINK_SCOREBOARD = -1; // "Scoreboard:0:0"
integer LINK_DICE_BRIDGE = -1; // "FURWARE text mesh:Dice:0:0"
integer LINK_LEADERBOARD_BRIDGE = -1; // "FURWARE text mesh:Leaderboard:0:0"
// Macro to discover core links by name
#define DISCOVER_CORE_LINKS() { \
integer _i; \
integer _total = llGetNumberOfPrims(); \
for (_i = 1; _i <= _total; _i++) { \
string _name = llGetLinkName(_i); \
if (_name == "Scoreboard:0:0") LINK_SCOREBOARD = _i; \
else if (_name == "FURWARE text mesh:Dice:0:0") LINK_DICE_BRIDGE = _i; \
else if (_name == "FURWARE text mesh:Leaderboard:0:0") LINK_LEADERBOARD_BRIDGE = _i; \
else if (LINK_CONTROLLER == 1 && _i == 1) { /* Default to root if not found */ } \
} \
}
// --- FURWARE Text Commands ---
#define FW_DATA "fw_data"
#define FW_CONF "fw_conf"
#define FW_READY "fw_ready"
#define FW_DONE "fw_done"
// --- System/Debug (9000-9999) ---
#define DEBUG_LOGS 1 // GLOBAL MASTER SWITCH - Set to 1 for dev feedback, 0 to STRIP from memory
#if DEBUG_LOGS
#define dbg(msg) llOwnerSay(msg)
#else
#define dbg(msg) ;
#endif
#define MSG_RESET_ALL -99999
#define MSG_EMERGENCY_RESET -99998
#define MSG_DEBUG_PICKS_ON 7001
#define MSG_DEBUG_PICKS_OFF 7002
#define MSG_UPDATE_MAIN_LISTS 9040
// --- Game Flow (3000-3099) ---
#define MSG_GAME_STATUS 3001
#define MSG_PLAYER_UPDATE 3002
#define MSG_CLEAR_GAME 3003
#define MSG_REMOVE_PLAYER 3004
#define MSG_UPDATE_PERIL_PLAYER 3005
#define MSG_UPDATE_WINNER 3006
#define MSG_GAME_WON 3010
#define MSG_GAME_LOST 3011
#define MSG_RESET_LEADERBOARD 3012
#define MSG_DICE_ROLL 3020
#define MSG_CLEAR_DICE 3021
#define MSG_DICE_CLEAR 3021 // Alias for consistency in dice bridge
#define MSG_ROLL_RESULT 102
#define MSG_PLAYER_WON 551
#define MSG_ELIMINATE_PLAYER 999
#define MSG_CONTINUE_GAME 998
#define MSG_CONTINUE_ROUND 998 // Alias for consistency across scripts
#define MSG_EFFECT_CONFETTI 995
#define MSG_GET_DICE_TYPE 1001
#define MSG_DICE_TYPE_RESULT 1005
// --- Player/Registration (9000-9099) ---
#define MSG_REGISTER_PLAYER 106 // Legacy Registration ID (Backward Compatibility)
#define MSG_REGISTER_PLAYER_REQUEST 9050 // New Standard Registration ID
#define MSG_OWNER_MESSAGE 9030
#define MSG_PUBLIC_MESSAGE 9031
#define MSG_REGION_MESSAGE 9032
#define MSG_DIALOG_REQUEST 9033
#define MSG_DIALOG_FORWARD_REQUEST 9060
#define MSG_SYNC_GAME_STATE 107
#define MSG_SYNC_PICKQUEUE 2001
#define MSG_SYNC_LIGHTWEIGHT 9070
#define MSG_SYNC_LEGACY 9071
#define MSG_SERIALIZE_GAME_STATE 9072
#define MSG_LEAVE_GAME_REQUEST 8006
#define MSG_LEAVE_GAME 8007
#define MSG_REQUEST_PLAYER_LIST_KICK 8009
#define MSG_LOCK_GAME 9001
#define MSG_UNLOCK_GAME 9002
// --- Dialogs & Menus (100-399) ---
#define MSG_SHOW_MENU 201
#define MSG_SHOW_DIALOG 101
#define MSG_GET_CURRENT_DIALOG 302
#define MSG_SHOW_ROLL_DIALOG 301
#define MSG_CLOSE_ALL_DIALOGS -9999
#define MSG_TOGGLE_READY 202
#define MSG_QUERY_READY_STATE 210
#define MSG_READY_STATE_RESULT 211
#define MSG_QUERY_OWNER_STATUS 213
#define MSG_OWNER_STATUS_RESULT 214
// --- Float Management (100-199) ---
#define MSG_REZ_FLOAT 105
#define MSG_REZ_FLOATER 105 // Alias for consistency
#define MSG_UPDATE_FLOAT 103
#define MSG_CLEANUP_FLOAT 104
#define MSG_CLEANUP_ALL_FLOATERS 212
// --- Data Requests (200-299) ---
#define MSG_REQUEST_PLAYER_LIST_PICK 202
#define MSG_PLAYER_LIST_PICK_RESULT 203
#define MSG_PLAYER_LIST_RESULT 203 // Alias for consistency across scripts
#define MSG_PICK_ACTION 204
#define MSG_PICK_LIST_RESULT 205
#define MSG_OWNER_PICK_MANAGER 206
#define MSG_LIFE_LOOKUP_REQUEST 207
#define MSG_LIFE_LOOKUP_RESULT 208
#define MSG_QUERY_OWNER_STATUS 213
// --- Memory Management (6000-6099) ---
#define MSG_MEMORY_CHECK 6001
#define MSG_MEMORY_STATS 6002
#define MSG_MEMORY_CLEANUP 6003
#define MSG_MEMORY_REPORT 6004
#define MSG_EMERGENCY_CLEANUP 6005
#define MSG_MEMORY_STATS_REQUEST 6006
#define MSG_MEMORY_STATS_RESULT 6007
// --- Bot Management ---
#define MSG_BOT_PICKED -9997
#define MSG_HUMAN_PICKED -9998
#define MSG_BOT_COMMAND -9999
// --- Helper Functions Support ---
#define MSG_GET_PICKS_REQUIRED 1002
#define MSG_GET_PICKER_INDEX 1003

View file

@ -1,281 +0,0 @@
// ====================================================================
// Peril Dice Updater - External Update Box
// ====================================================================
// This script goes in a standalone object (the "Updater Box")
// It downloads scripts from GitHub and installs them into the game
// ====================================================================
#define CURRENT_VERSION "2.8.9"
#define GITHUB_API_URL "https://api.github.com/repos/RebeccaNod1/Peril/releases/latest"
#define GITHUB_RAW_URL "https://raw.githubusercontent.com/RebeccaNod1/Peril/main/"
// Communication channels
#define UPDATER_CHANNEL -7723847 // Must match Update_Receiver.lsl
// State tracking
key targetGame = NULL_KEY;
integer targetPin = 0;
list downloadQueue = [];
string currentFile = "";
integer installIndex = 0;
integer isBusy = FALSE;
// HTTP tracking
key currentHttpRequest = NULL_KEY;
string currentOperation = "";
// Script manifest - List of all scripts to update
// Format: "ScriptName.lsl|LinkNumber"
// Note: Link numbers will be verified via UUID request
list SCRIPT_MANIFEST = [
"Main_Controller_Linkset.lsl|1",
"Game_Manager.lsl|1",
"Controller_Memory.lsl|1",
"Controller_MessageHandler.lsl|1",
"Player_RegistrationManager.lsl|1",
"Player_DialogHandler.lsl|1",
"NumberPicker_DialogHandler.lsl|1",
"Floater_Manager.lsl|1",
"Roll_ConfettiModule.lsl|1",
"Bot_Manager.lsl|1",
"Game_Calculator.lsl|1",
"Verbose_Logger.lsl|1",
"System_Debugger.lsl|1",
"Update_Receiver.lsl|1", // Updates the receiver itself!
"Game_Scoreboard_Manager_Linkset.lsl|12",
"Leaderboard_Communication_Linkset.lsl|35",
"XyzzyText_Dice_Bridge_Linkset.lsl|83",
"xyzzy_Master_script.lsl|XYZZY" // Special handling for xyzzy
];
// Map of Link Number -> UUID (populated by game response)
list linkUUIDs = []; // Format: [LinkNum, UUID, LinkNum, UUID...]
// Helper to get UUID for a link number
key getLinkUUID(integer linkNum) {
integer idx = llListFindList(linkUUIDs, [linkNum]);
if (idx != -1) {
return llList2Key(linkUUIDs, idx + 1);
}
return NULL_KEY;
}
// Manual JSON parsing (same as receiver)
string extractJsonValue(string json, string keyName) {
string quote = "\"";
string searchPattern = quote + keyName + quote + ":" + quote;
integer start = llSubStringIndex(json, searchPattern);
if (start == -1) return "";
integer valueStart = start + llStringLength(searchPattern);
integer valueEnd = llSubStringIndex(llGetSubString(json, valueStart, -1), quote);
if (valueEnd == -1) return "";
return llGetSubString(json, valueStart, valueStart + valueEnd - 1);
}
// Start the update process for a specific game
startUpdate(key gameId, string version, integer pin) {
if (isBusy) {
llRegionSayTo(gameId, UPDATER_CHANNEL, "UPDATE_BUSY");
return;
}
isBusy = TRUE;
targetGame = gameId;
targetPin = pin;
llOwnerSay("🚀 Starting update for game " + (string)gameId);
llRegionSayTo(targetGame, UPDATER_CHANNEL, "UPDATE_STARTING|" + version);
// Step 1: Request Link UUIDs from the game
// We need these to install scripts to specific links
llRegionSayTo(targetGame, UPDATER_CHANNEL, "REQUEST_LINK_UUIDS");
// Set timeout for UUID response
llSetTimerEvent(10.0);
}
// Continue update after getting UUIDs
startDownloadSequence() {
installIndex = 0;
downloadNextScript();
}
downloadNextScript() {
if (installIndex >= llGetListLength(SCRIPT_MANIFEST)) {
// All done!
finishUpdate();
return;
}
string entry = llList2String(SCRIPT_MANIFEST, installIndex);
list parts = llParseString2List(entry, ["|"], []);
currentFile = llList2String(parts, 0);
string linkTarget = llList2String(parts, 1);
llOwnerSay("📥 Downloading " + currentFile + "...");
string url = GITHUB_RAW_URL + currentFile;
currentOperation = "download_script";
currentHttpRequest = llHTTPRequest(url, [HTTP_METHOD, "GET"], "");
}
installScript(string content) {
string scriptEntry = llList2String(SCRIPT_MANIFEST, installIndex);
list scriptParts = llParseString2List(scriptEntry, ["|"], []);
string filename = llList2String(scriptParts, 0);
string linkTargetStr = llList2String(scriptParts, 1);
llOwnerSay("💾 Installing " + filename + "...");
// Determine target UUIDs
list targets = [];
key targetUUID;
if (linkTargetStr == "XYZZY") {
// Install to all xyzzy prims (links 35-82)
integer i;
for (i = 35; i <= 82; i++) {
targetUUID = getLinkUUID(i);
if (targetUUID != NULL_KEY) targets += [targetUUID];
}
} else {
integer linkNum = (integer)linkTargetStr;
if (linkNum == 1) {
targets = [targetGame]; // Root prim is the game object itself
} else {
targetUUID = getLinkUUID(linkNum);
if (targetUUID != NULL_KEY) targets += [targetUUID];
}
}
if (llGetListLength(targets) == 0) {
llOwnerSay("⚠️ No valid targets found for " + filename + " (Target: " + linkTargetStr + ")");
// Skip but continue
installIndex++;
downloadNextScript();
return;
}
// Install to all targets
integer t;
for (t = 0; t < llGetListLength(targets); t++) {
key target = llList2Key(targets, t);
llRemoteLoadScriptPin(target, filename, targetPin, TRUE, 0);
}
// Move to next
installIndex++;
downloadNextScript();
}
finishUpdate() {
llOwnerSay("✅ Update sequence complete!");
llRegionSayTo(targetGame, UPDATER_CHANNEL, "UPDATE_COMPLETE|" + CURRENT_VERSION);
isBusy = FALSE;
targetGame = NULL_KEY;
targetPin = 0;
linkUUIDs = [];
}
default {
state_entry() {
llSetText("📦 Peril Dice Updater\nv" + CURRENT_VERSION + "\nTouch to check version", <0,1,0>, 1.0);
llListen(UPDATER_CHANNEL, "", NULL_KEY, "");
}
touch_start(integer num) {
llOwnerSay("🔍 Checking GitHub for latest version...");
currentOperation = "version_check";
currentHttpRequest = llHTTPRequest(GITHUB_API_URL, [HTTP_METHOD, "GET"], "");
}
listen(integer channel, string name, key id, string message) {
if (channel == UPDATER_CHANNEL) {
list parts = llParseString2List(message, ["|"], []);
string command = llList2String(parts, 0);
if (command == "PING_UPDATER") {
llRegionSayTo(id, UPDATER_CHANNEL, "UPDATER_AVAILABLE|" + CURRENT_VERSION);
}
else if (command == "UPDATE_REQUEST") {
string version = llList2String(parts, 1); // Version requested (not used currently, we install latest)
integer pin = (integer)llList2String(parts, 2);
startUpdate(id, version, pin);
}
else if (command == "LINK_UUIDS_RESPONSE") {
if (id != targetGame) return;
// Parse UUIDs: "12:uuid,35:uuid,..."
string payload = llList2String(parts, 1);
list pairs = llParseString2List(payload, [","], []);
integer i;
for (i = 0; i < llGetListLength(pairs); i++) {
string pair = llList2String(pairs, i);
list kv = llParseString2List(pair, [":"], []);
if (llGetListLength(kv) == 2) {
integer ln = (integer)llList2String(kv, 0);
key uid = (key)llList2String(kv, 1);
// Update or add
integer idx = llListFindList(linkUUIDs, [ln]);
if (idx != -1) {
linkUUIDs = llListReplaceList(linkUUIDs, [uid], idx+1, idx+1);
} else {
linkUUIDs += [ln, uid];
}
}
}
// Reset timer as we got data
llSetTimerEvent(5.0); // Wait a bit more for other chunks if any
// If we have enough critical data, we can start
// For now, let's just wait for the timer to trigger the start
// This allows multiple chunks to arrive
}
}
}
timer() {
llSetTimerEvent(0.0);
if (isBusy && installIndex == 0) {
// Timer fired while waiting for UUIDs
if (llGetListLength(linkUUIDs) > 0) {
llOwnerSay("✅ Received link data. Starting installation...");
startDownloadSequence();
} else {
llOwnerSay("❌ Timed out waiting for Link UUIDs.");
llRegionSayTo(targetGame, UPDATER_CHANNEL, "UPDATE_FAILED|Timeout waiting for link data");
isBusy = FALSE;
}
}
}
http_response(key request_id, integer status, list metadata, string body) {
if (request_id != currentHttpRequest) return;
if (currentOperation == "version_check") {
if (status == 200) {
string latestVersion = extractJsonValue(body, "tag_name");
llOwnerSay("📊 Latest GitHub Version: " + latestVersion);
llOwnerSay("📦 Updater Version: v" + CURRENT_VERSION);
} else {
llOwnerSay("❌ GitHub check failed: " + (string)status);
}
}
else if (currentOperation == "download_script") {
if (status == 200) {
installScript(body);
} else {
llOwnerSay("❌ Failed to download " + currentFile + ": " + (string)status);
llRegionSayTo(targetGame, UPDATER_CHANNEL, "UPDATE_FAILED|Download failed for " + currentFile);
isBusy = FALSE;
}
}
}
}

View file

@ -1,291 +1,99 @@
#include "Peril_Constants.lsl"
// === Player Status Float - Enhanced Display ===
// Helper function to get display name with fallback to username
string getPlayerName(key id) {
string displayName = llGetDisplayName(id);
if (displayName == "") {
// Fallback to legacy username if display name is unavailable
displayName = llKey2Name(id);
}
return displayName;
}
key target;
string displayText;
string myName;
// Listen handle management
integer listenHandle = -1;
#define MSG_SYNC_GAME_STATE 107
list lives;
list picksData;
string perilPlayer;
list names;
// Memory reporting function
// Memory reporting function - defined before use
reportMemoryUsage(string scriptName) {
integer used = llGetUsedMemory();
integer free = llGetFreeMemory();
integer total = used + free;
float percentUsed = ((float)used / (float)total) * 100.0;
llOwnerSay("🧠 [" + scriptName + "] Memory: " +
dbg("🧠 [" + 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++) {
string entry = llList2String(picksData, i);
list parts = llParseString2List(entry, ["|"], []);
if (llGetListLength(parts) >= 2 && llList2String(parts, 0) == nameInput) {
string picks = llList2String(parts, 1);
if (picks == "") {
return [];
}
// Check for corruption markers (^ symbols that shouldn't be in picks)
if (llSubStringIndex(picks, "^") != -1) {
return [];
}
// Convert semicolons back to commas, then parse
picks = llDumpList2String(llParseString2List(picks, [";"], []), ",");
return llParseString2List(picks, [","], []);
}
}
return [];
}
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 + ")");
string currentDesc = llGetObjectDesc();
if (currentDesc == "" || currentDesc == "(No Description)") currentDesc = "Unknown";
reportMemoryUsage("📱 Player Float (" + currentDesc + ")");
llSetText("⏳ Waiting...", <1,1,1>, 1.0);
llSetTimerEvent(1.0);
}
on_rez(integer start_param) {
// Clean up any existing listeners
if (listenHandle != -1) {
llListenRemove(listenHandle);
}
// Set up managed listener
if (listenHandle != -1) llListenRemove(listenHandle);
listenHandle = llListen(start_param, "", NULL_KEY, "");
// 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
// Correct LSL function name is llSetRemoteScriptAccessPin
llSetRemoteScriptAccessPin(1337);
}
listen(integer channel, string name, key id, string message) {
if (llSubStringIndex(message, "FLOAT:") == 0) {
// Universal Floater API - Format: CMD:avKey|T=Text|G=Glow|C=Color|H=HeartTexture
if (llSubStringIndex(message, "CMD:") == 0) {
list parts = llParseString2List(message, ["|"], []);
if (llGetListLength(parts) >= 2) {
target = (key)llGetSubString(llList2String(parts, 0), 6, -1);
displayText = llList2String(parts, 1);
// Extract life count from the display text to set heart texture
// Look for "Lives: X" pattern in the text
integer livesPos = llSubStringIndex(displayText, "Lives: ");
if (livesPos != -1) {
string livesStr = llGetSubString(displayText, livesPos + 7, livesPos + 7);
integer lifeCount = (integer)livesStr;
string heartTexture = (string)lifeCount + "_hearts";
// Set heart texture based on current life count
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)
if (llGetListLength(parts) < 1) return;
string header = llList2String(parts, 0);
target = (key)llGetSubString(header, 4, -1);
integer i;
for (i = 1; i < llGetListLength(parts); i++) {
string pair = llList2String(parts, i);
integer eqIdx = llSubStringIndex(pair, "=");
if (eqIdx != -1) {
string keyStr = llGetSubString(pair, 0, eqIdx - 1);
string valStr = llGetSubString(pair, eqIdx + 1, -1);
// IMPROVED: Special visual treatment for elimination (0 hearts)
if (lifeCount == 0) {
// Make 0 hearts more prominent with red glow
if (keyStr == "T") {
displayText = valStr;
llSetText(displayText, <1,1,1>, 1.0);
}
else if (keyStr == "G") {
llSetLinkPrimitiveParamsFast(LINK_THIS, [PRIM_GLOW, ALL_SIDES, (float)valStr]);
}
else if (keyStr == "C") {
llSetLinkPrimitiveParamsFast(LINK_THIS, [PRIM_COLOR, ALL_SIDES, (vector)valStr, 1.0]);
}
else if (keyStr == "H") {
llSetLinkPrimitiveParamsFast(LINK_THIS, [
PRIM_GLOW, ALL_SIDES, 0.3,
PRIM_COLOR, ALL_SIDES, <1.0, 0.0, 0.0>, 1.0
PRIM_TEXTURE, 1, valStr, <1,1,0>, ZERO_VECTOR, 0.0,
PRIM_TEXTURE, 2, valStr, <1,1,0>, ZERO_VECTOR, 0.0,
PRIM_TEXTURE, 3, valStr, <1,1,0>, ZERO_VECTOR, 0.0,
PRIM_TEXTURE, 4, valStr, <1,1,0>, ZERO_VECTOR, 0.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"], []);
list filteredLines = [];
integer i;
for (i = 0; i < llGetListLength(lines); i++) {
string line = llList2String(lines, i);
if (llSubStringIndex(line, "Lives: ") != 0) {
filteredLines += [line];
}
}
displayText = llDumpList2String(filteredLines, "\n");
}
llSetText(displayText, <1,1,1>, 1.0);
}
}
else if (message == "CLEANUP") {
llOwnerSay("🪟 Cleaning up...");
llDie();
}
else if (llSubStringIndex(message, "SET_NAME:") == 0) {
myName = llGetSubString(message, 9, -1);
// 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);
}
}
link_message(integer sender, integer num, string str, key id) {
if (num == MSG_SYNC_GAME_STATE) {
list parts = llParseString2List(str, ["~"], []);
if (llGetListLength(parts) < 4) return;
lives = llCSV2List(llList2String(parts, 0));
// Use ^ delimiter for picksData to match the main system
string picksDataStr = llList2String(parts, 1);
if (picksDataStr == "" || picksDataStr == "EMPTY") {
picksData = [];
} else {
picksData = llParseString2List(picksDataStr, ["^"], []);
}
string receivedPeril = llList2String(parts, 2);
if (receivedPeril == "NONE") {
perilPlayer = ""; // Convert placeholder back to empty
} else {
perilPlayer = receivedPeril;
}
names = llCSV2List(llList2String(parts, 3));
integer nameIdx = llListFindList(names, [myName]);
if (nameIdx == -1) return;
list picks = getPicksFor(myName);
integer lifeCount = llList2Integer(lives, nameIdx);
string perilDisplay;
// If the perilPlayer string is empty or contains a comma (indicating
// multiple names), treat the game as not yet started. This avoids
// showing multiple players as the peril player before the first
// round begins. Once a single peril player is assigned, it will
// not contain a comma and will be displayed normally.
if (perilPlayer == "" || llSubStringIndex(perilPlayer, ",") != -1) {
perilDisplay = "Waiting for game to start...";
} else {
perilDisplay = perilPlayer;
}
string picksDisplay = llList2CSV(picks);
// 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;
}
llSetText(txt, <1,1,1>, 1.0);
}
}
timer() {
if (target != NULL_KEY && getPlayerName(target) != "") {
vector pos = llList2Vector(llGetObjectDetails(target, [OBJECT_POS]), 0) + <1,0,1>;
llSetRegionPos(pos);
if (target != NULL_KEY) {
list details = llGetObjectDetails(target, [OBJECT_POS]);
if (llGetListLength(details) > 0) {
vector pos = llList2Vector(details, 0) + <1,0,1>;
llSetRegionPos(pos);
}
}
}
}

View file

@ -1,9 +1,4 @@
#define DEBUG_LOGS 0 // Set to 1 to enable logs, 0 to STRIP from memory completely
#if DEBUG_LOGS
#define dbg(msg) dbg(msg)
#else
#define dbg(msg)
#endif
#include "Peril_Constants.lsl"
// === Dialog Handler (Owner & Player) with unified Ready/Leave menu and join support ===
@ -24,7 +19,7 @@ string getPlayerName(key id) {
// DYNAMIC CHANNEL CONFIGURATION
// =============================================================================
// Base channel offset - should match Main.lsl
// Base channel offset must match Main.lsl
#define CHANNEL_BASE -77000
// Calculate channels dynamically to avoid hardcoded conflicts
@ -32,7 +27,8 @@ integer calculateChannel(integer offset) {
// Use BOTH owner's key AND object's key to make channels unique per game instance
// This prevents interference when same owner has multiple game tables
string ownerStr = (string)llGetOwner();
string objectStr = (string)llGetKey();
// CRITICAL: Always use root prim (1) for channel calculation so scripts in child prims match
string objectStr = (string)llGetLinkKey(1);
string combinedStr = ownerStr + objectStr;
// Create a more unique hash using both keys
@ -80,21 +76,6 @@ rotation controller_rot;
list foundDisplays; // Store found display info: [type, key, offset, rotation, ...]
integer scanInProgress = FALSE;
#define MSG_SHOW_MENU 201
#define MSG_PICK_ACTION 204
#define MSG_PLAYER_LIST_RESULT 203
#define MSG_PICK_LIST_RESULT 205
#define MSG_LIFE_LOOKUP 207
#define MSG_REGISTER_PLAYER 106
#define MSG_REZ_FLOAT 105
#define MSG_TOGGLE_READY 202
#define MSG_QUERY_READY_STATE 210
#define MSG_READY_STATE_RESULT 211
#define MSG_CLEANUP_ALL_FLOATERS 212
#define MSG_QUERY_OWNER_STATUS 213
#define MSG_OWNER_STATUS_RESULT 214
#define MSG_GET_CURRENT_DIALOG 302
// Categorized owner menu system
list mainOwnerOptions = ["👥 Player Management", "🔄 Reset Options", "🛠️ Troubleshooting", "⬅️ Back to Game"];
list mainOwnerOptionsLocked = ["👥 Player Management", "🔄 Reset Options", "🛠️ Troubleshooting", "🔓 Unlock Game", "⬅️ Back to Game"];
@ -103,7 +84,7 @@ list mainOwnerOptionsUnlocked = ["👥 Player Management", "🔄 Reset Options",
// Sub-menu options for each category
list playerManagementOptions = ["Add Test Player", "Kick Player", "⬅️ Back to Main"];
list resetOptions = ["Reset Game", "Reset Leaderboard", "Reset All", "⬅️ Back to Main"];
list troubleshootingOptions = ["Cleanup Floaters", "Force Floaters", "🔍 Toggle Verbose Logs", "⬅️ Back to Main"];
list troubleshootingOptions = ["Cleanup Floaters", "Force Floaters", "⬅️ Back to Main"];
// State tracking for dynamic ready menu with race condition protection
key pendingMenuPlayer = NULL_KEY;
@ -125,7 +106,7 @@ reportMemoryUsage(string scriptName) {
integer total = used + free;
float percentUsed = ((float)used / (float)total) * 100.0;
llOwnerSay("🧠 [" + scriptName + "] Memory: " +
dbg("🧠 [" + scriptName + "] Memory: " +
(string)used + " used, " +
(string)free + " free (" +
llGetSubString((string)percentUsed, 0, 4) + "% used)");
@ -420,7 +401,7 @@ default {
link_message(integer sender, integer num, string str, key id) {
// Handle full reset from main controller
if (num == -99999 && str == "FULL_RESET") {
if (num == MSG_RESET_ALL && str == "FULL_RESET") {
// Reset dialog state including race condition protection
pendingMenuPlayer = NULL_KEY;
pendingMenuIsStarter = FALSE;
@ -496,7 +477,7 @@ default {
showPickManageMenu(id, playerNames);
}
// Handle player list result for kick functionality
else if (num == 8009) {
else if (num == MSG_REQUEST_PLAYER_LIST_KICK) {
dbg("🔍 [Dialog Handler] Received kick player list string: '" + str + "'");
list rawNames = llParseString2List(str, [","], []);
dbg("🔍 [Dialog Handler] Raw parsed into " + (string)llGetListLength(rawNames) + " names: " + llList2CSV(rawNames));
@ -527,12 +508,12 @@ default {
if (llStringTrim(returnedName, STRING_TRIM) == llStringTrim(currentPickTarget, STRING_TRIM)) {
dbg("✅ Matched pick list for: " + returnedName);
currentPickList = llCSV2List(picks);
llMessageLinked(LINK_THIS, MSG_LIFE_LOOKUP, currentPickTarget, id);
llMessageLinked(LINK_THIS, MSG_LIFE_LOOKUP_REQUEST, currentPickTarget, id);
} else {
dbg("❌ Name mismatch: got " + returnedName + " but expected " + currentPickTarget);
}
}
else if (num == MSG_LIFE_LOOKUP) {
else if (num == MSG_LIFE_LOOKUP_REQUEST) {
list parts = llParseString2List(str, ["|"], []);
if (llList2String(parts, 0) == currentPickTarget) {
currentPickLimit = (integer)llList2String(parts, 1);
@ -609,7 +590,7 @@ default {
if (id == gameOwner) {
isLocked = TRUE;
// Notify Main Controller to update floating text
llMessageLinked(LINK_SET, 9001, "LOCK_GAME", id);
llMessageLinked(LINK_SET, MSG_LOCK_GAME, "LOCK_GAME", id);
dbg("🔒 Game has been LOCKED - Only owner can access any features");
llSay(0, "🔒 Game has been locked by the owner. Only the owner can access any features.");
showOwnerMenu(id); // Refresh the owner menu
@ -621,7 +602,7 @@ default {
if (id == gameOwner) {
isLocked = FALSE;
// Notify Main Controller to update floating text
llMessageLinked(LINK_SET, 9002, "UNLOCK_GAME", id);
llMessageLinked(LINK_SET, MSG_UNLOCK_GAME, "UNLOCK_GAME", id);
dbg("🔓 Game has been UNLOCKED - All players can access menus");
llSay(0, "🔓 Game has been unlocked by the owner. All features are now available.");
showOwnerMenu(id); // Refresh the owner menu
@ -664,13 +645,13 @@ default {
else if (msg == "Join Game") {
// Use the avatar's key as the identifier
string pname = getPlayerName(id);
llMessageLinked(LINK_SET, MSG_REGISTER_PLAYER, pname + "|" + (string)id, NULL_KEY);
llMessageLinked(LINK_SET, MSG_REGISTER_PLAYER_REQUEST, pname + "|" + (string)id, id);
// Floater Manager will automatically rez the floater during registration
}
else if (msg == "Leave Game") {
// Send leave game message to Controller_MessageHandler (not sync channel 107!)
string pname = getPlayerName(id);
llMessageLinked(LINK_SET, 8007, "LEAVE_GAME|" + pname + "|" + (string)id, NULL_KEY);
llMessageLinked(LINK_SET, MSG_LEAVE_GAME, "LEAVE_GAME|" + pname + "|" + (string)id, id);
}
else if (msg == "Ready" || msg == "Not Ready") {
// Toggle ready state (both buttons do the same thing - toggle)
@ -679,7 +660,7 @@ default {
}
else if (msg == "Kick Player") {
// Show kick player menu - request player list first
llMessageLinked(LINK_SET, 8009, "REQUEST_PLAYER_LIST_KICK", id);
llMessageLinked(LINK_SET, MSG_REQUEST_PLAYER_LIST_KICK, "REQUEST_PLAYER_LIST_KICK", id);
}
else if (msg == "Cleanup Floaters") {
// Universal floater cleanup that works even after script resets
@ -692,35 +673,28 @@ default {
// This will be handled by Main Controller's dialog listener
// No need to forward via link_message - the Main Controller is already listening on DIALOG_CHANNEL
}
else if (msg == "🔍 Toggle Verbose Logs") {
// Toggle verbose logging system-wide
dbg("🔍 Toggling verbose logging system-wide...");
llMessageLinked(LINK_SET, 9010, "TOGGLE_VERBOSE_LOGS", id);
// Return to troubleshooting menu after toggling
showTroubleshootingMenu(id);
}
else if (msg == "Manage Picks") {
llMessageLinked(LINK_THIS, 202, "REQUEST_PLAYER_LIST", id);
llMessageLinked(LINK_THIS, MSG_REQUEST_PLAYER_LIST_PICK, "REQUEST_PLAYER_LIST", id);
}
else if (llSubStringIndex(msg, "🛠 ") == 0) {
currentPickTarget = llStringTrim(llDeleteSubString(msg, 0, 1), STRING_TRIM);
dbg("🎯 Pick target set to: " + currentPickTarget);
currentPickList = [];
llMessageLinked(LINK_THIS, 206, currentPickTarget, id);
llMessageLinked(LINK_THIS, MSG_OWNER_PICK_MANAGER, currentPickTarget, id);
}
else if (msg == "Add Pick") {
askForNewPick(id);
}
else if (msg == "⬅️ Back") {
llMessageLinked(LINK_THIS, 202, "REQUEST_PLAYER_LIST", id);
llMessageLinked(LINK_THIS, MSG_REQUEST_PLAYER_LIST_PICK, "REQUEST_PLAYER_LIST", id);
}
else if (llSubStringIndex(msg, "REMOVE: ") == 0) {
string pick = llGetSubString(msg, 8, -1);
string payload = "REMOVE_PICK~" + currentPickTarget + "|" + pick;
llMessageLinked(LINK_THIS, MSG_PICK_ACTION, payload, id);
llSleep(0.2);
llMessageLinked(LINK_THIS, 206, currentPickTarget, id);
llMessageLinked(LINK_THIS, MSG_OWNER_PICK_MANAGER, currentPickTarget, id);
}
// Handle kick player selection
else if (llSubStringIndex(msg, "👢 ") == 0) {
@ -730,7 +704,7 @@ default {
string playerToKick = llList2String(kickOriginalNames, idx);
llRegionSayTo(id, 0, "👢 Kicking player: " + playerToKick + " from the game...");
// Send kick message to Controller_MessageHandler (not sync channel 107!)
llMessageLinked(LINK_SET, 8007, "KICK_PLAYER|" + playerToKick + "|" + (string)id, NULL_KEY);
llMessageLinked(LINK_SET, MSG_LEAVE_GAME, "KICK_PLAYER|" + playerToKick + "|" + (string)id, id);
} else {
dbg("⚠️ Could not find original name for kick selection: " + msg);
}
@ -759,7 +733,7 @@ default {
string payload = "ADD_PICK~" + currentPickTarget + "|" + msg;
llMessageLinked(LINK_THIS, MSG_PICK_ACTION, payload, id);
llSleep(0.2);
llMessageLinked(LINK_THIS, 206, currentPickTarget, id);
llMessageLinked(LINK_THIS, MSG_OWNER_PICK_MANAGER, currentPickTarget, id);
}
// Handle position scan responses from follower displays
else if ((channel == SCOREBOARD_DATA_CHANNEL || channel == LEADERBOARD_DATA_CHANNEL || channel == DICE_DATA_CHANNEL) &&

View file

@ -1,25 +1,9 @@
#include "Peril_Constants.lsl"
// === Player Registration Manager ===
// Handles all player registration, join/leave operations to reduce Main Controller memory usage
// This script takes over the memory-heavy player management operations
// Message constants for communication
#define MSG_REGISTER_PLAYER_REQUEST 9050 // Dedicated message from Main Controller for registration
#define MSG_OWNER_MESSAGE 9030
#define MSG_PUBLIC_MESSAGE 9031
#define MSG_REGION_MESSAGE 9032
#define MSG_SHOW_MENU 201
#define MSG_UPDATE_MAIN_LISTS 9040 // Optimized message to update main controller lists
#define MSG_SYNC_GAME_STATE 107
// Scoreboard and display message constants
#define SCOREBOARD_LINK 12
#define MSG_PLAYER_UPDATE 3002
// Dialog forwarding constants
#define MSG_SHOW_DIALOG 101
#define MSG_SHOW_ROLL_DIALOG 301
#define MSG_DIALOG_FORWARD_REQUEST 9060 // New message for dialog forwarding requests
// Game state tracking (synced from Main Controller)
list players = [];
list names = [];
@ -37,7 +21,7 @@ reportMemoryUsage(string scriptName) {
integer total = used + free;
float percentUsed = ((float)used / (float)total) * 100.0;
llOwnerSay("🧠 [" + scriptName + "] Memory: " +
dbg("🧠 [" + scriptName + "] Memory: " +
(string)used + " used, " +
(string)free + " free (" +
llGetSubString((string)percentUsed, 0, 4) + "% used)");
@ -68,7 +52,7 @@ regionMsg(key player, string msg) {
handlePlayerRegistration(string regData, key requesterId) {
// MEMORY DEBUG: Check memory at start
integer memStart = llGetFreeMemory();
llOwnerSay("🧠 [RegMgr] Memory at start: " + (string)memStart + " free");
dbg("🔍 [RegMgr] Memory at start: " + (string)memStart + " free");
list parts = llParseString2List(regData, ["|"], []);
string newName = llList2String(parts, 0);
@ -102,7 +86,7 @@ handlePlayerRegistration(string regData, key requesterId) {
// MEMORY DEBUG: Check memory after adding to lists
integer memAfterLists = llGetFreeMemory();
llOwnerSay("🧠 [RegMgr] Memory after adding to lists: " + (string)memAfterLists + " free (used " + (string)(memStart - memAfterLists) + ")");
dbg("🔍 [RegMgr] Memory after adding to lists: " + (string)memAfterLists + " free (used " + (string)(memStart - memAfterLists) + ")");
// Count human players to determine if this is the starter (needed for both bot and human logic)
integer humanCount = 0;
@ -133,7 +117,7 @@ handlePlayerRegistration(string regData, key requesterId) {
// OPTIMIZED: Handle all heavy processing here to minimize Main Controller work
// Send direct scoreboard update (Main Controller doesn't need to do this)
llMessageLinked(SCOREBOARD_LINK, MSG_PLAYER_UPDATE, newName + "|3|" + (string)newKey, NULL_KEY);
llMessageLinked(LINK_SCOREBOARD, MSG_PLAYER_UPDATE, newName + "|3|" + (string)newKey, NULL_KEY);
// Prepare sync message (Main Controller doesn't need to build this)
string syncMessage = "";
@ -154,7 +138,7 @@ handlePlayerRegistration(string regData, key requesterId) {
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);
llMessageLinked(LINK_SET, MSG_REZ_FLOATER, newName + "|" + (string)newKey, newKey);
// Send minimal update to Main Controller - just the essential data it needs
string updateData = (string)ch + "~" + newName;
@ -171,13 +155,13 @@ handlePlayerRegistration(string regData, key requesterId) {
default {
state_entry() {
reportMemoryUsage("Player Registration Manager");
llOwnerSay("🎯 Player Registration Manager ready - handling player join/leave operations");
DISCOVER_CORE_LINKS();
dbg("🎯 [RegMgr] Player Registration Manager ready - discovery complete! Scoreboard: " + (string)LINK_SCOREBOARD);
}
on_rez(integer start_param) {
reportMemoryUsage("Player Registration Manager");
llOwnerSay("🎯 Player Registration Manager rezzed - ready for player management");
DISCOVER_CORE_LINKS();
dbg("🎯 [RegMgr] Player Registration Manager rezzed - resetting state...");
// Reset local state
players = [];
names = [];
@ -215,7 +199,7 @@ default {
}
// KEEP our player keys - they are authoritative
llOwnerSay("🔄 [RegMgr] Synced game state, preserving " + (string)llGetListLength(players) + " player keys");
dbg("🔄 [RegMgr] Synced game state, preserving " + (string)llGetListLength(players) + " player keys");
}
return;
}
@ -242,30 +226,30 @@ default {
if (playerKey != NULL_KEY) {
if (dialogType == "SHOW_DIALOG") {
llMessageLinked(LINK_SET, MSG_SHOW_DIALOG, dialogPayload, playerKey);
llOwnerSay("📋 [RegMgr] Forwarded dialog to " + targetPlayerName + ": " + dialogPayload);
dbg("📋 [RegMgr] Forwarded dialog to " + targetPlayerName + ": " + dialogPayload);
} else if (dialogType == "SHOW_ROLL_DIALOG") {
llMessageLinked(LINK_SET, MSG_SHOW_ROLL_DIALOG, dialogPayload, playerKey);
llOwnerSay("🎲 [RegMgr] Forwarded roll dialog to " + targetPlayerName);
dbg("🎲 [RegMgr] Forwarded roll dialog to " + targetPlayerName);
}
} else {
llOwnerSay("⚠️ [RegMgr] Player " + targetPlayerName + " has NULL_KEY");
dbg("⚠️ [RegMgr] Player " + targetPlayerName + " has NULL_KEY");
}
} else {
llOwnerSay("⚠️ [RegMgr] Player " + targetPlayerName + " not found in registry");
dbg("⚠️ [RegMgr] Player " + targetPlayerName + " not found in registry");
}
}
return;
}
// Handle reset
if (num == -99999 && str == "FULL_RESET") {
if (num == MSG_RESET_ALL && str == "FULL_RESET") {
players = [];
names = [];
lives = [];
readyPlayers = [];
roundStarted = FALSE;
gameStarting = FALSE;
llOwnerSay("🎯 Player Registration Manager reset complete");
dbg("🎯 [RegMgr] Player Registration Manager reset complete");
return;
}
}

View file

@ -1,13 +1,15 @@
# 🎲 Peril Dice — Professional Single Linkset Game System for Second Life
**Created by Rebecca Nod and Noose the Bunny**
**Current Version: 3.0.0 - Commercial CasperVend Release**
**Current Version: 3.1.0 - The "Linkset Stability" Update**
## 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 v3.0.0**: **COMMERCIAL CASPERVEND RELEASE & MEMORY OPTIMIZATION** - Fully integrated with the commercial CasperVend delivery system. Over 170 legacy debug mechanisms were stripped using an advanced pre-processor macro to free critical memory for 10-player stability. Completely eliminated Z-fighting UI alignment glitches with new `on_rez` state synchronization routines.
**🚀 NEW in v3.1.0**: **LINKSET STABILITY & DYNAMIC DISCOVERY** - Complete architectural decentralization. Scripts now discovery their linked prims by name at runtime (`Scoreboard:0:0`, `profile:R:C`), making the game board immune to link-order changes or region-side glitches. Full migration from legacy XYText to modern mesh-based **FURWARE Text** for dice and leaderboards.
**📦 NEW in v3.0.0**: **COMMERCIAL CASPERVEND RELEASE & EXPERIENCE LEADERBOARD**
**🔍 NEW in v2.8.7**: **ENHANCED LINKSCANNER SYSTEM** - Complete 84-prim structure analysis and verification tools. *(Note: The legacy GitHub Update System introduced in v2.8.7 has been superseded by CasperVend in v3.0.0).*
@ -25,6 +27,26 @@ 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 v3.1.0 Improvements (Current) 🚀
### 🔍 **Dynamic Link Discovery System**
- **Zero-Fixed Links**: Eliminated all hardcoded link indices. Each script now performs a "Dynamic Discovery" sweep on startup to find its sibling prims by name.
- **Relink-Resilient Architecture**: You can now edit the linkset, add decorative prims, or reorder the board without breaking the game logic.
- **Region-Cross Safety**: Solves "vanishing link" issues often seen during region restarts or teleports by automatically re-scanning for link targets.
### 🎭 **FURWARE Text Mesh Migration**
- **Enhanced Visuals**: Replaced the legacy 192nd-century XYText (prim-flipping) with modern, mesh-based **FURWARE Text**.
- **Simplified Bridging**: New bridge scripts handle all dice and leaderboard formatting, ensuring 100% synchronization across all displays.
- **Performance Boost**: Mesh-based text mapping is significantly more efficient than standard prim texture-flipping for complex board data.
### 🧩 **Centralized System Constants**
- **`Peril_Constants.lsl`**: All system-wide settings, message IDs, and macros are now centralized in a single header file.
- **Unified Debugging**: Implemented a global `dbg()` pre-processor macro. One switch in the constants file toggles all debug levels for the entire game board simultaneously.
### 🧠 **Restricted Scope & Loop Stability**
- **Compiler Safety**: Refactored the entire codebase to use top-level variable declarations for all search loops, ensuring compatibility with strict LSL compiler versions.
- **Loop Reset Protection**: Eliminated `continue` keywords in critical logic blocks, replacing them with robust `if` validation to prevent loop-hang race conditions.
## Major v3.0.0 Improvements 🚀
### 📦 **Commercial CasperVend Integration**
@ -37,6 +59,10 @@ Peril Dice is a multiplayer elimination game where each player selects numbers b
- **Bytecode Reduction**: Stripped over 170 verbose `llOwnerSay` debug statements, significantly reducing compiled bytecode size.
- **Load Stability**: Freed critical script memory in the Main Controller, Game Manager, and Dialog Handler to ensure rock-solid stability during 10-player stress tests.
### 🏆 **Experience-Based Leaderboard**
- **Persistent Storage**: Leaderboard data is now stored in Second Life Experience Keys (KVP), ensuring lifetime persistence of player statistics.
- **Region Independence**: Player wins and losses are tracked globally, surviving object resets and region changes.
### 🎨 **Z-Fighting & Alignment Fixes**
- **Microscopic Position Offsets**: Resolved persistent Z-fighting on the custom UI (scoreboard, header, start button) by applying microscopic local axis offsets (`0.005m` and `0.008m` relative to the backboard).
- **Initialization Stability**: Patched Scoreboard, Leaderboard, and Dice Bridge scripts with `on_rez` auto-reset handlers to ensure flawless texture synchronization and state recovery upon rezzing.
@ -594,11 +620,18 @@ Dice type is automatically chosen to ensure at least 3 picks per player:
## Version
**Current Version**: 3.0.0
**Last Updated**: March 24, 2026
**Status**: Production Ready - Commercial CasperVend Release
**Current Version**: 3.1.0
**Last Updated**: April 04, 2026
**Status**: Production Ready - Linkset Stability Release
### Key Achievements in v2.8.3:
### Key Achievements in v3.1.0:
- ✅ **Architectural Decentralization**: Implemented Dynamic Link Discovery (no hardcoded links).
- ✅ **FURWARE Integration**: Complete migration to mesh-based text displays.
- ✅ **Centralized Constants**: All logic settings moved to `Peril_Constants.lsl`.
- ✅ **Compiler Compatibility**: Resolved all scope/brace errors and optimized loop logic for 10-player stability.
- ✅ **Resilient Communication**: System now survives link-order changes and region restarts.
### Key Achievements in v3.0.0:
- ✅ **CRITICAL FIX**: Shield detection logic corrected - no more undeserved damage when shields are provided
- ✅ **MAJOR FIX**: Complete initialization system overhaul - games work immediately after rezzing
- ✅ Added comprehensive `on_rez()` handlers to all critical scripts (7 scripts updated)

View file

@ -2,13 +2,13 @@
========================================================
CREATED BY REBECCA NOD AND NOOSE THE BUNNY
CURRENT VERSION: 3.0.0 - COMMERCIAL CASPERVEND RELEASE
CURRENT VERSION: 3.1.0 - THE "LINKSET STABILITY" UPDATE
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 V3.0.0: Fully integrated with the commercial CasperVend delivery system. Over 170 legacy debug mechanisms were stripped using an advanced pre-processor macro to free memory for 10-player stability. Completely eliminated Z-fighting UI alignment glitches with new on_rez state synchronization routines.
NEW IN V3.1.0: LINKSET STABILITY & DYNAMIC DISCOVERY! Complete architectural decentralization—scripts now find their linked prims by name, making the game board immune to link-order changes or region-side glitches. Full migration from legacy XYText to modern mesh-based FURWARE Text for dice and leaderboards. Centralized system constants in Peril_Constants.lsl for 100% project consistency.
NEW IN V2.8.7: Enhanced LinkScanner System for complete 84-prim structure analysis and verification tools. (Legacy GitHub update system replaced by CasperVend in v3.0.0).
@ -103,6 +103,27 @@ Player Count | Dice Type
5–6 | d20
7–10 | d30
RECENT IMPROVEMENTS (V3.1.0)
============================
🔍 DYNAMIC LINK DISCOVERY SYSTEM
- Zero-Fixed Links: Eliminated all hardcoded link indices. Each script now performs a "Dynamic Discovery" sweep on startup to find its sibling prims by name.
- Relink-Resilient Architecture: You can now edit the linkset, add decorative prims, or reorder the board without breaking the game logic.
- Region-Cross Safety: Solves "vanishing link" issues often seen during region restarts or teleports by automatically re-scanning for link targets on rez.
🎭 FURWARE TEXT MESH MIGRATION
- Enhanced Visuals: Replaced the legacy XYText (prim-flipping) with modern, mesh-based FURWARE Text.
- Simplified Bridging: New bridge scripts handle all dice and leaderboard formatting, ensuring 100% synchronization across all displays.
- Performance Boost: Mesh-based text mapping is significantly more efficient than standard prim texture-flipping for complex board data.
🧩 CENTRALIZED SYSTEM CONSTANTS
- Peril_Constants.lsl: All system-wide settings, message IDs, and macros are now centralized in a single header file.
- Unified Debugging: Implemented a global dbg() pre-processor macro. One switch in the constants file toggles all debug levels for the entire game board simultaneously.
🧠 RESTRICTED SCOPE & LOOP STABILITY
- Compiler Safety: Refactored the entire codebase to use top-level variable declarations for all search loops, ensuring compatibility with strict LSL compiler versions.
- Loop Reset Protection: Eliminated continue keywords in critical logic blocks, replacing them with robust if validation to prevent loop-hang race conditions.
RECENT IMPROVEMENTS (V3.0.0)
============================
@ -116,6 +137,10 @@ RECENT IMPROVEMENTS (V3.0.0)
- Bytecode Reduction: Stripped over 170 verbose llOwnerSay debug statements, significantly reducing compiled bytecode size.
- Load Stability: Freed critical script memory in the Main Controller, Game Manager, and Dialog Handler to ensure rock-solid stability during 10-player stress tests.
🏆 EXPERIENCE-BASED LEADERBOARD
- Persistent Storage: Leaderboard data is now stored in Second Life Experience Keys (KVP), ensuring lifetime persistence of player statistics.
- Region Independence: Player wins and losses are tracked globally, surviving object resets and region changes.
🎨 Z-FIGHTING & ALIGNMENT FIXES
- Initialization Stability: Patched Scoreboard, Leaderboard, and Dice Bridge scripts with on_rez auto-reset handlers to ensure flawless texture synchronization upon rezzing.
- Microscopic Position Offsets: Resolved persistent Z-fighting on the custom UI (scoreboard, header, start button) by applying microscopic local axis offsets.
@ -522,9 +547,9 @@ PREVIOUS IMPROVEMENTS (V2.4.0)
VERSION INFORMATION
===================
Current Version: 3.0.0
Last Updated: March 24, 2026
Status: Production Ready - Commercial CasperVend Release
Current Version: 3.1.0
Last Updated: April 04, 2026
Status: Production Ready - Linkset Stability Release
ORIGINAL GAME RULES CREDIT
==========================

View file

@ -1,22 +1,12 @@
#include "Peril_Constants.lsl"
// === Roll and Confetti Module (with Roll Dialog Handler) ===
#define MSG_SHOW_DIALOG 101
#define MSG_ROLL_RESULT 102
#define MSG_UPDATE_FLOAT 103
#define MSG_CLEANUP_FLOAT 104
#define MSG_REZ_FLOAT 105
#define MSG_SYNC_GAME_STATE 107
#define MSG_SHOW_ROLL_DIALOG 301
// Dice messages (sent to Main Controller)
#define MSG_DICE_ROLL 3020
#define MSG_CLEAR_DICE 3021
// =============================================================================
// DYNAMIC CHANNEL CONFIGURATION
// =============================================================================
// Base channel offset - should match Main.lsl
// Base channel offset must match Main.lsl
#define CHANNEL_BASE -77000
// Calculate channels dynamically to avoid hardcoded conflicts
@ -48,10 +38,10 @@ initializeChannels() {
SCOREBOARD_CHANNEL_3 = calculateChannel(8); // ~-77800 range
// Report channels to owner for debugging
llOwnerSay("🔧 [Roll Confetti] Dynamic channels initialized:");
llOwnerSay(" Roll Dialog: " + (string)ROLLDIALOG_CHANNEL);
llOwnerSay(" Scoreboard 1: " + (string)SCOREBOARD_CHANNEL_1);
llOwnerSay(" Scoreboard 3: " + (string)SCOREBOARD_CHANNEL_3);
dbg("🎲 [Roll Module] 🔧 [Roll Confetti] Dynamic channels initialized:");
dbg("🎲 [Roll Module] Roll Dialog: " + (string)ROLLDIALOG_CHANNEL);
dbg("🎲 [Roll Module] Scoreboard 1: " + (string)SCOREBOARD_CHANNEL_1);
dbg("🎲 [Roll Module] Scoreboard 3: " + (string)SCOREBOARD_CHANNEL_3);
}
integer rollDialogChannel; // Legacy variable, will be set dynamically
@ -67,10 +57,8 @@ integer diceType = 6; // Store the dice type for rolling
integer shouldRoll = FALSE; // Flag to trigger roll after dice type is received
integer rollInProgress = FALSE; // Prevent multiple simultaneous rolls
integer lastRollTime = 0; // Track when last roll happened
integer diceTypeProcessed = FALSE; // Prevent duplicate dice type processing
// Verbose logging control - toggled by owner
integer VERBOSE_LOGGING = FALSE;
integer diceTypeProcessed = FALSE; // Confetti particle system for winner celebrations
#define PARTICLE_TEXTURE "7d8ae121-e171-12ae-f5b6-7cc3c0395c7b"
// Memory reporting function
reportMemoryUsage(string scriptName) {
@ -79,7 +67,7 @@ reportMemoryUsage(string scriptName) {
integer total = used + free;
float percentUsed = ((float)used / (float)total) * 100.0;
llOwnerSay("🧠 [" + scriptName + "] Memory: " +
dbg("🧠 [" + scriptName + "] Memory: " +
(string)used + " used, " +
(string)free + " free (" +
llGetSubString((string)percentUsed, 0, 4) + "% used)");
@ -142,11 +130,9 @@ confetti() {
default {
state_entry() {
reportMemoryUsage("🎲 Roll Confetti Module");
// Initialize dynamic channels
initializeChannels();
rollDialogChannel = ROLLDIALOG_CHANNEL; // Set legacy variable
DISCOVER_CORE_LINKS();
reportMemoryUsage("Roll Module");
dbg("🎲 [Roll Module] ready - discovery complete! Bridge: " + (string)LINK_DICE_BRIDGE);
// Clean up any existing listeners
if (listenHandle != -1) {
@ -166,12 +152,13 @@ default {
// Set up managed listener with dynamic channel
listenHandle = llListen(rollDialogChannel, "", NULL_KEY, "");
llOwnerSay("🎲 Roll Confetti Module ready!");
dbg("🎲 [Roll Module] 🎲 Roll Confetti Module ready!");
}
on_rez(integer start_param) {
reportMemoryUsage("🎲 Roll Confetti Module");
llOwnerSay("🔄 Roll Confetti Module rezzed - reinitializing...");
DISCOVER_CORE_LINKS();
reportMemoryUsage("Roll Module");
dbg("🎲 [Roll Module] reset via rez...");
// Re-initialize dynamic channels
initializeChannels();
@ -198,33 +185,19 @@ default {
// Set up managed listener with dynamic channel
listenHandle = llListen(rollDialogChannel, "", NULL_KEY, "");
llOwnerSay("✅ Roll Confetti Module reset complete after rez!");
dbg("🎲 [Roll Module] ✅ Roll Confetti Module reset complete after rez!");
}
link_message(integer sender, integer num, string str, key id) {
// Debug: Log critical messages to see if Roll Module is working
if (num == MSG_SHOW_ROLL_DIALOG) {
llOwnerSay("🎲 [Roll Module] RECEIVED MSG_SHOW_ROLL_DIALOG: " + str + " id: " + (string)id);
dbg("🎲 [Roll Module] 🎲 [Roll Module] RECEIVED MSG_SHOW_ROLL_DIALOG: " + str + " id: " + (string)id);
}
if (num == 301) {
llOwnerSay("🎲 [Roll Module] RECEIVED 301: " + str + " id: " + (string)id);
if (num == MSG_SHOW_ROLL_DIALOG) {
dbg("🎲 [Roll Module] 🎲 [Roll Module] RECEIVED 301: " + str + " id: " + (string)id);
}
if (num == 996) {
llOwnerSay("🎲 [Roll Module] RECEIVED GET_DICE_TYPE: " + str);
}
// Handle verbose logging toggle from Main Controller
if (num == 9011 && llSubStringIndex(str, "VERBOSE_LOGGING|") == 0) {
list parts = llParseString2List(str, ["|"], []);
if (llGetListLength(parts) >= 2) {
VERBOSE_LOGGING = (integer)llList2String(parts, 1);
if (VERBOSE_LOGGING) {
llOwnerSay("🔍 [Roll Module] Verbose logging ON");
} else {
llOwnerSay("🔍 [Roll Module] Verbose logging OFF");
}
}
return;
if (num == MSG_GET_DICE_TYPE) {
dbg("🎲 [Roll Module] 🎲 [Roll Module] RECEIVED GET_DICE_TYPE: " + str);
}
// Handle full reset from main controller
@ -236,19 +209,19 @@ default {
perilPlayer = "";
// Stop any active particles
llParticleSystem([]);
llOwnerSay("🎆 Roll Confetti Module reset!");
dbg("🎲 [Roll Module] 🎆 Roll Confetti Module reset!");
return;
}
// Handle game state sync from main controller
if (num == MSG_SYNC_GAME_STATE) {
if (VERBOSE_LOGGING) llOwnerSay("🔍 [Roll Module] Received sync: " + str);
dbg("🎲 [Roll Module] 🔍 [Roll Module] Received sync: " + str);
list parts = llParseString2List(str, ["~"], []);
if (VERBOSE_LOGGING) llOwnerSay("🔍 [Roll Module] Parsed into " + (string)llGetListLength(parts) + " parts");
dbg("🎲 [Roll Module] 🔍 [Roll Module] Parsed into " + (string)llGetListLength(parts) + " parts");
// Handle special RESET sync message
if (llGetListLength(parts) >= 5 && llList2String(parts, 0) == "RESET") {
llOwnerSay("🔄 [Roll Module] Received reset sync - ignoring during reset");
dbg("🎲 [Roll Module] 🔄 [Roll Module] Received reset sync - ignoring during reset");
return;
}
@ -256,29 +229,32 @@ default {
lives = llCSV2List(llList2String(parts, 0));
// Use ^ delimiter for picksData to avoid comma conflicts
string picksDataStr = llList2String(parts, 1);
if (VERBOSE_LOGGING) llOwnerSay("🔍 [Roll Module] Received picksDataStr: '" + picksDataStr + "'");
dbg("🎲 [Roll Module] 🔍 [Roll Module] Received picksDataStr: '" + picksDataStr + "'");
if (picksDataStr == "" || picksDataStr == "EMPTY") {
picksData = [];
if (VERBOSE_LOGGING) llOwnerSay("🔍 [Roll Module] Set picksData to empty");
dbg("🎲 [Roll Module] 🔍 [Roll Module] Set picksData to empty");
} else {
picksData = llParseString2List(picksDataStr, ["^"], []);
if (VERBOSE_LOGGING) llOwnerSay("🔍 [Roll Module] Set picksData: " + llDumpList2String(picksData, " | "));
dbg("🎲 [Roll Module] 🔍 [Roll Module] Set picksData: " + llDumpList2String(picksData, " | "));
}
string receivedPeril = llList2String(parts, 2);
if (receivedPeril == "NONE") {
perilPlayer = "";
} else {
perilPlayer = receivedPeril;
}
names = llCSV2List(llList2String(parts, 3));
if (VERBOSE_LOGGING) llOwnerSay("🔍 [Roll Module] Updated names: " + llDumpList2String(names, ", "));
dbg("🎲 [Roll Module] 🔍 [Roll Module] Updated names: " + llDumpList2String(names, ", "));
}
return;
}
if (num == 995) {
if (num == MSG_EFFECT_CONFETTI) {
if (str == "VICTORY_CONFETTI") {
llOwnerSay("✨ ULTIMATE VICTORY CELEBRATION!");
dbg("🎲 [Roll Module] ✨ ULTIMATE VICTORY CELEBRATION!");
confetti();
}
return;
@ -288,25 +264,25 @@ default {
// Check if this is a next round prompt
if (llSubStringIndex(str, "_NEXT_ROUND") != -1) {
string playerName = llGetSubString(str, 0, llSubStringIndex(str, "_NEXT_ROUND") - 1);
llOwnerSay("🎯 Prompting " + playerName + " to start next round...");
dbg("🎲 [Roll Module] 🎯 Prompting " + playerName + " to start next round...");
// 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
llMessageLinked(LINK_SET, MSG_CONTINUE_GAME, "", NULL_KEY); // Empty peril player - Game Manager will select one
} else {
// Clear dice display directly (link 83)
llMessageLinked(83, 3021, "", NULL_KEY);
llMessageLinked(LINK_DICE_BRIDGE, MSG_CLEAR_DICE, "", NULL_KEY);
// Update peril player from the roll dialog message
perilPlayer = str;
llOwnerSay("🎲 Prompting " + str + " to roll the dice. Setting perilPlayer to: " + perilPlayer);
dbg("🎲 [Roll Module] 🎲 Prompting " + str + " to roll the dice. Setting perilPlayer to: " + perilPlayer);
// Check if this is a bot (Bot names)
if (llSubStringIndex(str, "Bot") == 0) {
// Auto-roll for bots - request dice type first
llOwnerSay("🤖 " + str + " (bot) is requesting dice type for auto-roll...");
dbg("🎲 [Roll Module] 🤖 " + str + " (bot) is requesting dice type for auto-roll...");
rollInProgress = TRUE; // Set roll in progress for bot
shouldRoll = TRUE; // Set flag to perform roll when dice type is received
// Request current dice type from Calculator
llMessageLinked(LINK_SET, 1001, (string)llGetListLength(names), NULL_KEY);
llMessageLinked(LINK_SET, MSG_GET_DICE_TYPE, (string)llGetListLength(names), NULL_KEY);
} else {
// Reset any previous roll state before showing dialog
rollInProgress = FALSE;
@ -319,19 +295,19 @@ default {
// Handle dice type response - store dice type and perform roll if requested
else if (num == 1005) { // MSG_DICE_TYPE_RESULT
else if (num == MSG_DICE_TYPE_RESULT) { // MSG_DICE_TYPE_RESULT
diceType = (integer)str;
llOwnerSay("🎲 [Roll Module] Received dice type: d" + (string)diceType);
dbg("🎲 [Roll Module] 🎲 [Roll Module] Received dice type: d" + (string)diceType);
// If roll was requested from listen handler, perform it now
if (shouldRoll && rollInProgress) {
shouldRoll = FALSE; // Reset flag
llOwnerSay("🎲 [Roll Module] Performing requested roll with d" + (string)diceType);
dbg("🎲 [Roll Module] 🎲 [Roll Module] Performing requested roll with d" + (string)diceType);
// Use MSG_ROLL_RESULT handler to perform the actual roll
llMessageLinked(LINK_THIS, MSG_ROLL_RESULT, (string)diceType, NULL_KEY);
} else if (shouldRoll && !rollInProgress) {
llOwnerSay("⚠️ [Roll Module] Roll was cancelled or already completed, ignoring dice type");
dbg("🎲 [Roll Module] ⚠️ [Roll Module] Roll was cancelled or already completed, ignoring dice type");
shouldRoll = FALSE; // Clear stale flag
}
return;
@ -341,7 +317,7 @@ default {
else if (num == MSG_ROLL_RESULT) {
// Additional safety check
if (!rollInProgress) {
llOwnerSay("⚠️ [Roll Module] Ignoring roll request - no roll in progress");
dbg("🎲 [Roll Module] ⚠️ [Roll Module] Ignoring roll request - no roll in progress");
return;
}
@ -357,10 +333,10 @@ default {
}
if (!hasAnyPicks) {
llOwnerSay("⚠️ [Roll Module] No picks data available - delaying roll to wait for sync");
dbg("🎲 [Roll Module] ⚠️ [Roll Module] No picks data available - delaying roll to wait for sync");
rollInProgress = FALSE; // Reset roll state
// Request a re-sync of game state
llMessageLinked(LINK_SET, 998, "REQUEST_SYNC", NULL_KEY);
llMessageLinked(LINK_SET, MSG_CONTINUE_GAME, "REQUEST_SYNC", NULL_KEY);
return;
}
@ -371,7 +347,7 @@ default {
llSay(0, "🎲 THE D" + (string)diceType + " OF FATE! " + perilPlayer + " rolled a " + resultStr + " on the " + (string)diceType + "-sided die! 🎲");
// Send dice roll directly to dice display (link 83)
llMessageLinked(83, 3020, perilPlayer + "|" + resultStr, NULL_KEY);
llMessageLinked(LINK_DICE_BRIDGE, MSG_DICE_ROLL, perilPlayer + "|" + resultStr, NULL_KEY);
string newPeril = "";
integer matched = FALSE;
@ -380,19 +356,19 @@ default {
// Find all players who picked the rolled number
llOwnerSay("🔍 [Roll Module] Checking picks for rolled number: " + resultStr);
llOwnerSay("🔍 [Roll Module] Current picksData: " + llDumpList2String(picksData, " | "));
dbg("🎲 [Roll Module] 🔍 [Roll Module] Checking picks for rolled number: " + resultStr);
dbg("🎲 [Roll Module] 🔍 [Roll Module] Current picksData: " + llDumpList2String(picksData, " | "));
for (i = 0; i < llGetListLength(names); i++) {
string pname = llList2String(names, i);
list picks = getPicksFor(pname);
llOwnerSay("🔍 [Roll Module] " + pname + "'s picks: " + llDumpList2String(picks, ","));
dbg("🎲 [Roll Module] 🔍 [Roll Module] " + pname + "'s picks: " + llDumpList2String(picks, ","));
if (llListFindList(picks, [resultStr]) != -1) {
matched = TRUE;
matchedPlayers += [pname];
llOwnerSay("🔍 [Roll Module] MATCH! " + pname + " picked " + resultStr);
dbg("🎲 [Roll Module] 🔍 [Roll Module] MATCH! " + pname + " picked " + resultStr);
}
}
llOwnerSay("🔍 [Roll Module] Final matched status: " + (string)matched + ", players: " + llDumpList2String(matchedPlayers, ","));
dbg("🎲 [Roll Module] 🔍 [Roll Module] Final matched status: " + (string)matched + ", players: " + llDumpList2String(matchedPlayers, ","));
// If anyone matched, pick the first non-peril player as new peril
@ -409,10 +385,10 @@ default {
perilPlayer = newPeril;
// Direct scoreboard status update
llMessageLinked(12, 3001, "Plot Twist", NULL_KEY);
llMessageLinked(LINK_SCOREBOARD, MSG_GAME_STATUS, "Plot Twist", NULL_KEY);
// IMPORTANT: Send peril player update to scoreboard for glow effect
llMessageLinked(12, 3005, newPeril, NULL_KEY); // MSG_UPDATE_PERIL_PLAYER
llMessageLinked(LINK_SCOREBOARD, MSG_UPDATE_PERIL_PLAYER, newPeril, NULL_KEY); // MSG_UPDATE_PERIL_PLAYER
// Add delay to let status display before next phase
llSleep(2.0);
@ -423,14 +399,14 @@ default {
integer pidx = llListFindList(names, [perilPlayer]);
if (pidx != -1) {
integer currentLives = llList2Integer(lives, pidx);
llOwnerSay("🔧 DEBUG: Before lives update - " + perilPlayer + " at index " + (string)pidx + " has " + (string)currentLives + " lives");
llOwnerSay("🔧 DEBUG: Current lives list: " + llList2CSV(lives));
dbg("🎲 [Roll Module] 🔧 DEBUG: Before lives update - " + perilPlayer + " at index " + (string)pidx + " has " + (string)currentLives + " lives");
dbg("🎲 [Roll Module] 🔧 DEBUG: Current lives list: " + llList2CSV(lives));
lives = llListReplaceList(lives, [currentLives - 1], pidx, pidx);
llOwnerSay("🔧 DEBUG: After lives update - lives list: " + llList2CSV(lives));
dbg("🎲 [Roll Module] 🔧 DEBUG: After lives update - lives list: " + llList2CSV(lives));
// Send direct scoreboard update when lives change
llMessageLinked(12, 3002, perilPlayer + "|" + (string)(currentLives - 1) + "|" + "NULL_KEY", NULL_KEY);
llOwnerSay("💗 Player lives updated: " + perilPlayer + " now has " + (string)(currentLives - 1) + " lives");
llMessageLinked(LINK_SCOREBOARD, MSG_PLAYER_UPDATE, perilPlayer + "|" + (string)(currentLives - 1) + "|" + "NULL_KEY", NULL_KEY);
dbg("🎲 [Roll Module] 💗 Player lives updated: " + perilPlayer + " now has " + (string)(currentLives - 1) + " lives");
// Check if peril player picked the rolled number
list perilPicks = getPicksFor(perilPlayer);
@ -444,19 +420,19 @@ default {
if (matched && perilPickedIt) {
llSay(0, "🩸 DIRECT HIT! " + perilPlayer + " picked their own doom - the d" + (string)diceType + " landed on " + resultStr + "! 🩸");
// Direct scoreboard status update
llMessageLinked(12, 3001, "Direct Hit", NULL_KEY);
llMessageLinked(LINK_SCOREBOARD, MSG_GAME_STATUS, "Direct Hit", NULL_KEY);
// Add delay to let status display before next phase
llSleep(2.0);
} else if (!matched) {
llSay(0, "🩸 NO SHIELD! Nobody picked " + resultStr + " - " + perilPlayer + " takes the hit from the d" + (string)diceType + "! 🩸");
// Direct scoreboard status update
llMessageLinked(12, 3001, "No Shield", NULL_KEY);
llMessageLinked(LINK_SCOREBOARD, MSG_GAME_STATUS, "No Shield", NULL_KEY);
// Add delay to let status display before next phase
llSleep(2.0);
} else {
// This should never happen in normal game flow since Plot Twist case is handled above
// But if it does, it means someone else picked it - this should have been Plot Twist
llOwnerSay("⚠️ LOGIC ERROR: Someone picked " + resultStr + " but not handled as Plot Twist!");
dbg("🎲 [Roll Module] ⚠️ LOGIC ERROR: Someone picked " + resultStr + " but not handled as Plot Twist!");
llSay(0, "🩸 SHIELD FAILED! " + perilPlayer + " takes the hit despite someone picking " + resultStr + "! 🩸");
llSleep(2.0);
}
@ -466,7 +442,7 @@ default {
// Check for elimination
if (currentLives - 1 <= 0) {
// Player elimination will be handled by Main Controller via sync and elimination messages
llOwnerSay("💀 Elimination detected: " + perilPlayer + " has 0 hearts remaining");
dbg("🎲 [Roll Module] 💀 Elimination detected: " + perilPlayer + " has 0 hearts remaining");
llSay(0, "🐻 PUNISHMENT TIME! " + perilPlayer + " has been ELIMINATED!");
@ -477,16 +453,16 @@ default {
if (elimIdx != -1) {
// 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");
dbg("🎲 [Roll Module] 🔄 [Roll Module] Updated local state - set " + perilPlayer + "'s lives to 0 but kept in lists");
dbg("🎲 [Roll Module] 🔄 [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);
llMessageLinked(LINK_SET, MSG_ELIMINATE_PLAYER, "ELIMINATE_PLAYER|" + perilPlayer, NULL_KEY);
// CRITICAL: Don't send any sync messages after elimination
// 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");
dbg("🎲 [Roll Module] 🎯 [Roll Module] Elimination complete - letting Main Controller handle state sync and continuation");
// Clear roll protection after processing is complete
rollInProgress = FALSE;
@ -513,7 +489,7 @@ default {
llSleep(0.2);
// Additional floater updates to ensure correct peril player display
llOwnerSay("🔄 Updating all floaters with new peril player: " + perilPlayer);
dbg("🎲 [Roll Module] 🔄 Updating all floaters with new peril player: " + perilPlayer);
integer f;
for (f = 0; f < llGetListLength(names); f++) {
string fname = llList2String(names, f);
@ -521,14 +497,14 @@ default {
}
// After roll is processed and sync is sent, send continuation directly to Game Manager
llOwnerSay("🎯 Round complete - sending continuation directly to Game Manager");
dbg("🎲 [Roll Module] 🎯 Round complete - sending continuation directly to Game Manager");
// 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);
llMessageLinked(LINK_SET, MSG_CONTINUE_GAME, perilForSync, NULL_KEY);
// Clear roll protection after processing is complete
rollInProgress = FALSE;
@ -540,20 +516,20 @@ default {
// Prevent multiple rolls within 3 seconds
integer currentTime = llGetUnixTime();
if (rollInProgress) {
llOwnerSay("⚠️ Roll already in progress, ignoring duplicate click by " + name);
dbg("🎲 [Roll Module] ⚠️ Roll already in progress, ignoring duplicate click by " + name);
return;
}
if (currentTime - lastRollTime < 3) {
llOwnerSay("⚠️ Roll too soon after previous roll, ignoring click by " + name);
dbg("🎲 [Roll Module] ⚠️ Roll too soon after previous roll, ignoring click by " + name);
return;
}
// Handle dice roll - request current dice type from Calculator
llOwnerSay("🎲 Roll button clicked by " + name + " (key: " + (string)id + ")");
dbg("🎲 [Roll Module] 🎲 Roll button clicked by " + name + " (key: " + (string)id + ")");
rollInProgress = TRUE; // Lock out other rolls
shouldRoll = TRUE; // Set flag to perform roll when dice type is received
// Request current dice type from Calculator
llMessageLinked(LINK_SET, 1001, (string)llGetListLength(names), NULL_KEY);
llMessageLinked(LINK_SET, MSG_GET_DICE_TYPE, (string)llGetListLength(names), NULL_KEY);
} else {
llMessageLinked(LINK_THIS, channel, msg, id);
}

View file

@ -1,12 +1,8 @@
#include "Peril_Constants.lsl"
// === System Debugger - Game State Validation ===
// Validates game state consistency and reports issues
// Verbose logging control
integer VERBOSE_LOGGING = TRUE; // Global flag for verbose debug logs
#define MSG_TOGGLE_VERBOSE_LOGS 9998 // Message to toggle verbose logging
#define MSG_SYNC_GAME_STATE 107
list validatePicksData(list picksData, list names) {
list allPicksFlat = [];
integer i;
@ -27,7 +23,7 @@ list validatePicksData(list picksData, list names) {
// Check if name is in names list
if (names != [] && llListFindList(names, [name]) == -1) {
llOwnerSay("⚠️ picksData name not in names list: " + name);
dbg("🔍 [System Debugger] ⚠️ picksData name not in names list: " + name);
}
// Check for duplicate picks in this player's list
@ -37,7 +33,7 @@ list validatePicksData(list picksData, list names) {
string pick = llList2String(pickNums, j);
// Skip validation warnings for cosmetic issues
if (~llListFindList(uniqueCheck, [pick])) {
llOwnerSay("🚨 Duplicate pick in " + name + "'s picks: " + pick);
dbg("🔍 [System Debugger] 🚨 Duplicate pick in " + name + "'s picks: " + pick);
} else {
uniqueCheck += [pick];
}
@ -45,12 +41,12 @@ list validatePicksData(list picksData, list names) {
}
// Only show picks if player has actually picked something
if (picks != "" && VERBOSE_LOGGING) {
llOwnerSay("✅ " + name + "'s picks: " + llList2CSV(pickNums));
if (picks != "") {
dbg("🔍 [System Debugger] ✅ " + name + "'s picks: " + llList2CSV(pickNums));
}
} else if (entry != "") {
// Only warn about non-empty malformed entries
llOwnerSay("⚠️ Malformed entry in picksData: " + entry);
dbg("🔍 [System Debugger] ⚠️ Malformed entry in picksData: " + entry);
}
}
@ -60,7 +56,7 @@ list validatePicksData(list picksData, list names) {
for (k = 0; k < llGetListLength(allPicksFlat); k++) {
string val = llList2String(allPicksFlat, k);
if (~llListFindList(seen, [val])) {
llOwnerSay("❌ Duplicate pick across players: " + val);
dbg("🔍 [System Debugger] ❌ Duplicate pick across players: " + val);
} else {
seen += [val];
}
@ -73,34 +69,24 @@ list validatePicksData(list picksData, list names) {
default {
state_entry() {
llOwnerSay("🔍 System Debugger ready!");
dbg("🔍 [System Debugger] 🔍 System Debugger ready!");
}
link_message(integer sender, integer num, string str, key id) {
// Handle verbose logging toggle
if (num == MSG_TOGGLE_VERBOSE_LOGS) {
VERBOSE_LOGGING = !VERBOSE_LOGGING;
if (VERBOSE_LOGGING) {
llOwnerSay("🔊 [System Debugger] Verbose logging ENABLED");
} else {
llOwnerSay("🔊 [System Debugger] Verbose logging DISABLED");
}
return;
}
// Handle legacy verbose logging toggle (OBSELETE)
// Handle full reset from main controller
if (num == -99999 && str == "FULL_RESET") {
if (num == MSG_RESET_ALL && str == "FULL_RESET") {
// Debugger doesn't maintain state, but acknowledge reset
llOwnerSay("🔍 System Debugger reset!");
dbg("🔍 [System Debugger] 🔍 System Debugger reset!");
return;
}
if (num == MSG_SYNC_GAME_STATE) {
list parts = llParseString2List(str, ["~"], []);
if (llGetListLength(parts) < 4) {
if (VERBOSE_LOGGING) {
llOwnerSay("⚠️ Incomplete game state received.");
}
dbg("🔍 [System Debugger] ✅ Validation completed - no critical issues found.");
return;
}

View file

@ -1,33 +0,0 @@
// Temporary Link Scanner - Find overlay prim link numbers
// Put this in any prim in the linkset and touch it to get a full link report
default {
state_entry() {
llOwnerSay("🔍 Link Scanner ready - touch to scan all links in the linkset");
}
touch_start(integer total_number) {
if (llDetectedKey(0) != llGetOwner()) return;
integer totalLinks = llGetNumberOfPrims();
llOwnerSay("📊 LINKSET SCAN - Total prims: " + (string)totalLinks);
llOwnerSay("==========================================");
integer i;
for (i = 1; i <= totalLinks; i++) {
string primName = llGetLinkName(i);
vector pos = llList2Vector(llGetLinkPrimitiveParams(i, [PRIM_POSITION]), 0);
llOwnerSay("Link " + (string)i + ": " + primName + " at " + (string)pos);
// Small delay to prevent message spam
if (i % 10 == 0) {
llSleep(1.0);
}
}
llOwnerSay("==========================================");
llOwnerSay("✅ Scan complete! Look for the new overlay prims you added");
llOwnerSay("💡 The overlay prims should be the highest link numbers");
}
}

View file

@ -1,79 +0,0 @@
// ====================================================================
// Test Updater - Debug llRemoteLoadScriptPin
// ====================================================================
// Simple test to see if llRemoteLoadScriptPin works at all
#define TEST_VERSION "1.0.0"
#define UPDATER_CHANNEL -7723847
// Test state
key targetGameKey = NULL_KEY;
integer testPin = 0;
integer testStep = 0;
// Simple test script content
#define TEST_SCRIPT "default {
state_entry() {
llOwnerSay(\"✅ Test script installed successfully!\")\n llOwnerSay(\"📝 This proves llRemoteLoadScriptPin is working.\");\n }\n}";
default {
state_entry() {
llOwnerSay("🧪 Test Updater v" + TEST_VERSION + " ready");
llOwnerSay("📍 This will test llRemoteLoadScriptPin with a tiny script");
llOwnerSay("👆 Touch your Peril Dice game, then touch this updater");
llListen(UPDATER_CHANNEL, "", NULL_KEY, "");
llSetText("🧪 Test Updater\nReady to test llRemoteLoadScriptPin\nTouch your game first, then me",
<1.0, 0.8, 0.2>, 1.0);
}
listen(integer channel, string name, key id, string message) {
if (channel != UPDATER_CHANNEL) return;
list parts = llParseString2List(message, ["|"], []);
string command = llList2String(parts, 0);
if (command == "TEST_REQUEST") {
string version = llList2String(parts, 1);
integer pin = (integer)llList2String(parts, 2);
llOwnerSay("📨 Test request from " + name + " (PIN: " + (string)pin + ")");
// Store the test info
targetGameKey = id;
testPin = pin;
testStep = 1;
// Start the test
llOwnerSay("🚀 Step 1: Testing tiny script installation...");
llOwnerSay("📊 Script size: " + (string)llStringLength(TEST_SCRIPT) + " characters");
llOwnerSay("🎯 Target: " + (string)targetGameKey);
llOwnerSay("🔑 PIN: " + (string)testPin);
// Try to install the test script
llRemoteLoadScriptPin(targetGameKey, TEST_SCRIPT, testPin, TRUE, 0);
llOwnerSay("✅ llRemoteLoadScriptPin called - check your game for new script");
llOwnerSay("💡 If successful, you'll see a 'Test script installed' message");
}
else if (command == "PING_UPDATER") {
llRegionSayTo(id, UPDATER_CHANNEL, "TEST_UPDATER_AVAILABLE|" + TEST_VERSION);
}
}
touch_start(integer total_number) {
key toucher = llDetectedKey(0);
if (testStep == 0) {
llOwnerSay("🔍 Touch your Peril Dice game first to set up the PIN");
llOwnerSay("📝 The game needs to call llSetRemoteScriptAccessPin()");
} else {
llOwnerSay("🧪 Test Status:");
llOwnerSay("📍 Target: " + (string)targetGameKey);
llOwnerSay("🔑 PIN: " + (string)testPin);
llOwnerSay("📊 Test script size: " + (string)llStringLength(TEST_SCRIPT) + " chars");
llOwnerSay("❓ Did you see 'Test script installed successfully' message?");
}
}
}

View file

@ -1,431 +0,0 @@
// ====================================================================
// Update Checker - GitHub-based Update System for Peril Dice
// ====================================================================
// VERIFIED LINKSET PLACEMENT: Link 1 (Root Prim) "Peril Dice Controller V2"
// Based on actual linkset scan: 84 prims total, exactly matching architecture
// Replaces eliminated UpdateHelper.lsl from v2.8.5 memory optimization
//
// INTEGRATION:
// - Integrates with existing admin menu system via Player_DialogHandler.lsl
// - Uses link message communication following existing patterns
// - Memory optimized to fit alongside 13 other Link 1 scripts
//
// VERIFIED STRUCTURE (from linkset scan):
// - Link 1: Root controller with 13+ scripts
// - Link 12: "scoreboard manager cube" - Game_Scoreboard_Manager_Linkset.lsl
// - Link 35: "leaderboard row 1 col 1" - Leaderboard_Communication_Linkset.lsl
// - Links 35-82: XyzzyText prims (48 total) - each needs xyzzy_Master_script.lsl
// - Link 83: "dice col 1" - XyzzyText_Dice_Bridge_Linkset.lsl
// - Link 84: "dice col 2" - no script needed
// ====================================================================
#define CURRENT_VERSION "2.8.7"
#define GITHUB_API_URL "https://api.github.com/repos/RebeccaNod1/Peril/releases/latest"
#define GITHUB_RAW_URL "https://raw.githubusercontent.com/RebeccaNod1/Peril/main/"
// VERIFIED Link 1 Scripts (from actual scan of "Peril Dice Controller V2")
list LINK1_SCRIPTS = [
"Main_Controller_Linkset.lsl", // Master coordination
"Game_Manager.lsl", // Round management
"Controller_Memory.lsl", // Memory monitoring
"Controller_MessageHandler.lsl", // Message routing
"Player_RegistrationManager.lsl", // Player registry
"Player_DialogHandler.lsl", // Admin menus
"NumberPicker_DialogHandler.lsl", // Number picking
"Floater_Manager.lsl", // Floating HUDs
"Roll_ConfettiModule.lsl", // Dice rolling
"Bot_Manager.lsl", // AI players
"Game_Calculator.lsl", // Dice calculations
"Verbose_Logger.lsl", // Logging system
"System_Debugger.lsl" // Debug tools
];
// VERIFIED Link-Specific Scripts (from actual linkset scan)
list LINKSET_SPECIFIC_SCRIPTS = [
"Game_Scoreboard_Manager_Linkset.lsl", // Link 12: "scoreboard manager cube"
"Leaderboard_Communication_Linkset.lsl", // Link 35: "leaderboard row 1 col 1"
"XyzzyText_Dice_Bridge_Linkset.lsl" // Link 83: "dice col 1"
];
// XyzzyText system - VERIFIED 48 prims (Links 35-82)
#define XYZZY_SCRIPT "xyzzy_Master_script.lsl"
// Essential documentation files
list ESSENTIAL_DOCS = [
"README.md", "CHANGELOG.md", "README_Notecard.txt", "CHANGELOG_Notecard.txt"
];
// HTTP operation tracking
key currentHttpRequest = NULL_KEY;
string currentOperation = "";
integer updateCheckInProgress = FALSE;
integer downloadMode = FALSE;
string downloadingFile = "";
// Message constants - following existing Peril Dice patterns
#define MSG_ADMIN_MENU_RESPONSE 888 // Integration with admin menu
#define MSG_UPDATE_CHECK_REQUEST 2100 // Request from admin menu
#define MSG_TOGGLE_VERBOSE_LOGS 9999 // Verbose logging toggle
// Internal verbose logging (follows existing pattern)
integer VERBOSE_LOGGING = FALSE;
// Memory usage reporting (matches existing scripts)
reportMemoryUsage(string scriptName) {
integer memory = llGetUsedMemory();
integer freeMemory = llGetFreeMemory();
float memoryPercent = (float)memory / (memory + freeMemory) * 100.0;
llOwnerSay("📊 " + scriptName + ": " + (string)memory + " bytes used (" +
llGetSubString((string)memoryPercent, 0, 4) + "% memory)");
}
// Version comparison (handles "v2.8.6" and "2.8.6" formats)
integer isNewerVersion(string latestVersion, string currentVersion) {
// Remove 'v' prefix if present
if (llGetSubString(latestVersion, 0, 0) == "v") {
latestVersion = llGetSubString(latestVersion, 1, -1);
}
if (llGetSubString(currentVersion, 0, 0) == "v") {
currentVersion = llGetSubString(currentVersion, 1, -1);
}
return (latestVersion != currentVersion);
}
// Check GitHub API for updates
checkForUpdates() {
if (updateCheckInProgress) {
llOwnerSay("⏳ Update check already in progress...");
return;
}
updateCheckInProgress = TRUE;
currentOperation = "version_check";
llOwnerSay("🔍 Checking GitHub for Peril Dice updates...");
if (VERBOSE_LOGGING) {
llOwnerSay("🔗 API URL: " + GITHUB_API_URL);
}
currentHttpRequest = llHTTPRequest(GITHUB_API_URL, [
HTTP_METHOD, "GET"
], "");
}
// Download file content from GitHub raw repository
downloadFileContent(string filename) {
if (updateCheckInProgress && !downloadMode) {
llOwnerSay("⏳ Please wait for version check to complete...");
return;
}
// Validate file is in essential lists
if (llListFindList(LINK1_SCRIPTS, [filename]) == -1 &&
llListFindList(LINKSET_SPECIFIC_SCRIPTS, [filename]) == -1 &&
llListFindList(ESSENTIAL_DOCS, [filename]) == -1 &&
filename != XYZZY_SCRIPT) {
llOwnerSay("❌ File '" + filename + "' is not in the essential files list.");
llOwnerSay("📋 Say '/1 list' to see available files");
return;
}
downloadMode = TRUE;
downloadingFile = filename;
currentOperation = "file_download";
llOwnerSay("📥 Downloading " + filename + " from GitHub main branch...");
string url = GITHUB_RAW_URL + filename;
if (VERBOSE_LOGGING) {
llOwnerSay("🔗 Download URL: " + url);
}
currentHttpRequest = llHTTPRequest(url, [
HTTP_METHOD, "GET"
], "");
}
// Show help commands
showUpdateCommands() {
llOwnerSay("=== 🔄 PERIL DICE UPDATE SYSTEM ===");
llOwnerSay("📱 Current Version: v" + CURRENT_VERSION);
llOwnerSay("🌐 Repository: github.com/RebeccaNod1/Peril");
llOwnerSay("📍 Location: Link 1 (Root Prim) - 84-prim linkset verified");
llOwnerSay("💡 Note: GitHub releases start with v2.8.7+");
llOwnerSay("");
llOwnerSay("💬 Available Commands (say in local chat):");
llOwnerSay(" /1 check - Check GitHub for updates");
llOwnerSay(" /1 download ScriptName.lsl - Download specific file");
llOwnerSay(" /1 list - Show essential files with placement info");
llOwnerSay(" /1 help - Show these commands");
llOwnerSay("");
llOwnerSay("🎛️ Or use Owner Menu → Troubleshooting → Check for Updates");
llOwnerSay("📋 Example: /1 download xyzzy_Master_script.lsl");
}
// Show essential files list with VERIFIED linkset placement info
showEssentialFiles() {
llOwnerSay("=== 📋 ESSENTIAL GAME FILES (VERIFIED LINKSET STRUCTURE) ===");
llOwnerSay("🔧 Link 1 Scripts (Root Prim: \"Peril Dice Controller V2\"):");
integer i;
for (i = 0; i < llGetListLength(LINK1_SCRIPTS); i++) {
llOwnerSay(" • " + llList2String(LINK1_SCRIPTS, i));
}
llOwnerSay(" • Update_Checker.lsl (THIS SCRIPT)");
llOwnerSay("");
llOwnerSay("🎯 Link-Specific Scripts (VERIFIED placement):");
llOwnerSay(" • Game_Scoreboard_Manager_Linkset.lsl");
llOwnerSay(" → Link 12: \"scoreboard manager cube\"");
llOwnerSay(" • Leaderboard_Communication_Linkset.lsl");
llOwnerSay(" → Link 35: \"leaderboard row 1 col 1\"");
llOwnerSay(" • XyzzyText_Dice_Bridge_Linkset.lsl");
llOwnerSay(" → Link 83: \"dice col 1\"");
llOwnerSay("");
llOwnerSay("📺 XyzzyText Display System (VERIFIED 48 prims):");
llOwnerSay(" • xyzzy_Master_script.lsl");
llOwnerSay(" → Links 35-82: All leaderboard prims");
llOwnerSay(" → 4 banks × 12 rows each");
llOwnerSay(" → Bank 1: Links 35-46 (col 1)");
llOwnerSay(" → Bank 2: Links 47-58 (col 2)");
llOwnerSay(" → Bank 3: Links 59-70 (col 3)");
llOwnerSay(" → Bank 4: Links 71-82 (col 4)");
llOwnerSay(" ⚠️ Each prim needs this exact script!");
llOwnerSay("");
llOwnerSay("📖 Documentation (" + (string)llGetListLength(ESSENTIAL_DOCS) + " files):");
for (i = 0; i < llGetListLength(ESSENTIAL_DOCS); i++) {
llOwnerSay(" • " + llList2String(ESSENTIAL_DOCS, i));
}
llOwnerSay("");
llOwnerSay("ℹ️ Development files excluded: lsl_validator.py, test_*.py, etc.");
llOwnerSay("📊 Total Essential Files: " + (string)(llGetListLength(LINK1_SCRIPTS) + llGetListLength(LINKSET_SPECIFIC_SCRIPTS) + llGetListLength(ESSENTIAL_DOCS) + 1) + " unique files");
}
default {
state_entry() {
reportMemoryUsage("Update Checker");
llOwnerSay("🔄 Update Checker ready - GitHub integration active");
llOwnerSay("📍 Location: Link 1 (\"Peril Dice Controller V2\") - VERIFIED 84-prim structure");
llOwnerSay("💬 Say '/1 help' for commands or use Owner Menu → Troubleshooting");
llListen(1, "", llGetOwner(), "");
}
on_rez(integer start_param) {
// Reset state on rez (follows existing script patterns)
updateCheckInProgress = FALSE;
downloadMode = FALSE;
currentOperation = "";
downloadingFile = "";
currentHttpRequest = NULL_KEY;
llResetScript();
}
listen(integer channel, string name, key id, string message) {
if (id != llGetOwner()) return;
string msg = llToLower(llStringTrim(message, STRING_TRIM));
if (msg == "help") {
showUpdateCommands();
}
else if (msg == "check") {
checkForUpdates();
}
else if (msg == "list") {
showEssentialFiles();
}
else if (llGetSubString(msg, 0, 7) == "download") {
string filename = llStringTrim(llGetSubString(message, 8, -1), STRING_TRIM);
if (filename != "") {
downloadFileContent(filename);
} else {
llOwnerSay("❌ Usage: /1 download ScriptName.lsl");
llOwnerSay("📋 Say '/1 list' to see available files");
}
}
else {
llOwnerSay("❓ Unknown command. Say '/1 help' for available commands");
}
}
link_message(integer sender_num, integer num, string str, key id) {
// Handle verbose logging toggle (follows existing pattern)
if (num == MSG_TOGGLE_VERBOSE_LOGS) {
VERBOSE_LOGGING = !VERBOSE_LOGGING;
if (VERBOSE_LOGGING) {
llOwnerSay("🔊 [UpdateChecker] Verbose logging ENABLED");
} else {
llOwnerSay("🔊 [UpdateChecker] Verbose logging DISABLED");
}
return;
}
// Handle update check requests from admin menu
if (num == MSG_UPDATE_CHECK_REQUEST) {
if (VERBOSE_LOGGING) {
llOwnerSay("🔄 [UpdateChecker] Received update check request from admin menu");
}
checkForUpdates();
return;
}
// Respond to admin menu status queries
if (num == MSG_ADMIN_MENU_RESPONSE && str == "update_status") {
llMessageLinked(LINK_SET, MSG_ADMIN_MENU_RESPONSE,
"update_version|v" + CURRENT_VERSION, id);
return;
}
}
http_response(key request_id, integer status, list metadata, string body) {
if (request_id != currentHttpRequest) return;
currentHttpRequest = NULL_KEY;
if (currentOperation == "version_check") {
updateCheckInProgress = FALSE;
if (status == 200) {
if (VERBOSE_LOGGING) {
llOwnerSay("📊 GitHub response: " + (string)llStringLength(body) + " chars (LSL limit: 2048)");
}
// LSL truncates HTTP responses at 2048 chars, breaking JSON parser
// Use manual parsing instead
string latestVersion = "";
string htmlUrl = "";
string publishedAt = "";
// Extract tag_name manually
integer tagStart = llSubStringIndex(body, "\"tag_name\":\"");
if (tagStart != -1) {
integer versionStart = tagStart + 12; // Skip '"tag_name":"'
integer versionEnd = llSubStringIndex(llGetSubString(body, versionStart, -1), "\"");
if (versionEnd != -1) {
latestVersion = llGetSubString(body, versionStart, versionStart + versionEnd - 1);
}
}
// Extract html_url manually
integer urlStart = llSubStringIndex(body, "\"html_url\":\"");
if (urlStart != -1) {
integer urlValueStart = urlStart + 12; // Skip '"html_url":"'
integer urlEnd = llSubStringIndex(llGetSubString(body, urlValueStart, -1), "\"");
if (urlEnd != -1) {
htmlUrl = llGetSubString(body, urlValueStart, urlValueStart + urlEnd - 1);
}
}
// Extract published_at manually
integer pubStart = llSubStringIndex(body, "\"published_at\":\"");
if (pubStart != -1) {
integer pubValueStart = pubStart + 15; // Skip '"published_at":"'
integer pubEnd = llSubStringIndex(llGetSubString(body, pubValueStart, -1), "\"");
if (pubEnd != -1) {
publishedAt = llGetSubString(body, pubValueStart, pubValueStart + pubEnd - 1);
}
}
if (latestVersion == "") {
llOwnerSay("❌ Could not extract version from truncated GitHub response");
llOwnerSay("🔍 Response was cut off at 2048 characters");
return;
}
// Skip release notes parsing - response is truncated at 2048 chars
string releaseNotes = "Release notes available on GitHub (response truncated)";
llOwnerSay("=== 🔍 UPDATE CHECK RESULTS ===");
llOwnerSay("📊 Current Version: v" + CURRENT_VERSION);
llOwnerSay("✨ Latest GitHub Release: " + latestVersion);
if (isNewerVersion(latestVersion, CURRENT_VERSION)) {
llOwnerSay("🆕 NEW VERSION AVAILABLE!");
llOwnerSay("📅 Published: " + llGetSubString(publishedAt, 0, 9));
llOwnerSay("🌐 View Release: " + htmlUrl);
// Show release notes preview
if (releaseNotes != JSON_INVALID && releaseNotes != "") {
string shortNotes = releaseNotes;
if (llStringLength(shortNotes) > 200) {
shortNotes = llGetSubString(shortNotes, 0, 197) + "...";
}
llOwnerSay("📋 What's New: " + shortNotes);
}
llOwnerSay("");
llOwnerSay("📥 Individual Files: /1 download ScriptName.lsl");
llOwnerSay("📋 Available Files: /1 list");
llOwnerSay("🎁 Complete Release: " + htmlUrl);
} else {
llOwnerSay("✅ You have the latest version!");
llOwnerSay("🔄 Individual file updates available via /1 download");
}
} else {
llOwnerSay("❌ Update check failed (HTTP " + (string)status + ")");
if (status == 403) {
llOwnerSay(" ⏳ GitHub rate limited - try again in a few minutes");
} else if (status == 404) {
llOwnerSay(" 📋 No releases found yet - this is normal for new repositories");
llOwnerSay(" 🌐 Repository exists at: https://github.com/RebeccaNod1/Peril");
llOwnerSay(" 💡 Individual file downloads still available via /1 download");
llOwnerSay(" 🔄 Releases will appear here when v2.8.7+ is tagged");
}
}
}
else if (currentOperation == "file_download") {
downloadMode = FALSE;
if (status == 200) {
integer contentLength = llStringLength(body);
llOwnerSay("✅ Downloaded " + downloadingFile);
llOwnerSay("📊 Size: " + (string)contentLength + " characters");
// Show VERIFIED deployment instructions based on actual linkset scan
if (downloadingFile == "xyzzy_Master_script.lsl") {
llOwnerSay("📺 XyzzyText Script - Deploy to ALL Links 35-82 (48 prims)");
llOwnerSay(" 🎯 VERIFIED: \"leaderboard row X col Y\" prims");
llOwnerSay(" ⚠️ Each leaderboard prim needs this exact script!");
} else if (downloadingFile == "Game_Scoreboard_Manager_Linkset.lsl") {
llOwnerSay("🎯 Scoreboard Manager - Deploy to Link 12 ONLY");
llOwnerSay(" 📍 VERIFIED: \"scoreboard manager cube\"");
} else if (downloadingFile == "Leaderboard_Communication_Linkset.lsl") {
llOwnerSay("🎯 Leaderboard Bridge - Deploy to Link 35 ONLY");
llOwnerSay(" 📍 VERIFIED: \"leaderboard row 1 col 1\"");
} else if (downloadingFile == "XyzzyText_Dice_Bridge_Linkset.lsl") {
llOwnerSay("🎯 Dice Display Bridge - Deploy to Link 83 ONLY");
llOwnerSay(" 📍 VERIFIED: \"dice col 1\"");
} else if (llListFindList(LINK1_SCRIPTS, [downloadingFile]) != -1) {
llOwnerSay("🎯 Root Prim Script - Deploy to Link 1 ONLY");
llOwnerSay(" 📍 VERIFIED: \"Peril Dice Controller V2\"");
} else {
llOwnerSay("📖 Documentation file - no deployment needed");
}
// Show content preview
llOwnerSay("───── CONTENT PREVIEW (200 chars) ─────");
string preview = body;
if (llStringLength(preview) > 200) {
preview = llGetSubString(preview, 0, 197) + "...";
}
llOwnerSay(preview);
llOwnerSay("──────────────────────────────────────");
if (llGetSubString(downloadingFile, -4, -1) == ".lsl") {
llOwnerSay("🔧 Copy → Paste into script editor → SAVE → RESET");
llOwnerSay("📍 Deployment verified for 84-prim linkset structure");
}
} else {
llOwnerSay("❌ Download failed: " + downloadingFile + " (HTTP " + (string)status + ")");
if (status == 404) {
llOwnerSay(" 📁 File not found on GitHub - check spelling");
}
}
downloadingFile = "";
}
currentOperation = "";
}
}

View file

@ -1,507 +0,0 @@
// ====================================================================
// Update Receiver - GitHub Integration + External Updater Coordination
// ====================================================================
// VERIFIED LINKSET PLACEMENT: Link 1 (Root Prim) "Peril Dice Controller V2"
// Replaces Update_Checker.lsl with external updater box coordination
//
// This script:
// 1. Checks GitHub for new releases (same as before)
// 2. Scans for nearby Peril_Dice_Updater boxes
// 3. Coordinates automatic script installation via external updater
// 4. Provides professional one-click update experience
// ====================================================================
string CURRENT_VERSION = "2.8.8";
#define GITHUB_API_URL "https://api.github.com/repos/RebeccaNod1/Peril/releases/latest"
// External updater communication
#define UPDATER_CHANNEL -7723847 // Must match Peril_Dice_Updater.lsl
#define SENSOR_RANGE 96 // meters to scan for updater boxes
list nearbyUpdaters = [];
key selectedUpdater = NULL_KEY;
// Update process state
integer updateInProgress = FALSE;
integer updatePin = 0;
string availableVersion = "";
// HTTP operation tracking
key currentHttpRequest = NULL_KEY;
string currentOperation = "";
// Message constants - following existing Peril Dice patterns
#define MSG_ADMIN_MENU_RESPONSE 888
#define MSG_UPDATE_CHECK_REQUEST 2100
#define MSG_TOGGLE_VERBOSE_LOGS 9999
// Update dialog constants
#define UPDATE_DIALOG_CHANNEL -77401 // Unique channel for update dialogs
integer updateDialogHandle = -1;
key pendingUpdateUser = NULL_KEY;
// Internal verbose logging
integer VERBOSE_LOGGING = FALSE;
// Memory usage reporting
reportMemoryUsage(string scriptName) {
integer memory = llGetUsedMemory();
integer freeMemory = llGetFreeMemory();
float memoryPercent = (float)memory / (memory + freeMemory) * 100.0;
llOwnerSay("📊 " + scriptName + ": " + (string)memory + " bytes used (" +
llGetSubString((string)memoryPercent, 0, 4) + "% memory)");
}
// Version comparison (handles "v2.8.6" and "2.8.6" formats)
integer isNewerVersion(string latestVersion, string currentVersion) {
// Remove 'v' prefix if present
if (llGetSubString(latestVersion, 0, 0) == "v") {
latestVersion = llGetSubString(latestVersion, 1, -1);
}
if (llGetSubString(currentVersion, 0, 0) == "v") {
currentVersion = llGetSubString(currentVersion, 1, -1);
}
return (latestVersion != currentVersion);
}
// Manual JSON parsing for LSL 2048-char limit
string extractJsonValue(string json, string keyName) {
string quote = "\"";
string searchPattern = quote + keyName + quote + ":" + quote;
integer start = llSubStringIndex(json, searchPattern);
if (start == -1) return "";
integer valueStart = start + llStringLength(searchPattern);
integer valueEnd = llSubStringIndex(llGetSubString(json, valueStart, -1), quote);
if (valueEnd == -1) return "";
return llGetSubString(json, valueStart, valueStart + valueEnd - 1);
}
// Scan for nearby updater boxes
scanForUpdaters() {
nearbyUpdaters = [];
llOwnerSay("🔍 Scanning for GitHub updater boxes within " + (string)SENSOR_RANGE + "m...");
llSensor("", NULL_KEY, SCRIPTED, SENSOR_RANGE, PI);
}
// Check GitHub for updates
checkForUpdates() {
currentOperation = "version_check";
llOwnerSay("🔍 Checking GitHub for Peril Dice updates...");
if (VERBOSE_LOGGING) {
llOwnerSay("📊 GitHub API: " + GITHUB_API_URL);
}
currentHttpRequest = llHTTPRequest(GITHUB_API_URL, [
HTTP_METHOD, "GET"
], "");
}
// Start update installation process
startUpdateInstallation() {
if (selectedUpdater == NULL_KEY) {
llOwnerSay("❌ No updater box selected. Please rez a Peril Dice Updater nearby.");
return;
}
// Generate unique access pin
updatePin = (integer)(llFrand(2000000000)) + 1000000000;
// Set remote script access pin
llSetRemoteScriptAccessPin(updatePin);
updateInProgress = TRUE;
llOwnerSay("🔄 Starting automatic update installation...");
llOwnerSay("📊 Requesting " + availableVersion + " from GitHub updater box...");
// Request update from external updater
llRegionSayTo(selectedUpdater, UPDATER_CHANNEL,
"UPDATE_REQUEST|" + CURRENT_VERSION + "|" + (string)updatePin);
// Set timeout for update process
llSetTimerEvent(300.0); // 5 minute timeout
}
// Complete update process
completeUpdate(string newVersion) {
updateInProgress = FALSE;
llSetRemoteScriptAccessPin(0); // Clear access pin
llSetTimerEvent(0.0); // Clear timer
llOwnerSay("🎉 Update installation complete!");
llOwnerSay("✅ Peril Dice is now running version " + newVersion);
llOwnerSay("💡 You can now delete the updater box");
// Update our version number
// Note: In practice, this script itself would have been updated
// CURRENT_VERSION = newVersion;
}
// Handle update failure
handleUpdateFailure(string error) {
updateInProgress = FALSE;
llSetRemoteScriptAccessPin(0); // Clear access pin
llSetTimerEvent(0.0); // Clear timer
llOwnerSay("❌ Update installation failed: " + error);
llOwnerSay("💡 You can try again or delete the updater box");
}
// Show update installation dialog
showInstallUpdateDialog(key userId) {
if (availableVersion == "") {
llOwnerSay("❌ No update available to install");
return;
}
if (llGetListLength(nearbyUpdaters) == 0) {
llOwnerSay("📦 No updater boxes found. Scanning...");
scanForUpdaters();
return;
}
pendingUpdateUser = userId;
// Close previous dialog listener
if (updateDialogHandle != -1) {
llListenRemove(updateDialogHandle);
}
// Start new dialog listener
updateDialogHandle = llListen(UPDATE_DIALOG_CHANNEL, "", userId, "");
string dialogText = "🆕 NEW VERSION AVAILABLE!\n\n";
dialogText += "📊 Current Version: v" + CURRENT_VERSION + "\n";
dialogText += "✨ Available Version: " + availableVersion + "\n\n";
dialogText += "🔄 Ready to install " + availableVersion + " automatically?\n";
dialogText += "📦 Found " + (string)llGetListLength(nearbyUpdaters) + " updater box(es) nearby";
list options = ["🚀 Install Update", "❌ Cancel", "📦 Scan Again"];
llDialog(userId, dialogText, options, UPDATE_DIALOG_CHANNEL);
}
// Show help commands
showUpdateCommands() {
llOwnerSay("=== 🔄 PERIL DICE UPDATE SYSTEM ===");
llOwnerSay("📱 Current Version: v" + CURRENT_VERSION);
llOwnerSay("🌐 Repository: github.com/RebeccaNod1/Peril");
llOwnerSay("📍 Location: Link 1 (Root Prim) - VERIFIED 84-prim structure");
llOwnerSay("");
llOwnerSay("🔄 NEW: Automatic Update System!");
llOwnerSay("1️⃣ Rez a 'Peril Dice Updater' box nearby");
llOwnerSay("2️⃣ Touch this game → Admin Menu → 'Check for Updates'");
llOwnerSay("3️⃣ Click 'Install Update' for one-click installation");
llOwnerSay("4️⃣ Delete updater box when complete");
llOwnerSay("");
llOwnerSay("💬 Manual commands still available:");
llOwnerSay(" /1 check - Check GitHub for updates");
llOwnerSay(" /1 scan - Scan for nearby updater boxes");
llOwnerSay(" /1 test - Test llRemoteLoadScriptPin with tiny script");
llOwnerSay(" /1 help - Show these commands");
}
default {
state_entry() {
reportMemoryUsage("Update Receiver");
llOwnerSay("🔄 Update Receiver ready - GitHub + External Updater integration");
llOwnerSay("📍 Location: Link 1 (\"Peril Dice Controller V2\") - VERIFIED structure");
llOwnerSay("💬 Say '/1 help' for commands or use Owner Menu → Troubleshooting");
llListen(1, "", llGetOwner(), "");
llListen(UPDATER_CHANNEL, "", NULL_KEY, "");
}
on_rez(integer start_param) {
// Reset state on rez
updateInProgress = FALSE;
updatePin = 0;
availableVersion = "";
nearbyUpdaters = [];
selectedUpdater = NULL_KEY;
currentOperation = "";
currentHttpRequest = NULL_KEY;
llResetScript();
}
listen(integer channel, string name, key id, string message) {
if (channel == 1 && id == llGetOwner()) {
// Owner chat commands
string msg = llToLower(llStringTrim(message, STRING_TRIM));
if (msg == "help") {
showUpdateCommands();
}
else if (msg == "check") {
checkForUpdates();
}
else if (msg == "scan") {
scanForUpdaters();
}
else if (msg == "test") {
// Test llRemoteLoadScriptPin with tiny script
llOwnerSay("🧪 DEBUG: Testing llRemoteLoadScriptPin...");
// Generate test PIN
integer testPin = (integer)(llFrand(1000000)) + 100000;
llSetRemoteScriptAccessPin(testPin);
llOwnerSay("🔑 Test PIN set: " + (string)testPin);
llOwnerSay("📡 Broadcasting test request on channel " + (string)UPDATER_CHANNEL);
// Broadcast test request to any test updaters
llRegionSay(UPDATER_CHANNEL, "TEST_REQUEST|" + CURRENT_VERSION + "|" + (string)testPin);
llOwnerSay("🏷️ If you have a Test_Updater nearby, it should respond now");
}
else {
llOwnerSay("❓ Unknown command. Say '/1 help' for available commands");
}
}
else if (channel == UPDATE_DIALOG_CHANNEL) {
// Handle update dialog responses
if (id != pendingUpdateUser) return; // Only respond to the user who triggered the dialog
if (message == "🚀 Install Update") {
llOwnerSay("🚀 Starting automatic update installation...");
startUpdateInstallation();
}
else if (message == "📦 Scan Again") {
llOwnerSay("🔍 Scanning for GitHub updater boxes...");
scanForUpdaters();
// Dialog will be shown again when scan finds updaters
}
else if (message == "❌ Cancel") {
llOwnerSay("❌ Update installation cancelled");
}
// Clean up dialog listener and pending user
if (updateDialogHandle != -1) {
llListenRemove(updateDialogHandle);
updateDialogHandle = -1;
}
pendingUpdateUser = NULL_KEY;
}
else if (channel == UPDATER_CHANNEL) {
// Communication with updater boxes
list parts = llParseString2List(message, ["|"], []);
string command = llList2String(parts, 0);
if (command == "UPDATER_AVAILABLE") {
string updaterVersion = llList2String(parts, 1);
llOwnerSay("📦 Found GitHub updater: " + name + " v" + updaterVersion);
// Add to list if not already there
if (llListFindList(nearbyUpdaters, [id]) == -1) {
nearbyUpdaters += [id];
}
// Auto-select if we don't have one selected
if (selectedUpdater == NULL_KEY) {
selectedUpdater = id;
llOwnerSay("✅ Selected updater: " + name);
}
}
else if (command == "UPDATE_STARTING") {
string version = llList2String(parts, 1);
llOwnerSay("🚀 Updater is installing " + version + " from GitHub...");
}
else if (command == "UPDATE_COMPLETE") {
string version = llList2String(parts, 1);
completeUpdate(version);
}
else if (command == "UPDATE_FAILED") {
string error = llList2String(parts, 1);
handleUpdateFailure(error);
}
else if (command == "UPDATE_BUSY") {
llOwnerSay("⏳ Updater is busy with another installation");
}
else if (command == "REQUEST_LINK_UUIDS") {
// Updater is requesting our link UUIDs for proper script installation
llOwnerSay("📡 Updater requesting link UUIDs for linkset script installation...");
integer linkCount = llGetNumberOfPrims();
// Send critical links first
list criticalLinks = [12, 35, 83]; // Scoreboard, Leaderboard, Dice Bridge
string criticalPayload = "";
integer i;
for (i = 0; i < llGetListLength(criticalLinks); i++) {
integer linkNum = llList2Integer(criticalLinks, i);
if (linkNum <= linkCount) {
key linkUUID = llGetLinkKey(linkNum);
if (criticalPayload != "") criticalPayload += ",";
criticalPayload += (string)linkNum + ":" + (string)linkUUID;
}
}
llRegionSayTo(id, UPDATER_CHANNEL, "LINK_UUIDS_RESPONSE|" + criticalPayload);
llOwnerSay("✅ Sent critical link UUIDs (" + (string)llGetListLength(criticalLinks) + " links)");
if (VERBOSE_LOGGING) llOwnerSay("📊 Critical UUID Data: " + criticalPayload);
// Now send all xyzzy links (35-82) in safe-sized chunks as additional LINK_UUIDS_RESPONSE messages
if (linkCount >= 82) {
string chunk = "";
integer countInChunk = 0;
integer j;
for (j = 35; j <= 82; j++) {
key luuid = llGetLinkKey(j);
string piece = (string)j + ":" + (string)luuid;
// Flush if adding would exceed ~900 chars or ~20 entries
if ((llStringLength(chunk) + 1 + llStringLength(piece)) > 900 || countInChunk >= 20) {
llRegionSayTo(id, UPDATER_CHANNEL, "LINK_UUIDS_RESPONSE|" + chunk);
chunk = "";
countInChunk = 0;
}
if (chunk != "") chunk += ",";
chunk += piece;
countInChunk++;
}
if (chunk != "") {
llRegionSayTo(id, UPDATER_CHANNEL, "LINK_UUIDS_RESPONSE|" + chunk);
}
llOwnerSay("✅ Sent xyzzy UUIDs (links 35-82) in chunks");
}
}
}
}
link_message(integer sender_num, integer num, string str, key id) {
// Handle verbose logging toggle
if (num == MSG_TOGGLE_VERBOSE_LOGS) {
VERBOSE_LOGGING = !VERBOSE_LOGGING;
if (VERBOSE_LOGGING) {
llOwnerSay("🔊 [UpdateReceiver] Verbose logging ENABLED");
} else {
llOwnerSay("🔊 [UpdateReceiver] Verbose logging DISABLED");
}
return;
}
// Handle update check requests from admin menu
if (num == MSG_UPDATE_CHECK_REQUEST) {
if (VERBOSE_LOGGING) {
llOwnerSay("🔄 [UpdateReceiver] Received update check request from admin menu");
}
// Store the requesting user for potential dialog
if (availableVersion != "") {
// We already know there's an update - show install dialog immediately
showInstallUpdateDialog(id);
} else {
// Check for updates first
checkForUpdates();
// We'll show the dialog in the HTTP response if an update is found
pendingUpdateUser = id;
}
return;
}
// Respond to admin menu status queries
if (num == MSG_ADMIN_MENU_RESPONSE && str == "update_status") {
llMessageLinked(LINK_SET, MSG_ADMIN_MENU_RESPONSE,
"update_version|v" + CURRENT_VERSION, id);
return;
}
}
sensor(integer num_detected) {
// Found scripted objects - check if any are updater boxes
nearbyUpdaters = [];
integer i;
for (i = 0; i < num_detected; i++) {
key detected = llDetectedKey(i);
string detectedName = llDetectedName(i);
// Ping to see if it's an updater box
llRegionSayTo(detected, UPDATER_CHANNEL, "PING_UPDATER");
}
// Wait for responses
llSetTimerEvent(3.0);
}
no_sensor() {
llOwnerSay("📭 No GitHub updater boxes found within " + (string)SENSOR_RANGE + "m");
llOwnerSay("💡 Please rez a 'Peril Dice Updater' box nearby to enable automatic updates");
}
timer() {
if (updateInProgress) {
// Update timeout
llOwnerSay("⏰ Update installation timed out");
handleUpdateFailure("Installation timeout - updater may have failed");
} else {
// Sensor response timeout
if (llGetListLength(nearbyUpdaters) == 0) {
llOwnerSay("📭 No GitHub updater boxes responded");
llOwnerSay("💡 Please rez a 'Peril Dice Updater' box nearby");
} else {
llOwnerSay("✅ Found " + (string)llGetListLength(nearbyUpdaters) + " GitHub updater box(es)");
}
}
llSetTimerEvent(0.0);
}
http_response(key request_id, integer status, list metadata, string body) {
if (request_id != currentHttpRequest) return;
currentHttpRequest = NULL_KEY;
if (currentOperation == "version_check") {
if (status == 200) {
if (VERBOSE_LOGGING) {
llOwnerSay("📊 GitHub response: " + (string)llStringLength(body) + " chars (LSL limit: 2048)");
}
// Extract version using manual parsing
string latestVersion = extractJsonValue(body, "tag_name");
string htmlUrl = extractJsonValue(body, "html_url");
string publishedAt = extractJsonValue(body, "published_at");
if (latestVersion == "") {
llOwnerSay("❌ Could not extract version from GitHub response");
return;
}
llOwnerSay("=== 🔍 UPDATE CHECK RESULTS ===");
llOwnerSay("📊 Current Version: v" + CURRENT_VERSION);
llOwnerSay("✨ Latest GitHub Release: " + latestVersion);
if (isNewerVersion(latestVersion, CURRENT_VERSION)) {
availableVersion = latestVersion;
llOwnerSay("🆕 NEW VERSION AVAILABLE!");
llOwnerSay("📅 Published: " + llGetSubString(publishedAt, 0, 9));
llOwnerSay("🌐 View Release: " + htmlUrl);
llOwnerSay("");
// Check for updater boxes
if (llGetListLength(nearbyUpdaters) == 0) {
llOwnerSay("📦 Scanning for GitHub updater boxes...");
scanForUpdaters();
} else {
llOwnerSay("✅ GitHub updater box ready!");
llOwnerSay("🚀 Click 'Install Update' to automatically install " + latestVersion);
}
// Show install dialog if request came from admin menu
if (pendingUpdateUser != NULL_KEY) {
showInstallUpdateDialog(pendingUpdateUser);
pendingUpdateUser = NULL_KEY;
}
} else {
llOwnerSay("✅ You have the latest version!");
llOwnerSay("📦 GitHub updater system is ready for future updates");
pendingUpdateUser = NULL_KEY; // Clear pending user
}
} else {
llOwnerSay("❌ Update check failed (HTTP " + (string)status + ")");
if (status == 403) {
llOwnerSay(" ⏳ GitHub rate limited - try again in a few minutes");
}
}
}
currentOperation = "";
}
}

View file

@ -1,79 +0,0 @@
// === Verbose Logger - Simple Debug Toggle Controller ===
// Provides system-wide verbose logging toggle functionality
// Scripts use their own VERBOSE_LOGGING flags, this just coordinates the toggle
// Verbose logging control
integer VERBOSE_LOGGING = FALSE;
// Loop protection - prevent rapid toggle spam
float lastToggleTime = 0.0;
#define TOGGLE_COOLDOWN 2.0 // Minimum 2 seconds between toggles
// Message constants for verbose logging system
#define MSG_VERBOSE_TOGGLE 9010 // Toggle verbose logging on/off
#define MSG_VERBOSE_BROADCAST 9011 // Broadcast logging state to all modules
// Report memory status of this script
reportMemoryStatus() {
if (VERBOSE_LOGGING) {
integer free = llGetFreeMemory();
integer used = llGetUsedMemory();
llOwnerSay("🔍 [Verbose Logger] Memory - Free: " + (string)free + " Used: " + (string)used);
}
}
default {
state_entry() {
llOwnerSay("🔍 Verbose Logger initialized - ready to handle debug toggle");
}
on_rez(integer start_param) {
llOwnerSay("🔍 Verbose Logger rezzed - debug system ready");
VERBOSE_LOGGING = FALSE; // Default to off after rez
}
link_message(integer sender, integer num, string str, key id) {
// Handle verbose logging toggle - FIXED to prevent loops
if (num == MSG_VERBOSE_TOGGLE && str == "TOGGLE_VERBOSE_LOGS") {
// Only respond if sender is NOT the Main Controller (prevent forwarding loop)
if (sender == 1) {
// This came from Main Controller forwarding - ignore to prevent loop
return;
}
// Loop protection - prevent rapid toggle spam
float currentTime = llGetTime();
if (currentTime - lastToggleTime < TOGGLE_COOLDOWN) {
return; // Silent ignore during cooldown
}
lastToggleTime = currentTime;
VERBOSE_LOGGING = !VERBOSE_LOGGING;
string status = "OFF";
if (VERBOSE_LOGGING) {
status = "ON";
llOwnerSay("🔍 [Verbose Logger] Debug logging: ENABLED");
} else {
llOwnerSay("🔍 [Verbose Logger] Debug logging: DISABLED");
}
// Public announcement
llSay(0, "🔍 Verbose logging system-wide: " + status);
// Broadcast the new setting to all modules
llMessageLinked(LINK_SET, MSG_VERBOSE_BROADCAST, "VERBOSE_LOGGING|" + (string)VERBOSE_LOGGING, id);
return;
}
// Handle verbose logging broadcast from other scripts
if (num == MSG_VERBOSE_BROADCAST) {
list parts = llParseString2List(str, ["|"], []);
if (llList2String(parts, 0) == "VERBOSE_LOGGING") {
VERBOSE_LOGGING = llList2Integer(parts, 1);
return;
}
}
}
}

View file

@ -1,145 +0,0 @@
// XyzzyText Bridge Script - Dice Display Bridge (Linkset Version)
// This script goes in link 83 of the linkset
// Works with simple XyzzyText v1.0 script (like leaderboard)
// Handles dice roll display across 2 prims (20 characters total)
// =============================================================================
// LINKSET COMMUNICATION - NO DISCOVERY NEEDED
// =============================================================================
// Verbose logging control
integer VERBOSE_LOGGING = TRUE; // Global flag for verbose debug logs
#define MSG_TOGGLE_VERBOSE_LOGS 9998 // Message to toggle verbose logging
// Message constants for link communication
// Dice messages (from link 1 - controller)
#define MSG_DICE_ROLL 3020
#define MSG_CLEAR_DICE 3021
#define DISPLAY_STRING 204000
// Dice prim links
#define DICE_PRIM_1 83 // First dice prim (characters 0-9)
#define DICE_PRIM_2 84 // Second dice prim (characters 10-19)
// Function to display dice text across both prims
displayDiceText(string text) {
// Ensure exactly 20 characters
while (llStringLength(text) < 20) {
text += " ";
}
if (llStringLength(text) > 20) {
text = llGetSubString(text, 0, 19);
}
// Split into 10-character chunks for each prim
string leftText = llGetSubString(text, 0, 9); // Characters 0-9
string rightText = llGetSubString(text, 10, 19); // Characters 10-19
// Send to individual XyzzyText prims
llMessageLinked(DICE_PRIM_1, DISPLAY_STRING, leftText, "");
llMessageLinked(DICE_PRIM_2, DISPLAY_STRING, rightText, "");
if (VERBOSE_LOGGING) {
llOwnerSay("🎲 Dice display updated: '" + text + "'");
}
}
default {
on_rez(integer start_param) {
llResetScript(); // Fixes Z-fighting and alignment glitches on rez
}
state_entry() {
if (VERBOSE_LOGGING) {
llOwnerSay("🎲 Dice Display Bridge ready! (Linkset Version)");
llOwnerSay("🎲 This is link " + (string)llGetLinkNumber() + " - should be link 83");
llOwnerSay("🎲 Managing dice display links 83-84 (2 prims total)");
llOwnerSay("✅ Linkset communication active - listening for messages from link 1!");
}
// Initialize dice display with blank text (10 spaces each prim)
displayDiceText(" "); // 20 spaces
if (VERBOSE_LOGGING) {
llOwnerSay("Dice display initialized - using simple XyzzyText v1.0 (like leaderboard)");
}
}
link_message(integer sender, integer num, string str, key id) {
// Handle verbose logging toggle
if (num == MSG_TOGGLE_VERBOSE_LOGS) {
VERBOSE_LOGGING = !VERBOSE_LOGGING;
if (VERBOSE_LOGGING) {
llOwnerSay("🔊 [Dice Bridge] Verbose logging ENABLED");
} else {
llOwnerSay("🔊 [Dice Bridge] Verbose logging DISABLED");
}
return;
}
// Only listen to messages from the main controller (link 1)
if (sender != 1) return;
if (num == MSG_DICE_ROLL) {
// Handle different message formats
list parts = llParseString2List(str, ["|"], []);
if (llGetListLength(parts) >= 3) {
// Format: "player|result|dicetype" - single die roll
string player = llList2String(parts, 0);
string result = llList2String(parts, 1);
string diceType = llList2String(parts, 2);
// Create dice display: "Player1: rolled 4 "
string displayText = player;
if (llStringLength(displayText) > 8) {
displayText = llGetSubString(displayText, 0, 7);
}
displayText += ": rolled " + result;
displayDiceText(displayText);
}
else {
// Treat as direct display text
displayDiceText(str);
}
}
else if (num == MSG_CLEAR_DICE) {
// Clear dice display (20 spaces)
displayDiceText(" ");
if (VERBOSE_LOGGING) {
llOwnerSay("🎲 Dice display cleared");
}
}
}
touch_start(integer total_number) {
// Owner can get debug info and test display by touching dice
if (llDetectedKey(0) == llGetOwner()) {
llOwnerSay("🎲 Dice Display Status:");
llOwnerSay(" Link Number: " + (string)llGetLinkNumber());
llOwnerSay(" Total prims in linkset: " + (string)llGetNumberOfPrims());
llOwnerSay(" Managing links 83-84 (2 XyzzyText prims)");
llOwnerSay(" Listening for messages from link 1 (controller)");
llOwnerSay(" Using simple XyzzyText v1.0 (like leaderboard)");
// Test display - 20 characters total
displayDiceText("TEST: Player1 -> 6 ");
llOwnerSay("🎲 Test display sent: 'TEST: Player1 -> 6 '");
llOwnerSay("🎲 Touch again in 3 seconds to clear test");
// Set timer to clear test display
llSetTimerEvent(3.0);
} else {
// Non-owner touch - show friendly message
llSay(0, "🎲 Dice display shows the latest rolls! Watch for game actions!");
}
}
timer() {
// Clear test display
llSetTimerEvent(0.0); // Stop timer
displayDiceText(" "); // 20 spaces
llOwnerSay("🎲 Test display cleared");
}
}

View file

@ -1,443 +0,0 @@
////////////////////////////////////////////
// XyText v1.0.3 Script (5 Face, Multi Texture)
//
// Written by Xylor Baysklef
//
// Modified by Thraxis Epsilon January 20, 2006
// Added Support for 5 Face Prim, based on modification
// of [[XyText]] v1.1.1 by Kermitt Quick for Single Texture.
//
////////////////////////////////////////////
/////////////// CONSTANTS ///////////////////
// XyText Message Map.
#define DISPLAY_STRING 204000
#define DISPLAY_EXTENDED 204001
#define REMAP_INDICES 204002
#define RESET_INDICES 204003
#define SET_CELL_INFO 204004
#define SET_THICKNESS 204006
#define SET_COLOR 204007
// This is an extended character escape sequence.
#define ESCAPE_SEQUENCE "\e"
// This is used to get an index for the extended character.
#define EXTENDED_INDEX "123456789abcdef"
// Face numbers.
#define FACE_1 3
#define FACE_2 7
#define FACE_3 4
#define FACE_4 6
#define FACE_5 1
// Used to hide the text after a fade-out.
#define TRANSPARENT "701917a8-d614-471f-13dd-5f4644e36e3c"
// This is a list of textures for all 2-character combinations.
list CHARACTER_GRID = [
"00e9f9f7-0669-181c-c192-7f8e67678c8d",
"347a5cb6-0031-7ec0-2fcf-f298eebf3c0e",
"4e7e689e-37f1-9eca-8596-a958bbd23963",
"19ea9c21-67ba-8f6f-99db-573b1b877eb1",
"dde7b412-cda1-652f-6fc2-73f4641f96e1",
"af6fa3bb-3a6c-9c4f-4bf5-d1c126c830da",
"a201d3a2-364b-43b6-8686-5881c0f82a94",
"b674dec8-fead-99e5-c28d-2db8e4c51540",
"366e05f3-be6b-e5cf-c33b-731dff649caa",
"75c4925c-0427-dc0c-c71c-e28674ff4d27",
"dcbe166b-6a97-efb2-fc8e-e5bc6a8b1be6",
"0dca2feb-fc66-a762-db85-89026a4ecd68",
"a0fca76f-503a-946b-9336-0a918e886f7a",
"67fb375d-89a1-5a4f-8c7a-0cd1c066ffc4",
"300470b2-da34-5470-074c-1b8464ca050c",
"d1f8e91c-ce2b-d85e-2120-930d3b630946",
"2a190e44-7b29-dadb-0bff-c31adaf5a170",
"75d55e71-f6f8-9835-e746-a45f189f30a1",
"300fac33-2b30-3da3-26bc-e2d70428ec19",
"0747c776-011a-53ce-13ee-8b5bb9e87c1e",
"85a855c3-a94f-01ca-33e0-7dde92e727e2",
"cbc1dab2-2d61-2986-1949-7a5235c954e1",
"f7aef047-f266-9596-16df-641010edd8e1",
"4c34ebf7-e5e1-2e1a-579f-e224d9d5e71b",
"4a69e98c-26a5-ad05-e92e-b5b906ad9ef9",
"462a9226-2a97-91ac-2d89-57ab33334b78",
"20b24b3a-8c57-82ee-c6ed-555003f5dbcd",
"9b481daa-9ea8-a9fa-1ee4-ab9a0d38e217",
"c231dbdc-c842-15b0-7aa6-6da14745cfdc",
"c97e3cbb-c9a3-45df-a0ae-955c1f4bf9cf",
"f1e7d030-ff80-a242-cb69-f6951d4eae3b",
"ed32d6c4-d733-c0f1-f242-6df1d222220d",
"88f96a30-dccf-9b20-31ef-da0dfeb23c72",
"252f2595-58b8-4bcc-6515-fa274d0cfb65",
"f2838c4f-de80-cced-dff8-195dfdf36b2c",
"cc2594fe-add2-a3df-cdb3-a61711badf53",
"e0ce2972-da00-955c-129e-3289b3676776",
"3e0d336d-321f-ddfa-5c1b-e26131766f6a",
"d43b1dc4-6b51-76a7-8b90-38865b82bf06",
"06d16cbb-1868-fd1d-5c93-eae42164a37d",
"dd5d98cf-273e-3fd0-f030-48be58ee3a0b",
"0e47c89e-de4a-6233-a2da-cb852aad1b00",
"fb9c4a55-0e13-495b-25c4-f0b459dc06de",
"e3ce8def-312c-735b-0e48-018b6799c883",
"2f713216-4e71-d123-03ed-9c8554710c6b",
"4a417d8a-1f4f-404b-9783-6672f8527911",
"ca5e21ec-5b20-5909-4c31-3f90d7316b33",
"06a4fcc3-e1c4-296d-8817-01f88fbd7367",
"130ac084-6f3c-95de-b5b6-d25c80703474",
"59d540a0-ae9d-3606-5ae0-4f2842b64cfa",
"8612ae9a-f53c-5bf4-2899-8174d7abc4fd",
"12467401-e979-2c49-34e0-6ac761542797",
"d53c3eaa-0404-3860-0675-3e375596c3e3",
"9f5b26bd-81d3-b25e-62fe-5b671d1e3e79",
"f57f0b64-a050-d617-ee00-c8e9e3adc9cb",
"beff166a-f5f3-f05e-e020-98f2b00e27ed",
"02278a65-94ba-6d5e-0d2b-93f2e4f4bf70",
"a707197d-449e-5b58-846c-0c850c61f9d6",
"021d4b1a-9503-a44f-ee2b-976eb5d80e68",
"0ae2ffae-7265-524d-cb76-c2b691992706",
"f6e41cf2-1104-bd0b-0190-dffad1bac813",
"2b4bb15e-956d-56ae-69f5-d26a20de0ce7",
"f816da2c-51f1-612a-2029-a542db7db882",
"345fea05-c7be-465c-409f-9dcb3bd2aa07",
"b3017e02-c063-5185-acd5-1ef5f9d79b89",
"4dcff365-1971-3c2b-d73c-77e1dc54242a"
];
///////////// END CONSTANTS ////////////////
///////////// GLOBAL VARIABLES ///////////////
// All displayable characters. Default to ASCII order.
string gCharIndex;
// This is the channel to listen on while acting
// as a cell in a larger display.
integer gCellChannel = -1;
// This is the starting character position in the cell channel message
// to render.
integer gCellCharPosition = 0;
// This is whether or not to use the fade in/out special effect.
integer gCellUseFading = FALSE;
// This is how long to display the text before fading out (if using
// fading special effect).
// Note: < 0 means don't fade out.
float gCellHoldDelay = 1.0;
/////////// END GLOBAL VARIABLES ////////////
ResetCharIndex()
{
gCharIndex = " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`";
// \" <-- Fixes LSL syntax highlighting bug.
gCharIndex += "abcdefghijklmnopqrstuvwxyz{|}~";
gCharIndex += "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n";
}
vector GetGridPos(integer index1, integer index2)
{
// There are two ways to use the lookup table...
integer Col;
integer Row;
if (index1 >= index2)
{
// In this case, the row is the index of the first character:
Row = index1;
// And the col is the index of the second character (x2)
Col = index2 * 2;
}
// Index1 < Index2
else
{
// In this case, the row is the index of the second character:
Row = index2;
// And the col is the index of the first character, x2, offset by 1.
Col = index1 * 2 + 1;
}
return <Col, Row, 0>;
}
string GetGridTexture(vector grid_pos)
{
// Calculate the texture in the grid to use.
integer GridCol = llRound(grid_pos.x) / 20;
integer GridRow = llRound(grid_pos.y) / 10;
// Lookup the texture.
key Texture = llList2Key(CHARACTER_GRID, GridRow * (GridRow + 1) / 2 + GridCol);
return Texture;
}
vector GetGridOffset(vector grid_pos)
{
// Zoom in on the texture showing our character pair.
integer offsetCol = llRound(grid_pos.x) % 20;
integer offsetRow = llRound(grid_pos.y) % 10;
// Return the offset in the texture.
return <-0.45 + 0.05 * offsetCol, 0.45 - 0.1 * offsetRow, 0.0>;
}
ShowChars(vector grid_pos1, vector grid_pos2, vector grid_pos3, vector grid_pos4, vector grid_pos5)
{
// Set the primitive textures directly.
llSetLinkPrimitiveParamsFast(LINK_THIS, [
PRIM_TEXTURE, FACE_1, GetGridTexture(grid_pos1), <0.25, 0.1, 0>, GetGridOffset(grid_pos1) + <0.075, 0, 0>, 0.0,
PRIM_TEXTURE, FACE_2, GetGridTexture(grid_pos2), <0.1, 0.1, 0>, GetGridOffset(grid_pos2), 0.0,
PRIM_TEXTURE, FACE_3, GetGridTexture(grid_pos3), <-1.48, 0.1, 0>, GetGridOffset(grid_pos3)+ <0.37, 0, 0>, 0.0,
PRIM_TEXTURE, FACE_4, GetGridTexture(grid_pos4), <0.1, 0.1, 0>, GetGridOffset(grid_pos4), 0.0,
PRIM_TEXTURE, FACE_5, GetGridTexture(grid_pos5), <0.25, 0.1, 0>, GetGridOffset(grid_pos5) - <0.075, 0, 0>, 0.0]);
}
RenderString(string str)
{
// Get the grid positions for each pair of characters.
vector GridPos1 = GetGridPos( llSubStringIndex(gCharIndex, llGetSubString(str, 0, 0)),
llSubStringIndex(gCharIndex, llGetSubString(str, 1, 1)) );
vector GridPos2 = GetGridPos( llSubStringIndex(gCharIndex, llGetSubString(str, 2, 2)),
llSubStringIndex(gCharIndex, llGetSubString(str, 3, 3)) );
vector GridPos3 = GetGridPos( llSubStringIndex(gCharIndex, llGetSubString(str, 4, 4)),
llSubStringIndex(gCharIndex, llGetSubString(str, 5, 5)) );
vector GridPos4 = GetGridPos( llSubStringIndex(gCharIndex, llGetSubString(str, 6, 6)),
llSubStringIndex(gCharIndex, llGetSubString(str, 7, 7)) );
vector GridPos5 = GetGridPos( llSubStringIndex(gCharIndex, llGetSubString(str, 8, 8)),
llSubStringIndex(gCharIndex, llGetSubString(str, 9, 9)) );
// Use these grid positions to display the correct textures/offsets.
ShowChars(GridPos1, GridPos2, GridPos3, GridPos4, GridPos5);
}
RenderWithEffects(string str)
{
// Get the grid positions for each pair of characters.
vector EffectsPos1 = GetGridPos( llSubStringIndex(gCharIndex, llGetSubString(str, 0, 0)),
llSubStringIndex(gCharIndex, llGetSubString(str, 1, 1)) );
vector EffectsPos2 = GetGridPos( llSubStringIndex(gCharIndex, llGetSubString(str, 2, 2)),
llSubStringIndex(gCharIndex, llGetSubString(str, 3, 3)) );
vector EffectsPos3 = GetGridPos( llSubStringIndex(gCharIndex, llGetSubString(str, 4, 4)),
llSubStringIndex(gCharIndex, llGetSubString(str, 5, 5)) );
vector EffectsPos4 = GetGridPos( llSubStringIndex(gCharIndex, llGetSubString(str, 6, 6)),
llSubStringIndex(gCharIndex, llGetSubString(str, 7, 7)) );
vector EffectsPos5 = GetGridPos( llSubStringIndex(gCharIndex, llGetSubString(str, 8, 8)),
llSubStringIndex(gCharIndex, llGetSubString(str, 9, 9)) );
// First set the alpha to the lowest possible.
llSetAlpha(0.05, ALL_SIDES);
// Use these grid positions to display the correct textures/offsets.
ShowChars(EffectsPos1, EffectsPos2, EffectsPos3, EffectsPos4, EffectsPos5);
float Alpha;
for (Alpha = 0.10; Alpha <= 1.0; Alpha += 0.05)
llSetAlpha(Alpha, ALL_SIDES);
// See if we want to fade out as well.
if (gCellHoldDelay < 0.0)
// No, bail out. (Just keep showing the string at full strength).
return;
// Hold the text for a while.
llSleep(gCellHoldDelay);
// Now fade out.
for (Alpha = 0.95; Alpha >= 0.05; Alpha -= 0.05)
llSetAlpha(Alpha, ALL_SIDES);
// Make the text transparent to fully hide it.
llSetTexture(TRANSPARENT, ALL_SIDES);
}
RenderExtended(string str)
{
// Look for escape sequences.
list Parsed = llParseString2List(str, [], [ESCAPE_SEQUENCE]);
integer ParsedLen = llGetListLength(Parsed);
// Create a list of index values to work with.
list Indices;
// We start with room for 6 indices.
integer IndicesLeft = 10;
integer i;
string Token;
integer Clipped;
integer LastWasEscapeSequence = FALSE;
// Work from left to right.
for (i = 0; i < ParsedLen && IndicesLeft > 0; i++)
{
Token = llList2String(Parsed, i);
// If this is an escape sequence, just set the flag and move on.
if (Token == ESCAPE_SEQUENCE)
{
LastWasEscapeSequence = TRUE;
}
// Token != ESCAPE_SEQUENCE
else
{
// Otherwise this is a normal token. Check its length.
Clipped = FALSE;
integer TokenLength = llStringLength(Token);
// Clip if necessary.
if (TokenLength > IndicesLeft)
{
Token = llGetSubString(Token, 0, IndicesLeft - 1);
TokenLength = llStringLength(Token);
IndicesLeft = 0;
Clipped = TRUE;
}
else
IndicesLeft -= TokenLength;
// Was the previous token an escape sequence?
if (LastWasEscapeSequence)
{
// Yes, the first character is an escape character, the rest are normal.
// This is the extended character.
Indices += [llSubStringIndex(EXTENDED_INDEX, llGetSubString(Token, 0, 0)) + 95];
// These are the normal characters.
integer j;
for (j = 1; j < TokenLength; j++)
{
Indices += [llSubStringIndex(gCharIndex, llGetSubString(Token, j, j))];
}
}
// Normal string.
else
{
// Just add the characters normally.
integer k;
for (k = 0; k < TokenLength; k++)
Indices += [llSubStringIndex(gCharIndex, llGetSubString(Token, k, k))];
}
// Unset this flag, since this was not an escape sequence.
LastWasEscapeSequence = FALSE;
}
}
// Use the indices to create grid positions.
vector ExtendedPos1 = GetGridPos( llList2Integer(Indices, 0), llList2Integer(Indices, 1) );
vector ExtendedPos2 = GetGridPos( llList2Integer(Indices, 2), llList2Integer(Indices, 3) );
vector ExtendedPos3 = GetGridPos( llList2Integer(Indices, 4), llList2Integer(Indices, 5) );
vector ExtendedPos4 = GetGridPos( llList2Integer(Indices, 6), llList2Integer(Indices, 7) );
vector ExtendedPos5 = GetGridPos( llList2Integer(Indices, 8), llList2Integer(Indices, 9) );
// Use these grid positions to display the correct textures/offsets.
ShowChars(ExtendedPos1, ExtendedPos2, ExtendedPos3, ExtendedPos4, ExtendedPos5);
}
integer ConvertIndex(integer index)
{
// This converts from an ASCII based index to our indexing scheme.
if (index >= 32) // ' ' or higher
index -= 32;
// index < 32
else
{
// Quick bounds check.
if (index > 15)
index = 15;
index += 94; // extended characters
}
return index;
}
default
{
state_entry()
{
// Initialize the character index.
ResetCharIndex();
//llSay(0, "Free Memory: " + (string) llGetFreeMemory());
}
link_message(integer sender, integer channel, string data, key id)
{
if (channel == DISPLAY_STRING)
{
RenderString(data);
return;
}
if (channel == DISPLAY_EXTENDED)
{
RenderExtended(data);
return;
}
if (channel == gCellChannel)
{
// Extract the characters we are interested in, and use those to render.
string TextToRender = llGetSubString(data, gCellCharPosition, gCellCharPosition + 9);
if (gCellUseFading)
RenderWithEffects( TextToRender );
else // !gCellUseFading
RenderString( TextToRender );
return;
}
if (channel == REMAP_INDICES)
{
// Parse the message, splitting it up into index values.
list RemapParsed = llCSV2List(data);
integer m;
// Go through the list and swap each pair of indices.
for (m = 0; m < llGetListLength(RemapParsed); m += 2)
{
integer Index1 = ConvertIndex( llList2Integer(RemapParsed, m) );
integer Index2 = ConvertIndex( llList2Integer(RemapParsed, m + 1) );
// Swap these index values.
string Value1 = llGetSubString(gCharIndex, Index1, Index1);
string Value2 = llGetSubString(gCharIndex, Index2, Index2);
gCharIndex = llDeleteSubString(gCharIndex, Index1, Index1);
gCharIndex = llInsertString(gCharIndex, Index1, Value2);
gCharIndex = llDeleteSubString(gCharIndex, Index2, Index2);
gCharIndex = llInsertString(gCharIndex, Index2, Value1);
}
return;
}
if (channel == RESET_INDICES)
{
// Restore the character index back to default settings.
ResetCharIndex();
return;
}
if (channel == SET_CELL_INFO)
{
// Change the channel we listen to for cell commands, and the
// starting character position to extract from.
list CellParsed = llCSV2List(data);
gCellChannel = (integer) llList2String(CellParsed, 0);
gCellCharPosition = (integer) llList2String(CellParsed, 1);
gCellUseFading = (integer) llList2String(CellParsed, 2);
gCellHoldDelay = (float) llList2String(CellParsed, 3);
return;
}
if (channel == SET_THICKNESS)
{
// Set our z scale to thickness, while staying fixed
// in position relative the prim below us.
vector Scale = llGetScale();
float Thickness = (float) data;
// Reposition only if this isn't the root prim.
integer ThisLink = llGetLinkNumber();
if (ThisLink != 0 || ThisLink != 1)
{
// This is not the root prim.
vector Up = llRot2Up(llGetLocalRot());
float DistanceToMove = Thickness / 2.0 - Scale.z / 2.0;
vector Pos = llGetLocalPos();
llSetPos(Pos + DistanceToMove * Up);
}
// Apply the new thickness.
Scale.z = Thickness;
llSetScale(Scale);
return;
}
if (channel == SET_COLOR) {
vector newColor = (vector)data;
llSetColor(newColor, ALL_SIDES);
}
}
}