feat: implement sharded KVP leaderboard system with multi-page support and locking mechanisms
This commit is contained in:
parent
175e23be93
commit
1297b99e2c
6 changed files with 687 additions and 927 deletions
|
|
@ -5,17 +5,23 @@ string leftText = "";
|
|||
// Distribute text to the FURWARE "Leaderboard" boxes
|
||||
distributeFullText(string text) {
|
||||
if (llGetSubString(text, 0, 7) == "COLUMNS|") {
|
||||
// Use llParseStringKeepNulls to ensure empty columns don't break the registration logic
|
||||
// Use llParseStringKeepNulls to ensure empty columns don't break logic
|
||||
list parts = llParseStringKeepNulls(llGetSubString(text, 8, -1), ["|"], []);
|
||||
if (llGetListLength(parts) >= 3) {
|
||||
// Distribute each column to its virtual box
|
||||
llMessageLinked(LINK_SET, 0, llList2String(parts, 0), (key)(FW_DATA + ":LB_Rank"));
|
||||
llMessageLinked(LINK_SET, 0, llList2String(parts, 1), (key)(FW_DATA + ":LB_Name"));
|
||||
llMessageLinked(LINK_SET, 0, llList2String(parts, 2), (key)(FW_DATA + ":LB_WL"));
|
||||
llMessageLinked(LINK_SET, 0, llList2String(parts, 0), (key)"fw_data:LB_Rank");
|
||||
llMessageLinked(LINK_SET, 0, llList2String(parts, 1), (key)"fw_data:LB_Name");
|
||||
llMessageLinked(LINK_SET, 0, llList2String(parts, 2), (key)"fw_data:LB_WL");
|
||||
|
||||
// Route Navigation Arrows to dedicated button boxes
|
||||
if (llGetListLength(parts) >= 5) {
|
||||
llMessageLinked(LINK_SET, 0, llList2String(parts, 3), (key)"fw_data:LB_PREV_BTN");
|
||||
llMessageLinked(LINK_SET, 0, llList2String(parts, 4), (key)"fw_data:LB_NEXT_BTN");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Fallback for legacy text or titles (sent to parent box)
|
||||
llMessageLinked(LINK_SET, 0, text, (key)(FW_DATA + ":Leaderboard"));
|
||||
llMessageLinked(LINK_SET, 0, text, (key)"fw_data:Leaderboard");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -28,45 +34,68 @@ default {
|
|||
state_entry() {
|
||||
DISCOVER_CORE_LINKS();
|
||||
REPORT_MEMORY();
|
||||
dbg("📋 [Leaderboard] Leaderboard Communication Script ready! (Virtual Multi-Column)");
|
||||
dbg("📋 [Leaderboard] Bridge Active. Awaiting Engine...");
|
||||
|
||||
// Wait for engine to be ready, then register virtual columns
|
||||
llSleep(DELAY_LEADERBOARD_SYNC);
|
||||
// Wait for engine to be ready
|
||||
llSleep(2.0);
|
||||
|
||||
// Registering virtual columns inside the physical "Leaderboard" mesh
|
||||
// Format: fw_addbox:NewName:ParentName:x,y,w,h
|
||||
llMessageLinked(LINK_SET, 0, "", (key)("fw_addbox:LB_Rank:Leaderboard:0,1,4,11"));
|
||||
llMessageLinked(LINK_SET, 0, "", (key)("fw_addbox:LB_Name:Leaderboard:4,1,20,11"));
|
||||
llMessageLinked(LINK_SET, 0, "", (key)("fw_addbox:LB_WL:Leaderboard:24,1,8,11"));
|
||||
// Registering virtual columns + Navigation buttons
|
||||
// Coordinates: x, y, width, height (0-indexed)
|
||||
llMessageLinked(LINK_SET, 0, "", (key)"fw_addbox:LB_Rank:Leaderboard:0,1,4,10:");
|
||||
llMessageLinked(LINK_SET, 0, "", (key)"fw_addbox:LB_Name:Leaderboard:4,1,20,10:");
|
||||
llMessageLinked(LINK_SET, 0, "", (key)"fw_addbox:LB_WL:Leaderboard:24,1,8,10:");
|
||||
|
||||
dbg("📋 [Leaderboard] Virtual column registration requests sent.");
|
||||
// Navigation Row (Shifted down to Y=11)
|
||||
llMessageLinked(LINK_SET, 0, "", (key)"fw_addbox:LB_PREV_BTN:Leaderboard:4,11,10,1:");
|
||||
llMessageLinked(LINK_SET, 0, "", (key)"fw_addbox:LB_NEXT_BTN:Leaderboard:20,11,12,1:");
|
||||
|
||||
// --- STARTUP HANDSHAKE ---
|
||||
// Wait a moment for the engine to process ADD_BOX, then ask Scoreboard for a fresh update
|
||||
llSleep(DELAY_LEADERBOARD_READY);
|
||||
llMessageLinked(LINK_SET, MSG_DISPLAY_LEADERBOARD, "REFRESH_STARTUP", NULL_KEY);
|
||||
dbg("📋 [Leaderboard] Box registration sent.");
|
||||
|
||||
// Start with blank display
|
||||
distributeFullText("");
|
||||
// Give the engine time to build boxes before first refresh
|
||||
llSleep(2.0);
|
||||
// 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);
|
||||
}
|
||||
|
||||
touch_start(integer _n) {
|
||||
// Use exact syntax from example: fw_touchquery:link:face
|
||||
integer _link = llDetectedLinkNumber(0);
|
||||
integer _face = llDetectedTouchFace(0);
|
||||
|
||||
dbg("👆 [Leaderboard] Touch detected. Sending fw_touchquery:" + (string)_link + ":" + (string)_face);
|
||||
llMessageLinked(LINK_SET, 0, "LB_TOUCH", (key)("fw_touchquery:" + (string)_link + ":" + (string)_face));
|
||||
}
|
||||
|
||||
link_message(integer sender, integer num, string str, key id) {
|
||||
// Handle messages from the game logic
|
||||
if (num == MSG_DISPLAY_LEADERBOARD) {
|
||||
if (llGetSubString(str, 0, 14) == "FORMATTED_TEXT|") {
|
||||
distributeFullText(llGetSubString(str, 15, -1));
|
||||
} else if (llGetSubString(str, 0, 9) == "LEFT_TEXT|") {
|
||||
// For individual bank updates
|
||||
leftText = llGetSubString(str, 10, -1);
|
||||
distributeFullText(leftText);
|
||||
} else {
|
||||
// Pass directly for COLUMNS| or multi-column data
|
||||
distributeFullText(str);
|
||||
}
|
||||
}
|
||||
else if (llSubStringIndex(str, "LEADERBOARD") == 0) {
|
||||
dbg("📋 [Leaderboard] ⚠️ Received legacy LEADERBOARD format - ignoring");
|
||||
}
|
||||
}
|
||||
else if (id == "fw_touchreply") {
|
||||
dbg("👆 [Leaderboard] Raw Reply: " + str);
|
||||
// Format: boxName:dx:dy:rootName:x:y:userData
|
||||
list _parts = llParseStringKeepNulls(str, [":"], []);
|
||||
string _box = llList2String(_parts, 0);
|
||||
string _uData = llList2String(_parts, 6);
|
||||
|
||||
if (_uData == "LB_TOUCH") {
|
||||
dbg("👆 [Leaderboard] Click: " + _box);
|
||||
|
||||
if (_box == "LB_PREV_BTN") {
|
||||
llMessageLinked(LINK_SET, MSG_LB_PAGE_PREV, "", NULL_KEY);
|
||||
} else if (_box == "LB_NEXT_BTN") {
|
||||
llMessageLinked(LINK_SET, MSG_LB_PAGE_NEXT, "", NULL_KEY);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
376
Game_Leaderboard_Manager.lsl
Normal file
376
Game_Leaderboard_Manager.lsl
Normal file
|
|
@ -0,0 +1,376 @@
|
|||
#include "Peril_Constants.lsl"
|
||||
|
||||
// =============================================================================
|
||||
// PERIL WORLD RANKING - Leaderboard Manager
|
||||
// =============================================================================
|
||||
// Handles sharded KVP storage (up to 10 keys) and sorted ranking.
|
||||
// Capacity: 500+ Players (Max 10 keys x 50 entries)
|
||||
// =============================================================================
|
||||
|
||||
list leaderboardData = []; // Stride 3: [Name, Wins, Losses]
|
||||
integer currentLoadPage = 1;
|
||||
key kvpReadReq;
|
||||
key kvpWriteReq;
|
||||
key kvpLockReq; // Request for claiming leaderboard lock
|
||||
list writeQueue = []; // Pairs of [KeyName, DataString]
|
||||
integer isSaving = FALSE;
|
||||
integer isBusy = FALSE; // Read lock
|
||||
integer displayOffset = 0; // Current ranking page offset (0, 10, 20...)
|
||||
integer retryCount = 0; // Track number of timeouts
|
||||
string currentWriteKey;
|
||||
string currentWriteVal;
|
||||
|
||||
// --- Lock & Sync State ---
|
||||
string pendingPlayer = "";
|
||||
string pendingResult = "";
|
||||
integer isLocking = FALSE;
|
||||
string myLockVal = "";
|
||||
|
||||
// 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);
|
||||
return;
|
||||
}
|
||||
isSaving = TRUE;
|
||||
currentWriteKey = llList2String(writeQueue, 0);
|
||||
currentWriteVal = llList2String(writeQueue, 1);
|
||||
writeQueue = llDeleteSubList(writeQueue, 0, 1);
|
||||
kvpWriteReq = llUpdateKeyValue(currentWriteKey, currentWriteVal, FALSE, "");
|
||||
}
|
||||
|
||||
// Convert the whole list into sharded strings and start saving
|
||||
generateShardedData() {
|
||||
integer _shardI;
|
||||
integer _totalCount = llGetListLength(leaderboardData) / 3;
|
||||
writeQueue = [];
|
||||
|
||||
for (_shardI = 0; _shardI < MAX_LB_PAGES; _shardI++) {
|
||||
integer _start = _shardI * MAX_ENTRIES_PER_PAGE;
|
||||
if (_start >= _totalCount) jump shard_exit;
|
||||
|
||||
string _serialized = "";
|
||||
integer _j;
|
||||
for (_j = 0; _j < MAX_ENTRIES_PER_PAGE && (_start + _j) < _totalCount; _j++) {
|
||||
integer _base = (_start + _j) * 3;
|
||||
// NEW FORMAT: Wins,Losses,Name (CSV-like within colon segments)
|
||||
string _entry = (string)llList2Integer(leaderboardData, _base) + "," +
|
||||
(string)llList2Integer(leaderboardData, _base + 1) + "," +
|
||||
llList2String(leaderboardData, _base + 2);
|
||||
if (_serialized != "") _serialized += "|";
|
||||
_serialized += _entry;
|
||||
}
|
||||
writeQueue += ["Peril_LB_" + (string)(_shardI + 1), _serialized];
|
||||
}
|
||||
@shard_exit;
|
||||
|
||||
// If no data to write (reset case), release lock now
|
||||
if (llGetListLength(writeQueue) == 0) {
|
||||
kvpLockReq = llUpdateKeyValue(LB_LOCK_KEY, "", TRUE, myLockVal);
|
||||
} else {
|
||||
processWriteQueue();
|
||||
}
|
||||
}
|
||||
|
||||
loadLBPage(integer pageNum) {
|
||||
if (isBusy && pageNum == 1) return; // Prevent double loads on reset
|
||||
if (pageNum > 1) llSleep(1.0); // Anti-Throttle Delay
|
||||
|
||||
isBusy = TRUE;
|
||||
llSetTimerEvent(30.0); // 30 second watchdog (Lag Resistant)
|
||||
currentLoadPage = pageNum;
|
||||
string keyName = LB_KEY_PREFIX + (string)pageNum;
|
||||
kvpReadReq = llReadKeyValue(keyName);
|
||||
if (kvpReadReq == NULL_KEY) {
|
||||
dbg("❌ [Leaderboard] KVP Read FAILED: Not authorized for " + keyName);
|
||||
llSetTimerEvent(0.0);
|
||||
} else {
|
||||
dbg("📡 [Leaderboard] Loading " + keyName + "...");
|
||||
}
|
||||
}
|
||||
|
||||
list getSortedSlice(integer offset) {
|
||||
if (llGetListLength(leaderboardData) == 0) return [];
|
||||
|
||||
// In LSL, llListSort sorts by the FIRST element of each stride.
|
||||
// Our data format is [Wins, Losses, Name].
|
||||
// Sort by Wins (descending).
|
||||
list sorted = llListSort(leaderboardData, 3, FALSE);
|
||||
|
||||
integer start = offset * 3;
|
||||
return llList2List(sorted, start, start + 29);
|
||||
}
|
||||
|
||||
updatePlayerRecord(string pName, string pResult) {
|
||||
// Search for Name (index 2 of each 3-stride entry)
|
||||
integer _idx = -1;
|
||||
integer _it;
|
||||
for (_it = 2; _it < llGetListLength(leaderboardData); _it += 3) {
|
||||
if (llList2String(leaderboardData, _it) == pName) { _idx = _it; jump found; }
|
||||
}
|
||||
@found;
|
||||
|
||||
if (_idx == -1) {
|
||||
if (llGetListLength(leaderboardData) / 3 >= MAX_WORLD_RANKING) return;
|
||||
if (pResult == "WIN") leaderboardData += [1, 0, pName];
|
||||
else leaderboardData += [0, 1, pName];
|
||||
} else {
|
||||
integer _base = _idx - 2;
|
||||
integer _v;
|
||||
if (pResult == "WIN") {
|
||||
_v = llList2Integer(leaderboardData, _base);
|
||||
leaderboardData = llListReplaceList(leaderboardData, [_v + 1], _base, _base);
|
||||
} else {
|
||||
_v = llList2Integer(leaderboardData, _base + 1);
|
||||
leaderboardData = llListReplaceList(leaderboardData, [_v + 1], _base+1, _base+1);
|
||||
}
|
||||
}
|
||||
|
||||
// SORT before saving
|
||||
leaderboardData = llListSort(leaderboardData, 3, FALSE);
|
||||
generateShardedData();
|
||||
}
|
||||
|
||||
startUpdateSequence(string pName, string pResult) {
|
||||
if (isLocking || isSaving) {
|
||||
dbg("⚠️ [Leaderboard] Update already in progress. Retrying in 2s...");
|
||||
llSleep(2.0);
|
||||
}
|
||||
|
||||
pendingPlayer = pName;
|
||||
pendingResult = pResult;
|
||||
isLocking = TRUE;
|
||||
|
||||
dbg("🔐 [Leaderboard] Checking global lock for update...");
|
||||
kvpLockReq = llReadKeyValue(LB_LOCK_KEY);
|
||||
}
|
||||
|
||||
default {
|
||||
state_entry() {
|
||||
REPORT_MEMORY();
|
||||
dbg("⏳ [Leaderboard] Startup: Handshake link active.");
|
||||
}
|
||||
|
||||
dataserver(key _qid, string _qd) {
|
||||
if (_qid == kvpReadReq) {
|
||||
llSetTimerEvent(0.0); // Reset watchdog
|
||||
integer _c = llSubStringIndex(_qd, ",");
|
||||
string _st = _qd; string _vl = "";
|
||||
if (_c != -1) {
|
||||
_st = llGetSubString(_qd, 0, _c - 1);
|
||||
_vl = llGetSubString(_qd, _c + 1, -1);
|
||||
}
|
||||
|
||||
if (_st == "1") { // SUCCESS: Data found
|
||||
if (_vl != "" && llSubStringIndex(_vl, "Error") == -1) {
|
||||
list _ent = llParseString2List(_vl, ["|"], []);
|
||||
integer _ei;
|
||||
for (_ei = 0; _ei < llGetListLength(_ent); _ei++) {
|
||||
string _rawEntry = llList2String(_ent, _ei);
|
||||
if (llSubStringIndex(_rawEntry, ",") != -1) {
|
||||
// NEW FORMAT: Wins,Losses,Name
|
||||
list _it = llParseString2List(_rawEntry, [","], []);
|
||||
if (llGetListLength(_it) >= 3) {
|
||||
leaderboardData += [(integer)llList2String(_it, 0), (integer)llList2String(_it, 1), llList2String(_it, 2)];
|
||||
}
|
||||
} else {
|
||||
// OLD LEGACY FORMAT: Name:Wins:Losses
|
||||
list _it = llParseString2List(_rawEntry, [":"], []);
|
||||
if (llGetListLength(_it) >= 3) {
|
||||
// Translate to new stride structure in memory
|
||||
leaderboardData += [(integer)llList2String(_it, 1), (integer)llList2String(_it, 2), llList2String(_it, 0)];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (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 WE WERE SYNCING FOR AN UPDATE, APPLY IT NOW
|
||||
if (isLocking) {
|
||||
isLocking = FALSE;
|
||||
updatePlayerRecord(pendingPlayer, pendingResult);
|
||||
}
|
||||
}
|
||||
} 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 (_qid == kvpWriteReq) {
|
||||
integer _c = llSubStringIndex(_qd, ",");
|
||||
string _st = _qd; string _vl = "";
|
||||
if (_c != -1) {
|
||||
_st = llGetSubString(_qd, 0, _c - 1);
|
||||
_vl = llGetSubString(_qd, _c + 1, -1);
|
||||
}
|
||||
|
||||
if (_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); }
|
||||
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);
|
||||
}
|
||||
|
||||
if (isLocking) {
|
||||
// STAGE 1: Current Lock Read Result
|
||||
if (_st == "1") { // Success read
|
||||
string _curVal = _vl;
|
||||
integer _isStale = FALSE;
|
||||
if (_curVal != "") {
|
||||
list _p = llParseString2List(_curVal, [":"], []);
|
||||
integer _ts = (integer)llList2String(_p, 1);
|
||||
if (llGetUnixTime() - _ts > LB_LOCK_TIMEOUT) _isStale = TRUE;
|
||||
}
|
||||
|
||||
if (_curVal == "" || _isStale) {
|
||||
dbg("🔐 [Leaderboard] Lock available. 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);
|
||||
} else {
|
||||
dbg("⏳ [Leaderboard] Lock held by another board. Retrying in 5s...");
|
||||
llSetTimerEvent(5.0);
|
||||
}
|
||||
} 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);
|
||||
isLocking = FALSE;
|
||||
}
|
||||
} else {
|
||||
// RELEASE RESULT
|
||||
if (_st == "14") { dbg("🔓 [Leaderboard] Lock released."); }
|
||||
else { dbg("⚠️ [Leaderboard] Lock release issues: " + _vl); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
timer() {
|
||||
if (isLocking) {
|
||||
llSetTimerEvent(0);
|
||||
kvpLockReq = llReadKeyValue(LB_LOCK_KEY);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isBusy) {
|
||||
if (retryCount < 3) {
|
||||
retryCount++;
|
||||
dbg("⚠️ [Leaderboard] Shard " + (string)currentLoadPage + " timed out. Retrying (" + (string)retryCount + "/3)...");
|
||||
isBusy = FALSE; // Unlock so loadLBPage can run
|
||||
loadLBPage(currentLoadPage);
|
||||
} else {
|
||||
isBusy = FALSE;
|
||||
llSetTimerEvent(0);
|
||||
dbg("🏆 [Leaderboard] SYNC GAVE UP (Persistent timeout on Shard " + (string)currentLoadPage + ")");
|
||||
llMessageLinked(LINK_SET, MSG_LB_REQUEST_DISPLAY, "0", NULL_KEY);
|
||||
}
|
||||
} else {
|
||||
llSetTimerEvent(0);
|
||||
}
|
||||
}
|
||||
|
||||
link_message(integer sender, integer num, string str, key id) {
|
||||
if (num == MSG_LB_RECORD_PLAYER) {
|
||||
startUpdateSequence(str, (string)id);
|
||||
}
|
||||
else if (num == MSG_DISPLAY_LEADERBOARD) {
|
||||
if (str == "REFRESH_STARTUP") loadLBPage(1);
|
||||
}
|
||||
else if (num == MSG_LB_REQUEST_DISPLAY) {
|
||||
integer _offset = (integer)str;
|
||||
displayOffset = _offset; // SYNC GLOBAL VARIABLE
|
||||
list _sorted = getSortedSlice(_offset);
|
||||
string _rankCol = ""; string _nameCol = ""; string _statsCol = "";
|
||||
integer _i; integer _total = llGetListLength(_sorted) / 3;
|
||||
integer _globalTotal = llGetListLength(leaderboardData) / 3;
|
||||
|
||||
for (_i = 0; _i < 10; _i++) {
|
||||
integer _rn = _offset + _i + 1;
|
||||
string _rank = (string)_rn + "."; if (_rn < 10) _rank = "0" + _rank;
|
||||
_rankCol += _rank + "\n";
|
||||
if (_i < _total) {
|
||||
integer _b = _i * 3;
|
||||
_statsCol += "W:" + (string)llList2Integer(_sorted, _b) + "/L:" + (string)llList2Integer(_sorted, _b+1) + "\n";
|
||||
string _n = llList2String(_sorted, _b+2);
|
||||
if (llStringLength(_n) > 16) _n = llGetSubString(_n, 0, 13) + "...";
|
||||
_nameCol += _n + "\n";
|
||||
} else {
|
||||
_nameCol += "----------\n"; _statsCol += "W:0/L:0\n";
|
||||
}
|
||||
}
|
||||
string _p = " "; if (_offset > 0) _p = "[ < PREV ]";
|
||||
string _n = " "; if (_offset + 10 < _globalTotal) _n = "[ NEXT > ]";
|
||||
llMessageLinked(LINK_SET, MSG_DISPLAY_LEADERBOARD, "COLUMNS|" + _rankCol + "|" + _nameCol + "|" + _statsCol + "|" + _p + "|" + _n, NULL_KEY);
|
||||
}
|
||||
else if (num == MSG_LB_PAGE_NEXT) {
|
||||
integer _maxEntries = llGetListLength(leaderboardData) / 3;
|
||||
if (displayOffset + 10 < _maxEntries) {
|
||||
displayOffset += 10;
|
||||
llMessageLinked(LINK_SET, MSG_LB_REQUEST_DISPLAY, (string)displayOffset, NULL_KEY);
|
||||
}
|
||||
}
|
||||
else if (num == MSG_LB_PAGE_PREV) {
|
||||
if (displayOffset >= 10) {
|
||||
displayOffset -= 10;
|
||||
llMessageLinked(LINK_SET, MSG_LB_REQUEST_DISPLAY, (string)displayOffset, NULL_KEY);
|
||||
}
|
||||
}
|
||||
else if (num == MSG_RESET_LEADERBOARD) {
|
||||
leaderboardData = [];
|
||||
currentLoadPage = 1;
|
||||
isBusy = FALSE;
|
||||
displayOffset = 0;
|
||||
|
||||
if (str == "START_SYNC") {
|
||||
loadLBPage(1);
|
||||
}
|
||||
else if (str == "DEBUG_GENERATE" && llGetOwner() == llGetCreator()) {
|
||||
generateShardedData();
|
||||
}
|
||||
}
|
||||
else if (num == MSG_RESET_ALL) { llResetScript(); }
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -129,6 +129,7 @@ integer dialogHandle = -1;
|
|||
integer botHandle = -1;
|
||||
integer rollHandle = -1;
|
||||
key sentinelQueryID = NULL_KEY; // Track the Experience Sentinel ping
|
||||
integer EX_READY = FALSE; // Track the Experience Sentinel ping
|
||||
|
||||
// Channel initialization function
|
||||
initializeChannels() {
|
||||
|
|
@ -189,11 +190,12 @@ checkExperience() {
|
|||
llOwnerSay("🛡️ [Peril Dice] TO FIX: Open 'About Land' -> 'Experiences' -> 'Add' and search for 'Final Girlz I.N.C.'");
|
||||
|
||||
// Final probe check
|
||||
llReadKeyValue("_SENTINEL_");
|
||||
llSetTimerEvent(3.0);
|
||||
sentinelQueryID = llReadKeyValue("PERIL_SENTINEL");
|
||||
llSetTimerEvent(10.0); // Increased to 10s for reliability
|
||||
currentTimerMode = TIMER_XP_CHECK;
|
||||
} else {
|
||||
llOwnerSay("✅ [Peril Dice] Experience 'Final Girlz I.N.C.' is ENABLED on this parcel.");
|
||||
EX_READY = TRUE;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -354,6 +356,7 @@ default {
|
|||
resetGame();
|
||||
|
||||
// Experience Sentinel check - Moved after reset to protect timer
|
||||
llSleep(2.0);
|
||||
checkExperience();
|
||||
|
||||
dbg("✅ Main Controller initialization complete!");
|
||||
|
|
@ -381,6 +384,18 @@ default {
|
|||
}
|
||||
|
||||
touch_start(integer total_number) {
|
||||
// IGNORE TOUCHES INTENDED FOR THE LEADERBOARD
|
||||
string _rawNm = llGetLinkName(llDetectedLinkNumber(0));
|
||||
string _linkNm = llToLower(_rawNm);
|
||||
|
||||
// TEMPORARY DEBUG TO CATCH THE EXACT LINK NAME
|
||||
dbg("🎯 [Touch Filter] Clicked Link Name: '" + _rawNm + "'");
|
||||
|
||||
if (llSubStringIndex(_linkNm, "leaderboard") != -1 || llSubStringIndex(_linkNm, "furware") != -1 || llSubStringIndex(_linkNm, "lb_") != -1) {
|
||||
dbg("🛑 [Touch Filter] Bypassing menu for Leaderboard element!");
|
||||
return;
|
||||
}
|
||||
|
||||
key toucher = llDetectedKey(0);
|
||||
integer idx = llListFindList(players, [toucher]);
|
||||
|
||||
|
|
@ -1166,7 +1181,7 @@ default {
|
|||
dbg("✅ Floater creation requests sent for all players!");
|
||||
|
||||
// After forcing floaters, show appropriate menu based on owner registration status
|
||||
integer ownerIdx = llListFindList(players, [llGetOwner()]);
|
||||
integer ownerIdx = llGetListLength(players); // Simplified check
|
||||
if (ownerIdx != -1) {
|
||||
// Owner is registered - show registered owner menu
|
||||
integer ownerIsStarter = TRUE;
|
||||
|
|
@ -1293,14 +1308,18 @@ default {
|
|||
dataserver(key query_id, string data) {
|
||||
if (query_id == sentinelQueryID) {
|
||||
// WE GOT A RESPONSE!
|
||||
// If the land is not configured, data will often be an error string like "Error: XP_ERROR_NOT_EXPERIENCE"
|
||||
if (llGetSubString(data, 0, 4) != "Error") {
|
||||
integer c = llSubStringIndex(data, ",");
|
||||
string st = data;
|
||||
if (c != -1) st = llGetSubString(data, 0, c - 1);
|
||||
|
||||
sentinelQueryID = NULL_KEY;
|
||||
llSetTimerEvent(0);
|
||||
currentTimerMode = TIMER_IDLE;
|
||||
dbg("✅ [Peril Dice] Experience Sentinel: KVP Handshake Successful. Land is ready!");
|
||||
if (st == "1" || st == "3") { // Status 1 (Found) or 3 (Not Found) both prove Experience access
|
||||
dbg("✅ [Peril Dice] Experience Sentinel: KVP Heartbeat Confirmed.");
|
||||
EX_READY = TRUE;
|
||||
} else {
|
||||
dbg("⚠️ [Peril Dice] Sentinel handshake returned error: " + data);
|
||||
EX_READY = FALSE;
|
||||
dbg("⚠️ [Peril Dice] Sentinel handshake returned status: " + st);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ integer LINK_SCOREBOARD = -1; // "Scoreboard:0:0"
|
|||
integer LINK_DICE_BRIDGE = -1; // "FURWARE text mesh:Dice:0:0"
|
||||
integer LINK_LEADERBOARD_BRIDGE = -1; // "FURWARE text mesh:Leaderboard:0:0"
|
||||
integer LINK_STATUS_BRIDGE = -1; // "FURWARE text mesh:Status:0:0"
|
||||
integer LINK_BACKBOARD = -1; // "backboard:0:0"
|
||||
key EXPERIENCE_ID = "06926390-26e6-11f1-a452-0242ac110003"; // Final Girlz I.N.C.
|
||||
#define PARCEL_DETAILS_EXPERIENCE_LIST 18
|
||||
|
||||
|
|
@ -21,6 +22,7 @@ key EXPERIENCE_ID = "06926390-26e6-11f1-a452-0242ac110003"; // Final Girlz I.N.C
|
|||
else if (_name == "FURWARE text mesh:Dice:0:0") LINK_DICE_BRIDGE = _i; \
|
||||
else if (_name == "FURWARE text mesh:Leaderboard:0:0") LINK_LEADERBOARD_BRIDGE = _i; \
|
||||
else if (_name == "FURWARE text mesh:Status:0:0") LINK_STATUS_BRIDGE = _i; \
|
||||
else if (_name == "backboard:0:0") LINK_BACKBOARD = _i; \
|
||||
else if (LINK_CONTROLLER == 1 && _i == 1) { ; /* Default to root if not found */ } \
|
||||
} \
|
||||
/* Safety Fallback: Use Root if Scoreboard prim is missing */ \
|
||||
|
|
@ -60,6 +62,20 @@ key EXPERIENCE_ID = "06926390-26e6-11f1-a452-0242ac110003"; // Final Girlz I.N.C
|
|||
#define MSG_RESET_LEADERBOARD 3012
|
||||
#define MSG_DISPLAY_LEADERBOARD 3013
|
||||
#define MSG_STATUS_TEXT 3007
|
||||
|
||||
// --- World Ranking Leaderboard (4000-4099) ---
|
||||
#define MSG_LB_LOAD_COMPLETE 4001
|
||||
#define MSG_LB_RECORD_PLAYER 4002
|
||||
#define MSG_LB_REQUEST_DISPLAY 4003
|
||||
#define MSG_LB_DISPLAY_DATA 4004
|
||||
#define MSG_LB_PAGE_NEXT 4005
|
||||
#define MSG_LB_PAGE_PREV 4006
|
||||
#define MAX_LB_PAGES 10 // 10 keys (Peril_LB_1 to Peril_LB_10)
|
||||
#define LB_KEY_PREFIX "Peril_LB_" // Base name for sharded keys
|
||||
#define LB_LOCK_KEY "Peril_LB_LOCK" // KVP lock for multi-board synchronization
|
||||
#define LB_LOCK_TIMEOUT 60 // Lock expiry in seconds
|
||||
#define MAX_ENTRIES_PER_PAGE 50 // 50 players per key
|
||||
#define MAX_WORLD_RANKING 500 // Total capacity (Pages * Entries)
|
||||
#define MSG_DICE_ROLL 3020
|
||||
#define MSG_CLEAR_DICE 3021
|
||||
#define MSG_DICE_CLEAR 3021 // Alias for consistency in dice bridge
|
||||
|
|
|
|||
|
|
@ -405,6 +405,7 @@ default {
|
|||
}
|
||||
|
||||
if (num == MSG_SHOW_MENU) {
|
||||
dbg("🕵️ [Dialog Handler] Received MSG_SHOW_MENU (" + str + ") from script link/sender: " + (string)sender + ", ID: " + (string)id);
|
||||
list args = llParseString2List(str, ["|"], []);
|
||||
if (llGetListLength(args) < 2) {
|
||||
dbg("⚠️ Invalid message format: " + str);
|
||||
|
|
|
|||
Loading…
Reference in a new issue