refactor: stabilize game state synchronization, improve reset timing, and integrate authoritative leaderboard reporting.

This commit is contained in:
RebeccaNod1 2026-04-08 21:27:18 -06:00
parent 0974d19f12
commit 1e70b70b72
10 changed files with 501 additions and 250 deletions

View file

@ -47,6 +47,7 @@ integer listenHandle = -1;
#define MEMORY_WARNING_THRESHOLD 0.8 // Warn when using >80% of memory
// Track game state to validate bot commands
list players = [];
list names = [];
list lives = [];
string perilPlayer = "";
@ -236,6 +237,7 @@ default {
// Handle full reset from main controller
if (num == MSG_RESET_ALL && str == "FULL_RESET") {
// Reset bot manager state
players = [];
names = [];
lives = [];
perilPlayer = "";
@ -247,7 +249,7 @@ default {
// Handle game state sync to track player eliminations
if (num == MSG_SYNC_GAME_STATE) {
list parts = llParseString2List(str, ["~"], []);
list parts = llParseStringKeepNulls(str, ["~"], []);
if (llGetListLength(parts) >= 4) {
lives = llCSV2List(llList2String(parts, 0));
// Don't need picks data for validation, skip parts[1]
@ -274,13 +276,12 @@ default {
sentBotMessages = [];
dbg("🤖 [Bot Manager] New round detected (peril change), cleared processed commands and sent messages");
} else {
// Only clear if we detect truly empty picks data during an active game
// Don't clear during initial registration (when picks = "EMPTY")
// Standardized ~EMPTY~ token logic
string picksStr = llList2String(parts, 1);
if (picksStr != "EMPTY" && (picksStr == "" || llSubStringIndex(picksStr, "|") == -1) && perilPlayer != "") {
if (picksStr == "~EMPTY~" && perilPlayer != "") {
processedBotCommands = [];
sentBotMessages = [];
dbg("🤖 [Bot Manager] New round detected (picks cleared), cleared processed commands and sent messages");
dbg("🤖 [Bot Manager] Turn reset detected (picks cleared), cleared processed commands and sent messages");
}
}
}

View file

@ -91,8 +91,9 @@ default {
// Set the Top Line Header on the main box
llMessageLinked(LINK_SET, 0, "=== WORLD RANKING ===", (key)"fw_data:Leaderboard");
// Trigger Leaderboard Manager to download from KVP Database
llMessageLinked(LINK_SET, MSG_RESET_LEADERBOARD, "START_SYNC", NULL_KEY);
// Note: Leaderboard sync should be triggered by the Main Controller or Game Manager
// during game resets, rather than every time the bridge initializes.
// llMessageLinked(LINK_SET, MSG_RESET_LEADERBOARD, "START_SYNC", NULL_KEY);
// Start Blink Timer (0.5s intervals)
llSetTimerEvent(0.5);
@ -148,5 +149,6 @@ default {
}
}
}
else if (num == MSG_RESET_ALL) { llResetScript(); }
}
}

View file

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

View file

@ -1,5 +1,9 @@
#include "Peril_Constants.lsl"
debugMsg(string msg) {
if (DEBUG_LOGS) llMessageLinked(LINK_SET, MSG_DEBUG_TEXT, msg, NULL_KEY);
}
// =============================================================================
// PERIL WORLD RANKING - Leaderboard Manager
// =============================================================================
@ -11,7 +15,9 @@ list leaderboardData = []; // Stride 3: [Name, Wins, Losses]
integer currentLoadPage = 1;
key kvpReadReq;
key kvpWriteReq;
key kvpLockReq; // Request for claiming leaderboard lock
key kvpLockReq; // Stage 1: Check Lock
key kvpClaimReq; // Stage 2: Claim Lock
key kvpSentinelReq; // Startup Probe
list writeQueue = []; // Pairs of [KeyName, DataString]
integer isSaving = FALSE;
integer isBusy = FALSE; // Read lock
@ -21,18 +27,25 @@ string currentWriteKey;
string currentWriteVal;
// --- Lock & Sync State ---
string pendingPlayer = "";
string pendingResult = "";
list pendingRecords = []; // Stride 2: [Name, Result]
integer isLocking = FALSE;
string myLockVal = "";
integer isClaiming = FALSE; // Track if we are in the middle of a WRITE claim
// Helper: Process the next KVP write in the queue
processWriteQueue() {
if (llGetListLength(writeQueue) == 0) {
isSaving = FALSE;
dbg("🏆 [Leaderboard] Save sequence complete. Releasing lock...");
// releaseLock();
kvpLockReq = llUpdateKeyValue(LB_LOCK_KEY, "", TRUE, myLockVal);
// Batch Processing: Check if more records are queued before releasing lock
if (llGetListLength(pendingRecords) > 0) {
string nextName = llList2String(pendingRecords, 0);
string nextRes = llList2String(pendingRecords, 1);
pendingRecords = llDeleteSubList(pendingRecords, 0, 1);
dbg("🔄 [Leaderboard] Processing next queued record: " + nextName);
updatePlayerRecord(nextName, nextRes);
} else {
releaseLock();
}
return;
}
isSaving = TRUE;
@ -42,6 +55,12 @@ processWriteQueue() {
kvpWriteReq = llUpdateKeyValue(currentWriteKey, currentWriteVal, FALSE, "");
}
// Helper: Release the lock immediately
releaseLock() {
dbg("🏆 [Leaderboard] Releasing global lock...");
kvpLockReq = llUpdateKeyValue(LB_LOCK_KEY, "OFF", TRUE, myLockVal);
}
// Convert the whole list into sharded strings and start saving
generateShardedData() {
integer _shardI;
@ -76,20 +95,30 @@ generateShardedData() {
}
loadLBPage(integer pageNum) {
if (isBusy && pageNum == 1) return; // Prevent double loads on reset
if (pageNum > 1) llSleep(1.0); // Anti-Throttle Delay
if ((isBusy || isSaving || isLocking) && pageNum == 1) {
dbg("⏳ [Leaderboard] Sync requested while Busy/Saving. Ignoring duplicate reset.");
return;
}
// ANTI-THROTTLE: Add small 0.2s pause for Mono speed compatibility
if (pageNum == 1) llSleep(0.2);
else llSleep(1.0); // 1.0s delay for sharded pages
isBusy = TRUE;
llSetTimerEvent(30.0); // 30 second watchdog (Lag Resistant)
llSetTimerEvent(30.0); // Lag Resistant watchdog
currentLoadPage = pageNum;
string keyName = LB_KEY_PREFIX + (string)pageNum;
kvpReadReq = llReadKeyValue(keyName);
if (kvpReadReq == NULL_KEY) {
dbg("❌ [Leaderboard] KVP Read FAILED: Not authorized for " + keyName);
llSetTimerEvent(0.0);
} else {
dbg("📡 [Leaderboard] Loading " + keyName + "...");
dbg("⚠️ [Leaderboard] KVP Request FAILED (Throttle/NULL_KEY). Retrying " + keyName + " in 2s...");
isBusy = FALSE; // Unlock for retry
llSetTimerEvent(2.0); // Use timer to trigger the retry
return;
}
dbg("📡 [Leaderboard] Loading " + keyName + "...");
}
list getSortedSlice(integer offset) {
@ -135,15 +164,19 @@ updatePlayerRecord(string pName, string pResult) {
}
startUpdateSequence(string pName, string pResult) {
if (isLocking || isSaving) {
dbg("⚠️ [Leaderboard] Update already in progress. Retrying in 2s...");
llSleep(2.0);
llSetTimerEvent(0); // GHOST-BUSTER: Kill any pending retries/syncs
// Check if record already in queue to avoid duplicates
if (llListFindList(pendingRecords, [pName, pResult]) != -1) return;
pendingRecords += [pName, pResult]; // Add to Victory Queue
if (isBusy || isLocking || isSaving) {
dbg("⏳ [Leaderboard] Record for [" + pName + "] added to queue (" + (string)(llGetListLength(pendingRecords)/2) + " pending).");
return;
}
pendingPlayer = pName;
pendingResult = pResult;
isLocking = TRUE;
dbg("🔐 [Leaderboard] Checking global lock for update...");
kvpLockReq = llReadKeyValue(LB_LOCK_KEY);
}
@ -153,18 +186,45 @@ default {
state_entry() {
REPORT_MEMORY();
dbg("⏳ [Leaderboard] Startup: Handshake link active.");
pendingRecords = [];
dbg("⏳ [Leaderboard] v1.55 (Atomic Endurance) - Handshake link active. Probing Experience...");
// SENTINEL CHECK: Use a dedicated ID to prevent shard-read collisions
kvpSentinelReq = llReadKeyValue(LB_KEY_PREFIX + "1");
llSetTimerEvent(10.0); // 10s watchdog for sentinel
}
dataserver(key _qid, string _qd) {
if (_qid == kvpReadReq) {
integer _c = llSubStringIndex(_qd, ",");
string _st = _qd; string _vl = ""; string _rawVal = "";
if (_c != -1) {
_st = llGetSubString(_qd, 0, _c - 1);
_rawVal = llGetSubString(_qd, _c + 1, -1);
_vl = llStringTrim(_rawVal, STRING_TRIM);
}
if (_qid == kvpSentinelReq) {
llSetTimerEvent(0.0); // Reset watchdog
integer _c = llSubStringIndex(_qd, ",");
string _st = _qd; string _vl = "";
if (_c != -1) {
_st = llGetSubString(_qd, 0, _c - 1);
_vl = llGetSubString(_qd, _c + 1, -1);
kvpSentinelReq = NULL_KEY; // Kill it once used
if (_st == "1" || _st == "3") { // Success (1) or Not Found (3) prove access
dbg("✅ [Leaderboard] Experience Sentinel: KVP Access Confirmed. Starting Auto-Sync...");
loadLBPage(1);
} else if (_st == "0") {
string sentinelErr = _vl;
if (_vl == "1") sentinelErr = "Experience Throttled (Error 1)";
else if (_vl == "3") sentinelErr = "Invalid Parameters (Error 3)";
else if (_vl == "17") sentinelErr = "Experience Disabled/Not Permitted (Error 17)";
else sentinelErr = "KVP Error Status: " + _vl;
dbg("🚫 [Leaderboard] Experience Sentinel: FATAL " + sentinelErr);
llOwnerSay("🚫 [Leaderboard] WORLD RANKING DISABLED: " + sentinelErr);
}
return;
}
if (_qid == kvpReadReq) {
if (isBusy == FALSE && isLocking == FALSE) return; // Ignore stale reads
if (_st == "1") { // SUCCESS: Data found
if (_vl != "" && llSubStringIndex(_vl, "Error") == -1) {
@ -189,35 +249,55 @@ default {
}
}
if (currentLoadPage < 10) {
if (isBusy && currentLoadPage < 10) {
retryCount = 0; // Reset retries for next page
loadLBPage(currentLoadPage + 1);
} else {
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
if (isBusy) {
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
}
// IF WE WERE SYNCING FOR AN UPDATE, APPLY IT NOW
if (isLocking) {
isLocking = FALSE;
updatePlayerRecord(pendingPlayer, pendingResult);
if (isSaving && llGetListLength(pendingRecords) > 0) {
isSaving = FALSE;
string nextName = llList2String(pendingRecords, 0);
string nextRes = llList2String(pendingRecords, 1);
pendingRecords = llDeleteSubList(pendingRecords, 0, 1);
updatePlayerRecord(nextName, nextRes);
}
}
} else if (_st == "0" && _vl == "1") {
// XP_ERROR_THROTTLED
dbg("⚠️ [Leaderboard] Read throttled. Retrying Shard " + (string)currentLoadPage + " in 2s...");
llSleep(2.0);
isBusy = FALSE;
loadLBPage(currentLoadPage);
} else { // Empty or Error
isBusy = FALSE;
llSetTimerEvent(0);
if (_st == "0") { dbg("❌ [Leaderboard] KVP Error on Shard " + (string)currentLoadPage + ": " + _vl); }
dbg("🏆 [Leaderboard] SYNC COMPLETE - " + (string)(llGetListLength(leaderboardData)/3) + " players total.");
llMessageLinked(LINK_SET, MSG_LB_LOAD_COMPLETE, "", NULL_KEY);
llMessageLinked(LINK_SET, MSG_LB_REQUEST_DISPLAY, "0", NULL_KEY);
} else if (_st == "0") {
if (_vl == "1") { // XP_ERROR_THROTTLED
dbg("⚠️ [Leaderboard] Read throttled. Retrying Shard " + (string)currentLoadPage + " in 2s...");
llSleep(2.0);
isBusy = FALSE;
loadLBPage(currentLoadPage);
} else if (_vl == "14") { // XP_ERROR_KEY_NOT_FOUND (Value is empty)
isBusy = FALSE;
llSetTimerEvent(0);
dbg("🏆 [Leaderboard] Shard " + (string)currentLoadPage + " is empty (New).");
if (currentLoadPage < 10) {
loadLBPage(currentLoadPage + 1);
} else if (isBusy) {
dbg("🏆 [Leaderboard] SYNC COMPLETE - " + (string)(llGetListLength(leaderboardData)/3) + " players total.");
llMessageLinked(LINK_SET, MSG_LB_LOAD_COMPLETE, "", NULL_KEY);
llMessageLinked(LINK_SET, MSG_LB_REQUEST_DISPLAY, "0", NULL_KEY);
}
} else {
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) {
@ -228,64 +308,94 @@ default {
_vl = llGetSubString(_qd, _c + 1, -1);
}
if (_st == "0" && _vl == "1") {
if (_st == "1") {
dbg("✅ [Leaderboard] Write success to " + currentWriteKey);
processWriteQueue();
} else if (_st == "0" && _vl == "1") {
// XP_ERROR_THROTTLED
dbg("⚠️ [Leaderboard] Write throttled. Retrying " + currentWriteKey + " in 2s...");
llSleep(2.0);
kvpWriteReq = llUpdateKeyValue(currentWriteKey, currentWriteVal, FALSE, "");
} else {
if (_st == "0") { dbg("❌ [Leaderboard] Write Error (" + _vl + ") on " + currentWriteKey); }
dbg("❌ [Leaderboard] Write Error (" + _st + ":" + _vl + ") on " + currentWriteKey);
processWriteQueue();
}
}
else if (_qid == kvpLockReq) {
integer _c = llSubStringIndex(_qd, ",");
string _st = _qd; string _vl = "";
if (_c != -1) {
_st = llGetSubString(_qd, 0, _c - 1);
_vl = llGetSubString(_qd, _c + 1, -1);
else if (_qid == kvpClaimReq) {
kvpClaimReq = NULL_KEY; // DUPLICATE DEFENSE: Prevent zombie processing
if (_st == "1") {
dbg("🔐 [Leaderboard] LOCK ACQUIRED. Starting fresh sync...");
isClaiming = FALSE;
isLocking = FALSE;
isSaving = TRUE;
leaderboardData = [];
loadLBPage(1);
} else if (_vl == "15") { // ATOMIC CHECK FAILED
dbg("⏳ [Leaderboard] Race conflict (Loss). Yielding and retrying in 5s...");
isClaiming = FALSE;
isLocking = TRUE; // Stay in locking mode for retry
llSetTimerEvent(5.0);
} else {
dbg("❌ [Leaderboard] Lock claim failed. Status: " + _st + ":" + _vl);
isClaiming = FALSE;
isLocking = FALSE;
}
}
else if (_qid == kvpLockReq) {
kvpLockReq = NULL_KEY; // DUPLICATE DEFENSE: Prevent zombie processing
// --- STAGE 1: Check Result (Success Pulse = 1) ---
if (isLocking) {
// STAGE 1: Current Lock Read Result
if (_st == "1") { // Success read
string _curVal = _vl;
if (_st == "1" && _c != -1) {
string _curVal = _rawVal; // USE RAW DATA FOR ATOMIC CHECK
integer _isStale = FALSE;
list _p = [];
integer _ts = 0;
if (_curVal != "") {
list _p = llParseString2List(_curVal, [":"], []);
integer _ts = (integer)llList2String(_p, 1);
_p = llParseString2List(_curVal, [":"], []);
_ts = (integer)llList2String(_p, 1);
if (llGetUnixTime() - _ts > LB_LOCK_TIMEOUT) _isStale = TRUE;
}
if (_curVal == "" || _isStale) {
dbg("🔐 [Leaderboard] Lock available. Claiming...");
if (llList2String(_p, 0) == (string)llGetKey()) {
dbg("🔐 [Leaderboard] Lock already held by this board (Self-Sync). Proceeding...");
myLockVal = _curVal; // CRITICAL: Sync timestamp to allow future release
isLocking = FALSE;
isSaving = TRUE; // MARK THAT WE ARE SYNCING FOR A SAVE
leaderboardData = []; // PURGE STALE MEMORY
loadLBPage(1);
return; // Done
}
if (_curVal == "" || _curVal == "OFF" || _isStale) {
dbg("🔐 [Leaderboard] Lock available (or stale). Claiming...");
myLockVal = (string)llGetKey() + ":" + (string)llGetUnixTime();
// ATOMIC UPDATE: Only write if value is still what we just read
kvpLockReq = llUpdateKeyValue(LB_LOCK_KEY, myLockVal, TRUE, _curVal);
dbg("🔓 [Leaderboard] CLAIMING WITH: [" + myLockVal + "] (Len: " + (string)llStringLength(myLockVal) + ")");
isClaiming = TRUE;
kvpClaimReq = llUpdateKeyValue(LB_LOCK_KEY, myLockVal, TRUE, _curVal);
} else {
dbg("⏳ [Leaderboard] Lock held by another board. Retrying in 5s...");
dbg("⏳ [Leaderboard] Lock held by board [" + llList2String(_p, 0) + "] for " + (string)(llGetUnixTime() - _ts) + "s. Retrying in 5s...");
llSetTimerEvent(5.0);
}
} else if (_st == "0" && _vl == "14") { // KEY_NOT_FOUND (Lock key doesn't exist)
dbg("🔐 [Leaderboard] Lock doesn't exist. Creating initial lock...");
myLockVal = (string)llGetKey() + ":" + (string)llGetUnixTime();
dbg("🔓 [Leaderboard] CLAIMING WITH: [" + myLockVal + "] (Len: " + (string)llStringLength(myLockVal) + ")");
isClaiming = TRUE;
// INITIAL CREATION: Use kvpClaimReq to match Stage 2 handler
kvpClaimReq = llUpdateKeyValue(LB_LOCK_KEY, myLockVal, FALSE, "");
} else if (_st == "0" && _vl == "1") { // Throttled
dbg("⚠️ [Leaderboard] Lock check throttled. Retrying...");
llSleep(2.0);
kvpLockReq = llReadKeyValue(LB_LOCK_KEY);
} else if (_st == "14") { // SUCCESSFUL CLAIM (Atomic check passed)
dbg("🔐 [Leaderboard] LOCK ACQUIRED. Starting fresh sync...");
leaderboardData = []; // PURGE STALE MEMORY
loadLBPage(1);
} else if (_st == "0" && _vl == "15") { // ATOMIC CHECK FAILED (Someone else grabbed it)
dbg("⏳ [Leaderboard] Lock race lost. Retrying...");
llSleep(1.0);
kvpLockReq = llReadKeyValue(LB_LOCK_KEY);
} else {
dbg("❌ [Leaderboard] Lock Error: (" + _st + ") " + _vl);
dbg("❌ [Leaderboard] Lock logic failed (Code " + _st + ":" + _vl + ")");
isLocking = FALSE;
}
} else {
// RELEASE RESULT
if (_st == "14") { dbg("🔓 [Leaderboard] Lock released."); }
else { dbg("⚠️ [Leaderboard] Lock release issues: " + _vl); }
// RELEASE RESULT: Check for Status 1 (Success)
if (_st == "1") { dbg("🔓 [Leaderboard] Lock released."); }
else { dbg("⚠️ [Leaderboard] Lock release report: " + _st + ":" + _vl); }
}
}
}
@ -297,16 +407,16 @@ default {
return;
}
if (isBusy) {
if (isBusy || (kvpReadReq == NULL_KEY && currentLoadPage > 0)) {
if (retryCount < 3) {
retryCount++;
dbg("⚠️ [Leaderboard] Shard " + (string)currentLoadPage + " timed out. Retrying (" + (string)retryCount + "/3)...");
dbg("⏳ [Leaderboard] Retrying Shard " + (string)currentLoadPage + " (" + (string)retryCount + "/3)...");
isBusy = FALSE; // Unlock so loadLBPage can run
loadLBPage(currentLoadPage);
} else {
isBusy = FALSE;
llSetTimerEvent(0);
dbg("🏆 [Leaderboard] SYNC GAVE UP (Persistent timeout on Shard " + (string)currentLoadPage + ")");
dbg("🏆 [Leaderboard] SYNC GAVE UP (Persistent issues on Shard " + (string)currentLoadPage + ")");
llMessageLinked(LINK_SET, MSG_LB_REQUEST_DISPLAY, "0", NULL_KEY);
}
} else {
@ -316,7 +426,9 @@ default {
link_message(integer sender, integer num, string str, key id) {
if (num == MSG_LB_RECORD_PLAYER) {
startUpdateSequence(str, (string)id);
// str is "Name|Result" (e.g. "Bot1|WIN")
list _p = llParseString2List(str, ["|"], []);
startUpdateSequence(llList2String(_p, 0), llList2String(_p, 1));
}
else if (num == MSG_DISPLAY_LEADERBOARD) {
if (str == "REFRESH_STARTUP") loadLBPage(1);
@ -385,6 +497,8 @@ default {
generateShardedData();
}
}
else if (num == MSG_RESET_ALL) { llResetScript(); }
else if (num == MSG_RESET_ALL) {
if (str == "FULL_RESET" || str == "HARD_RESET") llResetScript();
}
}
}

View file

@ -27,6 +27,7 @@ integer roundStarted = FALSE;
integer diceTypeProcessed = FALSE; // Track if we've already processed dice type for this round
integer ignorePicksSync = FALSE; // Temporarily ignore picks data sync during round initialization
integer roundContinueInProgress = FALSE; // Prevent multiple rapid calls to continueCurrentRound
integer lastPlayerCount = 0; // Memory for dice type calculation (skips redundant requests)
integer lastSyncProcessTime = 0; // Track when we last processed a sync to prevent rapid duplicates
// Duplicate request prevention
@ -149,24 +150,28 @@ integer continueCurrentRound() {
}
currentPickerIdx = 0;
dbg("🎯 [Game Manager] Pick queue created: " + llList2CSV(pickQueue) + ", requesting dice type");
dbg("🎯 [Game Manager] Pick queue created: " + llList2CSV(pickQueue) + ", checking dice memory...");
// Request dice type directly from Calculator for the new round
llMessageLinked(LINK_SET, MSG_GET_DICE_TYPE, (string)llGetListLength(names), NULL_KEY);
integer currentPlayerCount = llGetListLength(names);
if (currentPlayerCount == lastPlayerCount && diceType > 0) {
dbg("🎯 [Game Manager] Player count unchanged (" + (string)currentPlayerCount + "), reusing stored dice type: d" + (string)diceType);
// NEW: Add a brief buffer to let the previous outcome status stay on screen
llSleep(DELAY_MEDIUM_SYNC);
// Reuse stored dice type and skip redundant request/status Flicker
diceTypeProcessed = TRUE;
llMessageLinked(LINK_SET, MSG_STATUS_TEXT, "NEW ROUND STARTING!\nPeril: <!c=red>" + perilPlayer, NULL_KEY);
showNextPickerDialog();
} else {
lastPlayerCount = currentPlayerCount;
// NEW: Update status bar for new round initialization
llMessageLinked(LINK_SET, MSG_STATUS_TEXT, "NEW ROUND STARTING!\nPeril: <!c=red>" + perilPlayer, NULL_KEY);
// Request dice type directly from Calculator for the new round
llMessageLinked(LINK_SET, MSG_GET_DICE_TYPE, (string)currentPlayerCount, NULL_KEY);
}
// NEW: Update status bar for new round initialization
llMessageLinked(LINK_SET, MSG_STATUS_TEXT, "PREPARING ROUND...\nCalculating dice size...", NULL_KEY);
// Dramatic pause to let the preparation message be read
llSleep(DELAY_LONG_SYNC);
// NEW: Update status bar for new round with colored highlight
llMessageLinked(LINK_SET, MSG_STATUS_TEXT, "NEW ROUND STARTING!\nPeril: <!c=red>" + perilPlayer, NULL_KEY);
// Dramatic pause to let the peril player be seen before the dice type is shown
llSleep(DELAY_LONG_SYNC);
// Note: showNextPickerDialog() will be called when dice type result is received
// Note: showNextPickerDialog() will be called when dice type result is received (or immediately if cached)
return 0;
}
@ -249,16 +254,25 @@ integer startNextRound() {
currentPickerIdx = 0;
dbg("🎯 Debug - Created pickQueue: " + llList2CSV(pickQueue) + ", currentPickerIdx: " + (string)currentPickerIdx);
// Request dice type for this round
llMessageLinked(LINK_SET, MSG_GET_DICE_TYPE, (string)llGetListLength(names), NULL_KEY);
integer 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);
diceTypeProcessed = TRUE;
llMessageLinked(LINK_SET, MSG_STATUS_TEXT, "ROUND STARTING!\nPeril: <!c=red>" + perilPlayer, NULL_KEY);
showNextPickerDialog();
} else {
lastPlayerCount = currentPlayerCount;
// Request dice type for this round
llMessageLinked(LINK_SET, MSG_GET_DICE_TYPE, (string)currentPlayerCount, NULL_KEY);
// NEW: Update status bar for round start
llMessageLinked(LINK_SET, MSG_STATUS_TEXT, "ROUND STARTING!\nPeril: <!c=red>" + perilPlayer, NULL_KEY);
}
// NEW: Update status bar for new round initialization
llMessageLinked(LINK_SET, MSG_STATUS_TEXT, "PREPARING ROUND...\nCalculating dice size...", NULL_KEY);
// Dramatic pause to let the preparation message be read
llSleep(DELAY_LONG_SYNC);
dbg("🎯 Game Manager round setup complete, requesting dice type...");
dbg("🎯 Game Manager round setup complete.");
return 0;
}
@ -484,7 +498,7 @@ default {
// Only skip sync messages if we're explicitly ignoring them
// Allow player registration syncs even when game is not active
list parts = llParseString2List(str, ["~"], []);
list parts = llParseStringKeepNulls(str, ["~"], []);
if (llGetListLength(parts) >= 4) {
list newLives = llCSV2List(llList2String(parts, 0));
string currentLivesStr = llList2CSV(lives);
@ -509,7 +523,7 @@ default {
if (livesChanged || perilChanged) {
string encodedPicksDataStr = llList2String(parts, 1);
list newPicksData = [];
if (encodedPicksDataStr != "") {
if (encodedPicksDataStr != "" && encodedPicksDataStr != "~EMPTY~") {
list encodedEntries = llParseString2List(encodedPicksDataStr, ["^"], []);
integer syncI;
for (syncI = 0; syncI < llGetListLength(encodedEntries); syncI++) {
@ -551,10 +565,8 @@ default {
}
}
if (livesChanged && newPerilPlayer != "" && newPerilPlayer != "NONE" && roundStarted && allPicksEmpty) {
dbg("🎯 Post-roll state update detected - continuing round");
continueCurrentRound();
}
// Note: Round continuation is now handled by the Main Controller's explicit MSG_CONTINUE_ROUND signal
// This prevents duplicate round starts and race conditions during Plot Twists.
}
}
return;
@ -778,7 +790,7 @@ default {
// Use complete avoid list for re-shown dialog too
list updatedCompleteAvoidList = buildCompleteAvoidanceList();
string dialogPayload = playerName + "|" + (string)diceType + "|" + (string)picksNeeded + "|" + llList2CSV(updatedCompleteAvoidList);
string dialogRequest = "SHOW_DIALOG|" + playerName + "|" + dialogPayload;
string dialogRequest = "SHOW_DIALOG~" + playerName + "~" + dialogPayload;
llMessageLinked(LINK_SET, MSG_DIALOG_FORWARD_REQUEST, dialogRequest, NULL_KEY);
}
return;
@ -839,7 +851,7 @@ default {
// Send roll dialog through Player_RegistrationManager
dbg("🎯 [Game Manager] Sending roll dialog request for: " + perilPlayer);
string rollRequest = "SHOW_ROLL_DIALOG|" + perilPlayer + "|" + perilPlayer;
string rollRequest = "SHOW_ROLL_DIALOG~" + perilPlayer + "~" + perilPlayer;
llMessageLinked(LINK_SET, MSG_DIALOG_FORWARD_REQUEST, rollRequest, NULL_KEY);
}
}
@ -898,7 +910,7 @@ default {
llMessageLinked(LINK_SET, MSG_BOT_COMMAND, "CLOSE_ALL_DIALOGS", NULL_KEY);
// Send roll dialog through Player_RegistrationManager
string rollRequest = "SHOW_ROLL_DIALOG|" + perilPlayer + "|" + perilPlayer;
string rollRequest = "SHOW_ROLL_DIALOG~" + perilPlayer + "~" + perilPlayer;
llMessageLinked(LINK_SET, MSG_DIALOG_FORWARD_REQUEST, rollRequest, NULL_KEY);
}
return; // Ignore duplicate - bot already has real picks
@ -1008,7 +1020,7 @@ default {
llMessageLinked(LINK_SET, MSG_BOT_COMMAND, "CLOSE_ALL_DIALOGS", NULL_KEY);
// Send roll dialog through Player_RegistrationManager
string rollRequest = "SHOW_ROLL_DIALOG|" + perilPlayer + "|" + perilPlayer;
string rollRequest = "SHOW_ROLL_DIALOG~" + perilPlayer + "~" + perilPlayer;
llMessageLinked(LINK_SET, MSG_DIALOG_FORWARD_REQUEST, rollRequest, NULL_KEY);
} else {
// Move to next picker
@ -1065,6 +1077,7 @@ default {
currentPicker = NULL_KEY;
roundStarted = FALSE;
diceTypeProcessed = FALSE; // Reset for new game
lastPlayerCount = 0; // Reset dice memory for new game
roundContinueInProgress = FALSE; // Reset protection flag
lastSyncProcessTime = 0; // Reset sync timing
dbg("🎯 Game Manager reset - ready for new game!");

View file

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

View file

@ -68,6 +68,7 @@ integer victoryInProgress = FALSE;
// Lockout system variables
integer isLocked = FALSE;
integer resetInProgress = FALSE; // Lockout flag for reset synchronization
key gameOwner;
list pickQueue = [];
@ -179,23 +180,34 @@ checkMemoryUsage(string context) {
// REMOVED: emergencyMemoryCleanup() - delegated to Controller_Memory.lsl
// REMOVED: reportMemoryStats() - delegated to Controller_Memory.lsl
// Experience Sentinel - Functional probe to check if Experience is allowed on land
// 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.
checkExperience() {
dbg("🔍 [Peril Dice] Experience Sentinel: Starting functional diagnostic...");
llOwnerSay("🛡️ [Peril Dice] Experience Sentinel: Verifying Land-Scope permissions for 'Final Girlz I.N.C.'...");
dbg("🔍 [Peril Dice] Experience Sentinel: Starting functional heartbeat...");
// PROBE 1: Owner Agent Check (Instant indicator for land-readiness)
// PROBE 1: Land-Scope Check (Crucial for HUD auto-attachment)
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.'");
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'.");
// Final probe check
// Silent probe for KVP (which works regardless)
sentinelQueryID = llReadKeyValue("PERIL_SENTINEL");
llSetTimerEvent(10.0); // Increased to 10s for reliability
llSetTimerEvent(10.0);
currentTimerMode = TIMER_XP_CHECK;
} else {
llOwnerSay("✅ [Peril Dice] Experience 'Final Girlz I.N.C.' is ENABLED on this parcel.");
llOwnerSay("✅ [Peril Dice] Experience 'Final Girlz I.N.C.' is ACTIVE. HUD Auto-attachment ready.");
EX_READY = TRUE;
// Full KVP heartbeat
sentinelQueryID = llReadKeyValue("PERIL_SENTINEL");
llSetTimerEvent(10.0);
currentTimerMode = TIMER_XP_CHECK;
}
}
@ -227,13 +239,17 @@ sendStatusMessage(string status) {
syncGameState() {
// Build sync message WITHOUT redundant player keys to save memory
// Format: lives~picks~perilPlayer~names
string picksStr = (string)llDumpList2String(picksData, "^");
if (picksStr == "") picksStr = "EMPTY"; // Standardized token
// DELIMITER FIX: Ensure each of the 4 parts is explicitly separated by ~
string syncMsg = llList2CSV(lives) + "~" +
(string)llDumpList2String(picksData, "^") + "~" +
picksStr + "~" +
(string)llList2String([perilPlayer, "NONE"], (perilPlayer == "")) + "~" +
llList2CSV(names);
llMessageLinked(LINK_SET, MSG_SYNC_GAME_STATE, syncMsg, NULL_KEY);
dbg("🌍 [Main Controller] 📡 Global State Sync broadcasted.");
dbg("🌍 [Main Controller] 📡 Global State Sync broadcasted (Picks: " + picksStr + ")");
}
@ -288,14 +304,13 @@ updateFloatingText() {
}
resetGame() {
resetInProgress = TRUE; // Start lockout
cleanupListeners();
// Let Floater Manager handle cleanup intelligently
// Always request cleanup on a reset to ensure no orphans are left behind
llMessageLinked(LINK_SET, MSG_CLEANUP_ALL_FLOATERS, "RESET", NULL_KEY);
llSleep(DELAY_STATE_TRANSITION);
llSleep(DELAY_STATE_TRANSITION);
// Reduced delay for snappy restart
llSleep(0.1);
players = names = lives = picksData = globalPickedNumbers = readyPlayers = [];
floaterChannels = [];
@ -317,6 +332,13 @@ resetGame() {
llMessageLinked(LINK_SCOREBOARD, MSG_UPDATE_WINNER, "", NULL_KEY); // Clear winner glow
llMessageLinked(LINK_SET, MSG_CLEAR_DICE, "", NULL_KEY);
// Give other scripts time to finish their reset before starting sync
// INCREASED to 2.0s for high-speed Mono startup with Debug Logs OFF
llSleep(2.0);
// NEW: Trigger Leaderboard to reload rankings from KVP on reset
llMessageLinked(LINK_SET, MSG_RESET_LEADERBOARD, "START_SYNC", NULL_KEY);
dbg("🎮 Game reset! All state cleared (including scoreboard).");
statusTimer = 0;
@ -328,15 +350,12 @@ resetGame() {
// NEW: Update status bar for idle state
sendStatusMessage("PERIL DICE GAME\nTouch to Join!");
llSleep(DELAY_STATE_TRANSITION);
// Quick buffer for Linkset to settle
llSleep(0.2);
llSetTimerEvent(0);
initListeners();
// SAFETY: Only call updateHelpers if we have actual game data to prevent startup issues
if (llGetListLength(names) > 0) {
updateHelpers();
}
resetInProgress = FALSE; // End lockout
}
default {
@ -384,15 +403,17 @@ default {
}
touch_start(integer total_number) {
// IGNORE TOUCHES INTENDED FOR THE LEADERBOARD
string _rawNm = llGetLinkName(llDetectedLinkNumber(0));
string _linkNm = llToLower(_rawNm);
integer clickedLink = llDetectedLinkNumber(0);
string clickedName = llGetLinkName(clickedLink);
// TEMPORARY DEBUG TO CATCH THE EXACT LINK NAME
dbg("🎯 [Touch Filter] Clicked Link Name: '" + _rawNm + "'");
// Ensure core links are discovered to prevent misidentification
if (LINK_LEADERBOARD_BRIDGE == -1) { DISCOVER_CORE_LINKS(); }
if (llSubStringIndex(_linkNm, "leaderboard") != -1 || llSubStringIndex(_linkNm, "furware") != -1 || llSubStringIndex(_linkNm, "lb_") != -1) {
dbg("🛑 [Touch Filter] Bypassing menu for Leaderboard element!");
// IGNORE TOUCHES ON UI BRIDGES ONLY
// Always allow touch on Root (link 1) or any prim containing 'Scoreboard' or NOT containing 'FURWARE'
// ENHANCED: Also filter out click on Leaderboard elements which was causing redundant diagnostic logs
if (clickedLink != 1 && (llSubStringIndex(clickedName, "FURWARE") != -1 || llSubStringIndex(clickedName, "LB_") != -1)) {
dbg("🛑 [Touch Filter] Bypassing menu for Bridge element: " + clickedName);
return;
}
@ -544,8 +565,11 @@ default {
if (pidx != -1) {
lives = llListReplaceList(lives, [newLifeCount], pidx, pidx);
dbg("🛡️ [Main Controller] 💗 Damage Report Confirmed: " + hitPlayer + " -> " + (string)newLifeCount + " hearts.");
// Broadcast the OFFICIAL ledger to the entire Linkset
syncGameState();
// AUTHORITATIVE: Signal Game Manager to continue to the next round after the damage report
llSleep(DELAY_SYNC_PROPAGATION);
llMessageLinked(LINK_SET, MSG_CONTINUE_ROUND, perilPlayer, NULL_KEY);
}
}
return;
@ -612,6 +636,9 @@ default {
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); // After removing one player
string potentialWinner = "";
@ -629,6 +656,9 @@ default {
@found_winner;
if (potentialWinner != "") {
// NEW: Broadcast sync BEFORE redraw to ensure Floater Manager has fresh heart count
syncGameState();
// 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
@ -755,7 +785,10 @@ default {
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
// RECORD WIN: Send ultimate survivor to World Ranking database
llMessageLinked(LINK_SET, MSG_LB_RECORD_PLAYER, winner + "|WIN", NULL_KEY);
// CHANGED: Send to scoreboard, which will handle visual updates (no more duplicate records)
llMessageLinked(LINK_SCOREBOARD, MSG_GAME_WON, winner, NULL_KEY);
// Use timer instead of llSleep to avoid immediate reset
@ -843,6 +876,18 @@ default {
return;
}
// Handle peril player updates from other modules
if (num == MSG_UPDATE_PERIL_PLAYER) {
perilPlayer = str;
dbg("🎯 [Main Controller] Peril Player Updated: " + perilPlayer);
syncGameState();
// AUTHORITATIVE: Signal Game Manager to continue to the next round after Plot Twist
llSleep(DELAY_SYNC_PROPAGATION);
llMessageLinked(LINK_SET, MSG_CONTINUE_ROUND, perilPlayer, NULL_KEY);
return;
}
// Receive lightweight updates from Game Manager
if (num == 9070) {
list parts = llParseString2List(str, ["|"], []);
@ -953,11 +998,11 @@ default {
string action = llList2String(parts, 0);
if (action == "LEAVE_GAME" || action == "KICK_PLAYER") {
string leavingName = llList2String(parts, 1);
// Find and remove the player from main controller lists
integer idx = llListFindList(names, [leavingName]);
if (idx != -1) {
// CHANGED: Remove player from scoreboard BEFORE updating lists
llMessageLinked(LINK_SCOREBOARD, MSG_REMOVE_PLAYER, leavingName, NULL_KEY);
// Find and remove the player from main controller lists
integer idx = llListFindList(names, [leavingName]);
if (idx != -1) {
// CRITICAL: Inform all scripts (Registration Manager) to scrub this player
llMessageLinked(LINK_SET, MSG_REMOVE_PLAYER, leavingName, NULL_KEY);
// Only announce if it's not the end of the game
if (llGetListLength(names) > 1) {
@ -1135,7 +1180,7 @@ default {
llRegionSayTo(id, 0, "🚫 Access Denied: Only the Global Admin can perform this action.");
return;
}
llMessageLinked(LINK_SCOREBOARD, MSG_RESET_LEADERBOARD, "WIPE", id); // Use 'id' to pass user key
llMessageLinked(LINK_SET, MSG_RESET_LEADERBOARD, "WIPE", id); // Use LINK_SET
llOwnerSay("🏆 GLOBAL LEADERBOARD WIPE confirmed and executed.");
return;
}
@ -1149,7 +1194,7 @@ default {
return;
}
resetGame();
llMessageLinked(LINK_SCOREBOARD, MSG_RESET_LEADERBOARD, "WIPE", id); // Use 'id' for security check
llMessageLinked(LINK_SET, MSG_RESET_LEADERBOARD, "WIPE", id); // Use LINK_SET
llOwnerSay("🔄 FULL RESET confirmed and executed.");
return;
}

View file

@ -43,6 +43,8 @@ key EXPERIENCE_ID = "06926390-26e6-11f1-a452-0242ac110003"; // Final Girlz I.N.C
#define dbg(msg) ;
#endif
#define MSG_DEBUG_TEXT 7000
#define MSG_RESET_ALL -99999
#define MSG_EMERGENCY_RESET -99998
#define MSG_DEBUG_PICKS_ON 7001

View file

@ -17,6 +17,8 @@ integer FLOATER_BASE_CHANNEL;
integer roundStarted = FALSE;
integer gameStarting = FALSE;
integer resetInProgress = FALSE; // Lockout for reset syncs
integer calculateChannel(integer offset) {
string ownerStr = (string)llGetOwner();
string objectStr = (string)llGetLinkKey(1);
@ -154,6 +156,10 @@ handlePlayerRegistration(string regData, key requesterId) {
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
integer isStarter = (humanCount <= 1);
llMessageLinked(LINK_SET, MSG_SHOW_MENU, "player|" + (string)isStarter, newKey);
}
ownerMsg("JOIN|" + newName);
@ -190,77 +196,150 @@ default {
// Handle game state sync from Main Controller
if (num == MSG_SYNC_GAME_STATE) {
if (resetInProgress) return; // IGNORE stale syncs during reset lockout
// PRESERVE PLAYER KEYS: Don't overwrite our authoritative player registry
// We only sync lives and names, but keep our real player keys
list parts = llParseString2List(str, ["~"], []);
list parts = llParseStringKeepNulls(str, ["~"], []);
if (llGetListLength(parts) >= 4) {
list newLives = llCSV2List(llList2String(parts, 0));
string picksPart = llList2String(parts, 1);
list newNames = llCSV2List(llList2String(parts, 3));
// Only update lives if list length matches (safety check)
if (llGetListLength(newLives) == llGetListLength(players)) {
lives = newLives;
}
// Only update names if they match our current registry
// This prevents sync messages from overwriting our authoritative data
if (llGetListLength(newNames) == llGetListLength(players)) {
names = newNames;
}
// KEEP our player keys - they are authoritative
dbg("🔄 [RegMgr] Synced game state, preserving " + (string)llGetListLength(players) + " player keys");
}
return;
}
// --- Status Queries ---
if (num == MSG_QUERY_READY_STATE) {
list parts = llParseString2List(str, ["|"], []);
if (llGetListLength(parts) >= 2) {
string name = llList2String(parts, 0);
string requestId = llList2String(parts, 1);
integer isReady = (llListFindList(readyPlayers, [name]) != -1);
integer isBot = (llSubStringIndex(name, "Bot") == 0);
llMessageLinked(LINK_SET, MSG_READY_STATE_RESULT,
name + "|" + (string)isReady + "|" + (string)isBot + "|" + requestId, id);
}
return;
}
if (num == MSG_QUERY_OWNER_STATUS) {
list parts = llParseString2List(str, ["|"], []);
if (llGetListLength(parts) >= 2) {
string name = llList2String(parts, 0);
string requestId = llList2String(parts, 1);
integer isRegistered = (llListFindList(players, [id]) != -1);
integer isPending = FALSE;
integer isStarter = FALSE;
if (isRegistered) {
integer starterIdx = -1;
integer i;
for (i = 0; i < llGetListLength(names) && starterIdx == -1; i++) {
if (llSubStringIndex(llList2String(names, i), "Bot") != 0) starterIdx = i;
}
if (starterIdx != -1 && llList2Key(players, starterIdx) == id) isStarter = TRUE;
}
dbg("📤 [RegMgr] Responding to OWNER STATUS (" + requestId + ") - Registered: " + (string)isRegistered);
llMessageLinked(LINK_SET, MSG_OWNER_STATUS_RESULT,
name + "|" + (string)isRegistered + "|" + (string)isPending + "|" + (string)isStarter + "|" + requestId, id);
}
return;
}
if (num == MSG_TOGGLE_READY) {
string name = str;
integer idx = llListFindList(readyPlayers, [name]);
if (idx == -1) {
readyPlayers += [name];
publicMsg("GAME|" + name + " is NOW READY!");
} else {
readyPlayers = llDeleteSubList(readyPlayers, idx, idx);
publicMsg("GAME|" + name + " is NO LONGER READY.");
}
// Broadcast the official state list to all scripts (Main Controller needs this)
llMessageLinked(LINK_SET, MSG_UPDATE_MAIN_LISTS, "READY_LIST|" + llList2CSV(readyPlayers), NULL_KEY);
return;
}
if (num == MSG_REMOVE_PLAYER) {
string leavingName = str;
integer idx = llListFindList(names, [leavingName]);
if (idx != -1) {
// Scrub from all synchronized lists
players = llDeleteSubList(players, idx, idx);
names = llDeleteSubList(names, idx, idx);
lives = llDeleteSubList(lives, idx, idx);
// ALSO scrub from ready list if they were there
integer rIdx = llListFindList(readyPlayers, [leavingName]);
if (rIdx != -1) readyPlayers = llDeleteSubList(readyPlayers, rIdx, rIdx);
dbg("🎯 [RegMgr] Deep Scrub Complete: " + leavingName + " removed from all registries.");
// Broadcast updated lists to keep Main Controller in sync
llMessageLinked(LINK_SET, MSG_UPDATE_MAIN_LISTS, "READY_LIST|" + llList2CSV(readyPlayers), NULL_KEY);
}
return;
}
// Handle dialog forwarding requests from Game Manager
if (num == MSG_DIALOG_FORWARD_REQUEST) {
list parts = llParseString2List(str, ["|"], []);
list parts = llParseString2List(str, ["~"], []); // Fixed delimiter to match forwarder
if (llGetListLength(parts) >= 3) {
string dialogType = llList2String(parts, 0); // "SHOW_DIALOG" or "SHOW_ROLL_DIALOG"
string dialogType = llList2String(parts, 0);
string targetPlayerName = llList2String(parts, 1);
// Reconstruct dialog payload from remaining parts
string dialogPayload = "";
integer i;
for (i = 2; i < llGetListLength(parts); i++) {
if (i > 2) dialogPayload += "|"; // Add separator between parts
if (i > 2) dialogPayload += "|";
dialogPayload += llList2String(parts, i);
}
// Find player key in our authoritative registry
integer playerIdx = llListFindList(names, [targetPlayerName]);
if (playerIdx != -1 && playerIdx < llGetListLength(players)) {
key playerKey = llList2Key(players, playerIdx);
if (playerKey != NULL_KEY) {
if (dialogType == "SHOW_DIALOG") {
llMessageLinked(LINK_SET, MSG_SHOW_DIALOG, dialogPayload, playerKey);
dbg("📋 [RegMgr] Forwarded dialog to " + targetPlayerName + ": " + dialogPayload);
} else if (dialogType == "SHOW_ROLL_DIALOG") {
llMessageLinked(LINK_SET, MSG_SHOW_ROLL_DIALOG, dialogPayload, playerKey);
dbg("🎲 [RegMgr] Forwarded roll dialog to " + targetPlayerName);
}
} else {
dbg("⚠️ [RegMgr] Player " + targetPlayerName + " has NULL_KEY");
}
} else {
dbg("⚠️ [RegMgr] Player " + targetPlayerName + " not found in registry");
}
}
return;
}
if (num == MSG_RESET_ALL && str == "FULL_RESET") {
players = [];
names = [];
lives = [];
readyPlayers = [];
roundStarted = FALSE;
gameStarting = FALSE;
dbg("🎯 [RegMgr] Player Registration Manager reset complete");
resetInProgress = TRUE;
players = []; names = []; lives = []; readyPlayers = [];
roundStarted = FALSE; gameStarting = FALSE;
// Activate brief lockout timer to ignore stale syncs while Linkset settles
llSetTimerEvent(0.2);
dbg("🎯 [RegMgr] Player Registration Manager reset complete (Lockout Active)");
return;
}
}
timer() {
resetInProgress = FALSE;
llSetTimerEvent(0);
dbg("🎯 [RegMgr] Reset lockout complete - sync active.");
}
}

View file

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