diff --git a/FURWARE_Leaderboard_Communication_Linkset.lsl b/FURWARE_Leaderboard_Communication_Linkset.lsl index 9d0238f..2528d3d 100644 --- a/FURWARE_Leaderboard_Communication_Linkset.lsl +++ b/FURWARE_Leaderboard_Communication_Linkset.lsl @@ -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); + } + } } } - } diff --git a/Game_Leaderboard_Manager.lsl b/Game_Leaderboard_Manager.lsl new file mode 100644 index 0000000..9ba7382 --- /dev/null +++ b/Game_Leaderboard_Manager.lsl @@ -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(); } + } +} diff --git a/Game_Scoreboard_Manager_Linkset.lsl b/Game_Scoreboard_Manager_Linkset.lsl index 58fdb0b..63ad448 100644 --- a/Game_Scoreboard_Manager_Linkset.lsl +++ b/Game_Scoreboard_Manager_Linkset.lsl @@ -1,999 +1,318 @@ #include "Peril_Constants.lsl" -// Game Scoreboard Manager - Linkset Version -// Shows current game players in grid layout -// Each player gets a prim showing profile picture + heart texture (lives) +// ============================================================================= +// PERIL SCOREBOARD - GRID DISPLAY MANAGER (v3.2.7 Final) +// ============================================================================= +// Shows current game players in grid layout (Profile Pic, Hearts, Overlay) +// Delegates persistent Leaderboard storage to Game_Leaderboard_Manager.lsl +// ============================================================================= // --- DYNAMIC LINK DISCOVERY --- integer BACKGROUND_PRIM = -1; integer ACTIONS_PRIM = -1; +list gridLinks = [0,0,0, 0,0,0, 0,0,0, 0,0,0, 0,0,0, 0,0,0, 0,0,0, 0,0,0, 0,0,0, 0,0,0]; -// Grid link mappings for player grid (Stride 3: Profile, Hearts, Overlay) -list gridLinks = [ - 0,0,0, 0,0,0, 0,0,0, 0,0,0, 0,0,0, - 0,0,0, 0,0,0, 0,0,0, 0,0,0, 0,0,0 -]; - -// Discover all relevant prims by name at startup discoverLinks() { - integer i; - integer total = llGetNumberOfPrims(); + integer _dl_i; + integer _dl_total = llGetNumberOfPrims(); + gridLinks = [0,0,0, 0,0,0, 0,0,0, 0,0,0, 0,0,0, 0,0,0, 0,0,0, 0,0,0, 0,0,0, 0,0,0]; - // Reset list (30 elements for 10 slots * 3 prims) - gridLinks = [ - 0,0,0, 0,0,0, 0,0,0, 0,0,0, 0,0,0, - 0,0,0, 0,0,0, 0,0,0, 0,0,0, 0,0,0 - ]; - - for (i = 1; i <= total; i++) { - string name = llGetLinkName(i); - - if (name == "backboard:0:0") BACKGROUND_PRIM = i; - else if (name == "TitleAction:0:0") ACTIONS_PRIM = i; - else if (name == "Scoreboard:0:0") LINK_SCOREBOARD = i; + for (_dl_i = 1; _dl_i <= _dl_total; _dl_i++) { + string _name = llGetLinkName(_dl_i); + if (_name == "backboard:0:0") BACKGROUND_PRIM = _dl_i; + else if (_name == "TitleAction:0:0") ACTIONS_PRIM = _dl_i; + else if (_name == "Scoreboard:0:0") LINK_SCOREBOARD = _dl_i; else { - // Pattern match: "type:row:col" - list parts = llParseString2List(name, [":"], []); - if (llGetListLength(parts) == 3) { - string type = llList2String(parts, 0); - integer row = (integer)llList2String(parts, 1); - integer col = (integer)llList2String(parts, 2); - integer index = (row * 2) + col; - - if (index >= 0 && index < 10) { - integer gridBase = index * 3; - if (type == "profile") gridLinks = llListReplaceList(gridLinks, [i], gridBase, gridBase); - else if (type == "life") gridLinks = llListReplaceList(gridLinks, [i], gridBase + 1, gridBase + 1); - else if (type == "overlay") gridLinks = llListReplaceList(gridLinks, [i], gridBase + 2, gridBase + 2); + list _p = llParseString2List(_name, [":"], []); + if (llGetListLength(_p) == 3) { + string _type = llList2String(_p, 0); + integer _idx = (((integer)llList2String(_p, 1)) * 2) + ((integer)llList2String(_p, 2)); + if (_idx >= 0 && _idx < 10) { + integer _gb = _idx * 3; + if (_type == "profile") gridLinks = llListReplaceList(gridLinks, [_dl_i], _gb, _gb); + else if (_type == "life") gridLinks = llListReplaceList(gridLinks, [_dl_i], _gb + 1, _gb + 1); + else if (_type == "overlay") gridLinks = llListReplaceList(gridLinks, [_dl_i], _gb + 2, _gb + 2); } } } } - dbg("Discovery done"); - if (BACKGROUND_PRIM == -1) { dbg("No backboard"); } - - // Safety Fallback: Use Root if Scoreboard prim is missing if (LINK_SCOREBOARD == -1) LINK_SCOREBOARD = 1; } -// --- Leaderboard Configuration --- -#define LEADERBOARD_WIDTH 32 // Standard width for 4x8 character Furware grids (4 columns of 8) -#define MAX_LEADERBOARD_ENTRIES 11 // Fits within 11 remaining rows (after 1-row header) +// --- Textures --- +#define LEADERBOARD_WIDTH 32 +#define TEXTURE_0_HEARTS "7d8ae121-e171-12ae-f5b6-7cc3c0395c7b" +#define TEXTURE_1_HEARTS "6605d25f-8e2d-2870-eb87-77c58cd47fa9" +#define TEXTURE_2_HEARTS "7ba6cb1e-f384-25a5-8e88-a90bbd7cc041" +#define TEXTURE_3_HEARTS "a5d16715-4648-6526-5582-e8068293f792" +#define TEXTURE_DEFAULT_PROFILE "1ce89375-6c3c-3845-26b1-1dc666bc9169" +#define TEXTURE_BOT_PROFILE "62f31722-04c1-8c29-c236-398543f2a6ae" +#define BLANK_TEXTURE "5748decc-f629-461c-9a36-a35a221fe21f" +#define TEXTURE_BACKGROUND "5748decc-f629-461c-9a36-a35a221fe21f" +#define TEXTURE_TITLE "624bb7a7-e856-965c-bae8-94d75226c1bc" +#define TEXTURE_ELIMINATED_X "90524092-03b0-1b3c-bcea-3ea5118c6dba" -// Heart texture UUIDs - REPLACE WITH YOUR ACTUAL TEXTURE UUIDs -#define TEXTURE_0_HEARTS "7d8ae121-e171-12ae-f5b6-7cc3c0395c7b" // 0 hearts (dead) -#define TEXTURE_1_HEARTS "6605d25f-8e2d-2870-eb87-77c58cd47fa9" // 1 heart -#define TEXTURE_2_HEARTS "7ba6cb1e-f384-25a5-8e88-a90bbd7cc041" // 2 hearts -#define TEXTURE_3_HEARTS "a5d16715-4648-6526-5582-e8068293f792" // 3 hearts +// Status textures +#define TEXTURE_PERIL "c5676fec-0c85-5567-3dd8-f939234e21d9" +#define TEXTURE_PERIL_SELECTED "a53ff601-3c8a-e312-9e0e-f6fa76f6773a" +#define TEXTURE_VICTORY "ec5bf10e-4970-fb63-e7bf-751e1dc27a8d" +#define TEXTURE_PUNISHMENT "acfabed0-84ad-bfd1-cdfc-2ada0aeeaa2f" +#define TEXTURE_DIRECT_HIT "ecd2dba2-3969-6c39-ad59-319747307f55" +#define TEXTURE_NO_SHIELD "2440174f-e385-44e2-8016-ac34934f11f5" +#define TEXTURE_PLOT_TWIST "ec533379-4f7f-8183-e877-e68af703dcce" -// Default textures - REPLACE WITH YOUR ACTUAL TEXTURE UUIDs -#define TEXTURE_DEFAULT_PROFILE "1ce89375-6c3c-3845-26b1-1dc666bc9169" // Default avatar -#define TEXTURE_BOT_PROFILE "62f31722-04c1-8c29-c236-398543f2a6ae" // Bot avatar picture -#define BLANK_TEXTURE "5748decc-f629-461c-9a36-a35a221fe21f" // White texture UUID -#define TEXTURE_BACKGROUND "5748decc-f629-461c-9a36-a35a221fe21f" // Background texture (blank + black color) -#define TEXTURE_ACTIONS "6aac8dce-1b83-f931-abe3-286f4f2faa29" // Punishment texture +list activePlayers = []; // Stride 3: Name, Lives, UUID/Texture +string currentPerilPlayer = ""; +string currentWinner = ""; +list httpRequests = []; -// Status textures - REPLACE WITH YOUR ACTUAL TEXTURE UUIDs -#define TEXTURE_PERIL "c5676fec-0c85-5567-3dd8-f939234e21d9" // Elimination texture -#define TEXTURE_PERIL_SELECTED "a53ff601-3c8a-e312-9e0e-f6fa76f6773a" // Peril Selected texture -#define TEXTURE_VICTORY "ec5bf10e-4970-fb63-e7bf-751e1dc27a8d" // Victory texture -#define TEXTURE_PUNISHMENT "acfabed0-84ad-bfd1-cdfc-2ada0aeeaa2f" // Punishment texture (fallback) -#define TEXTURE_DIRECT_HIT "ecd2dba2-3969-6c39-ad59-319747307f55" // Direct Hit texture -#define TEXTURE_NO_SHIELD "2440174f-e385-44e2-8016-ac34934f11f5" // No Shield texture -#define TEXTURE_PLOT_TWIST "ec533379-4f7f-8183-e877-e68af703dcce" // Plot Twist texture -#define TEXTURE_TITLE "624bb7a7-e856-965c-bae8-94d75226c1bc" // Title texture -#define TEXTURE_ELIMINATED_X "90524092-03b0-1b3c-bcea-3ea5118c6dba" // Red X overlay for eliminated players - -// Player data - used for BOTH current game display AND leaderboard -list activePlayers = []; // Current game state (Stride 3: Name, Lives, UUID) - -// Peril player tracking -string currentPerilPlayer = ""; // Track current peril player for glow effects -string currentWinner = ""; // Track current winner for green glow effects - -// Leaderboard data (persistent - stored in linkset data) - Stride of 3 [Name, Wins, Losses] -list leaderboardData = []; - -// HTTP tracking -list profileRequests = []; // Track which profile requests belong to which player -list httpRequests = []; // Track HTTP requests for profile pictures - -// KVP tracking -key kvpReadReq; -key kvpWriteReq; -string pendingSerialize; - - -// Profile picture extraction constants -// Profile picture extraction constants #define PROFILE_KEY_PREFIX ", <0,0,0>, 0.0, - PRIM_COLOR, ALL_SIDES, <0.0, 0.0, 0.0>, 1.0, // Black color - PRIM_TEXT, "", <0,0,0>, 0.0 // Remove any text - ]); - - dbg("Backboard Reset"); - return 0; +resetBackgroundPrim() { + if (BACKGROUND_PRIM != -1) { + llSetLinkPrimitiveParamsFast(BACKGROUND_PRIM, [PRIM_TEXTURE, ALL_SIDES, TEXTURE_BACKGROUND, <1,1,0>, <0,0,0>, 0.0, PRIM_COLOR, ALL_SIDES, <0.0, 0.0, 0.0>, 1.0, PRIM_TEXT, "", <0,0,0>, 0.0]); + } } -// Reset the scoreboard manager cube (link 12) to neutral state -integer resetManagerCube() { - llSetLinkPrimitiveParamsFast(LINK_SCOREBOARD, [ - PRIM_TEXTURE, ALL_SIDES, BLANK_TEXTURE, <1,1,0>, <0,0,0>, 0.0, - PRIM_COLOR, ALL_SIDES, <0.0, 0.0, 0.0>, 1.0, // Black color to blend in - PRIM_TEXT, "", <0,0,0>, 0.0 // Remove any text - ]); - - dbg("Manager Reset"); - return 0; +resetManagerCube() { + llSetLinkPrimitiveParamsFast(LINK_SCOREBOARD, [PRIM_TEXTURE, ALL_SIDES, BLANK_TEXTURE, <1,1,0>, <0,0,0>, 0.0, PRIM_COLOR, ALL_SIDES, <0.0, 0.0, 0.0>, 1.0, PRIM_TEXT, "", <0,0,0>, 0.0]); } -// Update actions prim with appropriate status texture -integer updateActionsPrim(string status) { - string textureToUse = ""; +updateActionsPrim(string status) { + string _uap_tex = ""; + if (status == "Elimination") _uap_tex = TEXTURE_PERIL; + else if (status == "Victory") _uap_tex = TEXTURE_VICTORY; + else if (status == "Punishment") _uap_tex = TEXTURE_PUNISHMENT; + else if (status == "Direct Hit") _uap_tex = TEXTURE_DIRECT_HIT; + else if (status == "No Shield") _uap_tex = TEXTURE_NO_SHIELD; + else if (status == "Peril Selected") _uap_tex = TEXTURE_PERIL_SELECTED; + else if (status == "Plot Twist") _uap_tex = TEXTURE_PLOT_TWIST; + else if (status == "Title") _uap_tex = TEXTURE_TITLE; - if (status == "Elimination") { - textureToUse = TEXTURE_PERIL; - } else if (status == "Victory") { - textureToUse = TEXTURE_VICTORY; - } else if (status == "Punishment") { - textureToUse = TEXTURE_PUNISHMENT; - } else if (status == "Direct Hit") { - textureToUse = TEXTURE_DIRECT_HIT; - } else if (status == "No Shield") { - textureToUse = TEXTURE_NO_SHIELD; - } else if (status == "Peril Selected") { - textureToUse = TEXTURE_PERIL_SELECTED; - } else if (status == "Plot Twist") { - textureToUse = TEXTURE_PLOT_TWIST; - } else if (status == "Title") { - textureToUse = TEXTURE_TITLE; - } else { - return 0; // Don't change texture for unrecognized statuses + if (ACTIONS_PRIM != -1 && _uap_tex != "") { + llSetLinkPrimitiveParamsFast(ACTIONS_PRIM, [PRIM_TEXTURE, ALL_SIDES, _uap_tex, <1,1,0>, <0,0,0>, 0.0, PRIM_TEXT, "", <0,0,0>, 0.0]); } - - // Replace with action prim texture update - if (ACTIONS_PRIM != -1) { - llSetLinkPrimitiveParamsFast(ACTIONS_PRIM, [ - PRIM_TEXTURE, ALL_SIDES, textureToUse, <1,1,0>, <0,0,0>, 0.0, - PRIM_TEXT, "", <0,0,0>, 0.0 // Remove existing floating text - ]); - } - return 0; } -// Load leaderboard data from Experience KVP database -integer loadLeaderboardData() { - kvpReadReq = llReadKeyValue("Peril_LB_Top50"); - dbg("LB Requesting"); - return 0; -} - -// Save leaderboard data to Experience KVP database -integer saveLeaderboardData() { - string serialized = ""; - integer totalEntries = llGetListLength(leaderboardData) / 3; - integer saveCount = totalEntries; - integer saveI = 0; - integer base = 0; - string save_entry = ""; - - if (saveCount > 50) saveCount = 50; // Cap at Top 50 - - for (saveI = 0; saveI < saveCount; saveI++) { - base = saveI * 3; - save_entry = llList2String(leaderboardData, base) + ":" + - (string)llList2Integer(leaderboardData, base + 1) + ":" + - (string)llList2Integer(leaderboardData, base + 2); - if (serialized != "") serialized += "|"; - serialized += save_entry; - } - - pendingSerialize = serialized; - kvpWriteReq = llUpdateKeyValue("Peril_LB_Top50", serialized, FALSE, ""); - - dbg("LB Saving"); - return 0; -} - -// Handle game won - update leaderboard -integer handleGameWon(string winnerName) { - integer idx = llListFindList(leaderboardData, [winnerName]); - if (idx == -1) { - // New player - leaderboardData += [winnerName, 1, 0]; - } else { - // Existing player - integer currentWins = llList2Integer(leaderboardData, idx + 1); - leaderboardData = llListReplaceList(leaderboardData, [currentWins + 1], idx + 1, idx + 1); - } - - saveLeaderboardData(); - generateLeaderboardText(); - return 0; -} - -// Handle game lost - update leaderboard -integer handleGameLost(string loserName) { - integer lostIdx = llListFindList(leaderboardData, [loserName]); - if (lostIdx == -1) { - // New player - leaderboardData += [loserName, 0, 1]; - } else { - // Existing player - integer currentLosses = llList2Integer(leaderboardData, lostIdx + 2); - leaderboardData = llListReplaceList(leaderboardData, [currentLosses + 1], lostIdx + 2, lostIdx + 2); - } - - saveLeaderboardData(); - generateLeaderboardText(); - return 0; -} - -list getSortedLeaderboard() { - list sortList = []; - integer i = 0; - integer totalEntries = llGetListLength(leaderboardData) / 3; - integer base = 0; - integer wins = 0; - integer losses = 0; - integer sortKey = 0; - list sortedData = []; - integer origIdx = 0; - - for (i = 0; i < totalEntries; i++) { - base = i * 3; - wins = llList2Integer(leaderboardData, base + 1); - losses = llList2Integer(leaderboardData, base + 2); - - if (wins > 65535) wins = 65535; - if (losses > 65535) losses = 65535; - - sortKey = (wins << 16) | (65535 - losses); - sortList += [sortKey, i]; - } - - // 2. Sort by key (stride of 2) in descending order (highest score first) - sortList = llListSort(sortList, 2, FALSE); - - // 3. Rebuild the leaderboard data in sorted order (Stided list: Name, Wins, Losses) - for (i = 0; i < llGetListLength(sortList); i += 2) { - origIdx = llList2Integer(sortList, i + 1); - base = origIdx * 3; - sortedData += [ - llList2String(leaderboardData, base), - llList2Integer(leaderboardData, base + 1), - llList2Integer(leaderboardData, base + 2) - ]; - } - - return sortedData; -} - -// Generate leaderboard text and send to separate leaderboard object -integer generateLeaderboardText() { - list sortedData = getSortedLeaderboard(); - string SPACES = " "; - string title = "TOP BATTLE RECORDS"; - string rankCol = ""; - string nameCol = ""; - string statsCol = ""; - - integer numSorted = llGetListLength(sortedData) / 3; - integer genI; - integer base; - string playerName; - integer genWins; - integer genLosses; - string rank; - string sWins; - string sLosses; - - // 1. Send Title to Parent Box (Row 0) - integer titleMargin = (LEADERBOARD_WIDTH - llStringLength(title)) / 2; - if (titleMargin < 0) titleMargin = 0; - string titleText = llGetSubString(SPACES, 0, titleMargin - 1) + title; - llMessageLinked(LINK_SET, MSG_DISPLAY_LEADERBOARD, "FORMATTED_TEXT|" + titleText, NULL_KEY); - - // Add actual player data column-by-column - for (genI = 0; genI < numSorted && genI < MAX_LEADERBOARD_ENTRIES; genI++) { - base = genI * 3; - playerName = llList2String(sortedData, base); - genWins = llList2Integer(sortedData, base + 1); - genLosses = llList2Integer(sortedData, base + 2); - - if (llStringLength(playerName) > 19) playerName = llGetSubString(playerName, 0, 16) + "..."; - - rank = (string)(genI + 1) + "."; - if (genI < 9) rank = "0" + rank; - - sWins = (string)genWins; - sLosses = (string)genLosses; - - rankCol += rank + "\n"; - nameCol += playerName + "\n"; - statsCol += "W:" + sWins + "/L:" + sLosses + "\n"; - } - - // Fill remaining positions with placeholders - for (genI = numSorted; genI < MAX_LEADERBOARD_ENTRIES; genI++) { - rank = (string)(genI + 1) + "."; - if (genI < 9) rank = "0" + rank; - - rankCol += rank + "\n"; - nameCol += "----------\n"; - statsCol += "W:0/L:0\n"; - } - - // Send 3-column data to bridge - llMessageLinked(LINK_SET, MSG_DISPLAY_LEADERBOARD, "COLUMNS|" + rankCol + "|" + nameCol + "|" + statsCol, NULL_KEY); - return 0; -} - -// Reset leaderboard data (SECURED for Creator Only) -integer resetLeaderboard() { - // PROTECT THE GLOBAL DATABASE! - if (llGetOwner() != llGetCreator()) { - llOwnerSay("❌ Access Denied: Only the creator of Peril Dice can wipe the Global Scoreboard!"); - return 0; - } - - leaderboardData = []; - pendingSerialize = ""; - kvpWriteReq = llUpdateKeyValue("Peril_LB_Top50", "", FALSE, ""); - generateLeaderboardText(); // Send empty leaderboard immediately - return 0; -} - -// Clear all current players from the scoreboard -integer clearAllPlayers() { +clearAllPlayers() { activePlayers = []; - - // Clear any pending HTTP requests httpRequests = []; - profileRequests = []; - - // Variable declarations for the loop - integer clearI; - integer clearProfilePrimIndex; - integer clearHeartsPrimIndex; - integer clearOverlayPrimIndex; - - // Reset all player prims to default state - for (clearI = 0; clearI < 10; clearI++) { - clearProfilePrimIndex = getProfilePrimLink(clearI); - clearHeartsPrimIndex = getHeartsPrimLink(clearI); - clearOverlayPrimIndex = getOverlayPrimLink(clearI); - - // SAFETY CHECK - use if block instead of continue for maximum compatibility - if (clearProfilePrimIndex > 0) { - // Reset profile prim to default and hide it - llSetLinkPrimitiveParamsFast(clearProfilePrimIndex, [ - PRIM_TEXTURE, ALL_SIDES, TEXTURE_DEFAULT_PROFILE, <1,1,0>, <0,0,0>, 0.0, - PRIM_COLOR, ALL_SIDES, <1.0, 1.0, 1.0>, 0.0, // Hidden by default - PRIM_TEXT, "", <1,1,1>, 0.0, - PRIM_GLOW, ALL_SIDES, 0.0 - ]); - - // Reset hearts prim to 3 hearts and hide it - llSetLinkPrimitiveParamsFast(clearHeartsPrimIndex, [ - PRIM_TEXTURE, ALL_SIDES, TEXTURE_3_HEARTS, <1,1,0>, <0,0,0>, 0.0, - PRIM_COLOR, ALL_SIDES, <1.0, 1.0, 1.0>, 0.0, // Hidden by default - PRIM_TEXT, "", <1,1,1>, 0.0, - PRIM_GLOW, ALL_SIDES, 0.0 - ]); - - // Reset overlay prim to transparent - llSetLinkPrimitiveParamsFast(clearOverlayPrimIndex, [ - PRIM_TEXTURE, ALL_SIDES, BLANK_TEXTURE, <1,1,0>, <0,0,0>, 0.0, - PRIM_COLOR, ALL_SIDES, <1.0, 1.0, 1.0>, 0.0, // Fully transparent - PRIM_TEXT, "", <1,1,1>, 0.0 - ]); + integer _ca_i; + for (_ca_i = 0; _ca_i < 10; _ca_i++) { + integer _pL = getProfilePrimLink(_ca_i); + integer _hL = getHeartsPrimLink(_ca_i); + integer _oL = getOverlayPrimLink(_ca_i); + if (_pL > 0) { + llSetLinkPrimitiveParamsFast(_pL, [PRIM_COLOR, ALL_SIDES, <1,1,1>, 0.0]); + llSetLinkPrimitiveParamsFast(_hL, [PRIM_COLOR, ALL_SIDES, <1,1,1>, 0.0]); + llSetLinkPrimitiveParamsFast(_oL, [PRIM_COLOR, ALL_SIDES, <1,1,1>, 0.0]); } } - return 0; } -// Remove a single player from the scoreboard display -integer removePlayer(string playerName) { - integer idx = llListFindList(activePlayers, [playerName]); - if (idx != -1) { - activePlayers = llDeleteSubList(activePlayers, idx, idx + 2); +removePlayer(string pName) { + integer _rem_idx = llListFindList(activePlayers, [pName]); + if (_rem_idx != -1) { + activePlayers = llDeleteSubList(activePlayers, _rem_idx, _rem_idx + 2); refreshPlayerDisplay(); - dbg("📈 Player removed: " + playerName); } - return 0; } -integer refreshPlayerDisplay() { - integer numActivePlayers = llGetListLength(activePlayers) / 3; - integer refreshI; - integer refreshProfilePrimIndex = 0; - integer refreshHeartsPrimIndex = 0; - integer refreshOverlayPrimIndex = 0; - - // Position-specific variables - string r_name; - integer r_lives; - string r_profileTexture; - integer r_profileIdx; - integer r_heartsIdx; - integer r_overlayIdx; - - // Only reset prims for unused slots (avoid flickering for active players) - for (refreshI = numActivePlayers; refreshI < 10; refreshI++) { - refreshProfilePrimIndex = getProfilePrimLink(refreshI); - refreshHeartsPrimIndex = getHeartsPrimLink(refreshI); - refreshOverlayPrimIndex = getOverlayPrimLink(refreshI); - - if (refreshProfilePrimIndex > 0) { - // Unused slots: Reset and HIDE - llSetLinkPrimitiveParamsFast(refreshProfilePrimIndex, [ - PRIM_TEXTURE, ALL_SIDES, TEXTURE_DEFAULT_PROFILE, <1,1,0>, <0,0,0>, 0.0, - PRIM_COLOR, ALL_SIDES, <1.0, 1.0, 1.0>, 0.0, // Invisible - PRIM_TEXT, "", <1,1,1>, 0.0 - ]); - - llSetLinkPrimitiveParamsFast(refreshHeartsPrimIndex, [ - PRIM_TEXTURE, ALL_SIDES, TEXTURE_3_HEARTS, <1,1,0>, <0,0,0>, 0.0, - PRIM_COLOR, ALL_SIDES, <1.0, 1.0, 1.0>, 0.0, // Invisible - PRIM_TEXT, "", <1,1,1>, 0.0 - ]); - - // Hide overlay prim (make transparent) - llSetLinkPrimitiveParamsFast(refreshOverlayPrimIndex, [ - PRIM_TEXTURE, ALL_SIDES, BLANK_TEXTURE, <1,1,0>, <0,0,0>, 0.0, - PRIM_COLOR, ALL_SIDES, <1.0, 1.0, 1.0>, 0.0, // Fully transparent - PRIM_TEXT, "", <1,1,1>, 0.0 - ]); - } - } - - // Now display all current players in their new positions - for (refreshI = 0; refreshI < numActivePlayers; refreshI++) { - integer base = refreshI * 3; - r_name = llList2String(activePlayers, base); - r_lives = llList2Integer(activePlayers, base + 1); - r_profileTexture = llList2String(activePlayers, base + 2); - - // Calculate prim indices for this position - r_profileIdx = getProfilePrimLink(refreshI); - r_heartsIdx = getHeartsPrimLink(refreshI); - r_overlayIdx = getOverlayPrimLink(refreshI); - - if (r_profileIdx > 0) { - // Determine profile texture to use - if (isBot(r_name)) { - r_profileTexture = TEXTURE_BOT_PROFILE; - } else if (r_profileTexture == "" || r_profileTexture == "00000000-0000-0000-0000-000000000000") { - r_profileTexture = TEXTURE_DEFAULT_PROFILE; - } - - // Set profile texture - llSetLinkPrimitiveParamsFast(r_profileIdx, [ - PRIM_TEXTURE, ALL_SIDES, r_profileTexture, <1,1,0>, <0,0,0>, 0.0, - PRIM_COLOR, ALL_SIDES, <1.0, 1.0, 1.0>, 1.0, - PRIM_TEXT, "", <1,1,1>, 0.0 - ]); - - // Set hearts texture - string heartTexture = getHeartTexture(r_lives); - llSetLinkPrimitiveParamsFast(r_heartsIdx, [ - PRIM_TEXTURE, ALL_SIDES, heartTexture, <1,1,0>, <0,0,0>, 0.0, - PRIM_COLOR, ALL_SIDES, <1.0, 1.0, 1.0>, 1.0, - PRIM_TEXT, "", <1,1,1>, 0.0 - ]); - - // Control overlay prim - show red X if eliminated, hide if alive - if (r_lives <= 0) { - llSetLinkPrimitiveParamsFast(r_overlayIdx, [ - PRIM_TEXTURE, ALL_SIDES, TEXTURE_ELIMINATED_X, <1,1,0>, <0,0,0>, 0.0, - PRIM_COLOR, ALL_SIDES, <1.0, 1.0, 1.0>, 1.0, - PRIM_TEXT, "", <1,1,1>, 0.0 - ]); +refreshPlayerDisplay() { + integer _rpd_num = llGetListLength(activePlayers) / 3; + integer _rpd_ri; + for (_rpd_ri = 0; _rpd_ri < 10; _rpd_ri++) { + integer _rpd_pL = getProfilePrimLink(_rpd_ri); + integer _rpd_hL = getHeartsPrimLink(_rpd_ri); + integer _rpd_oL = getOverlayPrimLink(_rpd_ri); + if (_rpd_pL > 0) { + if (_rpd_ri >= _rpd_num) { + llSetLinkPrimitiveParamsFast(_rpd_pL, [PRIM_COLOR, ALL_SIDES, <1,1,1>, 0.0]); + llSetLinkPrimitiveParamsFast(_rpd_hL, [PRIM_COLOR, ALL_SIDES, <1,1,1>, 0.0]); + llSetLinkPrimitiveParamsFast(_rpd_oL, [PRIM_COLOR, ALL_SIDES, <1,1,1>, 0.0]); } else { - llSetLinkPrimitiveParamsFast(r_overlayIdx, [ - PRIM_TEXTURE, ALL_SIDES, BLANK_TEXTURE, <1,1,0>, <0,0,0>, 0.0, - PRIM_COLOR, ALL_SIDES, <1.0, 1.0, 1.0>, 0.0, - PRIM_TEXT, "", <1,1,1>, 0.0 - ]); + integer _rpd_b = _rpd_ri * 3; + string _rpd_nm = llList2String(activePlayers, _rpd_b); + integer _rpd_lv = llList2Integer(activePlayers, _rpd_b + 1); + string _rpd_tx = llList2String(activePlayers, _rpd_b + 2); + if (isBot(_rpd_nm)) _rpd_tx = TEXTURE_BOT_PROFILE; + else if (_rpd_tx == "" || _rpd_tx == NULL_KEY) _rpd_tx = TEXTURE_DEFAULT_PROFILE; + + llSetLinkPrimitiveParamsFast(_rpd_pL, [PRIM_TEXTURE, ALL_SIDES, _rpd_tx, <1,1,0>, <0,0,0>, 0.0, PRIM_COLOR, ALL_SIDES, <1,1,1>, 1.0]); + llSetLinkPrimitiveParamsFast(_rpd_hL, [PRIM_TEXTURE, ALL_SIDES, getHeartTexture(_rpd_lv), <1,1,0>, <0,0,0>, 0.0, PRIM_COLOR, ALL_SIDES, <1,1,1>, 1.0]); + if (_rpd_lv <= 0) llSetLinkPrimitiveParamsFast(_rpd_oL, [PRIM_TEXTURE, ALL_SIDES, TEXTURE_ELIMINATED_X, <1,1,0>, <0,0,0>, 0.0, PRIM_COLOR, ALL_SIDES, <1,1,1>, 1.0]); + else llSetLinkPrimitiveParamsFast(_rpd_oL, [PRIM_COLOR, ALL_SIDES, <1,1,1>, 0.0]); } } } - - // Update player glow effects after refreshing all players updatePlayerGlowEffects(); - return 0; } -// Check if a player name indicates it's a bot -integer isBot(string playerName) { - return (llSubStringIndex(playerName, "Bot") == 0); -} - -// Map player index to profile prim link number -// Functions relocated to avoid hardcoded math -integer getProfilePrimLink(integer playerIndex) { return llList2Integer(gridLinks, playerIndex * 3); } -integer getHeartsPrimLink(integer playerIndex) { return llList2Integer(gridLinks, (playerIndex * 3) + 1); } -integer getOverlayPrimLink(integer playerIndex) { return llList2Integer(gridLinks, (playerIndex * 3) + 2); } +integer isBot(string pName) { return (llSubStringIndex(pName, "Bot") == 0); } +integer getProfilePrimLink(integer pI) { return llList2Integer(gridLinks, pI * 3); } +integer getHeartsPrimLink(integer pI) { return llList2Integer(gridLinks, (pI * 3) + 1); } +integer getOverlayPrimLink(integer pI) { return llList2Integer(gridLinks, (pI * 3) + 2); } string getHeartTexture(integer lives) { if (lives <= 0) return TEXTURE_0_HEARTS; - else if (lives == 1) return TEXTURE_1_HEARTS; - else if (lives == 2) return TEXTURE_2_HEARTS; - else return TEXTURE_3_HEARTS; // 3 or more + if (lives == 1) return TEXTURE_1_HEARTS; + if (lives == 2) return TEXTURE_2_HEARTS; + return TEXTURE_3_HEARTS; } -// Helper to set glow and color on a prim setGlow(integer link, float glow, vector color) { - if (link > 0) { - llSetLinkPrimitiveParamsFast(link, [PRIM_GLOW, ALL_SIDES, glow, PRIM_COLOR, ALL_SIDES, color, 1.0]); - } + if (link > 0) llSetLinkPrimitiveParamsFast(link, [PRIM_GLOW, ALL_SIDES, glow, PRIM_COLOR, ALL_SIDES, color, 1.0]); } -// Update all glow effects (peril and winner) -integer updatePlayerGlowEffects() { - integer numPlayers = llGetListLength(activePlayers) / 3; - integer glowI; - for (glowI = 0; glowI < numPlayers; glowI++) { - setGlow(getProfilePrimLink(glowI), 0.0, <1,1,1>); - setGlow(getHeartsPrimLink(glowI), 0.0, <1,1,1>); +updatePlayerGlowEffects() { + integer _uge_num = llGetListLength(activePlayers) / 3; + integer _uge_gi; + for (_uge_gi = 0; _uge_gi < _uge_num; _uge_gi++) { + setGlow(getProfilePrimLink(_uge_gi), 0.0, <1,1,1>); + setGlow(getHeartsPrimLink(_uge_gi), 0.0, <1,1,1>); } - - integer idx = -1; - vector col = <1,1,1>; - float glow = 0.0; - + integer _uge_idx = -1; vector _uge_col = <1,1,1>; float _uge_glow = 0.0; if (currentWinner != "" && currentWinner != "NONE") { - idx = llListFindList(activePlayers, [currentWinner]); - if (idx != -1) { idx = idx / 3; col = <0,1,0>; glow = 0.3; } + _uge_idx = llListFindList(activePlayers, [currentWinner]); + if (_uge_idx != -1) { _uge_idx = _uge_idx / 3; _uge_col = <0,1,0>; _uge_glow = 0.3; } } else if (currentPerilPlayer != "" && currentPerilPlayer != "NONE") { - idx = llListFindList(activePlayers, [currentPerilPlayer]); - if (idx != -1) { idx = idx / 3; col = <1,1,0>; glow = 0.2; } + _uge_idx = llListFindList(activePlayers, [currentPerilPlayer]); + if (_uge_idx != -1) { _uge_idx = _uge_idx / 3; _uge_col = <1,1,0>; _uge_glow = 0.2; } } - - if (idx != -1) { - setGlow(getProfilePrimLink(idx), glow, col); - setGlow(getHeartsPrimLink(idx), glow, col); + if (_uge_idx != -1) { + setGlow(getProfilePrimLink(_uge_idx), _uge_glow, _uge_col); + setGlow(getHeartsPrimLink(_uge_idx), _uge_glow, _uge_col); } - return 0; } -// Backward compatibility wrapper -integer updatePerilPlayerGlow() { - updatePlayerGlowEffects(); - return 0; -} - -// Update player display on the grid -integer updatePlayerDisplay(string playerName, integer lives, string profileUUID) { - integer playerIndex = llListFindList(activePlayers, [playerName]); - if (playerIndex != -1) playerIndex = playerIndex / 3; +updatePlayerDisplay(string pName, integer pLives, string pUUID) { + integer _upd_pIdx = llListFindList(activePlayers, [pName]); + if (_upd_pIdx != -1) _upd_pIdx = _upd_pIdx / 3; - if (playerIndex == -1) { - playerIndex = llGetListLength(activePlayers) / 3; - if (playerIndex >= 10) { - dbg("📈 MAX players reached: " + playerName); - return 0; - } - - string activeUUID = profileUUID; - if (isBot(playerName)) activeUUID = TEXTURE_BOT_PROFILE; - activePlayers += [playerName, lives, activeUUID]; - - // DEBUG: Show prim mapping - DISABLED to save memory - // if (VERBOSE_LOGGING) { - // integer debugProfile = getProfilePrimLink(playerIndex); - // integer debugHearts = getHeartsPrimLink(playerIndex); - // integer debugOverlay = getOverlayPrimLink(playerIndex); - // llOwnerSay("🔧 [Scoreboard] Player " + (string)playerIndex + " (" + playerName + "): Profile=" + (string)debugProfile + ", Hearts=" + (string)debugHearts + ", Overlay=" + (string)debugOverlay); - // llOwnerSay("🔍 [Scoreboard] Prim ranges: FIRST_PROFILE=" + (string)FIRST_PROFILE_PRIM + ", LAST_PROFILE=" + (string)LAST_PROFILE_PRIM); - // - // // Test if the calculated prim is actually valid - // if (debugProfile < FIRST_PROFILE_PRIM || debugProfile > LAST_PROFILE_PRIM) { - // llOwnerSay("⚠️ [Scoreboard] WARNING: Calculated profile prim " + (string)debugProfile + " is outside valid range!"); - // } - // } - + if (_upd_pIdx == -1) { + _upd_pIdx = llGetListLength(activePlayers) / 3; + if (_upd_pIdx >= 10) return; + string _upd_activeUUID = pUUID; + if (isBot(pName)) _upd_activeUUID = TEXTURE_BOT_PROFILE; + activePlayers += [pName, pLives, _upd_activeUUID]; } else { - activePlayers = llListReplaceList(activePlayers, [lives], (playerIndex * 3) + 1, (playerIndex * 3) + 1); + activePlayers = llListReplaceList(activePlayers, [pLives], (_upd_pIdx * 3) + 1, (_upd_pIdx * 3) + 1); } - // Update the physical prims for this player - integer updateProfilePrimIndex = getProfilePrimLink(playerIndex); - integer updateHeartsPrimIndex = getHeartsPrimLink(playerIndex); - integer updateOverlayPrimIndex = getOverlayPrimLink(playerIndex); + integer _upd_upP = getProfilePrimLink(_upd_pIdx); + integer _upd_upH = getHeartsPrimLink(_upd_pIdx); + integer _upd_upO = getOverlayPrimLink(_upd_pIdx); + if (_upd_upP <= 0) return; - if (updateProfilePrimIndex <= 0) return 0; + string _upd_uT; vector _upd_pC = <1,1,1>; + if (pLives <= 0) _upd_pC = <1.0, 0.3, 0.3>; - // Update profile prim texture - show red X if eliminated, otherwise show profile - string updateProfileTexture; - - if (lives <= 0) { - // Player is eliminated - keep profile picture but show red X on overlay prim - dbg("📈 [Scoreboard Manager] 💀 Scoreboard: Showing elimination X overlay for " + playerName); - - // CRITICAL: Still need to set updateProfileTexture for eliminated players - if (isBot(playerName)) { - updateProfileTexture = TEXTURE_BOT_PROFILE; - } else { - string updateCachedTexture = llList2String(activePlayers, (playerIndex * 3) + 2); - if (updateCachedTexture != "" && updateCachedTexture != profileUUID && - updateCachedTexture != TEXTURE_DEFAULT_PROFILE && updateCachedTexture != TEXTURE_BOT_PROFILE && - updateCachedTexture != TEXTURE_ELIMINATED_X) { - updateProfileTexture = updateCachedTexture; - } else { - updateProfileTexture = TEXTURE_DEFAULT_PROFILE; - } - } - } else { - // Player is alive - determine profile texture to use - updateProfileTexture = profileUUID; - - // Check if we already have a cached profile texture for this player - string liveCachedTexture = llList2String(activePlayers, (playerIndex * 3) + 2); - if (liveCachedTexture != "" && liveCachedTexture != profileUUID && - liveCachedTexture != TEXTURE_DEFAULT_PROFILE && liveCachedTexture != TEXTURE_BOT_PROFILE && - liveCachedTexture != TEXTURE_ELIMINATED_X) { - updateProfileTexture = liveCachedTexture; - } - - // If no cached texture or using original UUID, determine what to use - // if (VERBOSE_LOGGING) { - // llOwnerSay("🔍 [DEBUG] " + playerName + " - profileTexture='" + profileTexture + "', profileUUID='" + profileUUID + "', isBot=" + (string)isBot(playerName)); - // } - - if (updateProfileTexture == profileUUID) { - if (isBot(playerName)) { - updateProfileTexture = TEXTURE_BOT_PROFILE; - // if (VERBOSE_LOGGING) { - // llOwnerSay("🤖 [DEBUG] " + playerName + " is a bot, using bot texture"); - // } - } else if (updateProfileTexture == "" || updateProfileTexture == "00000000-0000-0000-0000-000000000000") { - updateProfileTexture = TEXTURE_DEFAULT_PROFILE; - dbg("Default texture"); - } else { - // CRITICAL: Set default texture FIRST, then request profile picture - updateProfileTexture = TEXTURE_DEFAULT_PROFILE; - - // if (VERBOSE_LOGGING) { - // llOwnerSay("🔄 [DEBUG] " + playerName + " making HTTP request for UUID: " + profileUUID); - // } - - // Request profile picture via HTTP (will update later) - #define URL_RESIDENT "https://world.secondlife.com/resident/" - key httpRequestID = llHTTPRequest(URL_RESIDENT + profileUUID, [HTTP_METHOD, "GET"], ""); - - // Store the HTTP request mapping: requestID -> playerIndex - httpRequests += [httpRequestID, playerIndex]; - } + if (isBot(pName)) _upd_uT = TEXTURE_BOT_PROFILE; + else { + _upd_uT = llList2String(activePlayers, (_upd_pIdx * 3) + 2); + if (_upd_uT == "" || _upd_uT == pUUID || _upd_uT == TEXTURE_DEFAULT_PROFILE) { + _upd_uT = TEXTURE_DEFAULT_PROFILE; + key _upd_rID = llHTTPRequest("https://world.secondlife.com/resident/" + pUUID, [HTTP_METHOD, "GET"], ""); + httpRequests += [_upd_rID, _upd_pIdx]; } } - // Enhanced visual effects for eliminated vs alive players - vector primColor; - if (lives <= 0) { - // Eliminated player - red tint to make X more prominent - primColor = <1.0, 0.3, 0.3>; // Red tinted - } else { - // Alive player - normal coloring - primColor = <1.0, 1.0, 1.0>; // White/normal - } - - // AGGRESSIVE VISUAL UPDATE: Force immediate rendering with glow - llSetLinkPrimitiveParamsFast(updateProfilePrimIndex, [ - PRIM_TEXTURE, ALL_SIDES, updateProfileTexture, <1,1,0>, <0,0,0>, 0.0, - PRIM_COLOR, ALL_SIDES, primColor, 1.0, - PRIM_TEXT, "", <1,1,1>, 0.0, // Remove player name text - PRIM_GLOW, ALL_SIDES, 0.01 // Tiny glow to force re-render - ]); - - // Remove the glow after a moment to make it normal + llSetLinkPrimitiveParamsFast(_upd_upP, [PRIM_TEXTURE, ALL_SIDES, _upd_uT, <1,1,0>, <0,0,0>, 0.0, PRIM_COLOR, ALL_SIDES, _upd_pC, 1.0, PRIM_GLOW, ALL_SIDES, 0.01]); llSleep(DELAY_SCOREBOARD_REFRESH); - llSetLinkPrimitiveParamsFast(updateProfilePrimIndex, [ - PRIM_GLOW, ALL_SIDES, 0.0 // Remove glow - ]); + llSetLinkPrimitiveParamsFast(_upd_upP, [PRIM_GLOW, ALL_SIDES, 0.0]); + llSetLinkPrimitiveParamsFast(_upd_upH, [PRIM_TEXTURE, ALL_SIDES, getHeartTexture(pLives), <1,1,0>, <0,0,0>, 0.0, PRIM_COLOR, ALL_SIDES, <1,1,1>, 1.0]); + if (pLives <= 0) llSetLinkPrimitiveParamsFast(_upd_upO, [PRIM_TEXTURE, ALL_SIDES, TEXTURE_ELIMINATED_X, <1,1,0>, <0,0,0>, 0.0, PRIM_COLOR, ALL_SIDES, <1,1,1>, 1.0]); + else llSetLinkPrimitiveParamsFast(_upd_upO, [PRIM_COLOR, ALL_SIDES, <1,1,1>, 0.0]); - // if (VERBOSE_LOGGING) { - // llOwnerSay("🖼️ [Scoreboard] Applied texture " + profileTexture + " to prim " + (string)profilePrimIndex + " with forced refresh"); - // } - - // Update hearts prim texture - string updateHeartTexture = getHeartTexture(lives); - llSetLinkPrimitiveParamsFast(updateHeartsPrimIndex, [ - PRIM_TEXTURE, ALL_SIDES, updateHeartTexture, <1,1,0>, <0,0,0>, 0.0, - PRIM_COLOR, ALL_SIDES, <1.0, 1.0, 1.0>, 1.0, - PRIM_TEXT, "", <1,0,0>, 0.0 // Remove lives text - ]); - - // Update overlay prim - show red X if eliminated, hide if alive - if (lives <= 0) { - // Show red X overlay for eliminated player - llSetLinkPrimitiveParamsFast(updateOverlayPrimIndex, [ - PRIM_TEXTURE, ALL_SIDES, TEXTURE_ELIMINATED_X, <1,1,0>, <0,0,0>, 0.0, - PRIM_COLOR, ALL_SIDES, <1.0, 1.0, 1.0>, 1.0, // Fully visible - PRIM_TEXT, "", <1,1,1>, 0.0 - ]); - } else { - // Hide overlay for alive player (make it transparent) - llSetLinkPrimitiveParamsFast(updateOverlayPrimIndex, [ - PRIM_TEXTURE, ALL_SIDES, BLANK_TEXTURE, <1,1,0>, <0,0,0>, 0.0, - PRIM_COLOR, ALL_SIDES, <1.0, 1.0, 1.0>, 0.0, // Fully transparent - PRIM_TEXT, "", <1,1,1>, 0.0 - ]); - } - - // Update player glow effects after display changes updatePlayerGlowEffects(); - return 0; } default { - on_rez(integer start_param) { - discoverLinks(); - llResetScript(); - } + on_rez(integer _sp) { discoverLinks(); llResetScript(); } state_entry() { discoverLinks(); llListen(1, "", llGetOwner(), ""); REPORT_MEMORY(); - dbg("Ready"); - - // Reset background prim to proper state resetBackgroundPrim(); - - // Reset manager cube to neutral state resetManagerCube(); - - // Load persistent leaderboard data from KVP Experience database - loadLeaderboardData(); - // Set a timer to passively auto-refresh the global scoreboard every 5 minutes - llSetTimerEvent(300.0); - // NOTE: generateLeaderboardText() will be triggered by dataserver automatically when data arrives - // No need to generate it randomly before data arrives - - // Ensure no glow effects remain from previous sessions - currentPerilPlayer = ""; - currentWinner = ""; updatePlayerGlowEffects(); - - dbg("Init complete"); + dbg("Ready"); } - link_message(integer sender, integer num, string str, key id) { - // Handle messages from the game logic and bridges - - if (num == MSG_GAME_STATUS) { - updateActionsPrim(str); - } - else if (num == MSG_PLAYER_UPDATE) { - list parts = llParseString2List(str, ["|"], []); - if (llGetListLength(parts) >= 3) { - string playerName = llList2String(parts, 0); - integer lives = (integer)llList2String(parts, 1); - string profileUUID = llList2String(parts, 2); - - // Debug: Show what data scoreboard received - DISABLED to save memory - dbg("Update: " + playerName); - // } - - updatePlayerDisplay(playerName, lives, profileUUID); + link_message(integer _s, integer _n, string _st, key _i) { + if (_n == MSG_GAME_STATUS) updateActionsPrim(_st); + else if (_n == MSG_PLAYER_UPDATE) { + list _parts = llParseString2List(_st, ["|"], []); + if (llGetListLength(_parts) >= 3) { + updatePlayerDisplay(llList2String(_parts, 0), (integer)llList2String(_parts, 1), llList2String(_parts, 2)); } } - else if (num == MSG_CLEAR_GAME) { - // Reset to default state - resetBackgroundPrim(); // Reset background to black - resetManagerCube(); // Reset manager cube to neutral - clearAllPlayers(); // Clear all current players - updateActionsPrim("Title"); // Reset to title - - // Clear peril player and winner tracking and remove any remaining glow - currentPerilPlayer = ""; - currentWinner = ""; - updatePlayerGlowEffects(); // This will remove glow from all players - - generateLeaderboardText(); // Refresh leaderboard display + else if (_n == MSG_CLEAR_GAME) { + resetBackgroundPrim(); resetManagerCube(); clearAllPlayers(); updateActionsPrim("Title"); + currentPerilPlayer = ""; currentWinner = ""; updatePlayerGlowEffects(); + llMessageLinked(LINK_SET, MSG_LB_REQUEST_DISPLAY, "", NULL_KEY); } - else if (num == MSG_GAME_WON) { - handleGameWon(str); + else if (_n == MSG_GAME_WON) llMessageLinked(LINK_SET, MSG_LB_RECORD_PLAYER, _st + "|WIN", NULL_KEY); + else if (_n == MSG_GAME_LOST) llMessageLinked(LINK_SET, MSG_LB_RECORD_PLAYER, _st + "|LOSS", NULL_KEY); + else if (_n == MSG_LB_DISPLAY_DATA) { + string _tl = "WORLD RANKING RECORD"; string _sp = " "; + integer _m = (LEADERBOARD_WIDTH - llStringLength(_tl)) / 2; + string _tt = llGetSubString(_sp, 0, _m - 1) + _tl; + llMessageLinked(LINK_SET, MSG_DISPLAY_LEADERBOARD, "FORMATTED_TEXT|" + _tt, NULL_KEY); + llMessageLinked(LINK_SET, MSG_DISPLAY_LEADERBOARD, "COLUMNS|" + _st, NULL_KEY); } - else if (num == MSG_GAME_LOST) { - handleGameLost(str); - } - else if (num == MSG_RESET_LEADERBOARD) { - resetLeaderboard(); - } - else if (num == MSG_REMOVE_PLAYER) { - removePlayer(str); - } - else if (num == MSG_UPDATE_PERIL_PLAYER) { - // Update peril player and refresh glow effects - string oldPeril = currentPerilPlayer; - currentPerilPlayer = str; - if (currentPerilPlayer == "NONE") { - currentPerilPlayer = ""; - } - - dbg("Peril: " + currentPerilPlayer); - - // Update glow effects + else if (_n == MSG_LB_LOAD_COMPLETE) llMessageLinked(LINK_SET, MSG_LB_REQUEST_DISPLAY, "", NULL_KEY); + else if (_n == MSG_REMOVE_PLAYER) removePlayer(_st); + else if (_n == MSG_UPDATE_PERIL_PLAYER) { + if (_st == "NONE") currentPerilPlayer = ""; + else currentPerilPlayer = _st; updatePlayerGlowEffects(); } - else if (num == MSG_UPDATE_WINNER) { - // Update winner and refresh glow effects - string oldWinner = currentWinner; - currentWinner = str; - if (currentWinner == "NONE") { - currentWinner = ""; - } - - dbg("Winner: " + currentWinner); - - // Update glow effects - winner glow overrides peril glow + else if (_n == MSG_UPDATE_WINNER) { + if (_st == "NONE") currentWinner = ""; + else currentWinner = _st; updatePlayerGlowEffects(); } - else if (num == MSG_DISPLAY_LEADERBOARD && str == "REFRESH_STARTUP") { - // Startup handshake from leaderboard bridge - re-send text now that boxes are ready - dbg("📋 [Scoreboard] Handshake received! Refreshing virtual columns..."); - generateLeaderboardText(); - } + else if (_n == MSG_RESET_ALL) llResetScript(); } - http_response(key request_id, integer status, list metadata, string body) { - // Handle profile picture HTTP responses - integer requestIndex = llListFindList(httpRequests, [request_id]); - if (requestIndex != -1) { - integer playerIndex = llList2Integer(httpRequests, requestIndex + 1); - - // Remove this request from tracking - httpRequests = llDeleteSubList(httpRequests, requestIndex, requestIndex + 1); - - if (status == 200 && playerIndex < llGetListLength(activePlayers) / 3) { - // Parse the HTML to extract profile image UUID - string profileUUID = ""; - - // Try the meta tag method first - integer metaStart = llSubStringIndex(body, PROFILE_KEY_PREFIX); - if (metaStart != -1) { - metaStart += PROFILE_KEY_PREFIX_LENGTH; - string remainingBody = llGetSubString(body, metaStart, -1); - integer metaEnd = llSubStringIndex(remainingBody, "\""); - if (metaEnd != -1) { - profileUUID = llGetSubString(remainingBody, 0, metaEnd - 1); + http_response(key _rid, integer _status, list _meta, string _body) { + integer _rIdx = llListFindList(httpRequests, [_rid]); + if (_rIdx != -1) { + integer _pIdx = llList2Integer(httpRequests, _rIdx + 1); + httpRequests = llDeleteSubList(httpRequests, _rIdx, _rIdx + 1); + if (_status == 200 && _pIdx < llGetListLength(activePlayers) / 3) { + string _pU = ""; + integer _mS = llSubStringIndex(_body, PROFILE_KEY_PREFIX); + if (_mS != -1) { + _mS += PROFILE_KEY_PREFIX_LENGTH; + string _rem = llGetSubString(_body, _mS, -1); + integer _mE = llSubStringIndex(_rem, "\""); + if (_mE != -1) _pU = llGetSubString(_rem, 0, _mE - 1); + } + if (_pU == "" || _pU == NULL_KEY) { + integer _iS = llSubStringIndex(_body, PROFILE_IMG_PREFIX); + if (_iS != -1) { + _iS += PROFILE_IMG_PREFIX_LENGTH; + string _rem = llGetSubString(_body, _iS, -1); + integer _iE = llSubStringIndex(_rem, "/"); + if (_iE != -1) _pU = llGetSubString(_rem, 0, _iE - 1); } } - - // If meta tag failed, try img src method - if (profileUUID == "") { - integer imgStart = llSubStringIndex(body, PROFILE_IMG_PREFIX); - if (imgStart != -1) { - imgStart += PROFILE_IMG_PREFIX_LENGTH; - string remainingBody = llGetSubString(body, imgStart, -1); - integer imgEnd = llSubStringIndex(remainingBody, "/"); - if (imgEnd != -1) { - profileUUID = llGetSubString(remainingBody, 0, imgEnd - 1); - } - } - } - - // If we got a valid UUID, update the profile prim and cache it - if (profileUUID != "" && profileUUID != "00000000-0000-0000-0000-000000000000") { - // Update the cached profile texture in stride - activePlayers = llListReplaceList(activePlayers, [profileUUID], (playerIndex * 3) + 2, (playerIndex * 3) + 2); - - // Update the physical prim using correct mapping - integer profilePrimIndex = getProfilePrimLink(playerIndex); - - // Safety check: Ensure the prim link was found - if (profilePrimIndex > 0) { - // AGGRESSIVE VISUAL UPDATE: Force immediate rendering - llSetLinkPrimitiveParamsFast(profilePrimIndex, [ - PRIM_TEXTURE, ALL_SIDES, profileUUID, <1,1,0>, <0,0,0>, 0.0, - PRIM_COLOR, ALL_SIDES, <1.0, 1.0, 1.0>, 1.0, - PRIM_GLOW, ALL_SIDES, 0.01 // Tiny glow to force re-render - ]); - - // Remove the glow after a moment to make it normal + if (_pU != "" && _pU != NULL_KEY) { + activePlayers = llListReplaceList(activePlayers, [_pU], (_pIdx * 3) + 2, (_pIdx * 3) + 2); + integer _pL = getProfilePrimLink(_pIdx); + if (_pL > 0) { + llSetLinkPrimitiveParamsFast(_pL, [PRIM_TEXTURE, ALL_SIDES, _pU, <1,1,0>, <0,0,0>, 0.0, PRIM_COLOR, ALL_SIDES, <1,1,1>, 1.0, PRIM_GLOW, 0.01]); llSleep(DELAY_SCOREBOARD_REFRESH); - llSetLinkPrimitiveParamsFast(profilePrimIndex, [ - PRIM_GLOW, ALL_SIDES, 0.0 // Remove glow - ]); - - string playerName = llList2String(activePlayers, playerIndex * 3); - dbg("Profile updated: " + playerName); - } else { - dbg("Invalid prim: " + (string)profilePrimIndex); + llSetLinkPrimitiveParamsFast(_pL, [PRIM_GLOW, 0.0]); } } } } } - - dataserver(key queryid, string data) { - integer comma; - if (queryid == kvpReadReq) { - comma = llSubStringIndex(data, ","); - if (comma != -1) { - string status = llGetSubString(data, 0, comma - 1); - string val = llGetSubString(data, comma + 1, -1); - - if (status == "1") { - leaderboardData = []; - - list entries = llParseString2List(val, ["|"], []); - integer i; - for (i = 0; i < llGetListLength(entries); i++) { - string data_entry = llList2String(entries, i); - list parts = llParseString2List(data_entry, [":"], []); - if (llGetListLength(parts) >= 3) { - leaderboardData += [ - llList2String(parts, 0), - (integer)llList2String(parts, 1), - (integer)llList2String(parts, 2) - ]; - } - } - dbg("LB Loaded"); - } else if (status == "3") { - leaderboardData = []; - dbg("LB Empty"); - } else { - dbg("LB Error: " + status); - } - } - // Generate board with downloaded or empty data - generateLeaderboardText(); - } else if (queryid == kvpWriteReq) { - comma = llSubStringIndex(data, ","); - if (comma != -1) { - string status = llGetSubString(data, 0, comma - 1); - if (status == "3") { - // Update failed because key didn't exist yet! Create it. - llCreateKeyValue("Peril_LB_Top50", pendingSerialize); - dbg("LB Saved"); - pendingSerialize = ""; - } - } - } - } - - - timer() { - dbg("Timer"); - loadLeaderboardData(); - } - - listen(integer channel, string name, key id, string message) { - string msg = llToLower(llStringTrim(message, STRING_TRIM)); - if (msg == "testx") { - llOwnerSay("🛠️ [Scoreboard] Testing FULL LAYOUT on all 10 slots (Forced Alpha Reveal)..."); - integer i; - for (i = 0; i < 10; i++) { - integer pIdx = getProfilePrimLink(i); - integer hIdx = getHeartsPrimLink(i); - integer oIdx = getOverlayPrimLink(i); - - if (pIdx > 0) { - llSetLinkPrimitiveParamsFast(pIdx, [PRIM_COLOR, ALL_SIDES, <1,1,1>, 1.0]); - llSetLinkPrimitiveParamsFast(hIdx, [PRIM_COLOR, ALL_SIDES, <1,1,1>, 1.0]); - llSetLinkPrimitiveParamsFast(oIdx, [ - PRIM_TEXTURE, ALL_SIDES, TEXTURE_ELIMINATED_X, <1,1,0>, <0,0,0>, 0.0, - PRIM_COLOR, ALL_SIDES, <1.0, 1.0, 1.0>, 1.0 - ]); - } - } - } - else if (msg == "clearx") { - refreshPlayerDisplay(); - } - } } diff --git a/Main_Controller_Linkset.lsl b/Main_Controller_Linkset.lsl index 2896ec0..d45188e 100644 --- a/Main_Controller_Linkset.lsl +++ b/Main_Controller_Linkset.lsl @@ -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") { - sentinelQueryID = NULL_KEY; - llSetTimerEvent(0); - currentTimerMode = TIMER_IDLE; - dbg("✅ [Peril Dice] Experience Sentinel: KVP Handshake Successful. Land is ready!"); + integer c = llSubStringIndex(data, ","); + string st = data; + if (c != -1) st = llGetSubString(data, 0, c - 1); + + sentinelQueryID = NULL_KEY; + llSetTimerEvent(0); + 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); } } } diff --git a/Peril_Constants.lsl b/Peril_Constants.lsl index eefaeac..f672127 100644 --- a/Peril_Constants.lsl +++ b/Peril_Constants.lsl @@ -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 diff --git a/Player_DialogHandler.lsl b/Player_DialogHandler.lsl index 5524cef..54c2af5 100644 --- a/Player_DialogHandler.lsl +++ b/Player_DialogHandler.lsl @@ -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);