refactor: stabilize game state synchronization, improve reset timing, and integrate authoritative leaderboard reporting.
This commit is contained in:
parent
0974d19f12
commit
1e70b70b72
10 changed files with 501 additions and 250 deletions
|
|
@ -47,6 +47,7 @@ integer listenHandle = -1;
|
||||||
#define MEMORY_WARNING_THRESHOLD 0.8 // Warn when using >80% of memory
|
#define MEMORY_WARNING_THRESHOLD 0.8 // Warn when using >80% of memory
|
||||||
|
|
||||||
// Track game state to validate bot commands
|
// Track game state to validate bot commands
|
||||||
|
list players = [];
|
||||||
list names = [];
|
list names = [];
|
||||||
list lives = [];
|
list lives = [];
|
||||||
string perilPlayer = "";
|
string perilPlayer = "";
|
||||||
|
|
@ -236,6 +237,7 @@ default {
|
||||||
// Handle full reset from main controller
|
// Handle full reset from main controller
|
||||||
if (num == MSG_RESET_ALL && str == "FULL_RESET") {
|
if (num == MSG_RESET_ALL && str == "FULL_RESET") {
|
||||||
// Reset bot manager state
|
// Reset bot manager state
|
||||||
|
players = [];
|
||||||
names = [];
|
names = [];
|
||||||
lives = [];
|
lives = [];
|
||||||
perilPlayer = "";
|
perilPlayer = "";
|
||||||
|
|
@ -247,7 +249,7 @@ default {
|
||||||
|
|
||||||
// Handle game state sync to track player eliminations
|
// Handle game state sync to track player eliminations
|
||||||
if (num == MSG_SYNC_GAME_STATE) {
|
if (num == MSG_SYNC_GAME_STATE) {
|
||||||
list parts = llParseString2List(str, ["~"], []);
|
list parts = llParseStringKeepNulls(str, ["~"], []);
|
||||||
if (llGetListLength(parts) >= 4) {
|
if (llGetListLength(parts) >= 4) {
|
||||||
lives = llCSV2List(llList2String(parts, 0));
|
lives = llCSV2List(llList2String(parts, 0));
|
||||||
// Don't need picks data for validation, skip parts[1]
|
// Don't need picks data for validation, skip parts[1]
|
||||||
|
|
@ -274,13 +276,12 @@ default {
|
||||||
sentBotMessages = [];
|
sentBotMessages = [];
|
||||||
dbg("🤖 [Bot Manager] New round detected (peril change), cleared processed commands and sent messages");
|
dbg("🤖 [Bot Manager] New round detected (peril change), cleared processed commands and sent messages");
|
||||||
} else {
|
} else {
|
||||||
// Only clear if we detect truly empty picks data during an active game
|
// Standardized ~EMPTY~ token logic
|
||||||
// Don't clear during initial registration (when picks = "EMPTY")
|
|
||||||
string picksStr = llList2String(parts, 1);
|
string picksStr = llList2String(parts, 1);
|
||||||
if (picksStr != "EMPTY" && (picksStr == "" || llSubStringIndex(picksStr, "|") == -1) && perilPlayer != "") {
|
if (picksStr == "~EMPTY~" && perilPlayer != "") {
|
||||||
processedBotCommands = [];
|
processedBotCommands = [];
|
||||||
sentBotMessages = [];
|
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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -91,8 +91,9 @@ default {
|
||||||
// Set the Top Line Header on the main box
|
// Set the Top Line Header on the main box
|
||||||
llMessageLinked(LINK_SET, 0, "=== WORLD RANKING ===", (key)"fw_data:Leaderboard");
|
llMessageLinked(LINK_SET, 0, "=== WORLD RANKING ===", (key)"fw_data:Leaderboard");
|
||||||
|
|
||||||
// Trigger Leaderboard Manager to download from KVP Database
|
// Note: Leaderboard sync should be triggered by the Main Controller or Game Manager
|
||||||
llMessageLinked(LINK_SET, MSG_RESET_LEADERBOARD, "START_SYNC", NULL_KEY);
|
// 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)
|
// Start Blink Timer (0.5s intervals)
|
||||||
llSetTimerEvent(0.5);
|
llSetTimerEvent(0.5);
|
||||||
|
|
@ -148,5 +149,6 @@ default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else if (num == MSG_RESET_ALL) { llResetScript(); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -123,6 +123,7 @@ default {
|
||||||
// Handle full reset from main controller
|
// Handle full reset from main controller
|
||||||
if (num == MSG_RESET_ALL && str == "FULL_RESET") {
|
if (num == MSG_RESET_ALL && str == "FULL_RESET") {
|
||||||
allPlayerNames = [];
|
allPlayerNames = [];
|
||||||
|
allPlayerKeys = [];
|
||||||
names = [];
|
names = [];
|
||||||
players = [];
|
players = [];
|
||||||
lives = [];
|
lives = [];
|
||||||
|
|
@ -198,7 +199,7 @@ default {
|
||||||
|
|
||||||
integer syncIdx = llListFindList(names, [name]);
|
integer syncIdx = llListFindList(names, [name]);
|
||||||
key avKey = id;
|
key avKey = id;
|
||||||
integer lifeCount = 0;
|
integer lifeCount = 3; // DEFAULT TO HEALTHY (Prevent Red Flashes)
|
||||||
|
|
||||||
if (syncIdx != -1) {
|
if (syncIdx != -1) {
|
||||||
if (syncIdx < llGetListLength(players)) avKey = llList2Key(players, syncIdx);
|
if (syncIdx < llGetListLength(players)) avKey = llList2Key(players, syncIdx);
|
||||||
|
|
@ -228,14 +229,15 @@ default {
|
||||||
|
|
||||||
string txt = "🎲 Peril Dice\n👤 " + name + "\n" + perilStatus + "\n🔢 Picks: " + picksDisplay;
|
string txt = "🎲 Peril Dice\n👤 " + name + "\n" + perilStatus + "\n🔢 Picks: " + picksDisplay;
|
||||||
|
|
||||||
if (lifeCount <= 0) {
|
// COLOR PRIORITY: Green (Victory) > Red (Eliminated) > Yellow (Peril)
|
||||||
glow = 0.3;
|
if (livingPlayers == 1 && llGetListLength(names) >= 2 && lifeCount > 0) {
|
||||||
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;
|
glow = 0.3;
|
||||||
color = <0,1,0>; // Green
|
color = <0,1,0>; // Green
|
||||||
txt = "🎲 Peril Dice\n👤 " + name + "\n✨ ULTIMATE VICTORY! ✨\n🏆 ULTIMATE SURVIVOR 🏆\n🔢 Final Picks: " + picksDisplay;
|
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) {
|
} else if (name == perilPlayer) {
|
||||||
glow = 0.2;
|
glow = 0.2;
|
||||||
color = <1,1,0>; // Yellow
|
color = <1,1,0>; // Yellow
|
||||||
|
|
@ -309,12 +311,12 @@ default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (num == MSG_SYNC_GAME_STATE) {
|
else if (num == MSG_SYNC_GAME_STATE) {
|
||||||
list parts = llParseString2List(str, ["~"], []);
|
list parts = llParseStringKeepNulls(str, ["~"], []);
|
||||||
if (llGetListLength(parts) < 4) return;
|
if (llGetListLength(parts) < 4) return;
|
||||||
|
|
||||||
lives = llCSV2List(llList2String(parts, 0));
|
lives = llCSV2List(llList2String(parts, 0));
|
||||||
string picksDataStr = llList2String(parts, 1);
|
string picksDataStr = llList2String(parts, 1);
|
||||||
if (picksDataStr == "" || picksDataStr == "EMPTY") {
|
if (picksDataStr == "" || picksDataStr == "EMPTY" || picksDataStr == "~EMPTY~") {
|
||||||
picksData = [];
|
picksData = [];
|
||||||
} else {
|
} else {
|
||||||
picksData = llParseString2List(picksDataStr, ["^"], []);
|
picksData = llParseString2List(picksDataStr, ["^"], []);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,9 @@
|
||||||
#include "Peril_Constants.lsl"
|
#include "Peril_Constants.lsl"
|
||||||
|
|
||||||
|
debugMsg(string msg) {
|
||||||
|
if (DEBUG_LOGS) llMessageLinked(LINK_SET, MSG_DEBUG_TEXT, msg, NULL_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// PERIL WORLD RANKING - Leaderboard Manager
|
// PERIL WORLD RANKING - Leaderboard Manager
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
@ -11,7 +15,9 @@ list leaderboardData = []; // Stride 3: [Name, Wins, Losses]
|
||||||
integer currentLoadPage = 1;
|
integer currentLoadPage = 1;
|
||||||
key kvpReadReq;
|
key kvpReadReq;
|
||||||
key kvpWriteReq;
|
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]
|
list writeQueue = []; // Pairs of [KeyName, DataString]
|
||||||
integer isSaving = FALSE;
|
integer isSaving = FALSE;
|
||||||
integer isBusy = FALSE; // Read lock
|
integer isBusy = FALSE; // Read lock
|
||||||
|
|
@ -21,18 +27,25 @@ string currentWriteKey;
|
||||||
string currentWriteVal;
|
string currentWriteVal;
|
||||||
|
|
||||||
// --- Lock & Sync State ---
|
// --- Lock & Sync State ---
|
||||||
string pendingPlayer = "";
|
list pendingRecords = []; // Stride 2: [Name, Result]
|
||||||
string pendingResult = "";
|
|
||||||
integer isLocking = FALSE;
|
integer isLocking = FALSE;
|
||||||
string myLockVal = "";
|
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
|
// Helper: Process the next KVP write in the queue
|
||||||
processWriteQueue() {
|
processWriteQueue() {
|
||||||
if (llGetListLength(writeQueue) == 0) {
|
if (llGetListLength(writeQueue) == 0) {
|
||||||
isSaving = FALSE;
|
isSaving = FALSE;
|
||||||
dbg("🏆 [Leaderboard] Save sequence complete. Releasing lock...");
|
// Batch Processing: Check if more records are queued before releasing lock
|
||||||
// releaseLock();
|
if (llGetListLength(pendingRecords) > 0) {
|
||||||
kvpLockReq = llUpdateKeyValue(LB_LOCK_KEY, "", TRUE, myLockVal);
|
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;
|
return;
|
||||||
}
|
}
|
||||||
isSaving = TRUE;
|
isSaving = TRUE;
|
||||||
|
|
@ -42,6 +55,12 @@ processWriteQueue() {
|
||||||
kvpWriteReq = llUpdateKeyValue(currentWriteKey, currentWriteVal, FALSE, "");
|
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
|
// Convert the whole list into sharded strings and start saving
|
||||||
generateShardedData() {
|
generateShardedData() {
|
||||||
integer _shardI;
|
integer _shardI;
|
||||||
|
|
@ -76,20 +95,30 @@ generateShardedData() {
|
||||||
}
|
}
|
||||||
|
|
||||||
loadLBPage(integer pageNum) {
|
loadLBPage(integer pageNum) {
|
||||||
if (isBusy && pageNum == 1) return; // Prevent double loads on reset
|
if ((isBusy || isSaving || isLocking) && pageNum == 1) {
|
||||||
if (pageNum > 1) llSleep(1.0); // Anti-Throttle Delay
|
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;
|
isBusy = TRUE;
|
||||||
llSetTimerEvent(30.0); // 30 second watchdog (Lag Resistant)
|
llSetTimerEvent(30.0); // Lag Resistant watchdog
|
||||||
currentLoadPage = pageNum;
|
currentLoadPage = pageNum;
|
||||||
string keyName = LB_KEY_PREFIX + (string)pageNum;
|
string keyName = LB_KEY_PREFIX + (string)pageNum;
|
||||||
|
|
||||||
kvpReadReq = llReadKeyValue(keyName);
|
kvpReadReq = llReadKeyValue(keyName);
|
||||||
|
|
||||||
if (kvpReadReq == NULL_KEY) {
|
if (kvpReadReq == NULL_KEY) {
|
||||||
dbg("❌ [Leaderboard] KVP Read FAILED: Not authorized for " + keyName);
|
dbg("⚠️ [Leaderboard] KVP Request FAILED (Throttle/NULL_KEY). Retrying " + keyName + " in 2s...");
|
||||||
llSetTimerEvent(0.0);
|
isBusy = FALSE; // Unlock for retry
|
||||||
} else {
|
llSetTimerEvent(2.0); // Use timer to trigger the retry
|
||||||
dbg("📡 [Leaderboard] Loading " + keyName + "...");
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dbg("📡 [Leaderboard] Loading " + keyName + "...");
|
||||||
}
|
}
|
||||||
|
|
||||||
list getSortedSlice(integer offset) {
|
list getSortedSlice(integer offset) {
|
||||||
|
|
@ -135,15 +164,19 @@ updatePlayerRecord(string pName, string pResult) {
|
||||||
}
|
}
|
||||||
|
|
||||||
startUpdateSequence(string pName, string pResult) {
|
startUpdateSequence(string pName, string pResult) {
|
||||||
if (isLocking || isSaving) {
|
llSetTimerEvent(0); // GHOST-BUSTER: Kill any pending retries/syncs
|
||||||
dbg("⚠️ [Leaderboard] Update already in progress. Retrying in 2s...");
|
|
||||||
llSleep(2.0);
|
// 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;
|
isLocking = TRUE;
|
||||||
|
|
||||||
dbg("🔐 [Leaderboard] Checking global lock for update...");
|
dbg("🔐 [Leaderboard] Checking global lock for update...");
|
||||||
kvpLockReq = llReadKeyValue(LB_LOCK_KEY);
|
kvpLockReq = llReadKeyValue(LB_LOCK_KEY);
|
||||||
}
|
}
|
||||||
|
|
@ -153,18 +186,45 @@ default {
|
||||||
|
|
||||||
state_entry() {
|
state_entry() {
|
||||||
REPORT_MEMORY();
|
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) {
|
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
|
llSetTimerEvent(0.0); // Reset watchdog
|
||||||
integer _c = llSubStringIndex(_qd, ",");
|
kvpSentinelReq = NULL_KEY; // Kill it once used
|
||||||
string _st = _qd; string _vl = "";
|
|
||||||
if (_c != -1) {
|
if (_st == "1" || _st == "3") { // Success (1) or Not Found (3) prove access
|
||||||
_st = llGetSubString(_qd, 0, _c - 1);
|
dbg("✅ [Leaderboard] Experience Sentinel: KVP Access Confirmed. Starting Auto-Sync...");
|
||||||
_vl = llGetSubString(_qd, _c + 1, -1);
|
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 (_st == "1") { // SUCCESS: Data found
|
||||||
if (_vl != "" && llSubStringIndex(_vl, "Error") == -1) {
|
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
|
retryCount = 0; // Reset retries for next page
|
||||||
loadLBPage(currentLoadPage + 1);
|
loadLBPage(currentLoadPage + 1);
|
||||||
} else {
|
} else {
|
||||||
isBusy = FALSE;
|
if (isBusy) {
|
||||||
llSetTimerEvent(0);
|
isBusy = FALSE;
|
||||||
dbg("🏆 [Leaderboard] SYNC COMPLETE - " + (string)(llGetListLength(leaderboardData)/3) + " players.");
|
llSetTimerEvent(0);
|
||||||
llMessageLinked(LINK_SET, MSG_LB_LOAD_COMPLETE, "", NULL_KEY);
|
dbg("🏆 [Leaderboard] SYNC COMPLETE - " + (string)(llGetListLength(leaderboardData)/3) + " players.");
|
||||||
llMessageLinked(LINK_SET, MSG_LB_REQUEST_DISPLAY, "0", NULL_KEY); // Show page 1
|
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 WE WERE SYNCING FOR AN UPDATE, APPLY IT NOW
|
||||||
if (isLocking) {
|
if (isSaving && llGetListLength(pendingRecords) > 0) {
|
||||||
isLocking = FALSE;
|
isSaving = FALSE;
|
||||||
updatePlayerRecord(pendingPlayer, pendingResult);
|
string nextName = llList2String(pendingRecords, 0);
|
||||||
|
string nextRes = llList2String(pendingRecords, 1);
|
||||||
|
pendingRecords = llDeleteSubList(pendingRecords, 0, 1);
|
||||||
|
updatePlayerRecord(nextName, nextRes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (_st == "0" && _vl == "1") {
|
} else if (_st == "0") {
|
||||||
// XP_ERROR_THROTTLED
|
if (_vl == "1") { // XP_ERROR_THROTTLED
|
||||||
dbg("⚠️ [Leaderboard] Read throttled. Retrying Shard " + (string)currentLoadPage + " in 2s...");
|
dbg("⚠️ [Leaderboard] Read throttled. Retrying Shard " + (string)currentLoadPage + " in 2s...");
|
||||||
llSleep(2.0);
|
llSleep(2.0);
|
||||||
isBusy = FALSE;
|
isBusy = FALSE;
|
||||||
loadLBPage(currentLoadPage);
|
loadLBPage(currentLoadPage);
|
||||||
} else { // Empty or Error
|
} else if (_vl == "14") { // XP_ERROR_KEY_NOT_FOUND (Value is empty)
|
||||||
isBusy = FALSE;
|
isBusy = FALSE;
|
||||||
llSetTimerEvent(0);
|
llSetTimerEvent(0);
|
||||||
if (_st == "0") { dbg("❌ [Leaderboard] KVP Error on Shard " + (string)currentLoadPage + ": " + _vl); }
|
dbg("🏆 [Leaderboard] Shard " + (string)currentLoadPage + " is empty (New).");
|
||||||
dbg("🏆 [Leaderboard] SYNC COMPLETE - " + (string)(llGetListLength(leaderboardData)/3) + " players total.");
|
|
||||||
llMessageLinked(LINK_SET, MSG_LB_LOAD_COMPLETE, "", NULL_KEY);
|
if (currentLoadPage < 10) {
|
||||||
llMessageLinked(LINK_SET, MSG_LB_REQUEST_DISPLAY, "0", NULL_KEY);
|
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) {
|
else if (_qid == kvpWriteReq) {
|
||||||
|
|
@ -228,64 +308,94 @@ default {
|
||||||
_vl = llGetSubString(_qd, _c + 1, -1);
|
_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
|
// XP_ERROR_THROTTLED
|
||||||
dbg("⚠️ [Leaderboard] Write throttled. Retrying " + currentWriteKey + " in 2s...");
|
dbg("⚠️ [Leaderboard] Write throttled. Retrying " + currentWriteKey + " in 2s...");
|
||||||
llSleep(2.0);
|
llSleep(2.0);
|
||||||
kvpWriteReq = llUpdateKeyValue(currentWriteKey, currentWriteVal, FALSE, "");
|
kvpWriteReq = llUpdateKeyValue(currentWriteKey, currentWriteVal, FALSE, "");
|
||||||
} else {
|
} else {
|
||||||
if (_st == "0") { dbg("❌ [Leaderboard] Write Error (" + _vl + ") on " + currentWriteKey); }
|
dbg("❌ [Leaderboard] Write Error (" + _st + ":" + _vl + ") on " + currentWriteKey);
|
||||||
processWriteQueue();
|
processWriteQueue();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (_qid == kvpLockReq) {
|
else if (_qid == kvpClaimReq) {
|
||||||
integer _c = llSubStringIndex(_qd, ",");
|
kvpClaimReq = NULL_KEY; // DUPLICATE DEFENSE: Prevent zombie processing
|
||||||
string _st = _qd; string _vl = "";
|
if (_st == "1") {
|
||||||
if (_c != -1) {
|
dbg("🔐 [Leaderboard] LOCK ACQUIRED. Starting fresh sync...");
|
||||||
_st = llGetSubString(_qd, 0, _c - 1);
|
isClaiming = FALSE;
|
||||||
_vl = llGetSubString(_qd, _c + 1, -1);
|
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 (isLocking) {
|
||||||
// STAGE 1: Current Lock Read Result
|
if (_st == "1" && _c != -1) {
|
||||||
if (_st == "1") { // Success read
|
string _curVal = _rawVal; // USE RAW DATA FOR ATOMIC CHECK
|
||||||
string _curVal = _vl;
|
|
||||||
integer _isStale = FALSE;
|
integer _isStale = FALSE;
|
||||||
|
list _p = [];
|
||||||
|
integer _ts = 0;
|
||||||
|
|
||||||
if (_curVal != "") {
|
if (_curVal != "") {
|
||||||
list _p = llParseString2List(_curVal, [":"], []);
|
_p = llParseString2List(_curVal, [":"], []);
|
||||||
integer _ts = (integer)llList2String(_p, 1);
|
_ts = (integer)llList2String(_p, 1);
|
||||||
if (llGetUnixTime() - _ts > LB_LOCK_TIMEOUT) _isStale = TRUE;
|
if (llGetUnixTime() - _ts > LB_LOCK_TIMEOUT) _isStale = TRUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_curVal == "" || _isStale) {
|
if (llList2String(_p, 0) == (string)llGetKey()) {
|
||||||
dbg("🔐 [Leaderboard] Lock available. Claiming...");
|
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();
|
myLockVal = (string)llGetKey() + ":" + (string)llGetUnixTime();
|
||||||
// ATOMIC UPDATE: Only write if value is still what we just read
|
dbg("🔓 [Leaderboard] CLAIMING WITH: [" + myLockVal + "] (Len: " + (string)llStringLength(myLockVal) + ")");
|
||||||
kvpLockReq = llUpdateKeyValue(LB_LOCK_KEY, myLockVal, TRUE, _curVal);
|
isClaiming = TRUE;
|
||||||
|
kvpClaimReq = llUpdateKeyValue(LB_LOCK_KEY, myLockVal, TRUE, _curVal);
|
||||||
} else {
|
} 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);
|
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
|
} else if (_st == "0" && _vl == "1") { // Throttled
|
||||||
dbg("⚠️ [Leaderboard] Lock check throttled. Retrying...");
|
dbg("⚠️ [Leaderboard] Lock check throttled. Retrying...");
|
||||||
llSleep(2.0);
|
llSleep(2.0);
|
||||||
kvpLockReq = llReadKeyValue(LB_LOCK_KEY);
|
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 {
|
} else {
|
||||||
dbg("❌ [Leaderboard] Lock Error: (" + _st + ") " + _vl);
|
dbg("❌ [Leaderboard] Lock logic failed (Code " + _st + ":" + _vl + ")");
|
||||||
isLocking = FALSE;
|
isLocking = FALSE;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// RELEASE RESULT
|
// RELEASE RESULT: Check for Status 1 (Success)
|
||||||
if (_st == "14") { dbg("🔓 [Leaderboard] Lock released."); }
|
if (_st == "1") { dbg("🔓 [Leaderboard] Lock released."); }
|
||||||
else { dbg("⚠️ [Leaderboard] Lock release issues: " + _vl); }
|
else { dbg("⚠️ [Leaderboard] Lock release report: " + _st + ":" + _vl); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -297,16 +407,16 @@ default {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isBusy) {
|
if (isBusy || (kvpReadReq == NULL_KEY && currentLoadPage > 0)) {
|
||||||
if (retryCount < 3) {
|
if (retryCount < 3) {
|
||||||
retryCount++;
|
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
|
isBusy = FALSE; // Unlock so loadLBPage can run
|
||||||
loadLBPage(currentLoadPage);
|
loadLBPage(currentLoadPage);
|
||||||
} else {
|
} else {
|
||||||
isBusy = FALSE;
|
isBusy = FALSE;
|
||||||
llSetTimerEvent(0);
|
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);
|
llMessageLinked(LINK_SET, MSG_LB_REQUEST_DISPLAY, "0", NULL_KEY);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -316,7 +426,9 @@ default {
|
||||||
|
|
||||||
link_message(integer sender, integer num, string str, key id) {
|
link_message(integer sender, integer num, string str, key id) {
|
||||||
if (num == MSG_LB_RECORD_PLAYER) {
|
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) {
|
else if (num == MSG_DISPLAY_LEADERBOARD) {
|
||||||
if (str == "REFRESH_STARTUP") loadLBPage(1);
|
if (str == "REFRESH_STARTUP") loadLBPage(1);
|
||||||
|
|
@ -385,6 +497,8 @@ default {
|
||||||
generateShardedData();
|
generateShardedData();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (num == MSG_RESET_ALL) { llResetScript(); }
|
else if (num == MSG_RESET_ALL) {
|
||||||
|
if (str == "FULL_RESET" || str == "HARD_RESET") llResetScript();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ integer roundStarted = FALSE;
|
||||||
integer diceTypeProcessed = FALSE; // Track if we've already processed dice type for this round
|
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 ignorePicksSync = FALSE; // Temporarily ignore picks data sync during round initialization
|
||||||
integer roundContinueInProgress = FALSE; // Prevent multiple rapid calls to continueCurrentRound
|
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
|
integer lastSyncProcessTime = 0; // Track when we last processed a sync to prevent rapid duplicates
|
||||||
|
|
||||||
// Duplicate request prevention
|
// Duplicate request prevention
|
||||||
|
|
@ -149,24 +150,28 @@ integer continueCurrentRound() {
|
||||||
}
|
}
|
||||||
currentPickerIdx = 0;
|
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
|
integer currentPlayerCount = llGetListLength(names);
|
||||||
llMessageLinked(LINK_SET, MSG_GET_DICE_TYPE, (string)llGetListLength(names), NULL_KEY);
|
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
|
// Note: showNextPickerDialog() will be called when dice type result is received (or immediately if cached)
|
||||||
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;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -249,16 +254,25 @@ integer startNextRound() {
|
||||||
currentPickerIdx = 0;
|
currentPickerIdx = 0;
|
||||||
dbg("🎯 Debug - Created pickQueue: " + llList2CSV(pickQueue) + ", currentPickerIdx: " + (string)currentPickerIdx);
|
dbg("🎯 Debug - Created pickQueue: " + llList2CSV(pickQueue) + ", currentPickerIdx: " + (string)currentPickerIdx);
|
||||||
|
|
||||||
// Request dice type for this round
|
integer currentPlayerCount = llGetListLength(names);
|
||||||
llMessageLinked(LINK_SET, MSG_GET_DICE_TYPE, (string)llGetListLength(names), NULL_KEY);
|
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
|
dbg("🎯 Game Manager round setup complete.");
|
||||||
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;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -484,7 +498,7 @@ default {
|
||||||
// Only skip sync messages if we're explicitly ignoring them
|
// Only skip sync messages if we're explicitly ignoring them
|
||||||
// Allow player registration syncs even when game is not active
|
// Allow player registration syncs even when game is not active
|
||||||
|
|
||||||
list parts = llParseString2List(str, ["~"], []);
|
list parts = llParseStringKeepNulls(str, ["~"], []);
|
||||||
if (llGetListLength(parts) >= 4) {
|
if (llGetListLength(parts) >= 4) {
|
||||||
list newLives = llCSV2List(llList2String(parts, 0));
|
list newLives = llCSV2List(llList2String(parts, 0));
|
||||||
string currentLivesStr = llList2CSV(lives);
|
string currentLivesStr = llList2CSV(lives);
|
||||||
|
|
@ -509,7 +523,7 @@ default {
|
||||||
if (livesChanged || perilChanged) {
|
if (livesChanged || perilChanged) {
|
||||||
string encodedPicksDataStr = llList2String(parts, 1);
|
string encodedPicksDataStr = llList2String(parts, 1);
|
||||||
list newPicksData = [];
|
list newPicksData = [];
|
||||||
if (encodedPicksDataStr != "") {
|
if (encodedPicksDataStr != "" && encodedPicksDataStr != "~EMPTY~") {
|
||||||
list encodedEntries = llParseString2List(encodedPicksDataStr, ["^"], []);
|
list encodedEntries = llParseString2List(encodedPicksDataStr, ["^"], []);
|
||||||
integer syncI;
|
integer syncI;
|
||||||
for (syncI = 0; syncI < llGetListLength(encodedEntries); syncI++) {
|
for (syncI = 0; syncI < llGetListLength(encodedEntries); syncI++) {
|
||||||
|
|
@ -551,10 +565,8 @@ default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (livesChanged && newPerilPlayer != "" && newPerilPlayer != "NONE" && roundStarted && allPicksEmpty) {
|
// Note: Round continuation is now handled by the Main Controller's explicit MSG_CONTINUE_ROUND signal
|
||||||
dbg("🎯 Post-roll state update detected - continuing round");
|
// This prevents duplicate round starts and race conditions during Plot Twists.
|
||||||
continueCurrentRound();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
|
@ -778,7 +790,7 @@ default {
|
||||||
// Use complete avoid list for re-shown dialog too
|
// Use complete avoid list for re-shown dialog too
|
||||||
list updatedCompleteAvoidList = buildCompleteAvoidanceList();
|
list updatedCompleteAvoidList = buildCompleteAvoidanceList();
|
||||||
string dialogPayload = playerName + "|" + (string)diceType + "|" + (string)picksNeeded + "|" + llList2CSV(updatedCompleteAvoidList);
|
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);
|
llMessageLinked(LINK_SET, MSG_DIALOG_FORWARD_REQUEST, dialogRequest, NULL_KEY);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
|
@ -839,7 +851,7 @@ default {
|
||||||
|
|
||||||
// Send roll dialog through Player_RegistrationManager
|
// Send roll dialog through Player_RegistrationManager
|
||||||
dbg("🎯 [Game Manager] Sending roll dialog request for: " + perilPlayer);
|
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);
|
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);
|
llMessageLinked(LINK_SET, MSG_BOT_COMMAND, "CLOSE_ALL_DIALOGS", NULL_KEY);
|
||||||
|
|
||||||
// Send roll dialog through Player_RegistrationManager
|
// 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);
|
llMessageLinked(LINK_SET, MSG_DIALOG_FORWARD_REQUEST, rollRequest, NULL_KEY);
|
||||||
}
|
}
|
||||||
return; // Ignore duplicate - bot already has real picks
|
return; // Ignore duplicate - bot already has real picks
|
||||||
|
|
@ -1008,7 +1020,7 @@ default {
|
||||||
llMessageLinked(LINK_SET, MSG_BOT_COMMAND, "CLOSE_ALL_DIALOGS", NULL_KEY);
|
llMessageLinked(LINK_SET, MSG_BOT_COMMAND, "CLOSE_ALL_DIALOGS", NULL_KEY);
|
||||||
|
|
||||||
// Send roll dialog through Player_RegistrationManager
|
// 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);
|
llMessageLinked(LINK_SET, MSG_DIALOG_FORWARD_REQUEST, rollRequest, NULL_KEY);
|
||||||
} else {
|
} else {
|
||||||
// Move to next picker
|
// Move to next picker
|
||||||
|
|
@ -1065,6 +1077,7 @@ default {
|
||||||
currentPicker = NULL_KEY;
|
currentPicker = NULL_KEY;
|
||||||
roundStarted = FALSE;
|
roundStarted = FALSE;
|
||||||
diceTypeProcessed = FALSE; // Reset for new game
|
diceTypeProcessed = FALSE; // Reset for new game
|
||||||
|
lastPlayerCount = 0; // Reset dice memory for new game
|
||||||
roundContinueInProgress = FALSE; // Reset protection flag
|
roundContinueInProgress = FALSE; // Reset protection flag
|
||||||
lastSyncProcessTime = 0; // Reset sync timing
|
lastSyncProcessTime = 0; // Reset sync timing
|
||||||
dbg("🎯 Game Manager reset - ready for new game!");
|
dbg("🎯 Game Manager reset - ready for new game!");
|
||||||
|
|
|
||||||
|
|
@ -107,9 +107,10 @@ clearAllPlayers() {
|
||||||
integer _hL = getHeartsPrimLink(_ca_i);
|
integer _hL = getHeartsPrimLink(_ca_i);
|
||||||
integer _oL = getOverlayPrimLink(_ca_i);
|
integer _oL = getOverlayPrimLink(_ca_i);
|
||||||
if (_pL > 0) {
|
if (_pL > 0) {
|
||||||
llSetLinkPrimitiveParamsFast(_pL, [PRIM_COLOR, ALL_SIDES, <1,1,1>, 0.0]);
|
// Explicitly reset textures to blank to prevent profile persistence
|
||||||
llSetLinkPrimitiveParamsFast(_hL, [PRIM_COLOR, ALL_SIDES, <1,1,1>, 0.0]);
|
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(_oL, [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 {
|
default {
|
||||||
on_rez(integer _sp) { discoverLinks(); llResetScript(); }
|
on_rez(integer _sp) { discoverLinks(); clearAllPlayers(); llResetScript(); }
|
||||||
state_entry() {
|
state_entry() {
|
||||||
discoverLinks();
|
discoverLinks();
|
||||||
llListen(1, "", llGetOwner(), "");
|
llListen(1, "", llGetOwner(), "");
|
||||||
REPORT_MEMORY();
|
REPORT_MEMORY();
|
||||||
|
clearAllPlayers(); // Ensure board is clean on initialization
|
||||||
resetBackgroundPrim();
|
resetBackgroundPrim();
|
||||||
resetManagerCube();
|
resetManagerCube();
|
||||||
|
updateActionsPrim("Title");
|
||||||
updatePlayerGlowEffects();
|
updatePlayerGlowEffects();
|
||||||
dbg("Ready");
|
dbg("Ready");
|
||||||
}
|
}
|
||||||
|
|
@ -256,8 +259,10 @@ default {
|
||||||
currentPerilPlayer = ""; currentWinner = ""; updatePlayerGlowEffects();
|
currentPerilPlayer = ""; currentWinner = ""; updatePlayerGlowEffects();
|
||||||
llMessageLinked(LINK_SET, MSG_LB_REQUEST_DISPLAY, "", NULL_KEY);
|
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_WON || _n == MSG_GAME_LOST) {
|
||||||
else if (_n == MSG_GAME_LOST) llMessageLinked(LINK_SET, MSG_LB_RECORD_PLAYER, _st + "|LOSS", NULL_KEY);
|
// 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) {
|
else if (_n == MSG_LB_DISPLAY_DATA) {
|
||||||
string _tl = "WORLD RANKING RECORD"; string _sp = " ";
|
string _tl = "WORLD RANKING RECORD"; string _sp = " ";
|
||||||
integer _m = (LEADERBOARD_WIDTH - llStringLength(_tl)) / 2;
|
integer _m = (LEADERBOARD_WIDTH - llStringLength(_tl)) / 2;
|
||||||
|
|
@ -307,9 +312,9 @@ default {
|
||||||
activePlayers = llListReplaceList(activePlayers, [_pU], (_pIdx * 3) + 2, (_pIdx * 3) + 2);
|
activePlayers = llListReplaceList(activePlayers, [_pU], (_pIdx * 3) + 2, (_pIdx * 3) + 2);
|
||||||
integer _pL = getProfilePrimLink(_pIdx);
|
integer _pL = getProfilePrimLink(_pIdx);
|
||||||
if (_pL > 0) {
|
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);
|
llSleep(DELAY_SCOREBOARD_REFRESH);
|
||||||
llSetLinkPrimitiveParamsFast(_pL, [PRIM_GLOW, 0.0]);
|
llSetLinkPrimitiveParamsFast(_pL, [PRIM_GLOW, ALL_SIDES, 0.0]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,7 @@ integer victoryInProgress = FALSE;
|
||||||
|
|
||||||
// Lockout system variables
|
// Lockout system variables
|
||||||
integer isLocked = FALSE;
|
integer isLocked = FALSE;
|
||||||
|
integer resetInProgress = FALSE; // Lockout flag for reset synchronization
|
||||||
key gameOwner;
|
key gameOwner;
|
||||||
|
|
||||||
list pickQueue = [];
|
list pickQueue = [];
|
||||||
|
|
@ -179,23 +180,34 @@ checkMemoryUsage(string context) {
|
||||||
// REMOVED: emergencyMemoryCleanup() - delegated to Controller_Memory.lsl
|
// REMOVED: emergencyMemoryCleanup() - delegated to Controller_Memory.lsl
|
||||||
// REMOVED: reportMemoryStats() - 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() {
|
checkExperience() {
|
||||||
dbg("🔍 [Peril Dice] Experience Sentinel: Starting functional diagnostic...");
|
dbg("🔍 [Peril Dice] Experience Sentinel: Starting functional heartbeat...");
|
||||||
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)
|
// PROBE 1: Land-Scope Check (Crucial for HUD auto-attachment)
|
||||||
if (!llAgentInExperience(llGetOwner())) {
|
if (!llAgentInExperience(llGetOwner())) {
|
||||||
llOwnerSay("⚠️ [Peril Dice] SYSTEM WARNING: Experience Features are currently BLOCKED on this parcel.");
|
llOwnerSay("🛡️ [Peril Dice] LAND NOTICE: Experience 'Final Girlz I.N.C.' is NOT enabled on this land.");
|
||||||
llOwnerSay("🛡️ [Peril Dice] TO FIX: Open 'About Land' -> 'Experiences' -> 'Add' and search for 'Final Girlz I.N.C.'");
|
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");
|
sentinelQueryID = llReadKeyValue("PERIL_SENTINEL");
|
||||||
llSetTimerEvent(10.0); // Increased to 10s for reliability
|
llSetTimerEvent(10.0);
|
||||||
currentTimerMode = TIMER_XP_CHECK;
|
currentTimerMode = TIMER_XP_CHECK;
|
||||||
} else {
|
} 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;
|
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() {
|
syncGameState() {
|
||||||
// Build sync message WITHOUT redundant player keys to save memory
|
// Build sync message WITHOUT redundant player keys to save memory
|
||||||
// Format: lives~picks~perilPlayer~names
|
// 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 syncMsg = llList2CSV(lives) + "~" +
|
||||||
(string)llDumpList2String(picksData, "^") + "~" +
|
picksStr + "~" +
|
||||||
(string)llList2String([perilPlayer, "NONE"], (perilPlayer == "")) + "~" +
|
(string)llList2String([perilPlayer, "NONE"], (perilPlayer == "")) + "~" +
|
||||||
llList2CSV(names);
|
llList2CSV(names);
|
||||||
|
|
||||||
llMessageLinked(LINK_SET, MSG_SYNC_GAME_STATE, syncMsg, NULL_KEY);
|
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() {
|
resetGame() {
|
||||||
|
resetInProgress = TRUE; // Start lockout
|
||||||
cleanupListeners();
|
cleanupListeners();
|
||||||
|
|
||||||
// Let Floater Manager handle cleanup intelligently
|
// 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);
|
llMessageLinked(LINK_SET, MSG_CLEANUP_ALL_FLOATERS, "RESET", NULL_KEY);
|
||||||
llSleep(DELAY_STATE_TRANSITION);
|
// Reduced delay for snappy restart
|
||||||
|
llSleep(0.1);
|
||||||
llSleep(DELAY_STATE_TRANSITION);
|
|
||||||
|
|
||||||
players = names = lives = picksData = globalPickedNumbers = readyPlayers = [];
|
players = names = lives = picksData = globalPickedNumbers = readyPlayers = [];
|
||||||
floaterChannels = [];
|
floaterChannels = [];
|
||||||
|
|
@ -317,6 +332,13 @@ resetGame() {
|
||||||
llMessageLinked(LINK_SCOREBOARD, MSG_UPDATE_WINNER, "", NULL_KEY); // Clear winner glow
|
llMessageLinked(LINK_SCOREBOARD, MSG_UPDATE_WINNER, "", NULL_KEY); // Clear winner glow
|
||||||
llMessageLinked(LINK_SET, MSG_CLEAR_DICE, "", NULL_KEY);
|
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).");
|
dbg("🎮 Game reset! All state cleared (including scoreboard).");
|
||||||
|
|
||||||
statusTimer = 0;
|
statusTimer = 0;
|
||||||
|
|
@ -328,15 +350,12 @@ resetGame() {
|
||||||
// NEW: Update status bar for idle state
|
// NEW: Update status bar for idle state
|
||||||
sendStatusMessage("PERIL DICE GAME\nTouch to Join!");
|
sendStatusMessage("PERIL DICE GAME\nTouch to Join!");
|
||||||
|
|
||||||
llSleep(DELAY_STATE_TRANSITION);
|
// Quick buffer for Linkset to settle
|
||||||
|
llSleep(0.2);
|
||||||
llSetTimerEvent(0);
|
llSetTimerEvent(0);
|
||||||
|
|
||||||
initListeners();
|
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 {
|
default {
|
||||||
|
|
@ -384,15 +403,17 @@ default {
|
||||||
}
|
}
|
||||||
|
|
||||||
touch_start(integer total_number) {
|
touch_start(integer total_number) {
|
||||||
// IGNORE TOUCHES INTENDED FOR THE LEADERBOARD
|
integer clickedLink = llDetectedLinkNumber(0);
|
||||||
string _rawNm = llGetLinkName(llDetectedLinkNumber(0));
|
string clickedName = llGetLinkName(clickedLink);
|
||||||
string _linkNm = llToLower(_rawNm);
|
|
||||||
|
|
||||||
// TEMPORARY DEBUG TO CATCH THE EXACT LINK NAME
|
// Ensure core links are discovered to prevent misidentification
|
||||||
dbg("🎯 [Touch Filter] Clicked Link Name: '" + _rawNm + "'");
|
if (LINK_LEADERBOARD_BRIDGE == -1) { DISCOVER_CORE_LINKS(); }
|
||||||
|
|
||||||
if (llSubStringIndex(_linkNm, "leaderboard") != -1 || llSubStringIndex(_linkNm, "furware") != -1 || llSubStringIndex(_linkNm, "lb_") != -1) {
|
// IGNORE TOUCHES ON UI BRIDGES ONLY
|
||||||
dbg("🛑 [Touch Filter] Bypassing menu for Leaderboard element!");
|
// 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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -544,8 +565,11 @@ default {
|
||||||
if (pidx != -1) {
|
if (pidx != -1) {
|
||||||
lives = llListReplaceList(lives, [newLifeCount], pidx, pidx);
|
lives = llListReplaceList(lives, [newLifeCount], pidx, pidx);
|
||||||
dbg("🛡️ [Main Controller] 💗 Damage Report Confirmed: " + hitPlayer + " -> " + (string)newLifeCount + " hearts.");
|
dbg("🛡️ [Main Controller] 💗 Damage Report Confirmed: " + hitPlayer + " -> " + (string)newLifeCount + " hearts.");
|
||||||
// Broadcast the OFFICIAL ledger to the entire Linkset
|
|
||||||
syncGameState();
|
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;
|
return;
|
||||||
|
|
@ -612,6 +636,9 @@ default {
|
||||||
llMessageLinked(LINK_SCOREBOARD, MSG_PLAYER_UPDATE,
|
llMessageLinked(LINK_SCOREBOARD, MSG_PLAYER_UPDATE,
|
||||||
eliminatedPlayer + "|0|" + (string)llList2Key(players, idx), NULL_KEY);
|
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
|
// CRITICAL: Check for victory condition BEFORE assigning new peril player
|
||||||
integer willCauseVictory = (llGetListLength(names) <= 2); // After removing one player
|
integer willCauseVictory = (llGetListLength(names) <= 2); // After removing one player
|
||||||
string potentialWinner = "";
|
string potentialWinner = "";
|
||||||
|
|
@ -629,6 +656,9 @@ default {
|
||||||
@found_winner;
|
@found_winner;
|
||||||
|
|
||||||
if (potentialWinner != "") {
|
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
|
// SEND WINNER GLOW to scoreboard and trigger floater update
|
||||||
llMessageLinked(LINK_SCOREBOARD, MSG_UPDATE_WINNER, potentialWinner, NULL_KEY);
|
llMessageLinked(LINK_SCOREBOARD, MSG_UPDATE_WINNER, potentialWinner, NULL_KEY);
|
||||||
llMessageLinked(LINK_SET, MSG_UPDATE_FLOAT, potentialWinner, NULL_KEY); // Update winner's floater
|
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_SCOREBOARD, MSG_UPDATE_WINNER, winner, NULL_KEY);
|
||||||
llMessageLinked(LINK_SET, MSG_UPDATE_FLOAT, winner, NULL_KEY); // Update winner's floater
|
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);
|
llMessageLinked(LINK_SCOREBOARD, MSG_GAME_WON, winner, NULL_KEY);
|
||||||
|
|
||||||
// Use timer instead of llSleep to avoid immediate reset
|
// Use timer instead of llSleep to avoid immediate reset
|
||||||
|
|
@ -843,6 +876,18 @@ default {
|
||||||
return;
|
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
|
// Receive lightweight updates from Game Manager
|
||||||
if (num == 9070) {
|
if (num == 9070) {
|
||||||
list parts = llParseString2List(str, ["|"], []);
|
list parts = llParseString2List(str, ["|"], []);
|
||||||
|
|
@ -953,11 +998,11 @@ default {
|
||||||
string action = llList2String(parts, 0);
|
string action = llList2String(parts, 0);
|
||||||
if (action == "LEAVE_GAME" || action == "KICK_PLAYER") {
|
if (action == "LEAVE_GAME" || action == "KICK_PLAYER") {
|
||||||
string leavingName = llList2String(parts, 1);
|
string leavingName = llList2String(parts, 1);
|
||||||
// Find and remove the player from main controller lists
|
// Find and remove the player from main controller lists
|
||||||
integer idx = llListFindList(names, [leavingName]);
|
integer idx = llListFindList(names, [leavingName]);
|
||||||
if (idx != -1) {
|
if (idx != -1) {
|
||||||
// CHANGED: Remove player from scoreboard BEFORE updating lists
|
// CRITICAL: Inform all scripts (Registration Manager) to scrub this player
|
||||||
llMessageLinked(LINK_SCOREBOARD, MSG_REMOVE_PLAYER, leavingName, NULL_KEY);
|
llMessageLinked(LINK_SET, MSG_REMOVE_PLAYER, leavingName, NULL_KEY);
|
||||||
|
|
||||||
// Only announce if it's not the end of the game
|
// Only announce if it's not the end of the game
|
||||||
if (llGetListLength(names) > 1) {
|
if (llGetListLength(names) > 1) {
|
||||||
|
|
@ -1135,7 +1180,7 @@ default {
|
||||||
llRegionSayTo(id, 0, "🚫 Access Denied: Only the Global Admin can perform this action.");
|
llRegionSayTo(id, 0, "🚫 Access Denied: Only the Global Admin can perform this action.");
|
||||||
return;
|
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.");
|
llOwnerSay("🏆 GLOBAL LEADERBOARD WIPE confirmed and executed.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -1149,7 +1194,7 @@ default {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
resetGame();
|
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.");
|
llOwnerSay("🔄 FULL RESET confirmed and executed.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,8 @@ key EXPERIENCE_ID = "06926390-26e6-11f1-a452-0242ac110003"; // Final Girlz I.N.C
|
||||||
#define dbg(msg) ;
|
#define dbg(msg) ;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#define MSG_DEBUG_TEXT 7000
|
||||||
|
|
||||||
#define MSG_RESET_ALL -99999
|
#define MSG_RESET_ALL -99999
|
||||||
#define MSG_EMERGENCY_RESET -99998
|
#define MSG_EMERGENCY_RESET -99998
|
||||||
#define MSG_DEBUG_PICKS_ON 7001
|
#define MSG_DEBUG_PICKS_ON 7001
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,8 @@ integer FLOATER_BASE_CHANNEL;
|
||||||
integer roundStarted = FALSE;
|
integer roundStarted = FALSE;
|
||||||
integer gameStarting = FALSE;
|
integer gameStarting = FALSE;
|
||||||
|
|
||||||
|
integer resetInProgress = FALSE; // Lockout for reset syncs
|
||||||
|
|
||||||
integer calculateChannel(integer offset) {
|
integer calculateChannel(integer offset) {
|
||||||
string ownerStr = (string)llGetOwner();
|
string ownerStr = (string)llGetOwner();
|
||||||
string objectStr = (string)llGetLinkKey(1);
|
string objectStr = (string)llGetLinkKey(1);
|
||||||
|
|
@ -154,6 +156,10 @@ handlePlayerRegistration(string regData, key requesterId) {
|
||||||
if (newKey == llGetOwner()) {
|
if (newKey == llGetOwner()) {
|
||||||
integer isStarter = (humanCount <= 1);
|
integer isStarter = (humanCount <= 1);
|
||||||
llMessageLinked(LINK_SET, MSG_SHOW_MENU, "owner|" + (string)isStarter, newKey);
|
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);
|
ownerMsg("JOIN|" + newName);
|
||||||
|
|
@ -190,77 +196,150 @@ default {
|
||||||
|
|
||||||
// Handle game state sync from Main Controller
|
// Handle game state sync from Main Controller
|
||||||
if (num == MSG_SYNC_GAME_STATE) {
|
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
|
// PRESERVE PLAYER KEYS: Don't overwrite our authoritative player registry
|
||||||
// We only sync lives and names, but keep our real player keys
|
list parts = llParseStringKeepNulls(str, ["~"], []);
|
||||||
list parts = llParseString2List(str, ["~"], []);
|
|
||||||
if (llGetListLength(parts) >= 4) {
|
if (llGetListLength(parts) >= 4) {
|
||||||
list newLives = llCSV2List(llList2String(parts, 0));
|
list newLives = llCSV2List(llList2String(parts, 0));
|
||||||
|
string picksPart = llList2String(parts, 1);
|
||||||
list newNames = llCSV2List(llList2String(parts, 3));
|
list newNames = llCSV2List(llList2String(parts, 3));
|
||||||
|
|
||||||
// Only update lives if list length matches (safety check)
|
|
||||||
if (llGetListLength(newLives) == llGetListLength(players)) {
|
if (llGetListLength(newLives) == llGetListLength(players)) {
|
||||||
lives = newLives;
|
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)) {
|
if (llGetListLength(newNames) == llGetListLength(players)) {
|
||||||
names = newNames;
|
names = newNames;
|
||||||
}
|
}
|
||||||
|
|
||||||
// KEEP our player keys - they are authoritative
|
|
||||||
dbg("🔄 [RegMgr] Synced game state, preserving " + (string)llGetListLength(players) + " player keys");
|
dbg("🔄 [RegMgr] Synced game state, preserving " + (string)llGetListLength(players) + " player keys");
|
||||||
}
|
}
|
||||||
return;
|
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
|
// Handle dialog forwarding requests from Game Manager
|
||||||
if (num == MSG_DIALOG_FORWARD_REQUEST) {
|
if (num == MSG_DIALOG_FORWARD_REQUEST) {
|
||||||
list parts = llParseString2List(str, ["|"], []);
|
list parts = llParseString2List(str, ["~"], []); // Fixed delimiter to match forwarder
|
||||||
if (llGetListLength(parts) >= 3) {
|
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);
|
string targetPlayerName = llList2String(parts, 1);
|
||||||
|
|
||||||
// Reconstruct dialog payload from remaining parts
|
|
||||||
string dialogPayload = "";
|
string dialogPayload = "";
|
||||||
integer i;
|
integer i;
|
||||||
for (i = 2; i < llGetListLength(parts); i++) {
|
for (i = 2; i < llGetListLength(parts); i++) {
|
||||||
if (i > 2) dialogPayload += "|"; // Add separator between parts
|
if (i > 2) dialogPayload += "|";
|
||||||
dialogPayload += llList2String(parts, i);
|
dialogPayload += llList2String(parts, i);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find player key in our authoritative registry
|
|
||||||
integer playerIdx = llListFindList(names, [targetPlayerName]);
|
integer playerIdx = llListFindList(names, [targetPlayerName]);
|
||||||
if (playerIdx != -1 && playerIdx < llGetListLength(players)) {
|
if (playerIdx != -1 && playerIdx < llGetListLength(players)) {
|
||||||
key playerKey = llList2Key(players, playerIdx);
|
key playerKey = llList2Key(players, playerIdx);
|
||||||
if (playerKey != NULL_KEY) {
|
if (playerKey != NULL_KEY) {
|
||||||
if (dialogType == "SHOW_DIALOG") {
|
if (dialogType == "SHOW_DIALOG") {
|
||||||
llMessageLinked(LINK_SET, MSG_SHOW_DIALOG, dialogPayload, playerKey);
|
llMessageLinked(LINK_SET, MSG_SHOW_DIALOG, dialogPayload, playerKey);
|
||||||
dbg("📋 [RegMgr] Forwarded dialog to " + targetPlayerName + ": " + dialogPayload);
|
|
||||||
} else if (dialogType == "SHOW_ROLL_DIALOG") {
|
} else if (dialogType == "SHOW_ROLL_DIALOG") {
|
||||||
llMessageLinked(LINK_SET, MSG_SHOW_ROLL_DIALOG, dialogPayload, playerKey);
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (num == MSG_RESET_ALL && str == "FULL_RESET") {
|
if (num == MSG_RESET_ALL && str == "FULL_RESET") {
|
||||||
|
resetInProgress = TRUE;
|
||||||
players = [];
|
players = []; names = []; lives = []; readyPlayers = [];
|
||||||
names = [];
|
roundStarted = FALSE; gameStarting = FALSE;
|
||||||
lives = [];
|
|
||||||
readyPlayers = [];
|
// Activate brief lockout timer to ignore stale syncs while Linkset settles
|
||||||
roundStarted = FALSE;
|
llSetTimerEvent(0.2);
|
||||||
gameStarting = FALSE;
|
|
||||||
dbg("🎯 [RegMgr] Player Registration Manager reset complete");
|
dbg("🎯 [RegMgr] Player Registration Manager reset complete (Lockout Active)");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
timer() {
|
||||||
|
resetInProgress = FALSE;
|
||||||
|
llSetTimerEvent(0);
|
||||||
|
dbg("🎯 [RegMgr] Reset lockout complete - sync active.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -214,7 +214,7 @@ default {
|
||||||
string picksDataStr = llList2String(parts, 1);
|
string picksDataStr = llList2String(parts, 1);
|
||||||
dbg("🎲 [Roll Module] 🔍 [Roll Module] Received picksDataStr: '" + picksDataStr + "'");
|
dbg("🎲 [Roll Module] 🔍 [Roll Module] Received picksDataStr: '" + picksDataStr + "'");
|
||||||
|
|
||||||
if (picksDataStr == "" || picksDataStr == "EMPTY") {
|
if (picksDataStr == "" || picksDataStr == "EMPTY" || picksDataStr == "~EMPTY~") {
|
||||||
picksData = [];
|
picksData = [];
|
||||||
dbg("🎲 [Roll Module] 🔍 [Roll Module] Set picksData to empty");
|
dbg("🎲 [Roll Module] 🔍 [Roll Module] Set picksData to empty");
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -270,9 +270,18 @@ default {
|
||||||
llSleep(DELAY_CONFETTI_START);
|
llSleep(DELAY_CONFETTI_START);
|
||||||
|
|
||||||
rollInProgress = TRUE; // Set roll in progress for bot
|
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
|
// NEW: Use the dice type already received from Game Manager at round start!
|
||||||
llMessageLinked(LINK_SET, MSG_GET_DICE_TYPE, (string)llGetListLength(names), NULL_KEY);
|
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 {
|
} else {
|
||||||
// Reset any previous roll state before showing dialog
|
// Reset any previous roll state before showing dialog
|
||||||
rollInProgress = FALSE;
|
rollInProgress = FALSE;
|
||||||
|
|
@ -385,11 +394,12 @@ default {
|
||||||
llMessageLinked(LINK_SCOREBOARD, MSG_GAME_STATUS, "Plot Twist", NULL_KEY);
|
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);
|
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
|
// 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
|
// Update floaters immediately to show correct peril player before sync
|
||||||
llMessageLinked(LINK_SET, MSG_UPDATE_FLOAT, newPeril, NULL_KEY);
|
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);
|
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");
|
dbg("🎲 [Roll Module] 💗 Player lives updated: " + perilPlayer + " now has " + (string)(currentLives - 1) + " lives");
|
||||||
|
|
||||||
// NEW: REPORT LIFE REDUCTION to Main Controller (Source of Truth)
|
// NEW: Move life update signal DOWN to after the outcome sleeps
|
||||||
// This replaces the unreliable self-syncing and allows the Controller to broadcast the official state.
|
// This prevents Game Manager/Controller from starting next round too soon
|
||||||
llMessageLinked(LINK_SET, MSG_UPDATE_LIFE, perilPlayer + "|" + (string)(currentLives - 1), NULL_KEY);
|
|
||||||
|
|
||||||
// Check if peril player picked the rolled number
|
// Check if peril player picked the rolled number
|
||||||
list perilPicks = getPicksFor(perilPlayer);
|
list perilPicks = getPicksFor(perilPlayer);
|
||||||
|
|
@ -424,21 +433,30 @@ default {
|
||||||
// Direct scoreboard status update (texture and text)
|
// Direct scoreboard status update (texture and text)
|
||||||
llMessageLinked(LINK_SCOREBOARD, MSG_GAME_STATUS, "Direct Hit", NULL_KEY);
|
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);
|
llMessageLinked(LINK_SET, MSG_STATUS_TEXT, "DIRECT HIT!\n<!c=red>" + perilPlayer + " matched!", NULL_KEY);
|
||||||
|
|
||||||
// Add delay to let status display before next phase
|
// 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) {
|
} else if (!matched) {
|
||||||
llSay(0, "🩸 NO SHIELD! Nobody picked " + resultStr + " - " + perilPlayer + " takes the hit from the d" + (string)diceType + "! 🩸");
|
llSay(0, "🩸 NO SHIELD! Nobody picked " + resultStr + " - " + perilPlayer + " takes the hit from the d" + (string)diceType + "! 🩸");
|
||||||
// Direct scoreboard status update (texture and text)
|
// Direct scoreboard status update (texture and text)
|
||||||
llMessageLinked(LINK_SCOREBOARD, MSG_GAME_STATUS, "No Shield", NULL_KEY);
|
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);
|
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
|
// 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 {
|
} else {
|
||||||
// This should never happen in normal game flow since Plot Twist case is handled above
|
// 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
|
// 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!");
|
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 + "! 🩸");
|
llSay(0, "🩸 SHIELD FAILED! " + perilPlayer + " takes the hit despite someone picking " + resultStr + "! 🩸");
|
||||||
llSleep(DELAY_ELIMINATION_NOTICE);
|
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);
|
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
|
// Note: Win condition checking is handled by Main Controller after elimination
|
||||||
|
|
||||||
// Validate perilPlayer is not empty before syncing
|
// Note: State sync and round continuation are now handled exclusively by the Main Controller
|
||||||
string perilForSync = perilPlayer;
|
// after it receives the Damage/Elimination reports, ensuring a single source of truth.
|
||||||
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
|
// Clear roll protection after processing is complete
|
||||||
rollInProgress = FALSE;
|
rollInProgress = FALSE;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue