Compare commits

...

10 commits

37 changed files with 913 additions and 1824 deletions

1
.gitignore vendored
View file

@ -12,6 +12,7 @@ DEPLOYMENT_NOTES.txt
# IDE and editor files
.vscode/
.idea/
*.code-workspace
*.sublime-*
*~
.*.swp

View file

@ -1,4 +1,4 @@
#include "Peril_Constants.lsl"
#include "peril/Peril_Constants.lsl"
// Handles logic for test players (bots): picking numbers and rolling dice
@ -47,6 +47,7 @@ integer listenHandle = -1;
#define MEMORY_WARNING_THRESHOLD 0.8 // Warn when using >80% of memory
// Track game state to validate bot commands
list players = [];
list names = [];
list lives = [];
string perilPlayer = "";
@ -236,6 +237,7 @@ default {
// Handle full reset from main controller
if (num == MSG_RESET_ALL && str == "FULL_RESET") {
// Reset bot manager state
players = [];
names = [];
lives = [];
perilPlayer = "";
@ -247,7 +249,7 @@ default {
// Handle game state sync to track player eliminations
if (num == MSG_SYNC_GAME_STATE) {
list parts = llParseString2List(str, ["~"], []);
list parts = llParseStringKeepNulls(str, ["~"], []);
if (llGetListLength(parts) >= 4) {
lives = llCSV2List(llList2String(parts, 0));
// Don't need picks data for validation, skip parts[1]
@ -274,13 +276,12 @@ default {
sentBotMessages = [];
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")
// Standardized ~EMPTY~ token logic
string picksStr = llList2String(parts, 1);
if (picksStr != "EMPTY" && (picksStr == "" || llSubStringIndex(picksStr, "|") == -1) && perilPlayer != "") {
if (picksStr == "~EMPTY~" && perilPlayer != "") {
processedBotCommands = [];
sentBotMessages = [];
dbg("🤖 [Bot Manager] New round detected (picks cleared), cleared processed commands and sent messages");
dbg("🤖 [Bot Manager] Turn reset detected (picks cleared), cleared processed commands and sent messages");
}
}
}

View file

@ -4,15 +4,22 @@
All notable changes to Peril Dice will be documented in this file.
## [3.2.6] - 2026-04-05
## [3.3.0] - 2026-04-09
### 🏁 **Final Production Sweep (Tournament Edition)**
- **Global Silence**: Implemented unconditional stripping of all debug string literals using the `DEBUG_LOGS 0` pre-processor macro across the entire codebase.
- **Memory Hardening**: Set `SHOW_MEMORY 0` to eliminate runtime memory reporting overhead during high-traffic tournament play.
- **Repository Sanitization**: Purged all legacy GitHub updater documentation and development tools (`lsl_validator`, etc.) for a clean production package.
- **Sentinel finalization**: Verified "Auto-Pilot" Sentinel 7.9.2 for automated, no-touch land diagnostics.
### 🎰 **Hybrid HUD Evolution**
### 🎰 **v3.3.0: Final Production Sweep - Global Silence & Memory Hardening**
- **Personalized Gaming UI**: Human players now automatically receive a temporary, Experience-authorized HUD (Final Girlz I.N.C.) snapping to **Center 2**.
- **Spectator AI Visibility**: Bots continue to rez in-world floaters, ensuring spectators can follow along with the AI's performance.
- **Direct-Target Signaling**: Switched to `llRegionSayTo(avKey, ...)` for cleanup pulses, ensuring HUDs reliably hear their "Self-Destruct" command.
- **Auto-Cleanup Handshake**: Uses `llDetachFromAvatar()` to instantly purge temporary HUDs upon game end or reset without inventory clutter.
- **Root-Key Unification**: All channel calculations now use the **Root Link Key** (`llGetLinkKey(1)`), preventing "Signal Drift" between scripts in different prims.
## [3.2.6] - 2026-04-05
### 🛡️ **Experience Sentinel (Diag Prototype v7.3)**
- **Auto-Pilot Diagnostic (v7.9.2)**: Operates automatically on rez and reset—no manual touch-trigger required.
- **Logically Sequenced Status**: Reordered the diagnostic header to appear before the result, ensuring a professional chronological flow.

View file

@ -1,4 +1,4 @@
#include "Peril_Constants.lsl"
#include "peril/Peril_Constants.lsl"
// === Controller Memory Monitor ===
// Handles memory monitoring and reporting for the Main Controller

View file

@ -1,4 +1,4 @@
#include "Peril_Constants.lsl"
#include "peril/Peril_Constants.lsl"
// === Controller Message Handler Helper Script ===
// Handles non-critical message processing for the Main Controller

View file

@ -1,6 +1,10 @@
#include "Peril_Constants.lsl"
#include "peril/Peril_Constants.lsl"
string leftText = "";
string firstName = "";
integer blinkState = 0;
integer canBlink = 0;
// Distribute text to the FURWARE "Leaderboard" boxes
distributeFullText(string text) {
@ -10,7 +14,33 @@ distributeFullText(string text) {
if (llGetListLength(parts) >= 3) {
// Distribute each column to its virtual box
llMessageLinked(LINK_SET, 0, llList2String(parts, 0), (key)"fw_data:LB_Rank");
llMessageLinked(LINK_SET, 0, llList2String(parts, 1), (key)"fw_data:LB_Name");
// Distribute Name column (Split first line for blinking)
string nameText = llList2String(parts, 1);
integer firstLineEnd = llSubStringIndex(nameText, "\n");
if (firstLineEnd != -1) {
firstName = llGetSubString(nameText, 0, firstLineEnd - 1);
string remainingNames = llGetSubString(nameText, firstLineEnd + 1, -1);
llMessageLinked(LINK_SET, 0, firstName, (key)"fw_data:LB_Name_Top");
llMessageLinked(LINK_SET, 0, remainingNames, (key)"fw_data:LB_Name");
} else {
firstName = nameText;
llMessageLinked(LINK_SET, 0, firstName, (key)"fw_data:LB_Name_Top");
llMessageLinked(LINK_SET, 0, "", (key)"fw_data:LB_Name");
}
// Check if we should blink (#1 spot on first page only)
integer offset = (integer)llList2String(parts, 5);
if (offset == 0) {
canBlink = TRUE;
llSetTimerEvent(0.5);
} else {
canBlink = FALSE;
llSetTimerEvent(0.0);
// Ensure name is visible on non-blinking pages
llMessageLinked(LINK_SET, 0, firstName, (key)"fw_data:LB_Name_Top");
}
llMessageLinked(LINK_SET, 0, llList2String(parts, 2), (key)"fw_data:LB_WL");
// Route Navigation Arrows to dedicated button boxes
@ -41,8 +71,13 @@ default {
// Registering virtual columns + Navigation buttons
// Coordinates: x, y, width, height (0-indexed)
// border=lr added to create vertical separators between columns
llMessageLinked(LINK_SET, 0, "", (key)"fw_addbox:LB_Rank:Leaderboard:0,1,4,10:");
llMessageLinked(LINK_SET, 0, "", (key)"fw_addbox:LB_Name:Leaderboard:4,1,20,10:");
// Split Name into Top (for blinking) and Body
llMessageLinked(LINK_SET, 0, "", (key)"fw_addbox:LB_Name_Top:Leaderboard:4,1,20,1:border=lr");
llMessageLinked(LINK_SET, 0, "", (key)"fw_addbox:LB_Name:Leaderboard:4,2,20,9:border=lr");
llMessageLinked(LINK_SET, 0, "", (key)"fw_addbox:LB_WL:Leaderboard:24,1,8,10:");
// Navigation Row (Shifted down to Y=11)
@ -56,8 +91,25 @@ default {
// Set the Top Line Header on the main box
llMessageLinked(LINK_SET, 0, "=== WORLD RANKING ===", (key)"fw_data:Leaderboard");
// Trigger Leaderboard Manager to download from KVP Database
llMessageLinked(LINK_SET, MSG_RESET_LEADERBOARD, "START_SYNC", NULL_KEY);
// Note: Leaderboard sync should be triggered by the Main Controller or Game Manager
// during game resets, rather than every time the bridge initializes.
// llMessageLinked(LINK_SET, MSG_RESET_LEADERBOARD, "START_SYNC", NULL_KEY);
// Start Blink Timer (0.5s intervals)
llSetTimerEvent(0.5);
}
timer() {
if (!canBlink) {
llSetTimerEvent(0.0);
return;
}
blinkState = !blinkState;
if (blinkState) {
llMessageLinked(LINK_SET, 0, firstName, (key)"fw_data:LB_Name_Top");
} else {
llMessageLinked(LINK_SET, 0, "", (key)"fw_data:LB_Name_Top");
}
}
touch_start(integer _n) {
@ -97,5 +149,6 @@ default {
}
}
}
else if (num == MSG_RESET_ALL) { llResetScript(); }
}
}

View file

@ -1,4 +1,4 @@
#include "Peril_Constants.lsl"
#include "peril/Peril_Constants.lsl"
// =============================================================================
// FURWARE STATUS BRIDGE - Dynamic Game Info Display

View file

@ -1,4 +1,4 @@
#include "Peril_Constants.lsl"
#include "peril/Peril_Constants.lsl"
// =============================================================================
// LINKSET COMMUNICATION - NO DISCOVERY NEEDED

View file

@ -1,4 +1,4 @@
#include "Peril_Constants.lsl"
#include "peril/Peril_Constants.lsl"
// === FloatManager (Consolidated) ===
// This version enforces a maximum of 10 players.
@ -123,6 +123,7 @@ default {
// Handle full reset from main controller
if (num == MSG_RESET_ALL && str == "FULL_RESET") {
allPlayerNames = [];
allPlayerKeys = [];
names = [];
players = [];
lives = [];
@ -198,7 +199,7 @@ default {
integer syncIdx = llListFindList(names, [name]);
key avKey = id;
integer lifeCount = 0;
integer lifeCount = 3; // DEFAULT TO HEALTHY (Prevent Red Flashes)
if (syncIdx != -1) {
if (syncIdx < llGetListLength(players)) avKey = llList2Key(players, syncIdx);
@ -228,14 +229,15 @@ default {
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) {
// COLOR PRIORITY: Green (Victory) > Red (Eliminated) > Yellow (Peril)
if (livingPlayers == 1 && llGetListLength(names) >= 2 && lifeCount > 0) {
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 (lifeCount <= 0) {
glow = 0.3;
color = <1,0,0>; // Red
txt = "🎲 Peril Dice\n👤 " + name + "\n🔢 Picks: " + picksDisplay + "\n💀 ELIMINATED! 💀";
} else if (name == perilPlayer) {
glow = 0.2;
color = <1,1,0>; // Yellow
@ -309,12 +311,12 @@ default {
}
}
else if (num == MSG_SYNC_GAME_STATE) {
list parts = llParseString2List(str, ["~"], []);
list parts = llParseStringKeepNulls(str, ["~"], []);
if (llGetListLength(parts) < 4) return;
lives = llCSV2List(llList2String(parts, 0));
string picksDataStr = llList2String(parts, 1);
if (picksDataStr == "" || picksDataStr == "EMPTY") {
if (picksDataStr == "" || picksDataStr == "EMPTY" || picksDataStr == "~EMPTY~") {
picksData = [];
} else {
picksData = llParseString2List(picksDataStr, ["^"], []);

View file

@ -97,7 +97,7 @@ list setDataList; // Strided list of display set data.
// Per-prim data
list primLinkList; // List of the prims' link indices
list primFillList; // Tells us which faces are showing non-blanks.
#include "Peril_Constants.lsl"
#include "peril/Peril_Constants.lsl"
list primLayerList; // Contains layer assignments for each face.
@ -616,7 +616,7 @@ default {
state_entry() {
REPORT_MEMORY();
llOwnerSay("FURWARE text is starting...");
dbg("FURWARE text is starting...");
CHARS = llBase64ToString(
"IGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFS" +
@ -674,7 +674,7 @@ default {
// Abort if not a single element was found.
if (!dataLength) {
llOwnerSay("FW text: No text 2.x prims found.");
dbg("FW text: No text 2.x prims found.");
return;
}
@ -710,7 +710,7 @@ default {
if (faceCount) {
if (newFaceCount != faceCount) {
llOwnerSay("FW text: All prims within a set need to have the same number of faces.");
dbg("FW text: All prims within a set need to have the same number of faces.");
setCount = 0;
return;
}
@ -731,7 +731,7 @@ default {
if (colCount) {
if (newColCount != colCount) {
llOwnerSay("FW text: All rows within a set need to have the same number of prims.");
dbg("FW text: All rows within a set need to have the same number of prims.");
setCount = 0;
return;
}
@ -794,7 +794,7 @@ default {
first = llListFindList(boxNameList, [boxToken0]);
if (!~first) {
llOwnerSay(token0 + ": Box \"" + boxToken0 + "\" not found.");
dbg(token0 + ": Box \"" + boxToken0 + "\" not found.");
jump SkipBox;
}
@ -806,7 +806,7 @@ default {
last = llListFindList(boxNameList, [boxToken1]);
if (!~last) {
llOwnerSay(token0 + ": Box \"" + boxToken1 + "\" not found.");
dbg(token0 + ": Box \"" + boxToken1 + "\" not found.");
jump SkipBox;
}
@ -814,7 +814,7 @@ default {
integer secondSetIndex = (llList2Integer(boxDataList, last + BOX_STATUS) >> 4) & 0xFF;
if (~setIndex && setIndex != secondSetIndex) {
llOwnerSay(token0 + ": Box sets must match when specifying a range.");
dbg(token0 + ": Box Sets must match when specifying a range.");
jump SkipBox;
}
@ -837,7 +837,7 @@ default {
if (lastAction && lastAction != ACTION_CONTENT) refresh();
if (token1 == "") {
llOwnerSay("fw_var: No variable name given.");
dbg("fw_var: No variable name given.");
return;
}
@ -888,7 +888,7 @@ default {
if (token0 == "fw_addbox") {
if (token1 == "") {
llOwnerSay("fw_addbox: Box name cannot be empty.");
dbg("fw_addbox: Box name cannot be empty.");
return;
}
@ -897,14 +897,14 @@ default {
integer boxNameIndex = llListFindList(boxNameList, [token1]);
if (~boxNameIndex) {
llOwnerSay("fw_addbox: Box \"" + token1 + "\" already exists.");
dbg("fw_addbox: Box \"" + token1 + "\" already exists.");
return;
}
integer parNameIndex = llListFindList(boxNameList, [token2]);
if (!~parNameIndex) {
llOwnerSay("fw_addbox: No parent box \"" + token2 + "\".");
dbg("fw_addbox: No parent box \"" + token2 + "\".");
return;
}
@ -922,7 +922,7 @@ default {
}
if (layersUsed == 0xFFFF) {
llOwnerSay("fw_addbox: No layer available.");
dbg("fw_addbox: No layer available.");
return;
}
@ -941,7 +941,7 @@ default {
(boxGeom.x + boxGeom.z) > (setGeom.x * setGeom.z) ||
(boxGeom.y + boxGeom.s) > setGeom.y)
{
llOwnerSay("fw_addbox: Invalid box geometry.");
dbg("fw_addbox: Invalid box geometry.");
return;
}
@ -965,7 +965,7 @@ default {
integer boxNameIndex = llListFindList(boxNameList, [boxName]);
if (!~boxNameIndex || (boxNameIndex < setCount)) {
llOwnerSay("fw_delbox: Box \"" + boxName + "\" doesn't exist.");
dbg("fw_delbox: Box \"" + boxName + "\" doesn't exist.");
jump SkipDelBox;
}

View file

@ -172,7 +172,7 @@ The game features thematic messaging for maximum immersion:
- **Kick Player** functionality allows owners to remove disruptive players
- **Leave Game** system properly cleans up all player data
### **Enhanced Display System (UPDATED in v3.2.6):**
### **Enhanced Display System (CURRENT VERSION: 3.3.0):**
- **Hybrid HUD System** - Humans get personal HUDs (Center 2), Bots get in-world floaters! 🎰🤖
- **Experience Sentinel (v7.3)** - Built-in land-readiness diagnostic automatically verifies 'Final Girlz I.N.C.' connectivity. 🛡️
- **Experience Requirement** - Land must have **"Final Girlz I.N.C."** Experience enabled for the automatic HUD feature. 🔗
@ -299,8 +299,8 @@ While the core rules remain the same, consider these optional variations:
---
**Version:** 3.2.6
**Last Updated:** April 05, 2026
**Status:** Production Ready - Hybrid HUD Release
**Version: 3.3.0**
**Last Updated: April 09, 2026**
**Status: Production Ready - Tournament Edition Release**
**Credits:** Original concept by Noose the Bunny (djmusica28), Automated system implementation by Rebecca Nod

View file

@ -1,4 +1,4 @@
#include "Peril_Constants.lsl"
#include "peril/Peril_Constants.lsl"
// === Game Calculator Module (Max 10 players) ===
// Calculates dice types, pick requirements, and other game mechanics

View file

@ -1,4 +1,8 @@
#include "Peril_Constants.lsl"
#include "peril/Peril_Constants.lsl"
debugMsg(string msg) {
if (DEBUG_LOGS) llMessageLinked(LINK_SET, MSG_DEBUG_TEXT, msg, NULL_KEY);
}
// =============================================================================
// PERIL WORLD RANKING - Leaderboard Manager
@ -11,7 +15,9 @@ list leaderboardData = []; // Stride 3: [Name, Wins, Losses]
integer currentLoadPage = 1;
key kvpReadReq;
key kvpWriteReq;
key kvpLockReq; // Request for claiming leaderboard lock
key kvpLockReq; // Stage 1: Check Lock
key kvpClaimReq; // Stage 2: Claim Lock
key kvpSentinelReq; // Startup Probe
list writeQueue = []; // Pairs of [KeyName, DataString]
integer isSaving = FALSE;
integer isBusy = FALSE; // Read lock
@ -21,18 +27,25 @@ string currentWriteKey;
string currentWriteVal;
// --- Lock & Sync State ---
string pendingPlayer = "";
string pendingResult = "";
list pendingRecords = []; // Stride 2: [Name, Result]
integer isLocking = FALSE;
string myLockVal = "";
integer isClaiming = FALSE; // Track if we are in the middle of a WRITE claim
// Helper: Process the next KVP write in the queue
processWriteQueue() {
if (llGetListLength(writeQueue) == 0) {
isSaving = FALSE;
dbg("🏆 [Leaderboard] Save sequence complete. Releasing lock...");
// releaseLock();
kvpLockReq = llUpdateKeyValue(LB_LOCK_KEY, "", TRUE, myLockVal);
// Batch Processing: Check if more records are queued before releasing lock
if (llGetListLength(pendingRecords) > 0) {
string nextName = llList2String(pendingRecords, 0);
string nextRes = llList2String(pendingRecords, 1);
pendingRecords = llDeleteSubList(pendingRecords, 0, 1);
dbg("🔄 [Leaderboard] Processing next queued record: " + nextName);
updatePlayerRecord(nextName, nextRes);
} else {
releaseLock();
}
return;
}
isSaving = TRUE;
@ -42,6 +55,12 @@ processWriteQueue() {
kvpWriteReq = llUpdateKeyValue(currentWriteKey, currentWriteVal, FALSE, "");
}
// Helper: Release the lock immediately
releaseLock() {
dbg("🏆 [Leaderboard] Releasing global lock...");
kvpLockReq = llUpdateKeyValue(LB_LOCK_KEY, "OFF", TRUE, myLockVal);
}
// Convert the whole list into sharded strings and start saving
generateShardedData() {
integer _shardI;
@ -76,20 +95,30 @@ generateShardedData() {
}
loadLBPage(integer pageNum) {
if (isBusy && pageNum == 1) return; // Prevent double loads on reset
if (pageNum > 1) llSleep(1.0); // Anti-Throttle Delay
if ((isBusy || isSaving) && pageNum == 1) {
dbg("⏳ [Leaderboard] Sync requested while Busy/Saving. Ignoring duplicate reset.");
return;
}
// ANTI-THROTTLE: Add small 0.2s pause for Mono speed compatibility
if (pageNum == 1) llSleep(0.2);
else llSleep(1.0); // 1.0s delay for sharded pages
isBusy = TRUE;
llSetTimerEvent(30.0); // 30 second watchdog (Lag Resistant)
llSetTimerEvent(30.0); // Lag Resistant watchdog
currentLoadPage = pageNum;
string keyName = LB_KEY_PREFIX + (string)pageNum;
kvpReadReq = llReadKeyValue(keyName);
if (kvpReadReq == NULL_KEY) {
dbg("❌ [Leaderboard] KVP Read FAILED: Not authorized for " + keyName);
llSetTimerEvent(0.0);
} else {
dbg("📡 [Leaderboard] Loading " + keyName + "...");
dbg("⚠️ [Leaderboard] KVP Request FAILED (Throttle/NULL_KEY). Retrying " + keyName + " in 2s...");
// KEEP isBusy = TRUE to prevent collisions during retry
llSetTimerEvent(2.0);
return;
}
dbg("📡 [Leaderboard] Loading " + keyName + "...");
}
list getSortedSlice(integer offset) {
@ -118,14 +147,14 @@ updatePlayerRecord(string pName, string pResult) {
if (pResult == "WIN") leaderboardData += [1, 0, pName];
else leaderboardData += [0, 1, pName];
} else {
integer _base = _idx - 2;
integer _v;
integer _updBase = _idx - 2;
integer _updVal;
if (pResult == "WIN") {
_v = llList2Integer(leaderboardData, _base);
leaderboardData = llListReplaceList(leaderboardData, [_v + 1], _base, _base);
_updVal = llList2Integer(leaderboardData, _updBase);
leaderboardData = llListReplaceList(leaderboardData, [_updVal + 1], _updBase, _updBase);
} else {
_v = llList2Integer(leaderboardData, _base + 1);
leaderboardData = llListReplaceList(leaderboardData, [_v + 1], _base+1, _base+1);
_updVal = llList2Integer(leaderboardData, _updBase + 1);
leaderboardData = llListReplaceList(leaderboardData, [_updVal + 1], _updBase + 1, _updBase + 1);
}
}
@ -135,34 +164,67 @@ updatePlayerRecord(string pName, string pResult) {
}
startUpdateSequence(string pName, string pResult) {
if (isLocking || isSaving) {
dbg("⚠️ [Leaderboard] Update already in progress. Retrying in 2s...");
llSleep(2.0);
llSetTimerEvent(0); // GHOST-BUSTER: Kill any pending retries/syncs
// Check if record already in queue to avoid duplicates
if (llListFindList(pendingRecords, [pName, pResult]) != -1) return;
pendingRecords += [pName, pResult]; // Add to Victory Queue
if (isBusy || isLocking || isSaving) {
dbg("⏳ [Leaderboard] Record for [" + pName + "] added to queue (" + (string)(llGetListLength(pendingRecords)/2) + " pending).");
return;
}
pendingPlayer = pName;
pendingResult = pResult;
isLocking = TRUE;
dbg("🔐 [Leaderboard] Checking global lock for update...");
kvpLockReq = llReadKeyValue(LB_LOCK_KEY);
}
default {
on_rez(integer _p) { llResetScript(); }
state_entry() {
REPORT_MEMORY();
dbg("⏳ [Leaderboard] Startup: Handshake link active.");
pendingRecords = [];
dbg("⏳ [Leaderboard] v1.55 (Atomic Endurance) - Handshake link active. Probing Experience...");
// SENTINEL CHECK: Use a dedicated ID to prevent shard-read collisions
kvpSentinelReq = llReadKeyValue(LB_KEY_PREFIX + "1");
llSetTimerEvent(10.0); // 10s watchdog for sentinel
}
dataserver(key _qid, string _qd) {
if (_qid == kvpReadReq) {
integer _c = llSubStringIndex(_qd, ",");
string _st = _qd; string _vl = ""; string _rawVal = "";
if (_c != -1) {
_st = llGetSubString(_qd, 0, _c - 1);
_rawVal = llGetSubString(_qd, _c + 1, -1);
_vl = llStringTrim(_rawVal, STRING_TRIM);
}
if (_qid == kvpSentinelReq) {
llSetTimerEvent(0.0); // Reset watchdog
integer _c = llSubStringIndex(_qd, ",");
string _st = _qd; string _vl = "";
if (_c != -1) {
_st = llGetSubString(_qd, 0, _c - 1);
_vl = llGetSubString(_qd, _c + 1, -1);
kvpSentinelReq = NULL_KEY; // Kill it once used
if (_st == "1" || _st == "3") { // Success (1) or Not Found (3) prove access
dbg("✅ [Leaderboard] Experience Sentinel: KVP Access Confirmed. Starting Auto-Sync...");
loadLBPage(1);
} else if (_st == "0") {
string sentinelErr = _vl;
if (_vl == "1") sentinelErr = "Experience Throttled (Error 1)";
else if (_vl == "3") sentinelErr = "Invalid Parameters (Error 3)";
else if (_vl == "17") sentinelErr = "Experience Disabled/Not Permitted (Error 17)";
else sentinelErr = "KVP Error Status: " + _vl;
dbg("🚫 [Leaderboard] Experience Sentinel: FATAL " + sentinelErr);
llOwnerSay("🚫 [Leaderboard] WORLD RANKING DISABLED: " + sentinelErr);
}
return;
}
if (_qid == kvpReadReq) {
if (isBusy == FALSE && isLocking == FALSE) return; // Ignore stale reads
if (_st == "1") { // SUCCESS: Data found
if (_vl != "" && llSubStringIndex(_vl, "Error") == -1) {
@ -172,50 +234,84 @@ default {
string _rawEntry = llList2String(_ent, _ei);
if (llSubStringIndex(_rawEntry, ",") != -1) {
// NEW FORMAT: Wins,Losses,Name
list _it = llParseString2List(_rawEntry, [","], []);
if (llGetListLength(_it) >= 3) {
leaderboardData += [(integer)llList2String(_it, 0), (integer)llList2String(_it, 1), llList2String(_it, 2)];
list _itNew = llParseString2List(_rawEntry, [","], []);
if (llGetListLength(_itNew) >= 3) {
leaderboardData += [(integer)llList2String(_itNew, 0), (integer)llList2String(_itNew, 1), llList2String(_itNew, 2)];
}
} else {
// OLD LEGACY FORMAT: Name:Wins:Losses
list _it = llParseString2List(_rawEntry, [":"], []);
if (llGetListLength(_it) >= 3) {
list _itOld = llParseString2List(_rawEntry, [":"], []);
if (llGetListLength(_itOld) >= 3) {
// Translate to new stride structure in memory
leaderboardData += [(integer)llList2String(_it, 1), (integer)llList2String(_it, 2), llList2String(_it, 0)];
leaderboardData += [(integer)llList2String(_itOld, 1), (integer)llList2String(_itOld, 2), llList2String(_itOld, 0)];
}
}
}
}
if (currentLoadPage < 10) {
if (isBusy && currentLoadPage < 10) {
retryCount = 0; // Reset retries for next page
loadLBPage(currentLoadPage + 1);
} else {
isBusy = FALSE;
// SYNC COMPLETE (Natural end)
llSetTimerEvent(0);
dbg("🏆 [Leaderboard] SYNC COMPLETE - " + (string)(llGetListLength(leaderboardData)/3) + " players.");
llMessageLinked(LINK_SET, MSG_LB_LOAD_COMPLETE, "", NULL_KEY);
llMessageLinked(LINK_SET, MSG_LB_REQUEST_DISPLAY, "0", NULL_KEY); // Show page 1
// IF WE WERE SYNCING FOR AN UPDATE, APPLY IT NOW
if (isLocking) {
isLocking = FALSE;
updatePlayerRecord(pendingPlayer, pendingResult);
// Check if any records were queued while we were busy or saving
if (llGetListLength(pendingRecords) > 0) {
dbg("🔄 [Leaderboard] Processing queued record for [" + llList2String(pendingRecords, 0) + "]...");
string nextName = llList2String(pendingRecords, 0);
string nextRes = llList2String(pendingRecords, 1);
pendingRecords = llDeleteSubList(pendingRecords, 0, 1);
isBusy = FALSE;
isSaving = FALSE;
updatePlayerRecord(nextName, nextRes);
} else {
isBusy = FALSE;
isSaving = FALSE;
}
}
} else if (_st == "0" && _vl == "1") {
// XP_ERROR_THROTTLED
dbg("⚠️ [Leaderboard] Read throttled. Retrying Shard " + (string)currentLoadPage + " in 2s...");
llSleep(2.0);
isBusy = FALSE;
loadLBPage(currentLoadPage);
} else { // Empty or Error
isBusy = FALSE;
llSetTimerEvent(0);
if (_st == "0") { dbg("❌ [Leaderboard] KVP Error on Shard " + (string)currentLoadPage + ": " + _vl); }
dbg("🏆 [Leaderboard] SYNC COMPLETE - " + (string)(llGetListLength(leaderboardData)/3) + " players total.");
llMessageLinked(LINK_SET, MSG_LB_LOAD_COMPLETE, "", NULL_KEY);
llMessageLinked(LINK_SET, MSG_LB_REQUEST_DISPLAY, "0", NULL_KEY);
} else if (_st == "0") {
if (_vl == "1") { // XP_ERROR_THROTTLED
dbg("⚠️ [Leaderboard] Read throttled. Retrying Shard " + (string)currentLoadPage + " in 2s...");
llSleep(2.0);
// RETAIN isBusy/isSaving status during retry
loadLBPage(currentLoadPage);
} else if (_vl == "14") { // XP_ERROR_KEY_NOT_FOUND (Value is empty)
if (currentLoadPage < 10) {
loadLBPage(currentLoadPage + 1);
} else {
// SYNC COMPLETE (Empty trailing shards)
llSetTimerEvent(0);
dbg("🏆 [Leaderboard] SYNC COMPLETE - " + (string)(llGetListLength(leaderboardData)/3) + " players total.");
llMessageLinked(LINK_SET, MSG_LB_LOAD_COMPLETE, "", NULL_KEY);
llMessageLinked(LINK_SET, MSG_LB_REQUEST_DISPLAY, "0", NULL_KEY);
if (llGetListLength(pendingRecords) > 0) {
dbg("🔄 [Leaderboard] Processing queued record after empty shards...");
string nextName = llList2String(pendingRecords, 0);
string nextRes = llList2String(pendingRecords, 1);
pendingRecords = llDeleteSubList(pendingRecords, 0, 1);
isBusy = FALSE;
isSaving = FALSE;
updatePlayerRecord(nextName, nextRes);
} else {
isBusy = FALSE;
isSaving = FALSE;
}
}
} else {
dbg("❌ [Leaderboard] Read Error (Code " + _vl + ") on Shard " + (string)currentLoadPage);
isBusy = FALSE;
llSetTimerEvent(0);
}
} else if (_st == "3") { // Not Found
isBusy = FALSE;
llSetTimerEvent(0);
dbg("🏆 [Leaderboard] Shard " + (string)currentLoadPage + " not found.");
if (currentLoadPage < 10) loadLBPage(currentLoadPage + 1);
}
}
else if (_qid == kvpWriteReq) {
@ -226,64 +322,92 @@ default {
_vl = llGetSubString(_qd, _c + 1, -1);
}
if (_st == "0" && _vl == "1") {
if (_st == "1") {
dbg("✅ [Leaderboard] Write success to " + currentWriteKey);
processWriteQueue();
} else if (_st == "0" && _vl == "1") {
// XP_ERROR_THROTTLED
dbg("⚠️ [Leaderboard] Write throttled. Retrying " + currentWriteKey + " in 2s...");
llSleep(2.0);
kvpWriteReq = llUpdateKeyValue(currentWriteKey, currentWriteVal, FALSE, "");
} else {
if (_st == "0") { dbg("❌ [Leaderboard] Write Error (" + _vl + ") on " + currentWriteKey); }
dbg("❌ [Leaderboard] Write Error (" + _st + ":" + _vl + ") on " + currentWriteKey);
processWriteQueue();
}
}
else if (_qid == kvpLockReq) {
integer _c = llSubStringIndex(_qd, ",");
string _st = _qd; string _vl = "";
if (_c != -1) {
_st = llGetSubString(_qd, 0, _c - 1);
_vl = llGetSubString(_qd, _c + 1, -1);
else if (_qid == kvpClaimReq) {
kvpClaimReq = NULL_KEY; // DUPLICATE DEFENSE: Prevent zombie processing
if (_st == "1") {
dbg("🔐 [Leaderboard] LOCK ACQUIRED. Starting fresh sync...");
isClaiming = FALSE;
isLocking = FALSE;
isSaving = TRUE;
leaderboardData = [];
loadLBPage(1);
} else if (_vl == "15") { // ATOMIC CHECK FAILED
dbg("⏳ [Leaderboard] Race conflict (Loss). Yielding and retrying in 5s...");
isClaiming = FALSE;
isLocking = TRUE; // Stay in locking mode for retry
llSetTimerEvent(5.0);
} else {
dbg("❌ [Leaderboard] Lock claim failed. Status: " + _st + ":" + _vl);
isClaiming = FALSE;
isLocking = FALSE;
}
}
else if (_qid == kvpLockReq) {
kvpLockReq = NULL_KEY; // DUPLICATE DEFENSE: Prevent zombie processing
// --- STAGE 1: Check Result (Success Pulse = 1) ---
if (isLocking) {
// STAGE 1: Current Lock Read Result
if (_st == "1") { // Success read
string _curVal = _vl;
if (_st == "1" && _c != -1) {
string _curVal = _rawVal; // USE RAW DATA FOR ATOMIC CHECK
integer _isStale = FALSE;
list _p = [];
integer _ts = 0;
if (_curVal != "") {
list _p = llParseString2List(_curVal, [":"], []);
integer _ts = (integer)llList2String(_p, 1);
_p = llParseString2List(_curVal, [":"], []);
_ts = (integer)llList2String(_p, 1);
if (llGetUnixTime() - _ts > LB_LOCK_TIMEOUT) _isStale = TRUE;
}
if (_curVal == "" || _isStale) {
dbg("🔐 [Leaderboard] Lock available. Claiming...");
if (llList2String(_p, 0) == (string)llGetKey()) {
dbg("🔐 [Leaderboard] Lock already held by this board (Self-Sync). Proceeding...");
myLockVal = _curVal;
isLocking = FALSE;
// Set isBusy before loading to ensure the guard is handled correctly
isBusy = TRUE;
leaderboardData = [];
loadLBPage(1);
return;
}
if (_curVal == "" || _curVal == "OFF" || _isStale) {
dbg("🔐 [Leaderboard] Lock available (or stale). Claiming...");
myLockVal = (string)llGetKey() + ":" + (string)llGetUnixTime();
// ATOMIC UPDATE: Only write if value is still what we just read
kvpLockReq = llUpdateKeyValue(LB_LOCK_KEY, myLockVal, TRUE, _curVal);
isClaiming = TRUE;
kvpClaimReq = llUpdateKeyValue(LB_LOCK_KEY, myLockVal, TRUE, _curVal);
} else {
dbg("⏳ [Leaderboard] Lock held by another board. Retrying in 5s...");
dbg("⏳ [Leaderboard] Lock held by board [" + llList2String(_p, 0) + "] for " + (string)(llGetUnixTime() - _ts) + "s. Retrying in 5s...");
llSetTimerEvent(5.0);
}
} else if (_st == "0" && _vl == "14") { // KEY_NOT_FOUND (Lock key doesn't exist)
dbg("🔐 [Leaderboard] Lock doesn't exist. Creating initial lock...");
myLockVal = (string)llGetKey() + ":" + (string)llGetUnixTime();
isClaiming = TRUE;
kvpClaimReq = llUpdateKeyValue(LB_LOCK_KEY, myLockVal, FALSE, "");
} else if (_st == "0" && _vl == "1") { // Throttled
dbg("⚠️ [Leaderboard] Lock check throttled. Retrying...");
llSleep(2.0);
kvpLockReq = llReadKeyValue(LB_LOCK_KEY);
} else if (_st == "14") { // SUCCESSFUL CLAIM (Atomic check passed)
dbg("🔐 [Leaderboard] LOCK ACQUIRED. Starting fresh sync...");
leaderboardData = []; // PURGE STALE MEMORY
loadLBPage(1);
} else if (_st == "0" && _vl == "15") { // ATOMIC CHECK FAILED (Someone else grabbed it)
dbg("⏳ [Leaderboard] Lock race lost. Retrying...");
llSleep(1.0);
kvpLockReq = llReadKeyValue(LB_LOCK_KEY);
} else {
dbg("❌ [Leaderboard] Lock Error: (" + _st + ") " + _vl);
dbg("❌ [Leaderboard] Lock logic failed (Code " + _st + ":" + _vl + ")");
isLocking = FALSE;
}
} else {
// RELEASE RESULT
if (_st == "14") { dbg("🔓 [Leaderboard] Lock released."); }
else { dbg("⚠️ [Leaderboard] Lock release issues: " + _vl); }
// RELEASE RESULT: Check for Status 1 (Success)
if (_st == "1") { dbg("🔓 [Leaderboard] Lock released."); }
else { dbg("⚠️ [Leaderboard] Lock release report: " + _st + ":" + _vl); }
}
}
}
@ -295,16 +419,16 @@ default {
return;
}
if (isBusy) {
if (isBusy) { // DO NOT check isSaving here, as isBusy should be the authoritative lock for READS
if (retryCount < 3) {
retryCount++;
dbg("⚠️ [Leaderboard] Shard " + (string)currentLoadPage + " timed out. Retrying (" + (string)retryCount + "/3)...");
dbg("⏳ [Leaderboard] Retrying Shard " + (string)currentLoadPage + " (" + (string)retryCount + "/3)...");
isBusy = FALSE; // Unlock so loadLBPage can run
loadLBPage(currentLoadPage);
} else {
isBusy = FALSE;
llSetTimerEvent(0);
dbg("🏆 [Leaderboard] SYNC GAVE UP (Persistent timeout on Shard " + (string)currentLoadPage + ")");
dbg("🏆 [Leaderboard] SYNC GAVE UP (Persistent issues on Shard " + (string)currentLoadPage + ")");
llMessageLinked(LINK_SET, MSG_LB_REQUEST_DISPLAY, "0", NULL_KEY);
}
} else {
@ -314,7 +438,9 @@ default {
link_message(integer sender, integer num, string str, key id) {
if (num == MSG_LB_RECORD_PLAYER) {
startUpdateSequence(str, (string)id);
// str is "Name|Result" (e.g. "Bot1|WIN")
list _p = llParseString2List(str, ["|"], []);
startUpdateSequence(llList2String(_p, 0), llList2String(_p, 1));
}
else if (num == MSG_DISPLAY_LEADERBOARD) {
if (str == "REFRESH_STARTUP") loadLBPage(1);
@ -343,7 +469,7 @@ default {
}
string _p = " "; if (_offset > 0) _p = "[ < PREV ]";
string _n = " "; if (_offset + 10 < _globalTotal) _n = "[ NEXT > ]";
llMessageLinked(LINK_SET, MSG_DISPLAY_LEADERBOARD, "COLUMNS|" + _rankCol + "|" + _nameCol + "|" + _statsCol + "|" + _p + "|" + _n, NULL_KEY);
llMessageLinked(LINK_SET, MSG_DISPLAY_LEADERBOARD, "COLUMNS|" + _rankCol + "|" + _nameCol + "|" + _statsCol + "|" + _p + "|" + _n + "|" + (string)_offset, NULL_KEY);
}
else if (num == MSG_LB_PAGE_NEXT) {
integer _maxEntries = llGetListLength(leaderboardData) / 3;
@ -367,10 +493,24 @@ default {
if (str == "START_SYNC") {
loadLBPage(1);
}
else if (str == "WIPE" || str == "HARD_RESET") {
if (id != (key)GLOBAL_ADMIN) {
llOwnerSay("🚫 [Security] Unauthorized WIPE attempt blocked from: " + (string)id);
return;
}
// Soft Wipe: Clear all shards to empty in world datastore
integer _i;
for (_i = 1; _i <= MAX_LB_PAGES; _i++) {
llUpdateKeyValue(LB_KEY_PREFIX + (string)_i, "", FALSE, "");
}
dbg("🚫 [Leaderboard Manager] GLOBAL WIPE: All 10 datastore shards have been set to empty.");
}
else if (str == "DEBUG_GENERATE" && llGetOwner() == llGetCreator()) {
generateShardedData();
}
}
else if (num == MSG_RESET_ALL) { llResetScript(); }
else if (num == MSG_RESET_ALL) {
if (str == "HARD_RESET" || str == "ADMIN_RESET") llResetScript();
}
}
}

View file

@ -1,4 +1,4 @@
#include "Peril_Constants.lsl"
#include "peril/Peril_Constants.lsl"
// === Game Manager - Game Flow Logic ===
@ -27,6 +27,7 @@ integer roundStarted = FALSE;
integer diceTypeProcessed = FALSE; // Track if we've already processed dice type for this round
integer ignorePicksSync = FALSE; // Temporarily ignore picks data sync during round initialization
integer roundContinueInProgress = FALSE; // Prevent multiple rapid calls to continueCurrentRound
integer lastPlayerCount = 0; // Memory for dice type calculation (skips redundant requests)
integer lastSyncProcessTime = 0; // Track when we last processed a sync to prevent rapid duplicates
// Duplicate request prevention
@ -149,24 +150,28 @@ integer continueCurrentRound() {
}
currentPickerIdx = 0;
dbg("🎯 [Game Manager] Pick queue created: " + llList2CSV(pickQueue) + ", requesting dice type");
dbg("🎯 [Game Manager] Pick queue created: " + llList2CSV(pickQueue) + ", checking dice memory...");
// Request dice type directly from Calculator for the new round
llMessageLinked(LINK_SET, MSG_GET_DICE_TYPE, (string)llGetListLength(names), NULL_KEY);
integer currentPlayerCount = llGetListLength(names);
if (currentPlayerCount == lastPlayerCount && diceType > 0) {
dbg("🎯 [Game Manager] Player count unchanged (" + (string)currentPlayerCount + "), reusing stored dice type: d" + (string)diceType);
// NEW: Add a brief buffer to let the previous outcome status stay on screen
llSleep(DELAY_MEDIUM_SYNC);
// Reuse stored dice type and skip redundant request/status Flicker
diceTypeProcessed = TRUE;
llMessageLinked(LINK_SET, MSG_STATUS_TEXT, "NEW ROUND STARTING!\nPeril: <!c=red>" + perilPlayer, NULL_KEY);
showNextPickerDialog();
} else {
lastPlayerCount = currentPlayerCount;
// NEW: Update status bar for new round initialization
llMessageLinked(LINK_SET, MSG_STATUS_TEXT, "NEW ROUND STARTING!\nPeril: <!c=red>" + perilPlayer, NULL_KEY);
// Request dice type directly from Calculator for the new round
llMessageLinked(LINK_SET, MSG_GET_DICE_TYPE, (string)currentPlayerCount, NULL_KEY);
}
// NEW: Update status bar for new round initialization
llMessageLinked(LINK_SET, MSG_STATUS_TEXT, "PREPARING ROUND...\nCalculating dice size...", NULL_KEY);
// Dramatic pause to let the preparation message be read
llSleep(DELAY_LONG_SYNC);
// NEW: Update status bar for new round with colored highlight
llMessageLinked(LINK_SET, MSG_STATUS_TEXT, "NEW ROUND STARTING!\nPeril: <!c=red>" + perilPlayer, NULL_KEY);
// Dramatic pause to let the peril player be seen before the dice type is shown
llSleep(DELAY_LONG_SYNC);
// Note: showNextPickerDialog() will be called when dice type result is received
// Note: showNextPickerDialog() will be called when dice type result is received (or immediately if cached)
return 0;
}
@ -249,16 +254,25 @@ integer startNextRound() {
currentPickerIdx = 0;
dbg("🎯 Debug - Created pickQueue: " + llList2CSV(pickQueue) + ", currentPickerIdx: " + (string)currentPickerIdx);
// Request dice type for this round
llMessageLinked(LINK_SET, MSG_GET_DICE_TYPE, (string)llGetListLength(names), NULL_KEY);
integer totalPlayers = llGetListLength(names);
if (totalPlayers == lastPlayerCount && diceType > 0) {
dbg("🎯 [Game Manager] Player count unchanged (" + (string)totalPlayers + "), reusing stored dice type: d" + (string)diceType);
// NEW: Add a brief buffer to let the previous outcome status stay on screen
llSleep(DELAY_MEDIUM_SYNC);
diceTypeProcessed = TRUE;
llMessageLinked(LINK_SET, MSG_STATUS_TEXT, "ROUND STARTING!\nPeril: <!c=red>" + perilPlayer, NULL_KEY);
showNextPickerDialog();
} else {
lastPlayerCount = totalPlayers;
// Request dice type for this round
llMessageLinked(LINK_SET, MSG_GET_DICE_TYPE, (string)totalPlayers, NULL_KEY);
// NEW: Update status bar for round start
llMessageLinked(LINK_SET, MSG_STATUS_TEXT, "ROUND STARTING!\nPeril: <!c=red>" + perilPlayer, NULL_KEY);
}
// NEW: Update status bar for new round initialization
llMessageLinked(LINK_SET, MSG_STATUS_TEXT, "PREPARING ROUND...\nCalculating dice size...", NULL_KEY);
// Dramatic pause to let the preparation message be read
llSleep(DELAY_LONG_SYNC);
dbg("🎯 Game Manager round setup complete, requesting dice type...");
dbg("🎯 Game Manager round setup complete.");
return 0;
}
@ -484,7 +498,7 @@ default {
// Only skip sync messages if we're explicitly ignoring them
// Allow player registration syncs even when game is not active
list parts = llParseString2List(str, ["~"], []);
list parts = llParseStringKeepNulls(str, ["~"], []);
if (llGetListLength(parts) >= 4) {
list newLives = llCSV2List(llList2String(parts, 0));
string currentLivesStr = llList2CSV(lives);
@ -509,7 +523,7 @@ default {
if (livesChanged || perilChanged) {
string encodedPicksDataStr = llList2String(parts, 1);
list newPicksData = [];
if (encodedPicksDataStr != "") {
if (encodedPicksDataStr != "" && encodedPicksDataStr != "~EMPTY~") {
list encodedEntries = llParseString2List(encodedPicksDataStr, ["^"], []);
integer syncI;
for (syncI = 0; syncI < llGetListLength(encodedEntries); syncI++) {
@ -551,10 +565,8 @@ default {
}
}
if (livesChanged && newPerilPlayer != "" && newPerilPlayer != "NONE" && roundStarted && allPicksEmpty) {
dbg("🎯 Post-roll state update detected - continuing round");
continueCurrentRound();
}
// Note: Round continuation is now handled by the Main Controller's explicit MSG_CONTINUE_ROUND signal
// This prevents duplicate round starts and race conditions during Plot Twists.
}
}
return;
@ -778,7 +790,7 @@ default {
// Use complete avoid list for re-shown dialog too
list updatedCompleteAvoidList = buildCompleteAvoidanceList();
string dialogPayload = playerName + "|" + (string)diceType + "|" + (string)picksNeeded + "|" + llList2CSV(updatedCompleteAvoidList);
string dialogRequest = "SHOW_DIALOG|" + playerName + "|" + dialogPayload;
string dialogRequest = "SHOW_DIALOG~" + playerName + "~" + dialogPayload;
llMessageLinked(LINK_SET, MSG_DIALOG_FORWARD_REQUEST, dialogRequest, NULL_KEY);
}
return;
@ -839,7 +851,7 @@ default {
// Send roll dialog through Player_RegistrationManager
dbg("🎯 [Game Manager] Sending roll dialog request for: " + perilPlayer);
string rollRequest = "SHOW_ROLL_DIALOG|" + perilPlayer + "|" + perilPlayer;
string rollRequest = "SHOW_ROLL_DIALOG~" + perilPlayer + "~" + perilPlayer;
llMessageLinked(LINK_SET, MSG_DIALOG_FORWARD_REQUEST, rollRequest, NULL_KEY);
}
}
@ -898,7 +910,7 @@ default {
llMessageLinked(LINK_SET, MSG_BOT_COMMAND, "CLOSE_ALL_DIALOGS", NULL_KEY);
// Send roll dialog through Player_RegistrationManager
string rollRequest = "SHOW_ROLL_DIALOG|" + perilPlayer + "|" + perilPlayer;
string rollRequest = "SHOW_ROLL_DIALOG~" + perilPlayer + "~" + perilPlayer;
llMessageLinked(LINK_SET, MSG_DIALOG_FORWARD_REQUEST, rollRequest, NULL_KEY);
}
return; // Ignore duplicate - bot already has real picks
@ -1008,7 +1020,7 @@ default {
llMessageLinked(LINK_SET, MSG_BOT_COMMAND, "CLOSE_ALL_DIALOGS", NULL_KEY);
// Send roll dialog through Player_RegistrationManager
string rollRequest = "SHOW_ROLL_DIALOG|" + perilPlayer + "|" + perilPlayer;
string rollRequest = "SHOW_ROLL_DIALOG~" + perilPlayer + "~" + perilPlayer;
llMessageLinked(LINK_SET, MSG_DIALOG_FORWARD_REQUEST, rollRequest, NULL_KEY);
} else {
// Move to next picker
@ -1065,6 +1077,7 @@ default {
currentPicker = NULL_KEY;
roundStarted = FALSE;
diceTypeProcessed = FALSE; // Reset for new game
lastPlayerCount = 0; // Reset dice memory for new game
roundContinueInProgress = FALSE; // Reset protection flag
lastSyncProcessTime = 0; // Reset sync timing
dbg("🎯 Game Manager reset - ready for new game!");
@ -1084,5 +1097,90 @@ default {
dbg("🔒 [Game Manager] Emergency state reset complete");
return;
}
// Handle elimination requests (Offloaded from Main Controller)
if (num == MSG_ELIMINATE_PLAYER) {
list parts = llParseString2List(str, ["|"], []);
if (llList2String(parts, 0) == "ELIMINATE_PLAYER") {
string eliminatedPlayer = llList2String(parts, 1);
integer idx = llListFindList(names, [eliminatedPlayer]);
if (idx != -1) {
dbg("🎯 [Game Manager] Processing elimination for: " + eliminatedPlayer);
// NEW: Only announce elimination if it's not the end of the game (winner)
if (llGetListLength(names) > 1) {
llMessageLinked(LINK_SET, MSG_STATUS_TEXT, "PLAYER STRUCK!\n" + eliminatedPlayer + " has been eliminated!", NULL_KEY);
}
// FIRST: Set the player's lives to 0 (locally and visually)
lives = llListReplaceList(lives, [0], idx, idx);
// Update scoreboard visually for 0 hearts
llMessageLinked(LINK_SCOREBOARD, MSG_PLAYER_UPDATE,
eliminatedPlayer + "|0|" + (string)llList2Key(players, idx), NULL_KEY);
// RECORD LOSS: Send eliminated player to World Ranking database
llMessageLinked(LINK_SET, MSG_LB_RECORD_PLAYER, eliminatedPlayer + "|LOSS", NULL_KEY);
// CRITICAL: Check for victory condition BEFORE assigning new peril player
integer willCauseVictory = (llGetListLength(names) <= 2);
string potentialWinner = "";
if (willCauseVictory) {
integer w;
for (w = 0; w < llGetListLength(names); w++) {
string candidateName = llList2String(names, w);
if (candidateName != eliminatedPlayer) {
potentialWinner = candidateName;
jump found_winner;
}
}
@found_winner;
if (potentialWinner != "") {
// Synchronize visually
llMessageLinked(LINK_SCOREBOARD, MSG_UPDATE_WINNER, potentialWinner, NULL_KEY);
llMessageLinked(LINK_SET, MSG_UPDATE_FLOAT, potentialWinner, NULL_KEY);
}
}
// Visual elimination effect
llMessageLinked(LINK_SET, MSG_UPDATE_FLOAT, eliminatedPlayer, NULL_KEY);
llSleep(DELAY_ELIMINATION_NOTICE);
if (willCauseVictory) {
llSleep(DELAY_VICTORY_NOTICE);
}
// Inform Main Controller to perform deep cleanup (list deletion)
llMessageLinked(LINK_SET, MSG_PROCESS_ELIMINATION, eliminatedPlayer, NULL_KEY);
// Handle Ultimate Victory Sequence
if (willCauseVictory && potentialWinner != "") {
// Clear local state
roundStarted = FALSE;
llSay(0, "✨ ULTIMATE VICTORY! " + potentialWinner + " is the Ultimate Survivor!");
llMessageLinked(LINK_SET, MSG_STATUS_TEXT, "=== ULTIMATE VICTORY ===\n<!c=white>" + potentialWinner + " wins!", NULL_KEY);
llMessageLinked(LINK_SET, MSG_PLAYER_WON, potentialWinner, NULL_KEY);
llMessageLinked(LINK_SET, MSG_EFFECT_CONFETTI, "VICTORY_CONFETTI", NULL_KEY);
// RECORD WIN: Send ultimate survivor to World Ranking database
llMessageLinked(LINK_SET, MSG_LB_RECORD_PLAYER, potentialWinner + "|WIN", NULL_KEY);
// Inform scoreboard of victory visuals
llMessageLinked(LINK_SCOREBOARD, MSG_GAME_STATUS, "Victory", NULL_KEY);
llMessageLinked(LINK_SCOREBOARD, MSG_GAME_WON, potentialWinner, NULL_KEY);
// CRITICAL: Signal Main Controller to perform final reset after victory delay
// Main Controller expects 998 | "GAME_WON|name"
llMessageLinked(LINK_SET, MSG_CONTINUE_GAME, "GAME_WON|" + potentialWinner, NULL_KEY);
}
}
}
return;
}
}
}

View file

@ -1,4 +1,4 @@
#include "Peril_Constants.lsl"
#include "peril/Peril_Constants.lsl"
// =============================================================================
// PERIL SCOREBOARD - GRID DISPLAY MANAGER (v3.2.7 Final)
@ -107,9 +107,10 @@ clearAllPlayers() {
integer _hL = getHeartsPrimLink(_ca_i);
integer _oL = getOverlayPrimLink(_ca_i);
if (_pL > 0) {
llSetLinkPrimitiveParamsFast(_pL, [PRIM_COLOR, ALL_SIDES, <1,1,1>, 0.0]);
llSetLinkPrimitiveParamsFast(_hL, [PRIM_COLOR, ALL_SIDES, <1,1,1>, 0.0]);
llSetLinkPrimitiveParamsFast(_oL, [PRIM_COLOR, ALL_SIDES, <1,1,1>, 0.0]);
// Explicitly reset textures to blank to prevent profile persistence
llSetLinkPrimitiveParamsFast(_pL, [PRIM_TEXTURE, ALL_SIDES, BLANK_TEXTURE, <1,1,0>, <0,0,0>, 0.0, PRIM_COLOR, ALL_SIDES, <1,1,1>, 0.0]);
llSetLinkPrimitiveParamsFast(_hL, [PRIM_TEXTURE, ALL_SIDES, TEXTURE_0_HEARTS, <1,1,0>, <0,0,0>, 0.0, PRIM_COLOR, ALL_SIDES, <1,1,1>, 0.0]);
llSetLinkPrimitiveParamsFast(_oL, [PRIM_TEXTURE, ALL_SIDES, BLANK_TEXTURE, <1,1,0>, <0,0,0>, 0.0, PRIM_COLOR, ALL_SIDES, <1,1,1>, 0.0]);
}
}
}
@ -232,13 +233,15 @@ updatePlayerDisplay(string pName, integer pLives, string pUUID) {
}
default {
on_rez(integer _sp) { discoverLinks(); llResetScript(); }
on_rez(integer _sp) { discoverLinks(); clearAllPlayers(); llResetScript(); }
state_entry() {
discoverLinks();
llListen(1, "", llGetOwner(), "");
REPORT_MEMORY();
clearAllPlayers(); // Ensure board is clean on initialization
resetBackgroundPrim();
resetManagerCube();
updateActionsPrim("Title");
updatePlayerGlowEffects();
dbg("Ready");
}
@ -256,8 +259,10 @@ default {
currentPerilPlayer = ""; currentWinner = ""; updatePlayerGlowEffects();
llMessageLinked(LINK_SET, MSG_LB_REQUEST_DISPLAY, "", NULL_KEY);
}
else if (_n == MSG_GAME_WON) llMessageLinked(LINK_SET, MSG_LB_RECORD_PLAYER, _st + "|WIN", NULL_KEY);
else if (_n == MSG_GAME_LOST) llMessageLinked(LINK_SET, MSG_LB_RECORD_PLAYER, _st + "|LOSS", NULL_KEY);
else if (_n == MSG_GAME_WON || _n == MSG_GAME_LOST) {
// Visual winning/losing logic is already handled by specific update messages
// Record keeping is now centralized in Main Controller
}
else if (_n == MSG_LB_DISPLAY_DATA) {
string _tl = "WORLD RANKING RECORD"; string _sp = " ";
integer _m = (LEADERBOARD_WIDTH - llStringLength(_tl)) / 2;
@ -307,9 +312,9 @@ default {
activePlayers = llListReplaceList(activePlayers, [_pU], (_pIdx * 3) + 2, (_pIdx * 3) + 2);
integer _pL = getProfilePrimLink(_pIdx);
if (_pL > 0) {
llSetLinkPrimitiveParamsFast(_pL, [PRIM_TEXTURE, ALL_SIDES, _pU, <1,1,0>, <0,0,0>, 0.0, PRIM_COLOR, ALL_SIDES, <1,1,1>, 1.0, PRIM_GLOW, 0.01]);
llSetLinkPrimitiveParamsFast(_pL, [PRIM_TEXTURE, ALL_SIDES, _pU, <1,1,0>, <0,0,0>, 0.0, PRIM_COLOR, ALL_SIDES, <1,1,1>, 1.0, PRIM_GLOW, ALL_SIDES, 0.01]);
llSleep(DELAY_SCOREBOARD_REFRESH);
llSetLinkPrimitiveParamsFast(_pL, [PRIM_GLOW, 0.0]);
llSetLinkPrimitiveParamsFast(_pL, [PRIM_GLOW, ALL_SIDES, 0.0]);
}
}
}

View file

@ -1,154 +0,0 @@
# GitHub-Powered External Updater System
## Architecture: GitHub + Automatic Installation
### Components:
#### 1. **GitHub Integration** (Existing)
- ✅ GitHub Releases API for version detection
- ✅ GitHub raw file downloads for script content
- ✅ Zero infrastructure - uses existing GitHub + Jenkins
- ✅ Professional version management
#### 2. **Peril_Dice_Updater.lsl** (New - External Box)
- Downloads scripts directly from GitHub raw URLs
- Uses GitHub API to get latest release info
- Installs scripts automatically via llRemoteLoadScriptPin()
- Handles the 2048-character LSL limit properly
#### 3. **Update_Receiver.lsl** (Modified existing Update_Checker)
- Scans for nearby updater boxes
- Still checks GitHub API for version comparison
- Coordinates automatic installation with updater box
- Validates successful updates
## User Experience Flow:
### Step 1: Get Updater
- User receives "Peril Dice GitHub Updater v2.8.7" box
- Box description: "Automatic GitHub-powered updater for Peril Dice"
### Step 2: Check & Install
1. User rezzes updater box near their Peril Dice game
2. User touches game → Admin Menu → "Check for Updates"
3. Game checks GitHub API: "v2.8.7 available!"
4. User clicks "Install Update"
5. Game connects to nearby updater box
6. Updater downloads all needed scripts from GitHub
7. Updater installs scripts automatically: "Installing 3 of 17..."
8. Game validates: "✅ Update complete! Now running v2.8.7"
9. User deletes updater box
## Technical Implementation:
### GitHub URLs Used:
```
Version Check: https://api.github.com/repos/RebeccaNod1/Peril/releases/latest
Script Downloads: https://raw.githubusercontent.com/RebeccaNod1/Peril/main/ScriptName.lsl
```
### Script Installation Process:
```lsl
// In Updater Box:
1. llHTTPRequest() to download script from GitHub
2. llRemoteLoadScriptPin() to install in target game
3. Repeat for all 17 scripts in correct order
// In Target Game:
1. llSetRemoteScriptAccessPin() to allow updater access
2. Validate each script installation
3. Report progress to user
```
## Benefits of This Approach:
### ✅ **GitHub Integration Maintained:**
- Same zero-infrastructure benefits
- Automatic version detection
- Professional release management
- Leverages existing CI/CD pipeline
- No separate update server needed
### ✅ **User Experience Improved:**
- One-click automatic installation
- No manual copy/paste required
- Progress indicators and feedback
- Impossible to make installation errors
- Professional software-like experience
### ✅ **Creator Benefits:**
- Same development workflow (commit → push → tag)
- GitHub releases automatically available
- Single updater box for all users
- Easy to distribute via marketplace/group
## Files to Create:
### 1. `Peril_Dice_Updater.lsl`
```lsl
// External updater box script
- GitHub API integration (version checking)
- GitHub raw file downloads (script content)
- Remote script installation
- Progress reporting
- Error handling for network issues
```
### 2. `Update_Receiver.lsl`
```lsl
// Replace Update_Checker.lsl in main game
- Scan for nearby GitHub updater boxes
- GitHub version checking (same as before)
- Coordinate automatic installation
- Validate successful updates
```
### 3. `github-manifest.json` (in repository)
```json
{
"version": "2.8.7",
"scripts": [
{
"name": "Main_Controller_Linkset.lsl",
"link": 1,
"url": "https://raw.githubusercontent.com/RebeccaNod1/Peril/main/Main_Controller_Linkset.lsl",
"required": true
},
// ... all 17 scripts with GitHub URLs
]
}
```
## Advantages Over Current System:
| **Current (Manual)** | **New (GitHub + Auto)** |
|---------------------|-------------------------|
| ❌ User copies/pastes each script | ✅ One-click automatic installation |
| ❌ Easy to make mistakes | ✅ Impossible to mess up |
| ❌ Must update all scripts manually | ✅ Updates only changed scripts |
| ❌ No progress indication | ✅ Clear progress and status |
| ✅ Uses GitHub (good!) | ✅ Still uses GitHub (great!) |
## Implementation Steps:
### Phase 1: Build Core System
1. Create `Peril_Dice_Updater.lsl` with GitHub integration
2. Convert `Update_Checker.lsl` → `Update_Receiver.lsl`
3. Create `github-manifest.json` listing all scripts
4. Test basic single-script updates
### Phase 2: Full Integration
1. Test complete 17-script updates
2. Add progress indicators and error handling
3. Test with multiple game instances
4. Validate version tracking
### Phase 3: Distribution
1. Create professional updater box object
2. Write user instructions
3. Set up distribution (marketplace/group/etc.)
4. Create demo videos
This gives you **all the benefits of GitHub** (zero infrastructure, automatic detection, professional workflow) **plus** the automatic installation that users expect from professional Second Life products!
Should I start building the GitHub-powered external updater system?

View file

@ -63,6 +63,7 @@ EXPERIENCE: Required for KVP Leaderboard functionality
Perfect for nightclubs, venues, casinos, private parties, group entertainment, community gatherings.
VERSION HISTORY:
v3.3.0: Final Production Sweep - Global Silence & Memory Hardening (Tournament Ready)
v3.2.6: Hybrid HUD Evolution - Experience-based personal HUDs with direct-target detachment
v3.2.0: Sync Sanctity Release - Master Ledger Sync & Scoreboard Virtualization
v3.1.0: Dynamic Link Discovery, FURWARE Text Mesh, and Centralized Constants

View file

@ -1,60 +0,0 @@
# LSL Development MCP Server
This MCP (Model Context Protocol) server provides AI assistants with direct access to your LSL development tools and Peril Dice project.
## What it provides
### Available Tools:
1. **lint_lsl_file** - Lint/validate LSL syntax for a specific file using lslint
2. **lint_all_lsl** - Lint/validate all LSL files in the project using lslint
3. **list_lsl_files** - List all LSL files in the project (currently shows 20 files)
4. **git_status** - Get git status of the project
5. **analyze_script_architecture** - Analyze script architecture and communication patterns
6. **get_memory_usage_info** - Extract memory usage information from LSL files
### Available Resources:
- Project README.md
- CHANGELOG.md
- WARP.md (development guide)
## Files Created:
- `lsl_mcp_server.py` - The main MCP server
- `mcp_config.json` - Configuration for MCP clients
- `test_mcp_server.py` - Test script to verify server functionality
## How to Use:
### Testing the Server:
```bash
python3 test_mcp_server.py
```
### Manual Server Start:
```bash
python3 lsl_mcp_server.py
```
### Integration with AI Tools:
The `mcp_config.json` file can be used to configure MCP-compatible AI tools to connect to this server.
## Benefits:
With this MCP server, AI assistants can:
- Directly lint and validate your LSL code using lslint
- List and examine all 20 LSL files in your Peril project
- Check git status and project state
- Analyze your script architecture (Controllers, Handlers, Modules, Linkset scripts)
- Access project documentation
- Monitor memory usage information from script comments
This creates a much more interactive and helpful development experience, where the AI can actually run your development tools and provide real-time feedback on your Peril Dice project.
## Security:
The server only has access to:
- Your Peril project directory (`/home/richard/peril`)
- Your lslint tool (`/home/richard/lslint/lslint`)
- Git commands within the project directory
- Read-only access to project documentation
It cannot modify files outside the project scope or execute arbitrary system commands.

View file

@ -1,31 +0,0 @@
// === MEMORY REPORTING TEMPLATE ===
// Add this to the state_entry() of every script to track memory usage
// Add this function to every script:
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)");
}
// Add this to state_entry() in every script:
state_entry() {
// Replace "SCRIPT_NAME" with the actual script name
reportMemoryUsage("SCRIPT_NAME");
// ... rest of existing state_entry() code ...
}
// Add this to on_rez() in every script:
on_rez(integer start_param) {
// Replace "SCRIPT_NAME" with the actual script name
reportMemoryUsage("SCRIPT_NAME");
// ... rest of existing on_rez() code ...
}

View file

@ -1,4 +1,4 @@
#include "Peril_Constants.lsl"
#include "peril/Peril_Constants.lsl"
// === Main Controller - Peril Dice Game (Linkset Version) ===
@ -38,7 +38,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,6 +68,7 @@ integer victoryInProgress = FALSE;
// Lockout system variables
integer isLocked = FALSE;
integer resetInProgress = FALSE; // Lockout flag for reset synchronization
key gameOwner;
list pickQueue = [];
@ -179,25 +180,11 @@ checkMemoryUsage(string context) {
// REMOVED: emergencyMemoryCleanup() - delegated to Controller_Memory.lsl
// REMOVED: reportMemoryStats() - delegated to Controller_Memory.lsl
// Experience Sentinel - Functional probe to check if Experience is allowed on land
checkExperience() {
dbg("🔍 [Peril Dice] Experience Sentinel: Starting functional diagnostic...");
llOwnerSay("🛡️ [Peril Dice] Experience Sentinel: Verifying Land-Scope permissions for 'Final Girlz I.N.C.'...");
// PROBE 1: Owner Agent Check (Instant indicator for land-readiness)
if (!llAgentInExperience(llGetOwner())) {
llOwnerSay("⚠️ [Peril Dice] SYSTEM WARNING: Experience Features are currently BLOCKED on this parcel.");
llOwnerSay("🛡️ [Peril Dice] TO FIX: Open 'About Land' -> 'Experiences' -> 'Add' and search for 'Final Girlz I.N.C.'");
// Final probe check
sentinelQueryID = llReadKeyValue("PERIL_SENTINEL");
llSetTimerEvent(10.0); // Increased to 10s for reliability
currentTimerMode = TIMER_XP_CHECK;
} else {
llOwnerSay("✅ [Peril Dice] Experience 'Final Girlz I.N.C.' is ENABLED on this parcel.");
EX_READY = TRUE;
}
}
// Experience Sentinel - Functional probe to check if KVP is active
// NOTE FOR FUTURE AI/DEVS: KVP (World Rankings) ONLY requires the script to be
// compiled with the Experience. Land-Scope whitelisting (llAgentInExperience)
// is ONLY required for HUD auto-attachments and Animations.
// MOVED: checkExperience() - delegated to Player_RegistrationManager.lsl
// Send status message to scoreboard using link messages
sendStatusMessage(string status) {
@ -227,13 +214,17 @@ sendStatusMessage(string status) {
syncGameState() {
// Build sync message WITHOUT redundant player keys to save memory
// Format: lives~picks~perilPlayer~names
string picksStr = (string)llDumpList2String(picksData, "^");
if (picksStr == "") picksStr = "EMPTY"; // Standardized token
// DELIMITER FIX: Ensure each of the 4 parts is explicitly separated by ~
string syncMsg = llList2CSV(lives) + "~" +
(string)llDumpList2String(picksData, "^") + "~" +
picksStr + "~" +
(string)llList2String([perilPlayer, "NONE"], (perilPlayer == "")) + "~" +
llList2CSV(names);
llMessageLinked(LINK_SET, MSG_SYNC_GAME_STATE, syncMsg, NULL_KEY);
dbg("🌍 [Main Controller] 📡 Global State Sync broadcasted.");
dbg("🌍 [Main Controller] 📡 Global State Sync broadcasted (Picks: " + picksStr + ")");
}
@ -288,14 +279,13 @@ updateFloatingText() {
}
resetGame() {
resetInProgress = TRUE; // Start lockout
cleanupListeners();
// Let Floater Manager handle cleanup intelligently
// Always request cleanup on a reset to ensure no orphans are left behind
llMessageLinked(LINK_SET, MSG_CLEANUP_ALL_FLOATERS, "RESET", NULL_KEY);
llSleep(DELAY_STATE_TRANSITION);
llSleep(DELAY_STATE_TRANSITION);
// Reduced delay for snappy restart
llSleep(0.1);
players = names = lives = picksData = globalPickedNumbers = readyPlayers = [];
floaterChannels = [];
@ -317,6 +307,13 @@ resetGame() {
llMessageLinked(LINK_SCOREBOARD, MSG_UPDATE_WINNER, "", NULL_KEY); // Clear winner glow
llMessageLinked(LINK_SET, MSG_CLEAR_DICE, "", NULL_KEY);
// Give other scripts time to finish their reset before starting sync
// INCREASED to 5.0s for high-speed Mono startup with Debug Logs OFF
llSleep(5.0);
// NEW: Trigger Leaderboard to reload rankings from KVP on reset
llMessageLinked(LINK_SET, MSG_RESET_LEADERBOARD, "START_SYNC", NULL_KEY);
dbg("🎮 Game reset! All state cleared (including scoreboard).");
statusTimer = 0;
@ -328,15 +325,12 @@ resetGame() {
// NEW: Update status bar for idle state
sendStatusMessage("PERIL DICE GAME\nTouch to Join!");
llSleep(DELAY_STATE_TRANSITION);
// Quick buffer for Linkset to settle
llSleep(0.2);
llSetTimerEvent(0);
initListeners();
// SAFETY: Only call updateHelpers if we have actual game data to prevent startup issues
if (llGetListLength(names) > 0) {
updateHelpers();
}
resetInProgress = FALSE; // End lockout
}
default {
@ -355,9 +349,7 @@ default {
// Reset the game on startup to ensure clean state
resetGame();
// Experience Sentinel check - Moved after reset to protect timer
llSleep(2.0);
checkExperience();
// MOVED: checkExperience() - already called by Registration Management
dbg("✅ Main Controller initialization complete!");
}
@ -377,159 +369,18 @@ default {
// Reset the game to ensure clean state when rezzed
resetGame();
// Experience Sentinel check - Moved after reset to protect timer
checkExperience();
// MOVED: checkExperience()
dbg("✅ Main Controller reset complete after rez!");
}
touch_start(integer total_number) {
// IGNORE TOUCHES INTENDED FOR THE LEADERBOARD
string _rawNm = llGetLinkName(llDetectedLinkNumber(0));
string _linkNm = llToLower(_rawNm);
// TEMPORARY DEBUG TO CATCH THE EXACT LINK NAME
dbg("🎯 [Touch Filter] Clicked Link Name: '" + _rawNm + "'");
if (llSubStringIndex(_linkNm, "leaderboard") != -1 || llSubStringIndex(_linkNm, "furware") != -1 || llSubStringIndex(_linkNm, "lb_") != -1) {
dbg("🛑 [Touch Filter] Bypassing menu for Leaderboard element!");
return;
}
integer clickedLink = llDetectedLinkNumber(0);
string clickedName = llGetLinkName(clickedLink);
key toucher = llDetectedKey(0);
integer idx = llListFindList(players, [toucher]);
// Enhanced disconnect/reconnect handling - check if this player should be picking
if (idx != -1 && toucher != llGetOwner() && roundStarted) {
string playerName = llList2String(names, idx);
// ENHANCED: Check if this player is supposed to be the current picker based on game state
integer shouldBePicking = FALSE;
if (currentPickerIdx < llGetListLength(pickQueue)) {
string expectedPickerName = llList2String(pickQueue, currentPickerIdx);
if (playerName == expectedPickerName) {
shouldBePicking = TRUE;
// Fix corrupted currentPicker state
if (currentPicker != toucher) {
currentPicker = toucher;
}
}
}
// 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()) {
currentPicker = toucher;
shouldBePicking = TRUE;
}
}
// Check for dialog recovery (original logic OR enhanced detection)
if (currentPicker == toucher || shouldBePicking) {
llSay(0, "🔄 " + playerName + " - Welcome back! Restoring your number picking dialog...");
llMessageLinked(LINK_SET, MSG_GET_CURRENT_DIALOG, playerName, toucher);
return;
}
else if (playerName == perilPlayer && currentPickerIdx >= llGetListLength(pickQueue)) {
llSay(0, "🔄 " + playerName + " - Welcome back! Restoring your roll dialog...");
llMessageLinked(LINK_SET, MSG_SHOW_ROLL_DIALOG, perilPlayer, toucher);
return;
}
}
// Special handling for owner during gameplay
if (toucher == llGetOwner() && roundStarted) {
string ownerName = getPlayerName(toucher);
list options = [];
string menuText = "Game in progress. What would you like to do?";
integer hasActiveDialog = FALSE;
if (currentPicker == toucher) {
options += ["🔄 Recover Pick Dialog"];
hasActiveDialog = TRUE;
}
if (ownerName == perilPlayer && currentPickerIdx >= llGetListLength(pickQueue)) {
options += ["🔄 Recover Roll Dialog"];
hasActiveDialog = TRUE;
}
options += ["🔧 Admin Menu"];
if (!hasActiveDialog) {
llMessageLinked(LINK_SET, MSG_SHOW_MENU, "owner|0", toucher);
return;
}
llDialog(toucher, menuText, options, DIALOG_CHANNEL);
return;
}
if (toucher == llGetOwner()) {
dbg("🔍 [DEBUG] Owner touched - idx: " + (string)idx + ", players list: " + llList2CSV(players));
if (idx == -1) {
// Owner is not in game - show join/admin choice
if (llListFindList(pendingRegistrations, [toucher]) != -1) {
dbg("⏳ Registration already in progress for " + getPlayerName(toucher));
return;
}
// Owner is not in game - determine menu type based on player count
if (llGetListLength(players) >= 2) {
// Enough players for game - show starter menu
llMessageLinked(LINK_SET, MSG_SHOW_MENU, "unregistered_owner_starter|1", toucher);
} else {
// Not enough players - show basic menu
llMessageLinked(LINK_SET, MSG_SHOW_MENU, "unregistered_owner|0", toucher);
}
} else {
// Owner is already in game - show player menu with appropriate starter status
integer isStarter = TRUE;
integer k;
for (k = 0; k < idx && isStarter; k++) {
string existingName = llList2String(names, k);
if (llSubStringIndex(existingName, "Bot") != 0) {
isStarter = FALSE;
}
}
if (idx == 0) {
isStarter = TRUE;
}
llMessageLinked(LINK_SET, MSG_SHOW_MENU, "owner|" + (string)isStarter, toucher);
}
} else if (idx != -1) {
// Non-owner player who is already registered
integer isStarter = TRUE;
integer m;
for (m = 0; m < idx && isStarter; m++) {
string existingName = llList2String(names, m);
if (llSubStringIndex(existingName, "Bot") != 0) {
isStarter = FALSE;
}
}
llMessageLinked(LINK_SET, MSG_SHOW_MENU, "player|" + (string)isStarter, toucher);
} else {
string playerName = getPlayerName(toucher);
if (playerName != "") {
if (llListFindList(pendingRegistrations, [toucher]) != -1) {
dbg("⏳ Registration already in progress for " + playerName);
return;
}
pendingRegistrations += [toucher];
llMessageLinked(LINK_SET, MSG_REGISTER_PLAYER_REQUEST, playerName + "|" + (string)toucher, NULL_KEY);
integer isStarter = TRUE;
integer n;
for (n = 0; n < llGetListLength(names) && isStarter; n++) {
string existingName = llList2String(names, n);
if (llSubStringIndex(existingName, "Bot") != 0) {
isStarter = FALSE;
}
}
llMessageLinked(LINK_SET, MSG_SHOW_MENU, "player|" + (string)isStarter, toucher);
}
}
// DELIVER: Forward touch event to Dialog Handler to save bytecode in Main
llMessageLinked(LINK_SET, MSG_TOUCH_EVENT, (string)clickedLink + "|" + clickedName, toucher);
}
link_message(integer sender, integer num, string str, key id) {
@ -544,8 +395,11 @@ default {
if (pidx != -1) {
lives = llListReplaceList(lives, [newLifeCount], pidx, pidx);
dbg("🛡️ [Main Controller] 💗 Damage Report Confirmed: " + hitPlayer + " -> " + (string)newLifeCount + " hearts.");
// Broadcast the OFFICIAL ledger to the entire Linkset
syncGameState();
// AUTHORITATIVE: Signal Game Manager to continue to the next round after the damage report
llSleep(DELAY_SYNC_PROPAGATION);
llMessageLinked(LINK_SET, MSG_CONTINUE_ROUND, perilPlayer, NULL_KEY);
}
}
return;
@ -594,215 +448,35 @@ default {
}
// Handle elimination requests
if (num == MSG_ELIMINATE_PLAYER) {
list parts = llParseString2List(str, ["|"], []);
if (llList2String(parts, 0) == "ELIMINATE_PLAYER") {
string eliminatedPlayer = llList2String(parts, 1);
integer idx = llListFindList(names, [eliminatedPlayer]);
if (idx != -1) {
// NEW: Only announce elimination if it's not the end of the game (winner)
if (llGetListLength(names) > 1) {
sendStatusMessage("PLAYER STRUCK!\n" + eliminatedPlayer + " has been eliminated!");
// MOVED: MSG_ELIMINATE_PLAYER logic - delegated to Game_Manager.lsl
// Process ONLY the final list cleanup here after Game Manager finishes visuals
if (num == MSG_PROCESS_ELIMINATION) {
string eliminatedPlayer = str;
integer idx = llListFindList(names, [eliminatedPlayer]);
if (idx != -1) {
// Remove from indexed lists
players = llDeleteSubList(players, idx, idx);
names = llDeleteSubList(names, idx, idx);
lives = llDeleteSubList(lives, idx, idx);
floaterChannels = llDeleteSubList(floaterChannels, idx, idx);
// Remove from picksData
integer pickIdx = -1;
integer p;
for (p = 0; p < llGetListLength(picksData); p++) {
string entry = llList2String(picksData, p);
if (llSubStringIndex(entry, eliminatedPlayer + "|") == 0) {
pickIdx = p;
jump found_pick_entry;
}
// FIRST: Set the player's lives to 0 and show 0 hearts
lives = llListReplaceList(lives, [0], idx, idx);
// Send ONLY a direct scoreboard update to show 0 hearts
llMessageLinked(LINK_SCOREBOARD, MSG_PLAYER_UPDATE,
eliminatedPlayer + "|0|" + (string)llList2Key(players, idx), NULL_KEY);
// CRITICAL: Check for victory condition BEFORE assigning new peril player
integer willCauseVictory = (llGetListLength(names) <= 2); // After removing one player
string potentialWinner = "";
if (willCauseVictory) {
// Find the winner (the remaining non-eliminated player)
integer w;
for (w = 0; w < llGetListLength(names); w++) {
string candidateName = llList2String(names, w);
if (candidateName != eliminatedPlayer) {
potentialWinner = candidateName;
jump found_winner;
}
}
@found_winner;
if (potentialWinner != "") {
// SEND WINNER GLOW to scoreboard and trigger floater update
llMessageLinked(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);
}
}
// Only assign new peril player if NOT causing victory
if (!willCauseVictory && (perilPlayer == eliminatedPlayer || perilPlayer == "")) {
// Find first remaining alive player to be new peril player (before removing eliminated player)
string newPerilPlayer = "";
integer k;
for (k = 0; k < llGetListLength(names) && newPerilPlayer == ""; k++) {
string candidateName = llList2String(names, k);
if (candidateName != eliminatedPlayer) { // Skip the player being eliminated
integer candidateIdx = llListFindList(names, [candidateName]);
if (candidateIdx != -1 && candidateIdx < llGetListLength(lives)) {
integer candidateLives = llList2Integer(lives, candidateIdx);
if (candidateLives > 0) {
newPerilPlayer = candidateName;
}
}
}
}
if (newPerilPlayer != "") {
perilPlayer = newPerilPlayer;
// Send peril player update to scoreboard for glow effect
llMessageLinked(LINK_SCOREBOARD, MSG_UPDATE_PERIL_PLAYER, newPerilPlayer, NULL_KEY);
} else {
perilPlayer = "NONE";
// Clear peril player glow on scoreboard
llMessageLinked(LINK_SCOREBOARD, MSG_UPDATE_PERIL_PLAYER, "", NULL_KEY);
}
}
// UNIFIED SYNC: Replaces inline sync calls to prevent language rift
syncGameState();
// Direct floater update to ensure eliminated status is displayed
llMessageLinked(LINK_SET, MSG_UPDATE_FLOAT, eliminatedPlayer, llList2Key(players, idx));
llSleep(DELAY_ELIMINATION_NOTICE); // Give floater time to show eliminated status with red display
if (willCauseVictory) {
// If this elimination will cause victory, give extra time for 0 hearts display
llSleep(DELAY_VICTORY_NOTICE); // Additional time for victory scenarios
}
// CHANGED: Don't cleanup floaters during elimination - let them show eliminated status
// The floater will show red glow + "ELIMINATED!" and stay visible until game end
// integer ch = llList2Integer(floaterChannels, idx);
// llMessageLinked(LINK_SET, MSG_CLEANUP_FLOAT, (string)ch, NULL_KEY);
// llSleep(0.3); // Give floater time to cleanup
// CRITICAL: Remove eliminated player's picks data by searching for their entry
// NOT by index, since picksData format is "PlayerName|picks" strings
integer pickIdx = -1;
integer p;
for (p = 0; p < llGetListLength(picksData); p++) {
string entry = llList2String(picksData, p);
if (llSubStringIndex(entry, eliminatedPlayer + "|") == 0) {
pickIdx = p;
jump found_pick_entry;
}
}
@found_pick_entry;
if (pickIdx != -1) {
picksData = llDeleteSubList(picksData, pickIdx, pickIdx);
}
// Now remove from indexed lists (players, names, lives, floaterChannels)
players = llDeleteSubList(players, idx, idx);
names = llDeleteSubList(names, idx, idx);
lives = llDeleteSubList(lives, idx, idx);
floaterChannels = llDeleteSubList(floaterChannels, idx, idx);
// NEW: FINAL SYNC - Tell everyone the list has actually SHRUNK now
// This prevents "Zombie Respawns" and "Ghost Heals"
syncGameState();
// Peril player assignment already handled before sync message was sent
// CHANGED: Send to scoreboard, which will handle leaderboard updates
llMessageLinked(LINK_SCOREBOARD, MSG_GAME_LOST, eliminatedPlayer, NULL_KEY);
// Status message now sent directly by Roll Module
// Check for victory condition BEFORE calling updateHelpers()
if (llGetListLength(names) <= 1) {
if (llGetListLength(names) == 1) {
string winner = llList2String(names, 0);
// ENHANCED: Give eliminated player's floater time to show eliminated status
// before declaring victory and cleaning up
dbg("⏳ [Main Controller] Final elimination detected - allowing display time before victory");
llSleep(DELAY_ELIMINATION_NOTICE); // Give final eliminated player time to show red eliminated status
// CRITICAL: Set victory flag immediately to prevent sync processing
victoryInProgress = TRUE;
// CRITICAL: Clear all game state immediately before any sync messages
// This prevents broadcasting stale winner data to other scripts
players = [];
names = [];
lives = [];
picksData = [];
perilPlayer = "";
pickQueue = [];
currentPickerIdx = 0;
roundStarted = FALSE;
gameStarting = FALSE;
currentPicker = NULL_KEY;
llSay(0, "✨ ULTIMATE VICTORY! " + winner + " is the Ultimate Survivor!");
// NEW: Send victory status to board and display winner name!
sendStatusMessage("Victory");
llMessageLinked(LINK_SET, MSG_STATUS_TEXT, "=== ULTIMATE VICTORY ===\n<!c=white>" + winner + " wins!", NULL_KEY);
llMessageLinked(LINK_SET, MSG_PLAYER_WON, winner, NULL_KEY);
llMessageLinked(LINK_SET, MSG_EFFECT_CONFETTI, "VICTORY_CONFETTI", NULL_KEY);
// IMPORTANT: Send winner update to scoreboard and trigger floater update
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(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");
victoryDelayTimer = llGetUnixTime();
currentTimerMode = TIMER_VICTORY_DELAY;
llSetTimerEvent(1.0); // Check every second
return; // Exit here - timer will handle reset
} else {
// CRITICAL: Set victory flag and clear state for no survivors scenario too
victoryInProgress = TRUE;
players = [];
names = [];
lives = [];
picksData = [];
perilPlayer = "";
pickQueue = [];
currentPickerIdx = 0;
roundStarted = FALSE;
gameStarting = FALSE;
currentPicker = NULL_KEY;
llSay(0, "💀 DESPAIR WINS! No Ultimate Survivors remain!");
// Use timer for no survivors scenario too
victoryDelayTimer = llGetUnixTime();
currentTimerMode = TIMER_VICTORY_DELAY;
llSetTimerEvent(1.0);
return;
}
}
// CRITICAL: After elimination, sync state and continue game with new peril player
if (perilPlayer != "" && perilPlayer != "NONE") {
// Sync the updated game state to all modules (4-part format)
// Optimized to avoid large temporary string variables
llMessageLinked(LINK_SET, MSG_SYNC_GAME_STATE,
llList2CSV(lives) + "~EMPTY~" + perilPlayer + "~" + llList2CSV(names), NULL_KEY);
llSleep(DELAY_SYNC_PROPAGATION); // Give sync time to propagate
// Continue the game with the new peril player - delegate to Game Manager
llMessageLinked(LINK_SET, 998, perilPlayer, NULL_KEY);
}
// MINIMAL: Only send direct scoreboard update for remaining players
// Skip heavy updateHelpers() calls to prevent memory crashes
}
@found_pick_entry;
if (pickIdx != -1) {
picksData = llDeleteSubList(picksData, pickIdx, pickIdx);
}
syncGameState();
dbg("🎯 [Main Controller] Deep Cleanup Complete: " + eliminatedPlayer + " purged from state.");
}
return;
}
@ -843,6 +517,18 @@ default {
return;
}
// Handle peril player updates from other modules
if (num == MSG_UPDATE_PERIL_PLAYER) {
perilPlayer = str;
dbg("🎯 [Main Controller] Peril Player Updated: " + perilPlayer);
syncGameState();
// AUTHORITATIVE: Signal Game Manager to continue to the next round after Plot Twist
llSleep(DELAY_SYNC_PROPAGATION);
llMessageLinked(LINK_SET, MSG_CONTINUE_ROUND, perilPlayer, NULL_KEY);
return;
}
// Receive lightweight updates from Game Manager
if (num == 9070) {
list parts = llParseString2List(str, ["|"], []);
@ -884,37 +570,35 @@ default {
// Handle ready state queries
if (num == MSG_QUERY_READY_STATE) {
list queryParts = llParseString2List(str, ["|"], []);
string playerName;
integer requestID = 0;
list qParts = llParseString2List(str, ["|"], []);
string qPlayerName;
integer qRequestID = 0;
if (llGetListLength(queryParts) >= 2) {
playerName = llList2String(queryParts, 0);
requestID = (integer)llList2String(queryParts, 1);
if (llGetListLength(qParts) >= 2) {
qPlayerName = llList2String(qParts, 0);
qRequestID = (integer)llList2String(qParts, 1);
} else {
playerName = str;
qPlayerName = str;
}
integer isReady = llListFindList(readyPlayers, [playerName]) != -1;
integer isBot = llSubStringIndex(playerName, "Bot") == 0;
string result = playerName + "|" + (string)isReady + "|" + (string)isBot + "|" + (string)requestID;
llMessageLinked(LINK_SET, MSG_READY_STATE_RESULT, result, id);
integer isReady = llListFindList(readyPlayers, [qPlayerName]) != -1;
integer isBot = llSubStringIndex(qPlayerName, "Bot") == 0;
string resultStr = qPlayerName + "|" + (string)isReady + "|" + (string)isBot + "|" + (string)qRequestID;
llMessageLinked(LINK_SET, MSG_READY_STATE_RESULT, resultStr, id);
return;
}
// Handle owner status queries
if (num == MSG_QUERY_OWNER_STATUS) {
list queryParts = llParseString2List(str, ["|"], []);
string ownerName;
integer requestID = 0;
list osParts = llParseString2List(str, ["|"], []);
string osName;
integer osRequestID = 0;
// Owner status check (Now fully automated via on_rez / reset)
if (llGetListLength(queryParts) >= 2) {
ownerName = llList2String(queryParts, 0);
requestID = (integer)llList2String(queryParts, 1);
if (llGetListLength(osParts) >= 2) {
osName = llList2String(osParts, 0);
osRequestID = (integer)llList2String(osParts, 1);
} else {
ownerName = str;
osName = str;
}
// Check if owner is registered
@ -938,7 +622,7 @@ default {
}
}
string result = ownerName + "|" + (string)isRegistered + "|" + (string)isPending + "|" + (string)isStarter + "|" + (string)requestID;
string result = osName + "|" + (string)isRegistered + "|" + (string)isPending + "|" + (string)isStarter + "|" + (string)osRequestID;
llMessageLinked(LINK_SET, MSG_OWNER_STATUS_RESULT, result, id);
return;
}
@ -953,11 +637,11 @@ default {
string action = llList2String(parts, 0);
if (action == "LEAVE_GAME" || action == "KICK_PLAYER") {
string leavingName = llList2String(parts, 1);
// Find and remove the player from main controller lists
integer idx = llListFindList(names, [leavingName]);
if (idx != -1) {
// CHANGED: Remove player from scoreboard BEFORE updating lists
llMessageLinked(LINK_SCOREBOARD, MSG_REMOVE_PLAYER, leavingName, NULL_KEY);
// Find and remove the player from main controller lists
integer idx = llListFindList(names, [leavingName]);
if (idx != -1) {
// CRITICAL: Inform all scripts (Registration Manager) to scrub this player
llMessageLinked(LINK_SET, MSG_REMOVE_PLAYER, leavingName, NULL_KEY);
// Only announce if it's not the end of the game
if (llGetListLength(names) > 1) {
@ -1127,15 +811,30 @@ default {
return;
}
if (msg == "Reset Leaderboard") {
// CHANGED: Send to scoreboard, which will handle leaderboard reset
llMessageLinked(LINK_SCOREBOARD, MSG_RESET_LEADERBOARD, "", NULL_KEY);
dbg("🏆 Leaderboard scores reset - game wins cleared!");
llDialog(id, "⚠️ WARNING: This will PERMANENTLY WIPE the World Ranking database (all 10 shards). Are you sure?", ["YES, WIPE LB", "NO, CANCEL"], DIALOG_CHANNEL);
return;
}
if (msg == "YES, WIPE LB") {
if (id != (key)GLOBAL_ADMIN) {
llRegionSayTo(id, 0, "🚫 Access Denied: Only the Global Admin can perform this action.");
return;
}
llMessageLinked(LINK_SET, MSG_RESET_LEADERBOARD, "WIPE", id); // Use LINK_SET
dbg("🏆 GLOBAL LEADERBOARD WIPE confirmed and executed.");
return;
}
if (msg == "Reset All") {
llDialog(id, "⚠️ WARNING: This will factory reset the game AND permanently wipe the leaderboard. Are you sure?", ["YES, RESET ALL", "NO, CANCEL"], DIALOG_CHANNEL);
return;
}
if (msg == "YES, RESET ALL") {
if (id != (key)GLOBAL_ADMIN) {
llRegionSayTo(id, 0, "🚫 Access Denied: Only the Global Admin can perform this action.");
return;
}
resetGame();
llMessageLinked(LINK_SCOREBOARD, MSG_RESET_LEADERBOARD, "", NULL_KEY);
dbg("🔄 Complete reset - game and leaderboard cleared!");
llMessageLinked(LINK_SET, MSG_RESET_LEADERBOARD, "WIPE", id); // Use LINK_SET
dbg("🔄 FULL RESET confirmed and executed.");
return;
}
if (msg == "Memory Stats") {
@ -1253,8 +952,8 @@ default {
llSay(0, "⚡ ALL PARTICIPANTS READY! THE DEADLY PERIL DICE GAME BEGINS! ⚡");
integer actualPlayerCount = llGetListLength(names);
llSetText("🎮 GAME IN PROGRESS\n" + (string)actualPlayerCount + " players", <1.0, 0.2, 0.2>, 1.0);
integer currentLiveCount = llGetListLength(names);
llSetText("🎮 GAME IN PROGRESS\n" + (string)currentLiveCount + " players", <1.0, 0.2, 0.2>, 1.0);
// FIXED: Use proper announcement instead of internal texture name "Title"
sendStatusMessage("GAME STARTING!\nAll players are ready!");

View file

@ -1,4 +1,4 @@
#include "Peril_Constants.lsl"
#include "peril/Peril_Constants.lsl"
// === Dialog Handler (Updated for Peril Dice - Paginated Multiple Picks) ===

View file

@ -43,6 +43,8 @@ key EXPERIENCE_ID = "06926390-26e6-11f1-a452-0242ac110003"; // Final Girlz I.N.C
#define dbg(msg) ;
#endif
#define MSG_DEBUG_TEXT 7000
#define MSG_RESET_ALL -99999
#define MSG_EMERGENCY_RESET -99998
#define MSG_DEBUG_PICKS_ON 7001
@ -106,6 +108,9 @@ key EXPERIENCE_ID = "06926390-26e6-11f1-a452-0242ac110003"; // Final Girlz I.N.C
#define MSG_REQUEST_PLAYER_LIST_KICK 8009
#define MSG_LOCK_GAME 9001
#define MSG_UNLOCK_GAME 9002
#define MSG_TOUCH_EVENT 215
#define MSG_PROCESS_ELIMINATION 3015
// --- Dialogs & Menus (100-399) ---
#define MSG_SHOW_MENU 201
@ -138,15 +143,14 @@ key EXPERIENCE_ID = "06926390-26e6-11f1-a452-0242ac110003"; // Final Girlz I.N.C
#define MSG_QUERY_OWNER_STATUS 213
// --- Memory Management (6000-6099) ---
#define SHOW_MEMORY 1 // Toggle this to 1 to see memory usage on rez/reset
#define SHOW_MEMORY 0 // Toggle this to 1 to see memory usage on rez/reset
#define REPORT_MEMORY() { \
if (SHOW_MEMORY) { \
integer used = llGetUsedMemory(); \
integer free = llGetFreeMemory(); \
float percent = ((float)used / (float)(used + free)) * 100.0; \
llOwnerSay("🧠 [" + llGetScriptName() + "] Memory: " + (string)used + " used, " + (string)free + " free (" + llGetSubString((string)percent, 0, 4) + "% used)."); \
} \
integer used = llGetUsedMemory(); \
integer free = llGetFreeMemory(); \
integer total = used + free; \
float percent = ((float)used / (float)total) * 100.0; \
dbg("🧠 [" + llGetScriptName() + "] Memory: " + (string)used + " used, " + (string)free + " free (" + llGetSubString((string)percent, 0, 4) + "% used)."); \
}
#define MSG_MEMORY_CHECK 6001
@ -190,10 +194,12 @@ key EXPERIENCE_ID = "06926390-26e6-11f1-a452-0242ac110003"; // Final Girlz I.N.C
#define DELAY_CONFETTI_TICK 0.2 // Confetti particle interval (0.2s)
#define DELAY_CONFETTI_PREP 0.5 // Confetti preparation delay (0.5s)
#define DELAY_FLOAT_UPDATE 0.1 // Floater status update delay (0.1s)
// --- Game Timing Settings (Originals relocated here) ---
#define BOT_PICK_DELAY 2.0 // Bot picking decision time (2.0s)
#define HUMAN_PICK_DELAY 1.0 // Human picking buffer (1.0s)
#define DIALOG_DELAY 1.5 // Base dialog presentation delay (1.5s)
#define STATUS_DISPLAY_TIME 8.0 // How long status text stays on display (8.0s)
#define BOT_RESPONSE_DELAY 1.5 // Bot response timing (1.5s)
// --- Security ---
#define GLOBAL_ADMIN "3e01527d-a9ff-4776-b2b8-918ab622f70f"

View file

@ -1,4 +1,4 @@
#include "Peril_Constants.lsl"
#include "peril/Peril_Constants.lsl"
// === Player Status Float - Enhanced Display ===

View file

@ -1,4 +1,4 @@
#include "Peril_Constants.lsl"
#include "peril/Peril_Constants.lsl"
// === Dialog Handler (Owner & Player) with unified Ready/Leave menu and join support ===
@ -88,6 +88,11 @@ list troubleshootingOptions = ["Cleanup Floaters", "Force Floaters", "⬅️ Bac
// State tracking for dynamic ready menu with race condition protection
key pendingMenuPlayer = NULL_KEY;
integer roundStarted = FALSE;
list players = [];
list names = [];
list readyPlayers = [];
list pendingRegistrations = []; // Local track of registration attempts
integer pendingMenuIsStarter = FALSE;
integer pendingMenuIsOwner = FALSE;
integer pendingMenuRequestID = 0; // Unique ID for each menu request
@ -404,6 +409,63 @@ default {
return;
}
// --- TOP LEVEL EVENT HANDLERS ---
if (num == MSG_TOUCH_EVENT) {
list parts = llParseString2List(str, ["|"], []);
integer clickedLink = (integer)llList2String(parts, 0);
string clickedName = llList2String(parts, 1);
key toucher = id;
if (clickedLink != 1 && (llSubStringIndex(clickedName, "FURWARE") != -1 || llSubStringIndex(clickedName, "LB_") != -1)) return;
integer idx = llListFindList(players, [toucher]);
if (roundStarted) {
if (toucher == llGetOwner()) { llMessageLinked(LINK_SET, MSG_SHOW_MENU, "owner|0", toucher); return; }
if (idx != -1) { llMessageLinked(LINK_SET, MSG_SHOW_MENU, "player|0", toucher); return; }
}
if (toucher == llGetOwner()) {
if (idx == -1) {
if (llListFindList(pendingRegistrations, [toucher]) != -1) return;
if (llGetListLength(players) >= 2) llMessageLinked(LINK_SET, MSG_SHOW_MENU, "unregistered_owner_starter|1", toucher);
else llMessageLinked(LINK_SET, MSG_SHOW_MENU, "unregistered_owner|0", toucher);
} else {
integer isStarter = (idx == 0);
llMessageLinked(LINK_SET, MSG_SHOW_MENU, "owner|" + (string)isStarter, toucher);
}
} else if (idx != -1) {
integer isStarter = (idx == 0);
llMessageLinked(LINK_SET, MSG_SHOW_MENU, "player|" + (string)isStarter, toucher);
} else {
string playerName = llGetDisplayName(toucher);
if (playerName == "") playerName = llKey2Name(toucher);
if (llListFindList(pendingRegistrations, [toucher]) == -1) {
pendingRegistrations += [toucher];
if (llGetListLength(pendingRegistrations) > 10) pendingRegistrations = llDeleteSubList(pendingRegistrations, 0, 0);
llMessageLinked(LINK_SET, MSG_REGISTER_PLAYER_REQUEST, playerName + "|" + (string)toucher, NULL_KEY);
}
llMessageLinked(LINK_SET, MSG_SHOW_MENU, "player|0", toucher);
}
return;
}
if (num == MSG_SYNC_GAME_STATE) {
list parts = llParseStringKeepNulls(str, ["~"], []);
if (llGetListLength(parts) >= 4) names = llCSV2List(llList2String(parts, 3));
return;
}
if (num == MSG_UPDATE_MAIN_LISTS) {
if (llSubStringIndex(str, "READY_LIST|") == 0) readyPlayers = llCSV2List(llGetSubString(str, 11, -1));
else {
list parts = llParseString2List(str, ["~"], []);
if (llGetListLength(parts) >= 2 && llListFindList(players, [id]) == -1) players += [id];
}
return;
}
if (num == MSG_RESET_ALL) {
players = []; names = []; readyPlayers = []; pendingRegistrations = [];
roundStarted = FALSE;
return;
}
if (num == MSG_SHOW_MENU) {
dbg("🕵️ [Dialog Handler] Received MSG_SHOW_MENU (" + str + ") from script link/sender: " + (string)sender + ", ID: " + (string)id);
list args = llParseString2List(str, ["|"], []);
@ -414,44 +476,33 @@ default {
string targetType = llList2String(args, 0);
integer isStarter = (integer)llList2String(args, 1);
if (targetType == "owner") {
// Both starter and non-starter owners get ready/leave menu with Owner button
showReadyLeaveMenu(id, isStarter, TRUE);
} else if (targetType == "registered_owner") {
// Registered owner gets ready/leave menu with Owner button
showReadyLeaveMenu(id, isStarter, TRUE);
} else if (targetType == "player") {
showReadyLeaveMenu(id, isStarter, FALSE);
} else if (targetType == "admin") {
// Show admin category menu directly
showOwnerMenu(id);
} else if (targetType == "unregistered_owner") {
// Unregistered owner - basic menu (join, owner menu)
showUnregisteredOwnerMenu(id, FALSE);
} else if (targetType == "unregistered_owner_starter") {
// Unregistered owner with enough players to start game
showUnregisteredOwnerMenu(id, TRUE);
}
}
else if (num == MSG_READY_STATE_RESULT) {
// Parse ready state response and show appropriate menu
list parts = llParseString2List(str, ["|"], []);
if (llGetListLength(parts) >= 4) {
string playerName = llList2String(parts, 0);
integer isReady = (integer)llList2String(parts, 1);
integer isBot = (integer)llList2String(parts, 2);
integer responseRequestID = (integer)llList2String(parts, 3);
// Validate that this response matches our pending request
if (pendingMenuPlayer == id && responseRequestID == pendingMenuRequestID) {
// Check for timeout
if ((llGetTime() - pendingMenuTimestamp) > MENU_REQUEST_TIMEOUT) {
dbg("⏰ Menu request timed out, ignoring response");
pendingMenuPlayer = NULL_KEY;
return;
}
showReadyLeaveMenuWithState(pendingMenuPlayer, pendingMenuIsStarter, pendingMenuIsOwner, isReady, isBot);
// Reset pending state ONLY after successfully showing the dialog
pendingMenuPlayer = NULL_KEY;
pendingMenuRequestID = 0;
}
@ -465,29 +516,16 @@ default {
dbg("📋 Fetching list of players for pick management...");
showPickManageMenu(id, playerNames);
}
// Handle player list result for kick functionality
else if (num == MSG_REQUEST_PLAYER_LIST_KICK) {
dbg("🔍 [Dialog Handler] Received kick player list string: '" + str + "'");
list rawNames = llParseString2List(str, [","], []);
dbg("🔍 [Dialog Handler] Raw parsed into " + (string)llGetListLength(rawNames) + " names: " + llList2CSV(rawNames));
// Filter out any command strings that might have gotten mixed in
list playerNames = [];
integer i;
for (i = 0; i < llGetListLength(rawNames); i++) {
string name = llStringTrim(llList2String(rawNames, i), STRING_TRIM);
// Skip empty names and command strings
if (name != "" &&
llSubStringIndex(name, "REQUEST_") != 0 &&
llSubStringIndex(name, "COMMAND_") != 0 &&
llSubStringIndex(name, "MSG_") != 0) {
if (name != "" && llSubStringIndex(name, "REQUEST_") != 0 && llSubStringIndex(name, "COMMAND_") != 0 && llSubStringIndex(name, "MSG_") != 0) {
playerNames += [name];
} else {
dbg("⚠️ [Dialog Handler] Filtered out invalid name: '" + name + "'");
}
}
dbg("🔍 [Dialog Handler] Filtered to " + (string)llGetListLength(playerNames) + " valid names: " + llList2CSV(playerNames));
showKickPlayerMenu(id, playerNames);
}
else if (num == MSG_PICK_LIST_RESULT) {
@ -495,11 +533,8 @@ default {
string returnedName = llList2String(parts, 0);
string picks = llList2String(parts, 1);
if (llStringTrim(returnedName, STRING_TRIM) == llStringTrim(currentPickTarget, STRING_TRIM)) {
dbg("✅ Matched pick list for: " + returnedName);
currentPickList = llCSV2List(picks);
llMessageLinked(LINK_THIS, MSG_LIFE_LOOKUP_REQUEST, currentPickTarget, id);
} else {
dbg("❌ Name mismatch: got " + returnedName + " but expected " + currentPickTarget);
}
}
else if (num == MSG_LIFE_LOOKUP_REQUEST) {
@ -510,7 +545,6 @@ default {
}
}
else if (num == MSG_OWNER_STATUS_RESULT) {
// Parse owner status response and show appropriate menu
list parts = llParseString2List(str, ["|"], []);
if (llGetListLength(parts) >= 5) {
string ownerName = llList2String(parts, 0);
@ -518,45 +552,23 @@ default {
integer isPending = (integer)llList2String(parts, 2);
integer isStarter = (integer)llList2String(parts, 3);
integer responseRequestID = (integer)llList2String(parts, 4);
// Validate that this response matches our pending request
if (pendingMenuPlayer == id && responseRequestID == pendingMenuRequestID) {
// Check for timeout
if ((llGetTime() - pendingMenuTimestamp) > MENU_REQUEST_TIMEOUT) {
dbg("⏰ Owner status query timed out, ignoring response");
pendingMenuPlayer = NULL_KEY;
return;
}
// Decide which menu to show based on registration status
if (isRegistered) {
// Owner is registered - show ready/leave menu with Owner button
dbg("🔍 [Debug] Owner is registered, isStarter=" + (string)isStarter + ", showing ready/leave menu");
dbg("🔍 [Debug] About to call showReadyLeaveMenu with id=" + (string)id + ", isStarter=" + (string)isStarter + ", isOwner=TRUE");
// DON'T reset pending state here - showReadyLeaveMenu needs it!
// The ready state handler will reset it when the dialog is shown
showReadyLeaveMenu(id, isStarter, TRUE);
dbg("🔍 [Debug] showReadyLeaveMenu call completed");
} else if (isPending) {
llRegionSayTo(id, 0, "⏳ Registration already in progress for " + ownerName);
// Reset pending state for non-registered responses
pendingMenuPlayer = NULL_KEY;
pendingMenuRequestID = 0;
} else {
// Owner is not registered - show join/admin choice menu
list options = ["Join Game", "Owner Menu"];
string menuText = "👑 Owner Options:\n\n🎮 Join Game - Register as a player\n🔧 Owner Menu - Admin functions without joining";
llDialog(id, menuText, options, DIALOG_CHANNEL);
// Reset pending state for non-registered responses
showUnregisteredOwnerMenu(id, (llGetListLength(players) >= 2));
pendingMenuPlayer = NULL_KEY;
pendingMenuRequestID = 0;
}
} else {
dbg("⚠️ Owner status response mismatch - ignoring");
}
} else {
dbg("⚠️ Invalid owner status result format - expected 5 parts, got " + (string)llGetListLength(parts));
}
return;
}

View file

@ -1,4 +1,4 @@
#include "Peril_Constants.lsl"
#include "peril/Peril_Constants.lsl"
// === Player Registration Manager ===
// Handles all player registration, join/leave operations to reduce Main Controller memory usage
@ -17,6 +17,14 @@ integer FLOATER_BASE_CHANNEL;
integer roundStarted = FALSE;
integer gameStarting = FALSE;
// Experience Sentinel Tracking
key sentinelQueryID;
integer EX_READY = FALSE;
#define TIMER_XP_CHECK 1
integer currentTimerMode = 0;
integer resetInProgress = FALSE; // Lockout for reset syncs
integer calculateChannel(integer offset) {
string ownerStr = (string)llGetOwner();
string objectStr = (string)llGetLinkKey(1);
@ -54,6 +62,31 @@ regionMsg(key player, string msg) {
llMessageLinked(LINK_SET, MSG_REGION_MESSAGE, (string)player + "|" + msg, NULL_KEY);
}
// MOVED: Experience Sentinel Logic
checkExperience() {
dbg("🔍 [RegMgr] Experience Sentinel: Starting functional diagnostics...");
if (!llAgentInExperience(llGetOwner())) {
llOwnerSay("🛡️ [Peril Dice] LAND NOTICE: Experience 'Final Girlz I.N.C.' is NOT enabled on this land.");
llOwnerSay("⚠️ [Peril Dice] Automatic HUD attachments will FAIL. To fix this:");
llOwnerSay("👉 1. Right-click the ground -> 'About Land'");
llOwnerSay("👉 2. Go to the 'Experiences' tab.");
llOwnerSay("👉 3. Click the 'Add' button.");
llOwnerSay("👉 4. Search for 'Final Girlz I.N.C.' and click 'OK'.");
sentinelQueryID = llReadKeyValue("PERIL_SENTINEL");
llSetTimerEvent(10.0);
currentTimerMode = TIMER_XP_CHECK;
} else {
llOwnerSay("✅ [Peril Dice] Experience 'Final Girlz I.N.C.' is ACTIVE. HUD Auto-attachment ready.");
EX_READY = TRUE;
sentinelQueryID = llReadKeyValue("PERIL_SENTINEL");
llSetTimerEvent(10.0);
currentTimerMode = TIMER_XP_CHECK;
}
}
// Handle player registration with all the complex logic moved here
handlePlayerRegistration(string regData, key requesterId) {
// MEMORY DEBUG: Check memory at start
@ -151,9 +184,12 @@ handlePlayerRegistration(string regData, key requesterId) {
llMessageLinked(LINK_SET, MSG_UPDATE_MAIN_LISTS, updateData, newKey);
// Show appropriate menu to the new player
integer isStarter = (humanCount <= 1);
if (newKey == llGetOwner()) {
integer isStarter = (humanCount <= 1);
llMessageLinked(LINK_SET, MSG_SHOW_MENU, "owner|" + (string)isStarter, newKey);
} else {
// NEW: Also show menu for non-owners immediately after registration
llMessageLinked(LINK_SET, MSG_SHOW_MENU, "player|" + (string)isStarter, newKey);
}
ownerMsg("JOIN|" + newName);
@ -163,6 +199,7 @@ default {
state_entry() {
DISCOVER_CORE_LINKS();
initializeChannels();
checkExperience();
REPORT_MEMORY();
dbg("🎯 [RegMgr] Player Registration Manager ready - discovery complete! Scoreboard: " + (string)LINK_SCOREBOARD);
}
@ -170,6 +207,7 @@ default {
on_rez(integer start_param) {
DISCOVER_CORE_LINKS();
initializeChannels();
checkExperience();
REPORT_MEMORY();
dbg("🎯 [RegMgr] Player Registration Manager rezzed - resetting state...");
// Reset local state
@ -190,77 +228,162 @@ default {
// Handle game state sync from Main Controller
if (num == MSG_SYNC_GAME_STATE) {
if (resetInProgress) return; // IGNORE stale syncs during reset lockout
// PRESERVE PLAYER KEYS: Don't overwrite our authoritative player registry
// We only sync lives and names, but keep our real player keys
list parts = llParseString2List(str, ["~"], []);
list parts = llParseStringKeepNulls(str, ["~"], []);
if (llGetListLength(parts) >= 4) {
list newLives = llCSV2List(llList2String(parts, 0));
string picksPart = llList2String(parts, 1);
list newNames = llCSV2List(llList2String(parts, 3));
// Only update lives if list length matches (safety check)
if (llGetListLength(newLives) == llGetListLength(players)) {
lives = newLives;
}
// Only update names if they match our current registry
// This prevents sync messages from overwriting our authoritative data
if (llGetListLength(newNames) == llGetListLength(players)) {
names = newNames;
}
// KEEP our player keys - they are authoritative
dbg("🔄 [RegMgr] Synced game state, preserving " + (string)llGetListLength(players) + " player keys");
}
return;
}
// --- Status Queries ---
if (num == MSG_QUERY_READY_STATE) {
list parts = llParseString2List(str, ["|"], []);
if (llGetListLength(parts) >= 2) {
string name = llList2String(parts, 0);
string requestId = llList2String(parts, 1);
integer isReady = (llListFindList(readyPlayers, [name]) != -1);
integer isBot = (llSubStringIndex(name, "Bot") == 0);
llMessageLinked(LINK_SET, MSG_READY_STATE_RESULT,
name + "|" + (string)isReady + "|" + (string)isBot + "|" + requestId, id);
}
return;
}
if (num == MSG_QUERY_OWNER_STATUS) {
list parts = llParseString2List(str, ["|"], []);
if (llGetListLength(parts) >= 2) {
string name = llList2String(parts, 0);
string requestId = llList2String(parts, 1);
integer isRegistered = (llListFindList(players, [id]) != -1);
integer isPending = FALSE;
integer isStarter = FALSE;
if (isRegistered) {
integer starterIdx = -1;
integer i;
for (i = 0; i < llGetListLength(names) && starterIdx == -1; i++) {
if (llSubStringIndex(llList2String(names, i), "Bot") != 0) starterIdx = i;
}
if (starterIdx != -1 && llList2Key(players, starterIdx) == id) isStarter = TRUE;
}
dbg("📤 [RegMgr] Responding to OWNER STATUS (" + requestId + ") - Registered: " + (string)isRegistered);
llMessageLinked(LINK_SET, MSG_OWNER_STATUS_RESULT,
name + "|" + (string)isRegistered + "|" + (string)isPending + "|" + (string)isStarter + "|" + requestId, id);
}
return;
}
if (num == MSG_TOGGLE_READY) {
string name = str;
integer idx = llListFindList(readyPlayers, [name]);
if (idx == -1) {
readyPlayers += [name];
publicMsg("GAME|" + name + " is NOW READY!");
} else {
readyPlayers = llDeleteSubList(readyPlayers, idx, idx);
publicMsg("GAME|" + name + " is NO LONGER READY.");
}
// Broadcast the official state list to all scripts (Main Controller needs this)
llMessageLinked(LINK_SET, MSG_UPDATE_MAIN_LISTS, "READY_LIST|" + llList2CSV(readyPlayers), NULL_KEY);
return;
}
if (num == MSG_REMOVE_PLAYER) {
string leavingName = str;
integer idx = llListFindList(names, [leavingName]);
if (idx != -1) {
// Scrub from all synchronized lists
players = llDeleteSubList(players, idx, idx);
names = llDeleteSubList(names, idx, idx);
lives = llDeleteSubList(lives, idx, idx);
// ALSO scrub from ready list if they were there
integer rIdx = llListFindList(readyPlayers, [leavingName]);
if (rIdx != -1) readyPlayers = llDeleteSubList(readyPlayers, rIdx, rIdx);
dbg("🎯 [RegMgr] Deep Scrub Complete: " + leavingName + " removed from all registries.");
// Broadcast updated lists to keep Main Controller in sync
llMessageLinked(LINK_SET, MSG_UPDATE_MAIN_LISTS, "READY_LIST|" + llList2CSV(readyPlayers), NULL_KEY);
}
return;
}
// Handle dialog forwarding requests from Game Manager
if (num == MSG_DIALOG_FORWARD_REQUEST) {
list parts = llParseString2List(str, ["|"], []);
list parts = llParseString2List(str, ["~"], []); // Fixed delimiter to match forwarder
if (llGetListLength(parts) >= 3) {
string dialogType = llList2String(parts, 0); // "SHOW_DIALOG" or "SHOW_ROLL_DIALOG"
string dialogType = llList2String(parts, 0);
string targetPlayerName = llList2String(parts, 1);
// Reconstruct dialog payload from remaining parts
string dialogPayload = "";
integer i;
for (i = 2; i < llGetListLength(parts); i++) {
if (i > 2) dialogPayload += "|"; // Add separator between parts
if (i > 2) dialogPayload += "|";
dialogPayload += llList2String(parts, i);
}
// Find player key in our authoritative registry
integer playerIdx = llListFindList(names, [targetPlayerName]);
if (playerIdx != -1 && playerIdx < llGetListLength(players)) {
key playerKey = llList2Key(players, playerIdx);
if (playerKey != NULL_KEY) {
if (dialogType == "SHOW_DIALOG") {
llMessageLinked(LINK_SET, MSG_SHOW_DIALOG, dialogPayload, playerKey);
dbg("📋 [RegMgr] Forwarded dialog to " + targetPlayerName + ": " + dialogPayload);
} else if (dialogType == "SHOW_ROLL_DIALOG") {
llMessageLinked(LINK_SET, MSG_SHOW_ROLL_DIALOG, dialogPayload, playerKey);
dbg("🎲 [RegMgr] Forwarded roll dialog to " + targetPlayerName);
}
} else {
dbg("⚠️ [RegMgr] Player " + targetPlayerName + " has NULL_KEY");
}
} else {
dbg("⚠️ [RegMgr] Player " + targetPlayerName + " not found in registry");
}
}
return;
}
if (num == MSG_RESET_ALL && str == "FULL_RESET") {
players = [];
names = [];
lives = [];
readyPlayers = [];
roundStarted = FALSE;
gameStarting = FALSE;
dbg("🎯 [RegMgr] Player Registration Manager reset complete");
resetInProgress = TRUE;
players = []; names = []; lives = []; readyPlayers = [];
roundStarted = FALSE; gameStarting = FALSE;
// Activate brief lockout timer to ignore stale syncs while Linkset settles
llSetTimerEvent(0.2);
dbg("🎯 [RegMgr] Player Registration Manager reset complete (Lockout Active)");
return;
}
}
timer() {
if (currentTimerMode == TIMER_XP_CHECK) {
// Diagnostic timer complete
llSetTimerEvent(0);
currentTimerMode = 0;
return;
}
resetInProgress = FALSE;
llSetTimerEvent(0);
dbg("🎯 [RegMgr] Reset lockout complete - sync active.");
}
dataserver(key queryid, string data) {
if (queryid == sentinelQueryID) {
dbg("📡 [RegMgr] Experience KVP Heartbeat: " + data);
}
}
}

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.2.6 - The "Hybrid HUD" Update**
**Current Version: 3.3.0 - The "Tournament Edition" 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.2.6**: **HYBRID HUD EVOLUTION & EXPERIENCE SENTINEL** - Revolutionary dual-mode status display. Humans receive a personal, Experience-authorized HUD (Center 2), while bots maintain in-world floaters. Features a built-in **Experience Sentinel** (v7.3) that automatically diagnoses land-readiness for 'Final Girlz I.N.C.' and provides owner-facing fix instructions for blocked parcels.
**🎯 NEW in v3.3.0**: **TOURNAMENT EDITION** - Final production sweep featuring global silence hardening, library warning suppression, and repository sanitization for peak stability.
**🧪 NEW in v3.2.6**: **HYBRID HUD EVOLUTION & EXPERIENCE SENTINEL** - Revolutionary dual-mode status display. Humans receive a personal, Experience-authorized HUD (Center 2), while bots maintain in-world floaters. Features a built-in **Experience Sentinel** (v7.3) that automatically diagnoses land-readiness for 'Final Girlz I.N.C.' and provides owner-facing fix instructions for blocked parcels.
**🧪 NEW in v3.2.0**: **SYNC SANCTITY & SCOREBOARD VIRTUALIZATION** - Total refactoring of the synchronization engine. The Main Controller is now the single source of truth for life counts, permanently eliminating "Ghost Heals." The scoreboard is now "Virtualized"—supporting a 3-column layout (Rank, Name, WL) and dynamic Alpha 0.0 hiding for unused slots.
@ -15,7 +17,7 @@ Peril Dice is a multiplayer elimination game where each player selects numbers b
**📦 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).*
**🔍 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).*
**🏆 NEW in v2.8.6**: **WINNER GLOW CELEBRATION SYSTEM** - Revolutionary winner recognition with bright green glow effects on both scoreboard and floaters, plus 24-second victory celebration timer giving proper recognition to the Ultimate Survivor before game reset.
@ -31,7 +33,14 @@ 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.2.6 Improvements (Current) 🎯
## Major v3.3.0 Improvements (Current) 🎯
### 🏁 **Tournament Edition**
- **Global Silence**: Unconditional stripping of all debug string literals across the entire codebase.
- **Library Suppression**: Direct silencing of internal Furware Engine warnings for zero-chatter operation.
- **Repository Purge**: Removal of all legacy development files for a clean production package.
## Major v3.2.6 Improvements 🎯
### 🎰 **Hybrid HUD Evolution**
- **Personalized Gaming UI**: Human players automatically receive a temporary, Experience-authorized HUD (Final Girlz I.N.C.) snapping to **Center 2**.
@ -97,67 +106,16 @@ Peril Dice is a multiplayer elimination game where each player selects numbers b
- **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.
## Major v2.8.7 Improvements 🔄
## System Architecture (v3.3.0) 🏗️
### 🔄 **GitHub Update System (Superseded)**
> **⚠️ Note**: The GitHub Update System was fully replaced by the CasperVend update system in v3.0.0 for commercial release.
- **In-World Update Checker**: Revolutionary `Update_Checker.lsl` script provides direct GitHub integration
- **GitHub API Integration**: Connects to `https://github.com/RebeccaNod1/Peril/releases` for automatic update detection
- **Smart File Filtering**: Only downloads essential game files, excludes development/test files
- **Precise Deployment Instructions**: Tells you exactly which link each script belongs on
- **Owner Menu Integration**: Access via Owner Menu → Troubleshooting → Check for Updates
- **Chat Commands**: `/1 check`, `/1 download ScriptName.lsl`, `/1 list` for direct access
- **Memory Optimized**: Fits perfectly in the slot freed by eliminated UpdateHelper.lsl (v2.8.5)
- **Professional Error Handling**: Clear feedback for network issues, missing releases, file validation
Peril Dice is now a unified single-linkset system utilizing modern LSL pre-processor techniques and mesh text displays.
### 🔍 **Enhanced LinkScanner System**
- **Complete 84-Prim Analysis**: `Enhanced_LinkScanner.lsl` provides detailed linkset structure mapping
- **VERIFIED Structure**: Based on actual linkset scan showing exact prim names and positions
- **Script Placement Guide**: Shows which scripts belong on which links with verification
- **XyzzyText Mapping**: Detailed analysis of all 48 leaderboard prims (Links 35-82)
- **Update Deployment Guide**: Generates precise instructions for Update_Checker placement
- **Real-Time Validation**: Automated "Invisible Guardian" scans land permissions and verifies if the "Final Girlz I.N.C." experience is correctly configured on every rez and reset.
### 🎯 **Smart File Classification**
- **Essential Game Files Only**: Update system intelligently filters files for end users
- **✅ Included**: All LSL scripts, documentation (README, CHANGELOG, notecards)
- **🚫 Excluded**: lsl_validator.py, debug_mcp.py, test_*.py, MCP files, templates
- **Link-Specific Instructions**: Downloads include verified deployment locations
- **Link 1**: 13 root prim scripts + Update_Checker.lsl
- **Link 12**: "scoreboard manager cube" - Game_Scoreboard_Manager_Linkset.lsl
- **Link 35**: "leaderboard row 1 col 1" - Leaderboard_Communication_Linkset.lsl
- **Links 35-82**: All 48 XyzzyText prims need xyzzy_Master_script.lsl
- **Link 83**: "dice col 1" - XyzzyText_Dice_Bridge_Linkset.lsl
### 🚀 **Zero-Infrastructure Updates**
- **No Update Server Required**: Leverages existing GitHub + Jenkins CI/CD pipeline perfectly
- **Automatic Release Detection**: Finds new versions as soon as they're tagged and released
- **Individual File Updates**: Download specific scripts without full release packages
- **GitHub Raw Integration**: Direct access to latest files from main branch
- **Multi-Instance Safe**: Multiple game tables can check for updates independently
### 🎨 **Professional User Experience**
- **Clear Version Information**: Shows current vs latest versions with release dates
- **Release Notes Preview**: Displays first 200 characters of GitHub release notes
- **Download Progress**: File size reporting and content preview for verification
- **Deployment Verification**: Confirms updates are designed for verified 84-prim structure
- **Graceful Error Handling**: Helpful messages for GitHub rate limits, missing files, etc.
### 📊 **System Integration Benefits**
- **Replaces Eliminated UpdateHelper**: Perfect use of memory freed in v2.8.5 optimization
- **Existing Admin Menu**: Integrates seamlessly with current troubleshooting interface
- **Message Protocol Compatibility**: Uses established link message patterns and constants
- **Verbose Logging Support**: Follows existing debug toggle system
- **Memory Reporting**: Includes standard memory usage reporting like other scripts
### 💱 **Before vs After v2.8.7**
| **Before** | **After** |
|------------|----------|
| ❌ Manual update distribution via inventory | ✅ Automatic GitHub integration with update detection |
| ❌ No way to check for new versions | ✅ One-click update checking via `/1 check` or admin menu |
| ❌ Guessing which scripts go where | ✅ Precise deployment instructions verified by linkset scan |
| ❌ Development files mixed with game files | ✅ Smart filtering shows only essential files |
| ❌ No linkset structure validation | ✅ Complete 84-prim analysis and verification tools |
### **Core Components:**
- **Main Controller (Root)**: Authoritative source of truth for all game states.
- **Experience Sentinel (v7.9.2)**: Automated "Auto-Pilot" land diagnostics.
- **Hybrid HUD System**: personal HUDs for humans + in-world floaters for bots.
- **FURWARE Text Mesh**: High-performance mesh displays for dice and leaderboard.
- **Dynamic Link Discovery**: Root-level discovery of all sibling prims by name.
## Major v2.8.6 Improvements 🏆
@ -649,9 +607,14 @@ Dice type is automatically chosen to ensure at least 3 picks per player:
## Version
**Current Version**: 3.2.0
**Last Updated**: April 05, 2026
**Status**: Production Ready - Sync Sanctity Release
**Current Version:** 3.3.0
**Last Updated:** April 09, 2026
**Status:** Production Ready - Tournament Edition Release
### Key Achievements in v3.3.0:
- ✅ **Global Silence**: Absolute stripping of debug chatter.
- ✅ **Storage Hardening**: Production-ready memory management.
- ✅ **Sanitized Package**: Zero dev-clutter architecture.
### Key Achievements in v3.2.0:
- ✅ **Sync Sanctity Protocol**: 100% unified life counting via the Main Controller.

View file

@ -1,4 +1,4 @@
#include "Peril_Constants.lsl"
#include "peril/Peril_Constants.lsl"
// === Roll and Confetti Module (with Roll Dialog Handler) ===
@ -214,7 +214,7 @@ default {
string picksDataStr = llList2String(parts, 1);
dbg("🎲 [Roll Module] 🔍 [Roll Module] Received picksDataStr: '" + picksDataStr + "'");
if (picksDataStr == "" || picksDataStr == "EMPTY") {
if (picksDataStr == "" || picksDataStr == "EMPTY" || picksDataStr == "~EMPTY~") {
picksData = [];
dbg("🎲 [Roll Module] 🔍 [Roll Module] Set picksData to empty");
} else {
@ -270,9 +270,18 @@ default {
llSleep(DELAY_CONFETTI_START);
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, MSG_GET_DICE_TYPE, (string)llGetListLength(names), NULL_KEY);
// NEW: Use the dice type already received from Game Manager at round start!
if (diceType > 0) {
dbg("🎲 [Roll Module] 🤖 Bot using stored dice type: d" + (string)diceType);
// Trigger the roll directly
llMessageLinked(LINK_THIS, MSG_ROLL_RESULT, (string)diceType, NULL_KEY);
} else {
// Fallback: If for some reason we don't have it, request it
dbg("🎲 [Roll Module] 🤖 Fallback: Bot requesting dice type...");
shouldRoll = TRUE;
llMessageLinked(LINK_SET, MSG_GET_DICE_TYPE, (string)llGetListLength(names), NULL_KEY);
}
} else {
// Reset any previous roll state before showing dialog
rollInProgress = FALSE;
@ -385,11 +394,12 @@ default {
llMessageLinked(LINK_SCOREBOARD, MSG_GAME_STATUS, "Plot Twist", NULL_KEY);
llMessageLinked(LINK_SET, MSG_STATUS_TEXT, "PLOT TWIST!\n<!c=green>Shield Up!<!c=white> Target shifted!", NULL_KEY);
// IMPORTANT: Send peril player update to scoreboard for glow effect
llMessageLinked(LINK_SCOREBOARD, MSG_UPDATE_PERIL_PLAYER, newPeril, NULL_KEY); // MSG_UPDATE_PERIL_PLAYER
// Add delay to let status display before next phase
llSleep(DELAY_LONG_SYNC);
llSleep(DELAY_LONG_SYNC + DELAY_MEDIUM_SYNC);
// NEW: Move peril player update signal DOWN to after the sleep
// This prevents Game Manager from starting new round too soon
llMessageLinked(LINK_SET, MSG_UPDATE_PERIL_PLAYER, newPeril, NULL_KEY);
// Update floaters immediately to show correct peril player before sync
llMessageLinked(LINK_SET, MSG_UPDATE_FLOAT, newPeril, NULL_KEY);
@ -406,9 +416,8 @@ default {
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");
// NEW: REPORT LIFE REDUCTION to Main Controller (Source of Truth)
// This replaces the unreliable self-syncing and allows the Controller to broadcast the official state.
llMessageLinked(LINK_SET, MSG_UPDATE_LIFE, perilPlayer + "|" + (string)(currentLives - 1), NULL_KEY);
// NEW: Move life update signal DOWN to after the outcome sleeps
// This prevents Game Manager/Controller from starting next round too soon
// Check if peril player picked the rolled number
list perilPicks = getPicksFor(perilPlayer);
@ -424,21 +433,30 @@ default {
// Direct scoreboard status update (texture and text)
llMessageLinked(LINK_SCOREBOARD, MSG_GAME_STATUS, "Direct Hit", NULL_KEY);
llMessageLinked(LINK_SET, MSG_STATUS_TEXT, "DIRECT HIT!\n<!c=red>" + perilPlayer + " matched!", NULL_KEY);
// Add delay to let status display before next phase
llSleep(DELAY_LONG_SYNC);
llSleep(DELAY_LONG_SYNC + DELAY_MEDIUM_SYNC);
// SIGNAL DOWN: Now report life reduction to Controller
llMessageLinked(LINK_SET, MSG_UPDATE_LIFE, perilPlayer + "|" + (string)(currentLives - 1), NULL_KEY);
} else if (!matched) {
llSay(0, "🩸 NO SHIELD! Nobody picked " + resultStr + " - " + perilPlayer + " takes the hit from the d" + (string)diceType + "! 🩸");
// Direct scoreboard status update (texture and text)
llMessageLinked(LINK_SCOREBOARD, MSG_GAME_STATUS, "No Shield", NULL_KEY);
llMessageLinked(LINK_SET, MSG_STATUS_TEXT, "NO SHIELD!\n<!c=red>" + perilPlayer + " took the hit!", NULL_KEY);
// Add delay to let status display before next phase
llSleep(DELAY_LONG_SYNC);
llSleep(DELAY_LONG_SYNC + DELAY_MEDIUM_SYNC);
// SIGNAL DOWN: Now report life reduction to Controller
llMessageLinked(LINK_SET, MSG_UPDATE_LIFE, perilPlayer + "|" + (string)(currentLives - 1), NULL_KEY);
} 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
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(DELAY_ELIMINATION_NOTICE);
llMessageLinked(LINK_SET, MSG_UPDATE_LIFE, perilPlayer + "|" + (string)(currentLives - 1), NULL_KEY);
}
llMessageLinked(LINK_SET, MSG_UPDATE_FLOAT, perilPlayer, NULL_KEY);
@ -477,38 +495,8 @@ default {
// Note: Win condition checking is handled by Main Controller after elimination
// Validate perilPlayer is not empty before syncing
string perilForSync = perilPlayer;
if (perilForSync == "") {
perilForSync = "NONE";
}
// MEMORY OPTIMIZED: Direct sync message construction to avoid temporary string allocations
// Uses 4-part format to remain compatible with Game Manager
llMessageLinked(LINK_SET, MSG_SYNC_GAME_STATE,
llList2CSV(lives) + "~" +
llDumpList2String(picksData, "^") + "~" +
perilForSync + "~" +
llList2CSV(names), NULL_KEY);
llSleep(DELAY_DIALOG_REFRESH);
// Additional floater updates to ensure correct peril player display
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);
llMessageLinked(LINK_SET, MSG_UPDATE_FLOAT, fname, NULL_KEY);
}
// After roll is processed and sync is sent, send continuation directly to Game Manager
dbg("🎲 [Roll Module] 🎯 Round complete - sending continuation directly to Game Manager");
// Brief delay to ensure sync propagates
llSleep(DELAY_SYNC_PROPAGATION);
// Send continuation directly to Game Manager (more efficient than through Main Controller)
// For non-elimination cases, continue with current peril player
llMessageLinked(LINK_SET, MSG_CONTINUE_GAME, perilForSync, NULL_KEY);
// Note: State sync and round continuation are now handled exclusively by the Main Controller
// after it receives the Damage/Elimination reports, ensuring a single source of truth.
// Clear roll protection after processing is complete
rollInProgress = FALSE;

View file

@ -1,4 +1,4 @@
#include "Peril_Constants.lsl"
#include "peril/Peril_Constants.lsl"
// === System Debugger - Game State Validation ===
// Validates game state consistency and reports issues

View file

@ -1,127 +0,0 @@
# Peril Dice External Updater System
## Architecture Overview
### Components Needed:
#### 1. **Updater Box** (`Peril_Dice_Updater.lsl`)
- Standalone object that users rez temporarily
- Downloads scripts from GitHub when activated
- Installs scripts into target Peril Dice game via remote loading
- Self-contained - includes all update logic
#### 2. **Update Receiver** (in main game)
- Modified `Update_Checker.lsl` → `Update_Receiver.lsl`
- Scans for nearby updater boxes
- Requests updates when available
- Manages the script replacement process
#### 3. **Script Manifest** (GitHub hosted)
- JSON file listing all scripts and their GitHub URLs
- Version tracking for incremental updates
- Deployment instructions for updater
## Technical Implementation
### Update Process Flow:
```
1. User rezzes "Peril Dice Updater v2.8.7" box
2. User touches their Peril Dice game → "Check for Updates"
3. Game scans for nearby updater boxes (llSensor)
4. Game requests update from updater box
5. Updater downloads scripts from GitHub
6. Game sets script access pin (llSetRemoteScriptAccessPin)
7. Updater installs scripts via llRemoteLoadScriptPin()
8. Game validates installation and reports success
9. User deletes updater box
```
### Key LSL Functions:
- `llSensor()` - Find nearby updater boxes
- `llSetRemoteScriptAccessPin()` - Allow updater access
- `llRemoteLoadScriptPin()` - Install new scripts
- `llHTTPRequest()` - Download scripts from GitHub
- `llGetInventoryName()` - Validate installed scripts
## User Experience
### For Players:
1. Receive "Peril Dice Updater v2.8.7" from creator
2. Rez updater near their game
3. Touch game → Admin Menu → "Check for Updates"
4. See "Update Available: v2.8.6 → v2.8.7"
5. Click "Install Update"
6. Watch progress: "Installing script 3 of 17..."
7. See "✅ Update Complete! Game is now v2.8.7"
8. Delete updater box
### For Creator (You):
1. Build new scripts and test them
2. Commit to GitHub with version bump
3. Create updater box with new version number
4. Distribute updater to users via marketplace/group notices
5. Users get automatic professional updates
## Advantages Over Manual Updates
### ✅ Professional Experience:
- One-click updates like commercial software
- Progress indicators and status messages
- Automatic validation of successful installation
- No copy/paste errors or missed scripts
### ✅ Version Control:
- Updater knows exactly which scripts need updating
- Can do incremental updates (only changed files)
- Prevents version mismatches between scripts
- Tracks what was updated when
### ✅ User Friendly:
- No technical knowledge required
- Clear instructions and feedback
- Impossible to mess up the installation
- Works the same way every time
## Files to Create
### 1. `Peril_Dice_Updater.lsl`
- Main updater script for the external box
- GitHub API integration for script downloads
- Remote script installation logic
- Progress reporting and error handling
### 2. `Update_Receiver.lsl`
- Replace existing Update_Checker.lsl
- Scan for nearby updater boxes
- Coordinate with updater for script replacement
- Validate successful updates
### 3. `updater-manifest.json` (GitHub)
- List of all scripts with GitHub URLs
- Version requirements and dependencies
- Installation order and link assignments
### 4. Instructions for packaging updater boxes
- How to create updater objects for distribution
- Testing procedures for new versions
- Distribution methods (marketplace, group, etc.)
## Implementation Priority
### Phase 1: Core Updater System
1. Create basic updater box with GitHub integration
2. Modify existing Update_Checker to work with updater
3. Test with simple single-script updates
### Phase 2: Full Integration
1. Complete manifest system for all 17 scripts
2. Add progress indicators and error handling
3. Test complete game updates
### Phase 3: Polish & Distribution
1. Create professional updater box object
2. Write user documentation
3. Set up distribution system
This system would provide a truly professional update experience matching commercial Second Life products!

View file

@ -1,50 +0,0 @@
#!/usr/bin/env python3
import sys
import json
import traceback
def debug_main():
"""Debug version to see what's happening"""
print('{"debug": "server started"}', flush=True)
try:
while True:
line = sys.stdin.readline()
print(f'{{"debug": "received line: {line.strip()[:100]}"}}', flush=True)
if not line:
print('{"debug": "EOF received, exiting"}', flush=True)
break
line = line.strip()
if not line:
continue
try:
request = json.loads(line)
print(f'{{"debug": "parsed request: {request.get("method", "unknown")}"}}', flush=True)
if request.get("method") == "initialize":
response = {
"jsonrpc": "2.0",
"id": request.get("id"),
"result": {
"protocolVersion": "2025-03-26",
"capabilities": {"tools": {}, "resources": {}},
"serverInfo": {"name": "Debug LSL Server", "version": "1.0.0"}
}
}
print(json.dumps(response), flush=True)
print('{"debug": "sent initialize response"}', flush=True)
except Exception as e:
print(f'{{"debug": "error parsing: {str(e)}"}}', flush=True)
except Exception as e:
print(f'{{"debug": "main loop error: {str(e)}"}}', flush=True)
traceback.print_exc(file=sys.stderr)
print('{"debug": "server exiting"}', flush=True)
if __name__ == "__main__":
debug_main()

View file

@ -1,414 +0,0 @@
#!/usr/bin/env python3
"""
LSL Development MCP Server for Peril Dice Project
Provides tools for LSL validation, preprocessing, and project management.
"""
import json
import sys
import subprocess
import os
import glob
from pathlib import Path
from typing import Dict, List, Any, Optional
class LSLMCPServer:
def __init__(self, project_root: str = "/home/richard/peril"):
self.project_root = Path(project_root)
self.lsl_validator_path = self.project_root / "lsl_validator.py"
def handle_request(self, request: Dict[str, Any]) -> Dict[str, Any]:
"""Handle incoming MCP requests"""
method = request.get("method", "")
params = request.get("params", {})
if method == "initialize":
return self._handle_initialize(params)
elif method == "tools/list":
return self._list_tools()
elif method == "tools/call":
return self._call_tool(params)
elif method == "resources/list":
return self._list_resources()
elif method == "resources/read":
return self._read_resource(params)
else:
return {"error": f"Unknown method: {method}"}
def _handle_initialize(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Handle MCP initialization request"""
# Use the client's protocol version if provided
client_version = params.get("protocolVersion", "2024-11-05")
return {
"protocolVersion": client_version,
"capabilities": {
"tools": {},
"resources": {}
},
"serverInfo": {
"name": "LSL Development Server",
"version": "1.0.0"
}
}
def _list_tools(self) -> Dict[str, Any]:
"""List available LSL development tools"""
return {
"tools": [
{
"name": "validate_lsl_file",
"description": "Comprehensive LSL validation for a specific file (syntax, scope, style, performance, memory)",
"inputSchema": {
"type": "object",
"properties": {
"file_path": {"type": "string", "description": "Path to LSL file to validate"}
},
"required": ["file_path"]
}
},
{
"name": "validate_all_lsl",
"description": "Comprehensive LSL validation for all files in the project",
"inputSchema": {
"type": "object",
"properties": {}
}
},
{
"name": "list_lsl_files",
"description": "List all LSL files in the project",
"inputSchema": {
"type": "object",
"properties": {}
}
},
{
"name": "git_status",
"description": "Get git status of the project",
"inputSchema": {
"type": "object",
"properties": {}
}
},
{
"name": "analyze_script_architecture",
"description": "Analyze the script architecture and communication patterns",
"inputSchema": {
"type": "object",
"properties": {}
}
},
{
"name": "get_memory_usage_info",
"description": "Extract memory usage information from LSL files",
"inputSchema": {
"type": "object",
"properties": {}
}
}
]
}
def _call_tool(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Execute a specific tool"""
tool_name = params.get("name", "")
arguments = params.get("arguments", {})
try:
if tool_name == "validate_lsl_file":
return self._validate_lsl_file(arguments["file_path"])
elif tool_name == "validate_all_lsl":
return self._validate_all_lsl()
elif tool_name == "list_lsl_files":
return self._list_lsl_files()
elif tool_name == "git_status":
return self._git_status()
elif tool_name == "analyze_script_architecture":
return self._analyze_script_architecture()
elif tool_name == "get_memory_usage_info":
return self._get_memory_usage_info()
else:
return {"error": f"Unknown tool: {tool_name}"}
except Exception as e:
return {"error": f"Tool execution failed: {str(e)}"}
def _validate_lsl_file(self, file_path: str) -> Dict[str, Any]:
"""Validate a specific LSL file using the comprehensive LSL validator"""
full_path = self.project_root / file_path if not os.path.isabs(file_path) else Path(file_path)
if not full_path.exists():
return {"error": f"File not found: {full_path}"}
if not self.lsl_validator_path.exists():
return {"error": f"LSL validator not found at: {self.lsl_validator_path}"}
try:
result = subprocess.run([
"python3", str(self.lsl_validator_path), str(full_path)
], capture_output=True, text=True, cwd=str(self.project_root))
return {
"content": [
{
"type": "text",
"text": f"LSL Validation Result for {file_path}:\n\n"
f"Exit Code: {result.returncode}\n"
f"Output:\n{result.stdout}\n"
f"Errors:\n{result.stderr}"
}
]
}
except Exception as e:
return {"error": f"Failed to run LSL validator: {str(e)}"}
def _validate_all_lsl(self) -> Dict[str, Any]:
"""Validate all LSL files in the project using the comprehensive LSL validator"""
if not self.lsl_validator_path.exists():
return {"error": f"LSL validator not found at: {self.lsl_validator_path}"}
try:
result = subprocess.run([
"python3", str(self.lsl_validator_path), "."
], capture_output=True, text=True, cwd=str(self.project_root))
return {
"content": [
{
"type": "text",
"text": f"LSL Validation Results for All Files:\n\n"
f"Exit Code: {result.returncode}\n\n"
f"Output:\n{result.stdout}\n"
f"Errors:\n{result.stderr}"
}
]
}
except Exception as e:
return {"error": f"Failed to run LSL validator on all files: {str(e)}"}
def _list_lsl_files(self) -> Dict[str, Any]:
"""List all LSL files in the project"""
lsl_files = list(self.project_root.glob("*.lsl"))
file_list = [f.name for f in sorted(lsl_files)]
return {
"content": [
{
"type": "text",
"text": f"LSL Files in Project ({len(file_list)} total):\n\n" +
"\n".join([f"• {f}" for f in file_list])
}
]
}
def _git_status(self) -> Dict[str, Any]:
"""Get git status of the project"""
try:
result = subprocess.run([
"git", "status", "--porcelain"
], capture_output=True, text=True, cwd=str(self.project_root))
branch_result = subprocess.run([
"git", "branch", "--show-current"
], capture_output=True, text=True, cwd=str(self.project_root))
status_text = "Git Status:\n\n"
status_text += f"Current Branch: {branch_result.stdout.strip()}\n\n"
if result.stdout.strip():
status_text += "Modified Files:\n"
for line in result.stdout.strip().split('\n'):
status_text += f" {line}\n"
else:
status_text += "Working directory clean\n"
return {
"content": [
{
"type": "text",
"text": status_text
}
]
}
except Exception as e:
return {"error": f"Failed to get git status: {str(e)}"}
def _analyze_script_architecture(self) -> Dict[str, Any]:
"""Analyze the script architecture"""
lsl_files = list(self.project_root.glob("*.lsl"))
architecture_info = "Peril Dice Script Architecture Analysis:\n\n"
architecture_info += f"Total Scripts: {len(lsl_files)}\n\n"
# Categorize scripts by type
controllers = [f for f in lsl_files if "Controller" in f.name or "Manager" in f.name]
handlers = [f for f in lsl_files if "Handler" in f.name]
modules = [f for f in lsl_files if "Module" in f.name]
linkset_scripts = [f for f in lsl_files if "Linkset" in f.name]
architecture_info += f"Controllers/Managers ({len(controllers)}):\n"
for script in controllers:
architecture_info += f" • {script.name}\n"
architecture_info += f"\nHandlers ({len(handlers)}):\n"
for script in handlers:
architecture_info += f" • {script.name}\n"
architecture_info += f"\nModules ({len(modules)}):\n"
for script in modules:
architecture_info += f" • {script.name}\n"
architecture_info += f"\nLinkset-Specific Scripts ({len(linkset_scripts)}):\n"
for script in linkset_scripts:
architecture_info += f" • {script.name}\n"
return {
"content": [
{
"type": "text",
"text": architecture_info
}
]
}
def _get_memory_usage_info(self) -> Dict[str, Any]:
"""Extract memory usage information from comments in LSL files"""
lsl_files = list(self.project_root.glob("*.lsl"))
memory_info = "Memory Usage Information:\n\n"
for lsl_file in lsl_files:
try:
with open(lsl_file, 'r', encoding='utf-8') as f:
content = f.read()
# Look for memory usage comments
lines = content.split('\n')
memory_lines = [line for line in lines if 'memory' in line.lower() and ('%' in line or 'usage' in line.lower())]
if memory_lines:
memory_info += f"{lsl_file.name}:\n"
for line in memory_lines:
memory_info += f" {line.strip()}\n"
memory_info += "\n"
except Exception:
continue
return {
"content": [
{
"type": "text",
"text": memory_info if memory_info.strip() != "Memory Usage Information:" else "No memory usage information found in comments."
}
]
}
def _list_resources(self) -> Dict[str, Any]:
"""List available project resources"""
return {
"resources": [
{
"uri": "file:///home/richard/peril/README.md",
"name": "Project README",
"description": "Main project documentation",
"mimeType": "text/markdown"
},
{
"uri": "file:///home/richard/peril/CHANGELOG.md",
"name": "Changelog",
"description": "Project changelog",
"mimeType": "text/markdown"
},
{
"uri": "file:///home/richard/peril/WARP.md",
"name": "WARP Development Guide",
"description": "Development guidance for WARP",
"mimeType": "text/markdown"
}
]
}
def _read_resource(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Read a specific resource"""
uri = params.get("uri", "")
if uri.startswith("file://"):
file_path = Path(uri[7:]) # Remove file:// prefix
if not file_path.exists():
return {"error": f"File not found: {file_path}"}
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
return {
"contents": [
{
"uri": uri,
"mimeType": "text/markdown" if file_path.suffix == ".md" else "text/plain",
"text": content
}
]
}
except Exception as e:
return {"error": f"Failed to read file: {str(e)}"}
return {"error": f"Unsupported URI: {uri}"}
def main():
"""Main server loop"""
server = LSLMCPServer()
try:
# Read requests from stdin continuously
while True:
try:
line = sys.stdin.readline()
if not line: # EOF
break
line = line.strip()
if not line:
continue
request = json.loads(line)
response = server.handle_request(request)
# Wrap response in proper JSON-RPC format
json_rpc_response = {
"jsonrpc": "2.0",
"id": request.get("id", None)
}
if "error" in response:
json_rpc_response["error"] = response["error"]
else:
json_rpc_response["result"] = response
print(json.dumps(json_rpc_response))
sys.stdout.flush() # Ensure immediate output
except json.JSONDecodeError:
error_response = {
"jsonrpc": "2.0",
"error": {"code": -32700, "message": "Parse error"},
"id": None
}
print(json.dumps(error_response))
sys.stdout.flush()
except Exception as e:
error_response = {
"jsonrpc": "2.0",
"error": {"code": -32603, "message": str(e)},
"id": None
}
print(json.dumps(error_response))
sys.stdout.flush()
except KeyboardInterrupt:
pass
except EOFError:
pass
if __name__ == "__main__":
main()

View file

@ -1,60 +0,0 @@
#!/usr/bin/env python3
"""
Test script to verify lslint functionality with MCP server
"""
import json
import subprocess
import sys
def test_lslint_functionality():
"""Test lslint functionality through MCP server"""
# Test list_lsl_files first
list_request = {
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "list_lsl_files",
"arguments": {}
}
}
try:
# Start the MCP server process
process = subprocess.Popen([
'python3', '/home/richard/peril/lsl_mcp_server.py'
], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
# Send the request
stdout, stderr = process.communicate(input=json.dumps(list_request) + '\n', timeout=10)
print("=== LSL MCP Server - LSLint Test ===")
print("\nListing LSL Files:")
lines = stdout.strip().split('\n')
for line in lines:
if line.strip():
try:
response = json.loads(line)
if 'content' in response:
print(response['content'][0]['text'])
elif 'tools' in response:
print("Server initialized successfully")
except json.JSONDecodeError:
print(f"Non-JSON output: {line}")
if stderr:
print(f"\nServer Errors:\n{stderr}")
print("\n=== Test Complete ===")
except subprocess.TimeoutExpired:
print("Server test timed out")
process.kill()
except Exception as e:
print(f"Test failed: {e}")
if __name__ == "__main__":
test_lslint_functionality()

View file

@ -1,52 +0,0 @@
#!/usr/bin/env python3
"""
Simple test script to verify the LSL MCP server is working
"""
import json
import subprocess
import sys
def test_mcp_server():
"""Test basic MCP server functionality"""
# Test tools/list
test_request = {
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list",
"params": {}
}
try:
# Start the MCP server process
process = subprocess.Popen([
'python3', '/home/richard/peril/lsl_mcp_server.py'
], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
# Send the request
stdout, stderr = process.communicate(input=json.dumps(test_request) + '\n', timeout=10)
print("=== MCP Server Test Results ===")
print("\nServer Output:")
for line in stdout.strip().split('\n'):
if line.strip():
try:
response = json.loads(line)
print(json.dumps(response, indent=2))
except json.JSONDecodeError:
print(f"Non-JSON output: {line}")
if stderr:
print(f"\nServer Errors:\n{stderr}")
print("\n=== Test Complete ===")
except subprocess.TimeoutExpired:
print("Server test timed out")
process.kill()
except Exception as e:
print(f"Test failed: {e}")
if __name__ == "__main__":
test_mcp_server()

View file

@ -1,62 +0,0 @@
#!/usr/bin/env python3
"""
Test script to validate a specific LSL file using the comprehensive validator
"""
import json
import subprocess
import sys
def test_validate_lsl():
"""Test LSL validation through MCP server"""
# Test validate_lsl_file with Main_Controller_Linkset.lsl
validate_request = {
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "validate_lsl_file",
"arguments": {
"file_path": "Main_Controller_Linkset.lsl"
}
}
}
try:
# Start the MCP server process
process = subprocess.Popen([
'python3', '/home/richard/peril/lsl_mcp_server.py'
], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
# Send the request
stdout, stderr = process.communicate(input=json.dumps(validate_request) + '\n', timeout=15)
print("=== LSL Comprehensive Validation Test ===")
print("\nValidating Main_Controller_Linkset.lsl:")
lines = stdout.strip().split('\n')
for line in lines:
if line.strip():
try:
response = json.loads(line)
if 'content' in response:
print(response['content'][0]['text'])
elif 'method' in response and response['method'] == 'server/initialized':
print("✅ Server initialized successfully")
except json.JSONDecodeError:
print(f"Non-JSON output: {line}")
if stderr:
print(f"\nServer Errors:\n{stderr}")
print("\n=== Test Complete ===")
except subprocess.TimeoutExpired:
print("Server test timed out")
process.kill()
except Exception as e:
print(f"Test failed: {e}")
if __name__ == "__main__":
test_validate_lsl()

View file

@ -1,4 +1,4 @@
#include "Peril_Constants.lsl"
#include "peril/Peril_Constants.lsl"
// ====================================================================
// Enhanced Link Scanner for Peril Dice Linkset