Compare commits
No commits in common. "48226288f03b383c5921d9ee1c67327df2c69471" and "1297b99e2cbe6c5b788f4d3b271657c18d11884c" have entirely different histories.
48226288f0
...
1297b99e2c
37 changed files with 1824 additions and 913 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -12,7 +12,6 @@ DEPLOYMENT_NOTES.txt
|
|||
# IDE and editor files
|
||||
.vscode/
|
||||
.idea/
|
||||
*.code-workspace
|
||||
*.sublime-*
|
||||
*~
|
||||
.*.swp
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
#include "peril/Peril_Constants.lsl"
|
||||
#include "Peril_Constants.lsl"
|
||||
|
||||
// Handles logic for test players (bots): picking numbers and rolling dice
|
||||
|
||||
|
|
@ -47,7 +47,6 @@ 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 = "";
|
||||
|
|
@ -237,7 +236,6 @@ default {
|
|||
// Handle full reset from main controller
|
||||
if (num == MSG_RESET_ALL && str == "FULL_RESET") {
|
||||
// Reset bot manager state
|
||||
players = [];
|
||||
names = [];
|
||||
lives = [];
|
||||
perilPlayer = "";
|
||||
|
|
@ -249,7 +247,7 @@ default {
|
|||
|
||||
// Handle game state sync to track player eliminations
|
||||
if (num == MSG_SYNC_GAME_STATE) {
|
||||
list parts = llParseStringKeepNulls(str, ["~"], []);
|
||||
list parts = llParseString2List(str, ["~"], []);
|
||||
if (llGetListLength(parts) >= 4) {
|
||||
lives = llCSV2List(llList2String(parts, 0));
|
||||
// Don't need picks data for validation, skip parts[1]
|
||||
|
|
@ -276,12 +274,13 @@ default {
|
|||
sentBotMessages = [];
|
||||
dbg("🤖 [Bot Manager] New round detected (peril change), cleared processed commands and sent messages");
|
||||
} else {
|
||||
// Standardized ~EMPTY~ token logic
|
||||
// Only clear if we detect truly empty picks data during an active game
|
||||
// Don't clear during initial registration (when picks = "EMPTY")
|
||||
string picksStr = llList2String(parts, 1);
|
||||
if (picksStr == "~EMPTY~" && perilPlayer != "") {
|
||||
if (picksStr != "EMPTY" && (picksStr == "" || llSubStringIndex(picksStr, "|") == -1) && perilPlayer != "") {
|
||||
processedBotCommands = [];
|
||||
sentBotMessages = [];
|
||||
dbg("🤖 [Bot Manager] Turn reset detected (picks cleared), cleared processed commands and sent messages");
|
||||
dbg("🤖 [Bot Manager] New round detected (picks cleared), cleared processed commands and sent messages");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
11
CHANGELOG.md
11
CHANGELOG.md
|
|
@ -4,22 +4,15 @@
|
|||
|
||||
All notable changes to Peril Dice will be documented in this file.
|
||||
|
||||
## [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.
|
||||
## [3.2.6] - 2026-04-05
|
||||
|
||||
### 🎰 **v3.3.0: Final Production Sweep - Global Silence & Memory Hardening**
|
||||
### 🎰 **Hybrid HUD Evolution**
|
||||
- **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.
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
#include "peril/Peril_Constants.lsl"
|
||||
#include "Peril_Constants.lsl"
|
||||
|
||||
// === Controller Memory Monitor ===
|
||||
// Handles memory monitoring and reporting for the Main Controller
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
#include "peril/Peril_Constants.lsl"
|
||||
#include "Peril_Constants.lsl"
|
||||
|
||||
// === Controller Message Handler Helper Script ===
|
||||
// Handles non-critical message processing for the Main Controller
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
#include "peril/Peril_Constants.lsl"
|
||||
#include "Peril_Constants.lsl"
|
||||
|
||||
// ====================================================================
|
||||
// Enhanced Link Scanner for Peril Dice Linkset
|
||||
|
|
@ -1,10 +1,6 @@
|
|||
#include "peril/Peril_Constants.lsl"
|
||||
#include "Peril_Constants.lsl"
|
||||
|
||||
string leftText = "";
|
||||
string firstName = "";
|
||||
integer blinkState = 0;
|
||||
integer canBlink = 0;
|
||||
|
||||
|
||||
// Distribute text to the FURWARE "Leaderboard" boxes
|
||||
distributeFullText(string text) {
|
||||
|
|
@ -14,33 +10,7 @@ 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");
|
||||
|
||||
// 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, 1), (key)"fw_data:LB_Name");
|
||||
llMessageLinked(LINK_SET, 0, llList2String(parts, 2), (key)"fw_data:LB_WL");
|
||||
|
||||
// Route Navigation Arrows to dedicated button boxes
|
||||
|
|
@ -71,13 +41,8 @@ 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:");
|
||||
|
||||
// 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_Name:Leaderboard:4,1,20,10:");
|
||||
llMessageLinked(LINK_SET, 0, "", (key)"fw_addbox:LB_WL:Leaderboard:24,1,8,10:");
|
||||
|
||||
// Navigation Row (Shifted down to Y=11)
|
||||
|
|
@ -91,25 +56,8 @@ default {
|
|||
// Set the Top Line Header on the main box
|
||||
llMessageLinked(LINK_SET, 0, "=== WORLD RANKING ===", (key)"fw_data:Leaderboard");
|
||||
|
||||
// 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");
|
||||
}
|
||||
// Trigger Leaderboard Manager to download from KVP Database
|
||||
llMessageLinked(LINK_SET, MSG_RESET_LEADERBOARD, "START_SYNC", NULL_KEY);
|
||||
}
|
||||
|
||||
touch_start(integer _n) {
|
||||
|
|
@ -149,6 +97,5 @@ default {
|
|||
}
|
||||
}
|
||||
}
|
||||
else if (num == MSG_RESET_ALL) { llResetScript(); }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
#include "peril/Peril_Constants.lsl"
|
||||
#include "Peril_Constants.lsl"
|
||||
|
||||
// =============================================================================
|
||||
// FURWARE STATUS BRIDGE - Dynamic Game Info Display
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
#include "peril/Peril_Constants.lsl"
|
||||
#include "Peril_Constants.lsl"
|
||||
|
||||
// =============================================================================
|
||||
// LINKSET COMMUNICATION - NO DISCOVERY NEEDED
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
#include "peril/Peril_Constants.lsl"
|
||||
#include "Peril_Constants.lsl"
|
||||
|
||||
// === FloatManager (Consolidated) ===
|
||||
// This version enforces a maximum of 10 players.
|
||||
|
|
@ -123,7 +123,6 @@ default {
|
|||
// Handle full reset from main controller
|
||||
if (num == MSG_RESET_ALL && str == "FULL_RESET") {
|
||||
allPlayerNames = [];
|
||||
allPlayerKeys = [];
|
||||
names = [];
|
||||
players = [];
|
||||
lives = [];
|
||||
|
|
@ -199,7 +198,7 @@ default {
|
|||
|
||||
integer syncIdx = llListFindList(names, [name]);
|
||||
key avKey = id;
|
||||
integer lifeCount = 3; // DEFAULT TO HEALTHY (Prevent Red Flashes)
|
||||
integer lifeCount = 0;
|
||||
|
||||
if (syncIdx != -1) {
|
||||
if (syncIdx < llGetListLength(players)) avKey = llList2Key(players, syncIdx);
|
||||
|
|
@ -229,15 +228,14 @@ default {
|
|||
|
||||
string txt = "🎲 Peril Dice\n👤 " + name + "\n" + perilStatus + "\n🔢 Picks: " + picksDisplay;
|
||||
|
||||
// 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) {
|
||||
if (lifeCount <= 0) {
|
||||
glow = 0.3;
|
||||
color = <1,0,0>; // Red
|
||||
txt = "🎲 Peril Dice\n👤 " + name + "\n🔢 Picks: " + picksDisplay + "\n💀 ELIMINATED! 💀";
|
||||
} else if (livingPlayers == 1 && llGetListLength(names) >= 2) {
|
||||
glow = 0.3;
|
||||
color = <0,1,0>; // Green
|
||||
txt = "🎲 Peril Dice\n👤 " + name + "\n✨ ULTIMATE VICTORY! ✨\n🏆 ULTIMATE SURVIVOR 🏆\n🔢 Final Picks: " + picksDisplay;
|
||||
} else if (name == perilPlayer) {
|
||||
glow = 0.2;
|
||||
color = <1,1,0>; // Yellow
|
||||
|
|
@ -311,12 +309,12 @@ default {
|
|||
}
|
||||
}
|
||||
else if (num == MSG_SYNC_GAME_STATE) {
|
||||
list parts = llParseStringKeepNulls(str, ["~"], []);
|
||||
list parts = llParseString2List(str, ["~"], []);
|
||||
if (llGetListLength(parts) < 4) return;
|
||||
|
||||
lives = llCSV2List(llList2String(parts, 0));
|
||||
string picksDataStr = llList2String(parts, 1);
|
||||
if (picksDataStr == "" || picksDataStr == "EMPTY" || picksDataStr == "~EMPTY~") {
|
||||
if (picksDataStr == "" || picksDataStr == "EMPTY") {
|
||||
picksData = [];
|
||||
} else {
|
||||
picksData = llParseString2List(picksDataStr, ["^"], []);
|
||||
|
|
|
|||
|
|
@ -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/Peril_Constants.lsl"
|
||||
#include "Peril_Constants.lsl"
|
||||
|
||||
list primLayerList; // Contains layer assignments for each face.
|
||||
|
||||
|
|
@ -616,7 +616,7 @@ default {
|
|||
|
||||
state_entry() {
|
||||
REPORT_MEMORY();
|
||||
dbg("FURWARE text is starting...");
|
||||
llOwnerSay("FURWARE text is starting...");
|
||||
|
||||
CHARS = llBase64ToString(
|
||||
"IGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFS" +
|
||||
|
|
@ -674,7 +674,7 @@ default {
|
|||
|
||||
// Abort if not a single element was found.
|
||||
if (!dataLength) {
|
||||
dbg("FW text: No text 2.x prims found.");
|
||||
llOwnerSay("FW text: No text 2.x prims found.");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -710,7 +710,7 @@ default {
|
|||
|
||||
if (faceCount) {
|
||||
if (newFaceCount != faceCount) {
|
||||
dbg("FW text: All prims within a set need to have the same number of faces.");
|
||||
llOwnerSay("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) {
|
||||
dbg("FW text: All rows within a set need to have the same number of prims.");
|
||||
llOwnerSay("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) {
|
||||
dbg(token0 + ": Box \"" + boxToken0 + "\" not found.");
|
||||
llOwnerSay(token0 + ": Box \"" + boxToken0 + "\" not found.");
|
||||
jump SkipBox;
|
||||
}
|
||||
|
||||
|
|
@ -806,7 +806,7 @@ default {
|
|||
last = llListFindList(boxNameList, [boxToken1]);
|
||||
|
||||
if (!~last) {
|
||||
dbg(token0 + ": Box \"" + boxToken1 + "\" not found.");
|
||||
llOwnerSay(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) {
|
||||
dbg(token0 + ": Box Sets must match when specifying a range.");
|
||||
llOwnerSay(token0 + ": Box sets must match when specifying a range.");
|
||||
jump SkipBox;
|
||||
}
|
||||
|
||||
|
|
@ -837,7 +837,7 @@ default {
|
|||
if (lastAction && lastAction != ACTION_CONTENT) refresh();
|
||||
|
||||
if (token1 == "") {
|
||||
dbg("fw_var: No variable name given.");
|
||||
llOwnerSay("fw_var: No variable name given.");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -888,7 +888,7 @@ default {
|
|||
|
||||
if (token0 == "fw_addbox") {
|
||||
if (token1 == "") {
|
||||
dbg("fw_addbox: Box name cannot be empty.");
|
||||
llOwnerSay("fw_addbox: Box name cannot be empty.");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -897,14 +897,14 @@ default {
|
|||
integer boxNameIndex = llListFindList(boxNameList, [token1]);
|
||||
|
||||
if (~boxNameIndex) {
|
||||
dbg("fw_addbox: Box \"" + token1 + "\" already exists.");
|
||||
llOwnerSay("fw_addbox: Box \"" + token1 + "\" already exists.");
|
||||
return;
|
||||
}
|
||||
|
||||
integer parNameIndex = llListFindList(boxNameList, [token2]);
|
||||
|
||||
if (!~parNameIndex) {
|
||||
dbg("fw_addbox: No parent box \"" + token2 + "\".");
|
||||
llOwnerSay("fw_addbox: No parent box \"" + token2 + "\".");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -922,7 +922,7 @@ default {
|
|||
}
|
||||
|
||||
if (layersUsed == 0xFFFF) {
|
||||
dbg("fw_addbox: No layer available.");
|
||||
llOwnerSay("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)
|
||||
{
|
||||
dbg("fw_addbox: Invalid box geometry.");
|
||||
llOwnerSay("fw_addbox: Invalid box geometry.");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -965,7 +965,7 @@ default {
|
|||
integer boxNameIndex = llListFindList(boxNameList, [boxName]);
|
||||
|
||||
if (!~boxNameIndex || (boxNameIndex < setCount)) {
|
||||
dbg("fw_delbox: Box \"" + boxName + "\" doesn't exist.");
|
||||
llOwnerSay("fw_delbox: Box \"" + boxName + "\" doesn't exist.");
|
||||
jump SkipDelBox;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 (CURRENT VERSION: 3.3.0):**
|
||||
### **Enhanced Display System (UPDATED in v3.2.6):**
|
||||
- **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.3.0**
|
||||
**Last Updated: April 09, 2026**
|
||||
**Status: Production Ready - Tournament Edition Release**
|
||||
**Version:** 3.2.6
|
||||
**Last Updated:** April 05, 2026
|
||||
**Status:** Production Ready - Hybrid HUD Release
|
||||
|
||||
**Credits:** Original concept by Noose the Bunny (djmusica28), Automated system implementation by Rebecca Nod
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
#include "peril/Peril_Constants.lsl"
|
||||
#include "Peril_Constants.lsl"
|
||||
|
||||
// === Game Calculator Module (Max 10 players) ===
|
||||
// Calculates dice types, pick requirements, and other game mechanics
|
||||
|
|
|
|||
|
|
@ -1,8 +1,4 @@
|
|||
#include "peril/Peril_Constants.lsl"
|
||||
|
||||
debugMsg(string msg) {
|
||||
if (DEBUG_LOGS) llMessageLinked(LINK_SET, MSG_DEBUG_TEXT, msg, NULL_KEY);
|
||||
}
|
||||
#include "Peril_Constants.lsl"
|
||||
|
||||
// =============================================================================
|
||||
// PERIL WORLD RANKING - Leaderboard Manager
|
||||
|
|
@ -15,9 +11,7 @@ list leaderboardData = []; // Stride 3: [Name, Wins, Losses]
|
|||
integer currentLoadPage = 1;
|
||||
key kvpReadReq;
|
||||
key kvpWriteReq;
|
||||
key kvpLockReq; // Stage 1: Check Lock
|
||||
key kvpClaimReq; // Stage 2: Claim Lock
|
||||
key kvpSentinelReq; // Startup Probe
|
||||
key kvpLockReq; // Request for claiming leaderboard lock
|
||||
list writeQueue = []; // Pairs of [KeyName, DataString]
|
||||
integer isSaving = FALSE;
|
||||
integer isBusy = FALSE; // Read lock
|
||||
|
|
@ -27,25 +21,18 @@ string currentWriteKey;
|
|||
string currentWriteVal;
|
||||
|
||||
// --- Lock & Sync State ---
|
||||
list pendingRecords = []; // Stride 2: [Name, Result]
|
||||
string pendingPlayer = "";
|
||||
string pendingResult = "";
|
||||
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;
|
||||
// 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();
|
||||
}
|
||||
dbg("🏆 [Leaderboard] Save sequence complete. Releasing lock...");
|
||||
// releaseLock();
|
||||
kvpLockReq = llUpdateKeyValue(LB_LOCK_KEY, "", TRUE, myLockVal);
|
||||
return;
|
||||
}
|
||||
isSaving = TRUE;
|
||||
|
|
@ -55,12 +42,6 @@ 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;
|
||||
|
|
@ -95,30 +76,20 @@ generateShardedData() {
|
|||
}
|
||||
|
||||
loadLBPage(integer pageNum) {
|
||||
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
|
||||
if (isBusy && pageNum == 1) return; // Prevent double loads on reset
|
||||
if (pageNum > 1) llSleep(1.0); // Anti-Throttle Delay
|
||||
|
||||
isBusy = TRUE;
|
||||
llSetTimerEvent(30.0); // Lag Resistant watchdog
|
||||
llSetTimerEvent(30.0); // 30 second watchdog (Lag Resistant)
|
||||
currentLoadPage = pageNum;
|
||||
string keyName = LB_KEY_PREFIX + (string)pageNum;
|
||||
|
||||
kvpReadReq = llReadKeyValue(keyName);
|
||||
|
||||
if (kvpReadReq == NULL_KEY) {
|
||||
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] KVP Read FAILED: Not authorized for " + keyName);
|
||||
llSetTimerEvent(0.0);
|
||||
} else {
|
||||
dbg("📡 [Leaderboard] Loading " + keyName + "...");
|
||||
}
|
||||
}
|
||||
|
||||
list getSortedSlice(integer offset) {
|
||||
|
|
@ -147,14 +118,14 @@ updatePlayerRecord(string pName, string pResult) {
|
|||
if (pResult == "WIN") leaderboardData += [1, 0, pName];
|
||||
else leaderboardData += [0, 1, pName];
|
||||
} else {
|
||||
integer _updBase = _idx - 2;
|
||||
integer _updVal;
|
||||
integer _base = _idx - 2;
|
||||
integer _v;
|
||||
if (pResult == "WIN") {
|
||||
_updVal = llList2Integer(leaderboardData, _updBase);
|
||||
leaderboardData = llListReplaceList(leaderboardData, [_updVal + 1], _updBase, _updBase);
|
||||
_v = llList2Integer(leaderboardData, _base);
|
||||
leaderboardData = llListReplaceList(leaderboardData, [_v + 1], _base, _base);
|
||||
} else {
|
||||
_updVal = llList2Integer(leaderboardData, _updBase + 1);
|
||||
leaderboardData = llListReplaceList(leaderboardData, [_updVal + 1], _updBase + 1, _updBase + 1);
|
||||
_v = llList2Integer(leaderboardData, _base + 1);
|
||||
leaderboardData = llListReplaceList(leaderboardData, [_v + 1], _base+1, _base+1);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -164,68 +135,35 @@ updatePlayerRecord(string pName, string pResult) {
|
|||
}
|
||||
|
||||
startUpdateSequence(string pName, string pResult) {
|
||||
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;
|
||||
if (isLocking || isSaving) {
|
||||
dbg("⚠️ [Leaderboard] Update already in progress. Retrying in 2s...");
|
||||
llSleep(2.0);
|
||||
}
|
||||
|
||||
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();
|
||||
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
|
||||
dbg("⏳ [Leaderboard] Startup: Handshake link active.");
|
||||
}
|
||||
|
||||
dataserver(key _qid, string _qd) {
|
||||
if (_qid == kvpReadReq) {
|
||||
llSetTimerEvent(0.0); // Reset watchdog
|
||||
integer _c = llSubStringIndex(_qd, ",");
|
||||
string _st = _qd; string _vl = ""; string _rawVal = "";
|
||||
string _st = _qd; string _vl = "";
|
||||
if (_c != -1) {
|
||||
_st = llGetSubString(_qd, 0, _c - 1);
|
||||
_rawVal = llGetSubString(_qd, _c + 1, -1);
|
||||
_vl = llStringTrim(_rawVal, STRING_TRIM);
|
||||
_vl = llGetSubString(_qd, _c + 1, -1);
|
||||
}
|
||||
|
||||
if (_qid == kvpSentinelReq) {
|
||||
llSetTimerEvent(0.0); // Reset watchdog
|
||||
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) {
|
||||
list _ent = llParseString2List(_vl, ["|"], []);
|
||||
|
|
@ -234,84 +172,50 @@ default {
|
|||
string _rawEntry = llList2String(_ent, _ei);
|
||||
if (llSubStringIndex(_rawEntry, ",") != -1) {
|
||||
// NEW FORMAT: Wins,Losses,Name
|
||||
list _itNew = llParseString2List(_rawEntry, [","], []);
|
||||
if (llGetListLength(_itNew) >= 3) {
|
||||
leaderboardData += [(integer)llList2String(_itNew, 0), (integer)llList2String(_itNew, 1), llList2String(_itNew, 2)];
|
||||
list _it = llParseString2List(_rawEntry, [","], []);
|
||||
if (llGetListLength(_it) >= 3) {
|
||||
leaderboardData += [(integer)llList2String(_it, 0), (integer)llList2String(_it, 1), llList2String(_it, 2)];
|
||||
}
|
||||
} else {
|
||||
// OLD LEGACY FORMAT: Name:Wins:Losses
|
||||
list _itOld = llParseString2List(_rawEntry, [":"], []);
|
||||
if (llGetListLength(_itOld) >= 3) {
|
||||
list _it = llParseString2List(_rawEntry, [":"], []);
|
||||
if (llGetListLength(_it) >= 3) {
|
||||
// Translate to new stride structure in memory
|
||||
leaderboardData += [(integer)llList2String(_itOld, 1), (integer)llList2String(_itOld, 2), llList2String(_itOld, 0)];
|
||||
leaderboardData += [(integer)llList2String(_it, 1), (integer)llList2String(_it, 2), llList2String(_it, 0)];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isBusy && currentLoadPage < 10) {
|
||||
if (currentLoadPage < 10) {
|
||||
retryCount = 0; // Reset retries for next page
|
||||
loadLBPage(currentLoadPage + 1);
|
||||
} else {
|
||||
// SYNC COMPLETE (Natural end)
|
||||
isBusy = FALSE;
|
||||
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
|
||||
|
||||
// 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;
|
||||
// IF WE WERE SYNCING FOR AN UPDATE, APPLY IT NOW
|
||||
if (isLocking) {
|
||||
isLocking = FALSE;
|
||||
updatePlayerRecord(pendingPlayer, pendingResult);
|
||||
}
|
||||
}
|
||||
} else if (_st == "0") {
|
||||
if (_vl == "1") { // XP_ERROR_THROTTLED
|
||||
} else if (_st == "0" && _vl == "1") {
|
||||
// XP_ERROR_THROTTLED
|
||||
dbg("⚠️ [Leaderboard] Read throttled. Retrying Shard " + (string)currentLoadPage + " in 2s...");
|
||||
llSleep(2.0);
|
||||
// RETAIN isBusy/isSaving status during retry
|
||||
isBusy = FALSE;
|
||||
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)
|
||||
} 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);
|
||||
|
||||
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) {
|
||||
|
|
@ -322,92 +226,64 @@ default {
|
|||
_vl = llGetSubString(_qd, _c + 1, -1);
|
||||
}
|
||||
|
||||
if (_st == "1") {
|
||||
dbg("✅ [Leaderboard] Write success to " + currentWriteKey);
|
||||
processWriteQueue();
|
||||
} else if (_st == "0" && _vl == "1") {
|
||||
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 {
|
||||
dbg("❌ [Leaderboard] Write Error (" + _st + ":" + _vl + ") on " + currentWriteKey);
|
||||
if (_st == "0") { dbg("❌ [Leaderboard] Write Error (" + _vl + ") on " + currentWriteKey); }
|
||||
processWriteQueue();
|
||||
}
|
||||
}
|
||||
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) {
|
||||
if (_st == "1" && _c != -1) {
|
||||
string _curVal = _rawVal; // USE RAW DATA FOR ATOMIC CHECK
|
||||
integer _isStale = FALSE;
|
||||
list _p = [];
|
||||
integer _ts = 0;
|
||||
integer _c = llSubStringIndex(_qd, ",");
|
||||
string _st = _qd; string _vl = "";
|
||||
if (_c != -1) {
|
||||
_st = llGetSubString(_qd, 0, _c - 1);
|
||||
_vl = llGetSubString(_qd, _c + 1, -1);
|
||||
}
|
||||
|
||||
if (isLocking) {
|
||||
// STAGE 1: Current Lock Read Result
|
||||
if (_st == "1") { // Success read
|
||||
string _curVal = _vl;
|
||||
integer _isStale = FALSE;
|
||||
if (_curVal != "") {
|
||||
_p = llParseString2List(_curVal, [":"], []);
|
||||
_ts = (integer)llList2String(_p, 1);
|
||||
list _p = llParseString2List(_curVal, [":"], []);
|
||||
integer _ts = (integer)llList2String(_p, 1);
|
||||
if (llGetUnixTime() - _ts > LB_LOCK_TIMEOUT) _isStale = TRUE;
|
||||
}
|
||||
|
||||
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...");
|
||||
if (_curVal == "" || _isStale) {
|
||||
dbg("🔐 [Leaderboard] Lock available. Claiming...");
|
||||
myLockVal = (string)llGetKey() + ":" + (string)llGetUnixTime();
|
||||
isClaiming = TRUE;
|
||||
kvpClaimReq = llUpdateKeyValue(LB_LOCK_KEY, myLockVal, TRUE, _curVal);
|
||||
// ATOMIC UPDATE: Only write if value is still what we just read
|
||||
kvpLockReq = llUpdateKeyValue(LB_LOCK_KEY, myLockVal, TRUE, _curVal);
|
||||
} else {
|
||||
dbg("⏳ [Leaderboard] Lock held by board [" + llList2String(_p, 0) + "] for " + (string)(llGetUnixTime() - _ts) + "s. Retrying in 5s...");
|
||||
dbg("⏳ [Leaderboard] Lock held by another board. 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 logic failed (Code " + _st + ":" + _vl + ")");
|
||||
dbg("❌ [Leaderboard] Lock Error: (" + _st + ") " + _vl);
|
||||
isLocking = FALSE;
|
||||
}
|
||||
} else {
|
||||
// RELEASE RESULT: Check for Status 1 (Success)
|
||||
if (_st == "1") { dbg("🔓 [Leaderboard] Lock released."); }
|
||||
else { dbg("⚠️ [Leaderboard] Lock release report: " + _st + ":" + _vl); }
|
||||
// RELEASE RESULT
|
||||
if (_st == "14") { dbg("🔓 [Leaderboard] Lock released."); }
|
||||
else { dbg("⚠️ [Leaderboard] Lock release issues: " + _vl); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -419,16 +295,16 @@ default {
|
|||
return;
|
||||
}
|
||||
|
||||
if (isBusy) { // DO NOT check isSaving here, as isBusy should be the authoritative lock for READS
|
||||
if (isBusy) {
|
||||
if (retryCount < 3) {
|
||||
retryCount++;
|
||||
dbg("⏳ [Leaderboard] Retrying Shard " + (string)currentLoadPage + " (" + (string)retryCount + "/3)...");
|
||||
dbg("⚠️ [Leaderboard] Shard " + (string)currentLoadPage + " timed out. Retrying (" + (string)retryCount + "/3)...");
|
||||
isBusy = FALSE; // Unlock so loadLBPage can run
|
||||
loadLBPage(currentLoadPage);
|
||||
} else {
|
||||
isBusy = FALSE;
|
||||
llSetTimerEvent(0);
|
||||
dbg("🏆 [Leaderboard] SYNC GAVE UP (Persistent issues on Shard " + (string)currentLoadPage + ")");
|
||||
dbg("🏆 [Leaderboard] SYNC GAVE UP (Persistent timeout on Shard " + (string)currentLoadPage + ")");
|
||||
llMessageLinked(LINK_SET, MSG_LB_REQUEST_DISPLAY, "0", NULL_KEY);
|
||||
}
|
||||
} else {
|
||||
|
|
@ -438,9 +314,7 @@ default {
|
|||
|
||||
link_message(integer sender, integer num, string str, key id) {
|
||||
if (num == MSG_LB_RECORD_PLAYER) {
|
||||
// str is "Name|Result" (e.g. "Bot1|WIN")
|
||||
list _p = llParseString2List(str, ["|"], []);
|
||||
startUpdateSequence(llList2String(_p, 0), llList2String(_p, 1));
|
||||
startUpdateSequence(str, (string)id);
|
||||
}
|
||||
else if (num == MSG_DISPLAY_LEADERBOARD) {
|
||||
if (str == "REFRESH_STARTUP") loadLBPage(1);
|
||||
|
|
@ -469,7 +343,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 + "|" + (string)_offset, NULL_KEY);
|
||||
llMessageLinked(LINK_SET, MSG_DISPLAY_LEADERBOARD, "COLUMNS|" + _rankCol + "|" + _nameCol + "|" + _statsCol + "|" + _p + "|" + _n, NULL_KEY);
|
||||
}
|
||||
else if (num == MSG_LB_PAGE_NEXT) {
|
||||
integer _maxEntries = llGetListLength(leaderboardData) / 3;
|
||||
|
|
@ -493,24 +367,10 @@ 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) {
|
||||
if (str == "HARD_RESET" || str == "ADMIN_RESET") llResetScript();
|
||||
}
|
||||
else if (num == MSG_RESET_ALL) { llResetScript(); }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
166
Game_Manager.lsl
166
Game_Manager.lsl
|
|
@ -1,4 +1,4 @@
|
|||
#include "peril/Peril_Constants.lsl"
|
||||
#include "Peril_Constants.lsl"
|
||||
|
||||
// === Game Manager - Game Flow Logic ===
|
||||
|
||||
|
|
@ -27,7 +27,6 @@ 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
|
||||
|
|
@ -150,28 +149,24 @@ integer continueCurrentRound() {
|
|||
}
|
||||
currentPickerIdx = 0;
|
||||
|
||||
dbg("🎯 [Game Manager] Pick queue created: " + llList2CSV(pickQueue) + ", checking dice memory...");
|
||||
dbg("🎯 [Game Manager] Pick queue created: " + llList2CSV(pickQueue) + ", requesting dice type");
|
||||
|
||||
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);
|
||||
}
|
||||
llMessageLinked(LINK_SET, MSG_GET_DICE_TYPE, (string)llGetListLength(names), NULL_KEY);
|
||||
|
||||
// Note: showNextPickerDialog() will be called when dice type result is received (or immediately if cached)
|
||||
// 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
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -254,25 +249,16 @@ integer startNextRound() {
|
|||
currentPickerIdx = 0;
|
||||
dbg("🎯 Debug - Created pickQueue: " + llList2CSV(pickQueue) + ", currentPickerIdx: " + (string)currentPickerIdx);
|
||||
|
||||
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);
|
||||
}
|
||||
llMessageLinked(LINK_SET, MSG_GET_DICE_TYPE, (string)llGetListLength(names), NULL_KEY);
|
||||
|
||||
dbg("🎯 Game Manager round setup complete.");
|
||||
// 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...");
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -498,7 +484,7 @@ default {
|
|||
// Only skip sync messages if we're explicitly ignoring them
|
||||
// Allow player registration syncs even when game is not active
|
||||
|
||||
list parts = llParseStringKeepNulls(str, ["~"], []);
|
||||
list parts = llParseString2List(str, ["~"], []);
|
||||
if (llGetListLength(parts) >= 4) {
|
||||
list newLives = llCSV2List(llList2String(parts, 0));
|
||||
string currentLivesStr = llList2CSV(lives);
|
||||
|
|
@ -523,7 +509,7 @@ default {
|
|||
if (livesChanged || perilChanged) {
|
||||
string encodedPicksDataStr = llList2String(parts, 1);
|
||||
list newPicksData = [];
|
||||
if (encodedPicksDataStr != "" && encodedPicksDataStr != "~EMPTY~") {
|
||||
if (encodedPicksDataStr != "") {
|
||||
list encodedEntries = llParseString2List(encodedPicksDataStr, ["^"], []);
|
||||
integer syncI;
|
||||
for (syncI = 0; syncI < llGetListLength(encodedEntries); syncI++) {
|
||||
|
|
@ -565,8 +551,10 @@ default {
|
|||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
if (livesChanged && newPerilPlayer != "" && newPerilPlayer != "NONE" && roundStarted && allPicksEmpty) {
|
||||
dbg("🎯 Post-roll state update detected - continuing round");
|
||||
continueCurrentRound();
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
|
|
@ -790,7 +778,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;
|
||||
|
|
@ -851,7 +839,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);
|
||||
}
|
||||
}
|
||||
|
|
@ -910,7 +898,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
|
||||
|
|
@ -1020,7 +1008,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
|
||||
|
|
@ -1077,7 +1065,6 @@ 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!");
|
||||
|
|
@ -1097,90 +1084,5 @@ 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
#include "peril/Peril_Constants.lsl"
|
||||
#include "Peril_Constants.lsl"
|
||||
|
||||
// =============================================================================
|
||||
// PERIL SCOREBOARD - GRID DISPLAY MANAGER (v3.2.7 Final)
|
||||
|
|
@ -107,10 +107,9 @@ clearAllPlayers() {
|
|||
integer _hL = getHeartsPrimLink(_ca_i);
|
||||
integer _oL = getOverlayPrimLink(_ca_i);
|
||||
if (_pL > 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]);
|
||||
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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -233,15 +232,13 @@ updatePlayerDisplay(string pName, integer pLives, string pUUID) {
|
|||
}
|
||||
|
||||
default {
|
||||
on_rez(integer _sp) { discoverLinks(); clearAllPlayers(); llResetScript(); }
|
||||
on_rez(integer _sp) { discoverLinks(); llResetScript(); }
|
||||
state_entry() {
|
||||
discoverLinks();
|
||||
llListen(1, "", llGetOwner(), "");
|
||||
REPORT_MEMORY();
|
||||
clearAllPlayers(); // Ensure board is clean on initialization
|
||||
resetBackgroundPrim();
|
||||
resetManagerCube();
|
||||
updateActionsPrim("Title");
|
||||
updatePlayerGlowEffects();
|
||||
dbg("Ready");
|
||||
}
|
||||
|
|
@ -259,10 +256,8 @@ default {
|
|||
currentPerilPlayer = ""; currentWinner = ""; updatePlayerGlowEffects();
|
||||
llMessageLinked(LINK_SET, MSG_LB_REQUEST_DISPLAY, "", 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_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_LB_DISPLAY_DATA) {
|
||||
string _tl = "WORLD RANKING RECORD"; string _sp = " ";
|
||||
integer _m = (LEADERBOARD_WIDTH - llStringLength(_tl)) / 2;
|
||||
|
|
@ -312,9 +307,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, ALL_SIDES, 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, 0.01]);
|
||||
llSleep(DELAY_SCOREBOARD_REFRESH);
|
||||
llSetLinkPrimitiveParamsFast(_pL, [PRIM_GLOW, ALL_SIDES, 0.0]);
|
||||
llSetLinkPrimitiveParamsFast(_pL, [PRIM_GLOW, 0.0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
154
GitHub_Updater_System.md
Normal file
154
GitHub_Updater_System.md
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
# 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?
|
||||
|
|
@ -63,7 +63,6 @@ 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
|
||||
|
|
|
|||
60
MCP_README.md
Normal file
60
MCP_README.md
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
# 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.
|
||||
31
MEMORY_REPORT_TEMPLATE.lsl
Normal file
31
MEMORY_REPORT_TEMPLATE.lsl
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
// === 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 ...
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
#include "peril/Peril_Constants.lsl"
|
||||
#include "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;
|
||||
|
||||
dbg("🧠 [" + scriptName + "] Memory: " +
|
||||
llOwnerSay("🧠 [" + scriptName + "] Memory: " +
|
||||
(string)used + " used, " +
|
||||
(string)free + " free (" +
|
||||
llGetSubString((string)percentUsed, 0, 4) + "% used)");
|
||||
|
|
@ -68,7 +68,6 @@ integer victoryInProgress = FALSE;
|
|||
|
||||
// Lockout system variables
|
||||
integer isLocked = FALSE;
|
||||
integer resetInProgress = FALSE; // Lockout flag for reset synchronization
|
||||
key gameOwner;
|
||||
|
||||
list pickQueue = [];
|
||||
|
|
@ -180,11 +179,25 @@ checkMemoryUsage(string context) {
|
|||
// REMOVED: emergencyMemoryCleanup() - delegated to Controller_Memory.lsl
|
||||
// REMOVED: reportMemoryStats() - delegated to Controller_Memory.lsl
|
||||
|
||||
// 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
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
// Send status message to scoreboard using link messages
|
||||
sendStatusMessage(string status) {
|
||||
|
|
@ -214,17 +227,13 @@ 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) + "~" +
|
||||
picksStr + "~" +
|
||||
(string)llDumpList2String(picksData, "^") + "~" +
|
||||
(string)llList2String([perilPlayer, "NONE"], (perilPlayer == "")) + "~" +
|
||||
llList2CSV(names);
|
||||
|
||||
llMessageLinked(LINK_SET, MSG_SYNC_GAME_STATE, syncMsg, NULL_KEY);
|
||||
dbg("🌍 [Main Controller] 📡 Global State Sync broadcasted (Picks: " + picksStr + ")");
|
||||
dbg("🌍 [Main Controller] 📡 Global State Sync broadcasted.");
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -279,13 +288,14 @@ 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);
|
||||
// Reduced delay for snappy restart
|
||||
llSleep(0.1);
|
||||
llSleep(DELAY_STATE_TRANSITION);
|
||||
|
||||
llSleep(DELAY_STATE_TRANSITION);
|
||||
|
||||
players = names = lives = picksData = globalPickedNumbers = readyPlayers = [];
|
||||
floaterChannels = [];
|
||||
|
|
@ -307,13 +317,6 @@ 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;
|
||||
|
|
@ -325,12 +328,15 @@ resetGame() {
|
|||
// NEW: Update status bar for idle state
|
||||
sendStatusMessage("PERIL DICE GAME\nTouch to Join!");
|
||||
|
||||
// Quick buffer for Linkset to settle
|
||||
llSleep(0.2);
|
||||
llSleep(DELAY_STATE_TRANSITION);
|
||||
llSetTimerEvent(0);
|
||||
|
||||
initListeners();
|
||||
resetInProgress = FALSE; // End lockout
|
||||
|
||||
// SAFETY: Only call updateHelpers if we have actual game data to prevent startup issues
|
||||
if (llGetListLength(names) > 0) {
|
||||
updateHelpers();
|
||||
}
|
||||
}
|
||||
|
||||
default {
|
||||
|
|
@ -349,7 +355,9 @@ default {
|
|||
// Reset the game on startup to ensure clean state
|
||||
resetGame();
|
||||
|
||||
// MOVED: checkExperience() - already called by Registration Management
|
||||
// Experience Sentinel check - Moved after reset to protect timer
|
||||
llSleep(2.0);
|
||||
checkExperience();
|
||||
|
||||
dbg("✅ Main Controller initialization complete!");
|
||||
}
|
||||
|
|
@ -369,18 +377,159 @@ default {
|
|||
// Reset the game to ensure clean state when rezzed
|
||||
resetGame();
|
||||
|
||||
// MOVED: checkExperience()
|
||||
// Experience Sentinel check - Moved after reset to protect timer
|
||||
checkExperience();
|
||||
|
||||
dbg("✅ Main Controller reset complete after rez!");
|
||||
}
|
||||
|
||||
touch_start(integer total_number) {
|
||||
integer clickedLink = llDetectedLinkNumber(0);
|
||||
string clickedName = llGetLinkName(clickedLink);
|
||||
key toucher = llDetectedKey(0);
|
||||
// IGNORE TOUCHES INTENDED FOR THE LEADERBOARD
|
||||
string _rawNm = llGetLinkName(llDetectedLinkNumber(0));
|
||||
string _linkNm = llToLower(_rawNm);
|
||||
|
||||
// DELIVER: Forward touch event to Dialog Handler to save bytecode in Main
|
||||
llMessageLinked(LINK_SET, MSG_TOUCH_EVENT, (string)clickedLink + "|" + clickedName, toucher);
|
||||
// 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;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
link_message(integer sender, integer num, string str, key id) {
|
||||
|
|
@ -395,11 +544,8 @@ 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;
|
||||
|
|
@ -448,19 +594,97 @@ default {
|
|||
}
|
||||
|
||||
// Handle elimination requests
|
||||
// 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;
|
||||
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) {
|
||||
// Remove from indexed lists
|
||||
players = llDeleteSubList(players, idx, idx);
|
||||
names = llDeleteSubList(names, idx, idx);
|
||||
lives = llDeleteSubList(lives, idx, idx);
|
||||
floaterChannels = llDeleteSubList(floaterChannels, idx, idx);
|
||||
// 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!");
|
||||
}
|
||||
|
||||
// Remove from picksData
|
||||
// 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++) {
|
||||
|
|
@ -475,8 +699,110 @@ default {
|
|||
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();
|
||||
dbg("🎯 [Main Controller] Deep Cleanup Complete: " + eliminatedPlayer + " purged from state.");
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
|
@ -517,18 +843,6 @@ 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, ["|"], []);
|
||||
|
|
@ -570,35 +884,37 @@ default {
|
|||
|
||||
// Handle ready state queries
|
||||
if (num == MSG_QUERY_READY_STATE) {
|
||||
list qParts = llParseString2List(str, ["|"], []);
|
||||
string qPlayerName;
|
||||
integer qRequestID = 0;
|
||||
list queryParts = llParseString2List(str, ["|"], []);
|
||||
string playerName;
|
||||
integer requestID = 0;
|
||||
|
||||
if (llGetListLength(qParts) >= 2) {
|
||||
qPlayerName = llList2String(qParts, 0);
|
||||
qRequestID = (integer)llList2String(qParts, 1);
|
||||
if (llGetListLength(queryParts) >= 2) {
|
||||
playerName = llList2String(queryParts, 0);
|
||||
requestID = (integer)llList2String(queryParts, 1);
|
||||
} else {
|
||||
qPlayerName = str;
|
||||
playerName = str;
|
||||
}
|
||||
|
||||
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);
|
||||
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);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle owner status queries
|
||||
if (num == MSG_QUERY_OWNER_STATUS) {
|
||||
list osParts = llParseString2List(str, ["|"], []);
|
||||
string osName;
|
||||
integer osRequestID = 0;
|
||||
list queryParts = llParseString2List(str, ["|"], []);
|
||||
string ownerName;
|
||||
integer requestID = 0;
|
||||
|
||||
if (llGetListLength(osParts) >= 2) {
|
||||
osName = llList2String(osParts, 0);
|
||||
osRequestID = (integer)llList2String(osParts, 1);
|
||||
// Owner status check (Now fully automated via on_rez / reset)
|
||||
|
||||
if (llGetListLength(queryParts) >= 2) {
|
||||
ownerName = llList2String(queryParts, 0);
|
||||
requestID = (integer)llList2String(queryParts, 1);
|
||||
} else {
|
||||
osName = str;
|
||||
ownerName = str;
|
||||
}
|
||||
|
||||
// Check if owner is registered
|
||||
|
|
@ -622,7 +938,7 @@ default {
|
|||
}
|
||||
}
|
||||
|
||||
string result = osName + "|" + (string)isRegistered + "|" + (string)isPending + "|" + (string)isStarter + "|" + (string)osRequestID;
|
||||
string result = ownerName + "|" + (string)isRegistered + "|" + (string)isPending + "|" + (string)isStarter + "|" + (string)requestID;
|
||||
llMessageLinked(LINK_SET, MSG_OWNER_STATUS_RESULT, result, id);
|
||||
return;
|
||||
}
|
||||
|
|
@ -640,8 +956,8 @@ default {
|
|||
// 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);
|
||||
// CHANGED: Remove player from scoreboard BEFORE updating lists
|
||||
llMessageLinked(LINK_SCOREBOARD, MSG_REMOVE_PLAYER, leavingName, NULL_KEY);
|
||||
|
||||
// Only announce if it's not the end of the game
|
||||
if (llGetListLength(names) > 1) {
|
||||
|
|
@ -811,30 +1127,15 @@ default {
|
|||
return;
|
||||
}
|
||||
if (msg == "Reset Leaderboard") {
|
||||
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.");
|
||||
// CHANGED: Send to scoreboard, which will handle leaderboard reset
|
||||
llMessageLinked(LINK_SCOREBOARD, MSG_RESET_LEADERBOARD, "", NULL_KEY);
|
||||
dbg("🏆 Leaderboard scores reset - game wins cleared!");
|
||||
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_SET, MSG_RESET_LEADERBOARD, "WIPE", id); // Use LINK_SET
|
||||
dbg("🔄 FULL RESET confirmed and executed.");
|
||||
llMessageLinked(LINK_SCOREBOARD, MSG_RESET_LEADERBOARD, "", NULL_KEY);
|
||||
dbg("🔄 Complete reset - game and leaderboard cleared!");
|
||||
return;
|
||||
}
|
||||
if (msg == "Memory Stats") {
|
||||
|
|
@ -952,8 +1253,8 @@ default {
|
|||
|
||||
llSay(0, "⚡ ALL PARTICIPANTS READY! THE DEADLY PERIL DICE GAME BEGINS! ⚡");
|
||||
|
||||
integer currentLiveCount = llGetListLength(names);
|
||||
llSetText("🎮 GAME IN PROGRESS\n" + (string)currentLiveCount + " players", <1.0, 0.2, 0.2>, 1.0);
|
||||
integer actualPlayerCount = llGetListLength(names);
|
||||
llSetText("🎮 GAME IN PROGRESS\n" + (string)actualPlayerCount + " 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!");
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
#include "peril/Peril_Constants.lsl"
|
||||
#include "Peril_Constants.lsl"
|
||||
|
||||
// === Dialog Handler (Updated for Peril Dice - Paginated Multiple Picks) ===
|
||||
|
||||
|
|
|
|||
|
|
@ -43,8 +43,6 @@ 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
|
||||
|
|
@ -108,9 +106,6 @@ 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
|
||||
|
|
@ -143,14 +138,15 @@ 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 0 // Toggle this to 1 to see memory usage on rez/reset
|
||||
#define SHOW_MEMORY 1 // Toggle this to 1 to see memory usage on rez/reset
|
||||
|
||||
#define REPORT_MEMORY() { \
|
||||
if (SHOW_MEMORY) { \
|
||||
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)."); \
|
||||
float percent = ((float)used / (float)(used + free)) * 100.0; \
|
||||
llOwnerSay("🧠 [" + llGetScriptName() + "] Memory: " + (string)used + " used, " + (string)free + " free (" + llGetSubString((string)percent, 0, 4) + "% used)."); \
|
||||
} \
|
||||
}
|
||||
|
||||
#define MSG_MEMORY_CHECK 6001
|
||||
|
|
@ -194,12 +190,10 @@ 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"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
#include "peril/Peril_Constants.lsl"
|
||||
#include "Peril_Constants.lsl"
|
||||
|
||||
// === Player Status Float - Enhanced Display ===
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
#include "peril/Peril_Constants.lsl"
|
||||
#include "Peril_Constants.lsl"
|
||||
|
||||
// === Dialog Handler (Owner & Player) with unified Ready/Leave menu and join support ===
|
||||
|
||||
|
|
@ -88,11 +88,6 @@ 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
|
||||
|
|
@ -409,63 +404,6 @@ 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, ["|"], []);
|
||||
|
|
@ -476,33 +414,44 @@ 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;
|
||||
}
|
||||
|
|
@ -516,16 +465,29 @@ 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);
|
||||
if (name != "" && llSubStringIndex(name, "REQUEST_") != 0 && llSubStringIndex(name, "COMMAND_") != 0 && llSubStringIndex(name, "MSG_") != 0) {
|
||||
// Skip empty names and command strings
|
||||
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) {
|
||||
|
|
@ -533,8 +495,11 @@ 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) {
|
||||
|
|
@ -545,6 +510,7 @@ 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);
|
||||
|
|
@ -552,23 +518,45 @@ 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 {
|
||||
showUnregisteredOwnerMenu(id, (llGetListLength(players) >= 2));
|
||||
// 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
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
#include "peril/Peril_Constants.lsl"
|
||||
#include "Peril_Constants.lsl"
|
||||
|
||||
// === Player Registration Manager ===
|
||||
// Handles all player registration, join/leave operations to reduce Main Controller memory usage
|
||||
|
|
@ -17,14 +17,6 @@ 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);
|
||||
|
|
@ -62,31 +54,6 @@ 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
|
||||
|
|
@ -184,12 +151,9 @@ 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);
|
||||
|
|
@ -199,7 +163,6 @@ default {
|
|||
state_entry() {
|
||||
DISCOVER_CORE_LINKS();
|
||||
initializeChannels();
|
||||
checkExperience();
|
||||
REPORT_MEMORY();
|
||||
dbg("🎯 [RegMgr] Player Registration Manager ready - discovery complete! Scoreboard: " + (string)LINK_SCOREBOARD);
|
||||
}
|
||||
|
|
@ -207,7 +170,6 @@ default {
|
|||
on_rez(integer start_param) {
|
||||
DISCOVER_CORE_LINKS();
|
||||
initializeChannels();
|
||||
checkExperience();
|
||||
REPORT_MEMORY();
|
||||
dbg("🎯 [RegMgr] Player Registration Manager rezzed - resetting state...");
|
||||
// Reset local state
|
||||
|
|
@ -228,162 +190,77 @@ 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
|
||||
list parts = llParseStringKeepNulls(str, ["~"], []);
|
||||
// We only sync lives and names, but keep our real player keys
|
||||
list parts = llParseString2List(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, ["~"], []); // Fixed delimiter to match forwarder
|
||||
list parts = llParseString2List(str, ["|"], []);
|
||||
if (llGetListLength(parts) >= 3) {
|
||||
string dialogType = llList2String(parts, 0);
|
||||
string dialogType = llList2String(parts, 0); // "SHOW_DIALOG" or "SHOW_ROLL_DIALOG"
|
||||
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 += "|";
|
||||
if (i > 2) dialogPayload += "|"; // Add separator between parts
|
||||
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") {
|
||||
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)");
|
||||
players = [];
|
||||
names = [];
|
||||
lives = [];
|
||||
readyPlayers = [];
|
||||
roundStarted = FALSE;
|
||||
gameStarting = FALSE;
|
||||
dbg("🎯 [RegMgr] Player Registration Manager reset complete");
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
95
README.md
95
README.md
|
|
@ -1,15 +1,13 @@
|
|||
# 🎲 Peril Dice — Professional Single Linkset Game System for Second Life
|
||||
|
||||
**Created by Rebecca Nod and Noose the Bunny**
|
||||
**Current Version: 3.3.0 - The "Tournament Edition" Update**
|
||||
**Current Version: 3.2.6 - The "Hybrid HUD" 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.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.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.
|
||||
|
||||
|
|
@ -17,7 +15,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.
|
||||
|
||||
|
|
@ -33,14 +31,7 @@ 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.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 🎯
|
||||
## Major v3.2.6 Improvements (Current) 🎯
|
||||
|
||||
### 🎰 **Hybrid HUD Evolution**
|
||||
- **Personalized Gaming UI**: Human players automatically receive a temporary, Experience-authorized HUD (Final Girlz I.N.C.) snapping to **Center 2**.
|
||||
|
|
@ -106,16 +97,67 @@ 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.
|
||||
|
||||
## System Architecture (v3.3.0) 🏗️
|
||||
## Major v2.8.7 Improvements 🔄
|
||||
|
||||
Peril Dice is now a unified single-linkset system utilizing modern LSL pre-processor techniques and mesh text displays.
|
||||
### 🔄 **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
|
||||
|
||||
### **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.
|
||||
### 🔍 **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 |
|
||||
|
||||
## Major v2.8.6 Improvements 🏆
|
||||
|
||||
|
|
@ -607,14 +649,9 @@ Dice type is automatically chosen to ensure at least 3 picks per player:
|
|||
|
||||
## Version
|
||||
|
||||
**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.
|
||||
**Current Version**: 3.2.0
|
||||
**Last Updated**: April 05, 2026
|
||||
**Status**: Production Ready - Sync Sanctity Release
|
||||
|
||||
### Key Achievements in v3.2.0:
|
||||
- ✅ **Sync Sanctity Protocol**: 100% unified life counting via the Main Controller.
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
#include "peril/Peril_Constants.lsl"
|
||||
#include "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" || picksDataStr == "~EMPTY~") {
|
||||
if (picksDataStr == "" || picksDataStr == "EMPTY") {
|
||||
picksData = [];
|
||||
dbg("🎲 [Roll Module] 🔍 [Roll Module] Set picksData to empty");
|
||||
} else {
|
||||
|
|
@ -270,18 +270,9 @@ default {
|
|||
llSleep(DELAY_CONFETTI_START);
|
||||
|
||||
rollInProgress = TRUE; // Set roll in progress for bot
|
||||
|
||||
// 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;
|
||||
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);
|
||||
}
|
||||
} else {
|
||||
// Reset any previous roll state before showing dialog
|
||||
rollInProgress = FALSE;
|
||||
|
|
@ -394,12 +385,11 @@ 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);
|
||||
|
||||
// Add delay to let status display before next phase
|
||||
llSleep(DELAY_LONG_SYNC + DELAY_MEDIUM_SYNC);
|
||||
// IMPORTANT: Send peril player update to scoreboard for glow effect
|
||||
llMessageLinked(LINK_SCOREBOARD, MSG_UPDATE_PERIL_PLAYER, newPeril, NULL_KEY); // MSG_UPDATE_PERIL_PLAYER
|
||||
|
||||
// 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);
|
||||
// Add delay to let status display before next phase
|
||||
llSleep(DELAY_LONG_SYNC);
|
||||
|
||||
// Update floaters immediately to show correct peril player before sync
|
||||
llMessageLinked(LINK_SET, MSG_UPDATE_FLOAT, newPeril, NULL_KEY);
|
||||
|
|
@ -416,8 +406,9 @@ 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: Move life update signal DOWN to after the outcome sleeps
|
||||
// This prevents Game Manager/Controller from starting next round too soon
|
||||
// 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);
|
||||
|
||||
// Check if peril player picked the rolled number
|
||||
list perilPicks = getPicksFor(perilPlayer);
|
||||
|
|
@ -433,30 +424,21 @@ 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 + DELAY_MEDIUM_SYNC);
|
||||
|
||||
// SIGNAL DOWN: Now report life reduction to Controller
|
||||
llMessageLinked(LINK_SET, MSG_UPDATE_LIFE, perilPlayer + "|" + (string)(currentLives - 1), NULL_KEY);
|
||||
llSleep(DELAY_LONG_SYNC);
|
||||
} 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 + DELAY_MEDIUM_SYNC);
|
||||
|
||||
// SIGNAL DOWN: Now report life reduction to Controller
|
||||
llMessageLinked(LINK_SET, MSG_UPDATE_LIFE, perilPlayer + "|" + (string)(currentLives - 1), NULL_KEY);
|
||||
llSleep(DELAY_LONG_SYNC);
|
||||
} 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);
|
||||
|
|
@ -495,8 +477,38 @@ default {
|
|||
|
||||
// Note: Win condition checking is handled by Main Controller after elimination
|
||||
|
||||
// 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.
|
||||
// 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);
|
||||
|
||||
// Clear roll protection after processing is complete
|
||||
rollInProgress = FALSE;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
#include "peril/Peril_Constants.lsl"
|
||||
#include "Peril_Constants.lsl"
|
||||
|
||||
// === System Debugger - Game State Validation ===
|
||||
// Validates game state consistency and reports issues
|
||||
|
|
|
|||
127
UPDATER_SYSTEM_PLAN.md
Normal file
127
UPDATER_SYSTEM_PLAN.md
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
# 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!
|
||||
50
debug_mcp.py
Normal file
50
debug_mcp.py
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
#!/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()
|
||||
414
lsl_mcp_server.py
Executable file
414
lsl_mcp_server.py
Executable file
|
|
@ -0,0 +1,414 @@
|
|||
#!/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()
|
||||
60
test_lslint_mcp.py
Executable file
60
test_lslint_mcp.py
Executable file
|
|
@ -0,0 +1,60 @@
|
|||
#!/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()
|
||||
52
test_mcp_server.py
Executable file
52
test_mcp_server.py
Executable file
|
|
@ -0,0 +1,52 @@
|
|||
#!/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()
|
||||
62
test_validate_lsl.py
Executable file
62
test_validate_lsl.py
Executable file
|
|
@ -0,0 +1,62 @@
|
|||
#!/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()
|
||||
Loading…
Reference in a new issue