diff --git a/Bot Manager.lsl b/Bot Manager.lsl index 97d03e8..3a0eccc 100644 --- a/Bot Manager.lsl +++ b/Bot Manager.lsl @@ -5,6 +5,9 @@ integer LISTEN_CHANNEL = -9999; // Change if needed to match main controller integer MSG_SYNC_GAME_STATE = 107; +// Bot timing configuration to prevent dialog system overload +float BOT_RESPONSE_DELAY = 1.5; // Delay before bot responds to commands + // Track game state to validate bot commands list names = []; list lives = []; @@ -56,6 +59,9 @@ doBotPick(string botName, integer count, integer diceMax, list avoidNumbers) { llOwnerSay("[Bot Manager] āš ļø WARNING: " + botName + " only picked " + (string)llGetListLength(picks) + "/" + (string)count + " numbers after " + (string)attempts + " attempts"); } + // Add delay before responding to prevent dialog system overload + llSleep(BOT_RESPONSE_DELAY); + string pickString = llDumpList2String(picks, ";"); string response = "BOT_PICKED:" + botName + ":" + pickString; llMessageLinked(LINK_SET, -9997, response, NULL_KEY); @@ -81,6 +87,9 @@ doBotRoll(string botName, integer diceMax) { return; } + // Add delay before rolling to prevent dialog system overload + llSleep(BOT_RESPONSE_DELAY); + integer roll = 1 + (integer)(llFrand((float)diceMax)); llRegionSay(LISTEN_CHANNEL, "BOT_ROLL:" + botName + ":" + (string)roll); } @@ -94,6 +103,16 @@ default { } link_message(integer sender, integer num, string str, key id) { + // Handle full reset from main controller + if (num == -99999 && str == "FULL_RESET") { + // Reset bot manager state + names = []; + lives = []; + perilPlayer = ""; + llOwnerSay("[Bot Manager] Reset complete!"); + return; + } + // Handle game state sync to track player eliminations if (num == MSG_SYNC_GAME_STATE) { list parts = llParseString2List(str, ["~"], []); diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d3fc74..4026d28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,58 @@ # Changelog All notable changes to Peril Dice will be documented in this file. +## [2.3.0] - 2025-08-05 + + +### šŸ† Visual Scoreboard System +- **Dynamic Player Grid**: Real-time visual scoreboard displaying all players with profile pictures and heart count +- **Profile Picture Integration**: Automatic fetching of Second Life avatar profile pictures via HTTP requests +- **Heart Texture Display**: Visual life representation using custom heart textures (3, 2, 1, 0 hearts) +- **Bot Profile Support**: Special profile texture for TestBot players +- **Immediate Updates**: Hearts update instantly when lives change, before any dialogs appear + +### šŸŽ­ Enhanced Status Display System +- **Visual Status Actions**: Large action prim displaying current game status with custom textures +- **Specific Status Messages**: + - **Direct Hit**: When peril player picks their own rolled number + - **No Shield**: When nobody picks the rolled number + - **Plot Twist**: When peril switches to a new player + - **Elimination**: When a player is eliminated + - **Victory**: When someone wins the game + - **Peril Selected**: When a new peril player is chosen + - **Title**: Default/idle state + +### ⚔ Perfect Status Timing +- **Protected Display Time**: Each status shows for 8 seconds with automatic clearing +- **Strategic Delays**: 2-second delays after status messages prevent overwriting +- **Sequential Flow**: Elimination → 6.4s delay → Victory → 9s delay → Reset +- **Immediate Visual Feedback**: Hearts and status update instantly, delays protect display time + +### šŸŽÆ Comprehensive Leaderboard +- **Persistent Win Tracking**: Player victories saved across game sessions +- **XyzzyText Integration**: Three-prim text display system for leaderboard +- **Automatic Sorting**: Top players by win count with formatted display +- **Separate Reset Options**: Game reset vs. leaderboard reset vs. complete reset + +### šŸŽ® Enhanced User Interface +- **Categorized Owner Menus**: Organized into Game Control, Player Management, Reset Options, etc. +- **Dialog Recovery**: Players can recover lost dialogs by touching the controller +- **Admin Menu Access**: Owner can access admin functions during gameplay +- **Improved Menu Flow**: Streamlined navigation with clear categorization + +### šŸ”§ Technical Improvements +- **Multi-Object Architecture**: Scoreboard and leaderboard as separate linked objects +- **HTTP Profile Fetching**: Robust system for retrieving avatar profile pictures +- **Texture Caching**: Profile pictures cached to avoid repeated HTTP requests +- **Region-Based Communication**: Cross-object messaging using `llRegionSay` +- **Elimination Sequence**: Visual 0-hearts display before player removal + +### šŸ› Critical Fixes +- **Heart Update Timing**: Fixed hearts not updating until after next-turn dialog +- **Victory Overwriting Elimination**: Added proper delay between elimination and victory status +- **Status Message Conflicts**: All status messages now have protective delays +- **Profile Picture Fallbacks**: Proper handling of failed HTTP requests +- **Scoreboard Reset**: Fixed scoreboard not clearing when game resets ## [2.2.0] - 2025-08-04 diff --git a/Debugger.lsl b/Debugger.lsl index 1fd83c3..ec0008d 100644 --- a/Debugger.lsl +++ b/Debugger.lsl @@ -70,6 +70,13 @@ default { } link_message(integer sender, integer num, string str, key id) { + // Handle full reset from main controller + if (num == -99999 && str == "FULL_RESET") { + // Debugger doesn't maintain state, but acknowledge reset + llOwnerSay("šŸ” Debugger reset!"); + return; + } + if (num == MSG_SYNC_GAME_STATE) { list parts = llParseString2List(str, ["~"], []); if (llGetListLength(parts) < 4) { diff --git a/Floater Manager.lsl b/Floater Manager.lsl index 7c52aff..54a6069 100644 --- a/Floater Manager.lsl +++ b/Floater Manager.lsl @@ -73,6 +73,18 @@ default { } link_message(integer sender, integer num, string str, key id) { + // Handle full reset from main controller + if (num == -99999 && str == "FULL_RESET") { + // Reset all game state + players = []; + names = []; + lives = []; + picksData = []; + perilPlayer = ""; + llOwnerSay("šŸ“¦ Floater Manager reset!"); + return; + } + if (num == MSG_REGISTER_PLAYER) { // Enforce the maximum number of players if (llGetListLength(players) >= MAX_PLAYERS) { @@ -85,6 +97,8 @@ default { key avKey = llList2Key(info, 1); names += [name]; players += [avKey]; + lives += [3]; // Initialize with 3 lives for new players + picksData += [name + "|"]; // Initialize with empty picks llSay(0, "šŸ’€ " + name + " has entered the deadly game! Welcome to your potential doom! šŸ’€"); // Immediately rez the float after registration. This ensures the @@ -118,15 +132,21 @@ default { // Real avatars: rez next to the avatar with the standard offset pos = basePos + <1, 0, 1>; } else { - // Test players: space floats apart along the X axis so they don't overlap. - // Each test player gets an additional 0.5m offset based on their index. - // This uses the current prim's position as the base since there's no - // avatar position available. - pos = basePos + <1 + (float)idx * 0.5, 0, 1>; + // Test players: space floats apart and away from the scoreboard/leaderboard area. + // Move them further away (3-5 meters) and spread them out more. + // This prevents them from spawning directly on the leaderboard. + pos = basePos + <-4.0 - (float)idx * 1.0, 2.0 + (float)idx * 0.8, 1>; } integer ch = -777000 + idx; - llSetObjectDesc(name); llRezObject("StatFloat", pos, ZERO_VECTOR, ZERO_ROTATION, ch); + // Wait a moment then set the description + llSleep(0.2); + // Find the rezzed object and set its description + list nearby = llGetObjectDetails(llGetKey(), [OBJECT_POS]); + if (llGetListLength(nearby) > 0) { + // Use llRegionSay to tell the StatFloat its name + llRegionSay(ch, "SET_NAME:" + name); + } // After rezzing, immediately update the float so it displays the // correct lives, picks and peril status. Use the avatar key as // the id to keep the float tied to the player. This ensures that @@ -159,7 +179,7 @@ default { } string picksDisplay = llList2CSV(picks); - string txt = "šŸŽ² Peril Dice\nšŸ‘¤ " + name + "\nā¤ļø Lives: " + (string)lifeCount + "\n" + perilName + "\nšŸ”¢ Picks: " + picksDisplay; + string txt = "šŸŽ² Peril Dice\nšŸ‘¤ " + name + "\nLives: " + (string)lifeCount + "\n" + perilName + "\nšŸ”¢ Picks: " + picksDisplay; llRegionSay(ch, "FLOAT:" + (string)avKey + "|" + txt); } else if (num == MSG_CLEANUP_FLOAT) { diff --git a/Game_Scoreboard_Manager.lsl b/Game_Scoreboard_Manager.lsl new file mode 100644 index 0000000..58574d4 --- /dev/null +++ b/Game_Scoreboard_Manager.lsl @@ -0,0 +1,727 @@ +// Game Scoreboard Manager - Shows current game players in grid layout +// Each player gets a prim showing profile picture + heart texture (lives) + +// Heart texture UUIDs - REPLACE WITH YOUR ACTUAL TEXTURE UUIDs +integer LEADERBOARD_CHANNEL = -12346; +list playerWins = []; // Store wins data for persistent leaderboard +string TEXTURE_0_HEARTS = "7d8ae121-e171-12ae-f5b6-7cc3c0395c7b"; // 0 hearts (dead) +string TEXTURE_1_HEARTS = "6605d25f-8e2d-2870-eb87-77c58cd47fa9"; // 1 heart +string TEXTURE_2_HEARTS = "7ba6cb1e-f384-25a5-8e88-a90bbd7cc041"; // 2 hearts +string TEXTURE_3_HEARTS = "a5d16715-4648-6526-5582-e8068293f792"; // 3 hearts + +// Default textures - REPLACE WITH YOUR ACTUAL TEXTURE UUIDs +string TEXTURE_DEFAULT_PROFILE = "1ce89375-6c3c-3845-26b1-1dc666bc9169"; // Default avatar +string TEXTURE_BOT_PROFILE = "62f31722-04c1-8c29-c236-398543f2a6ae"; // Bot avatar picture +string BLANK_TEXTURE = "5748decc-f629-461c-9a36-a35a221fe21f"; // White texture UUID +string TEXTURE_BACKGROUND = "5748decc-f629-461c-9a36-a35a221fe21f"; // Background texture (blank + black color) +string TEXTURE_ACTIONS = "6aac8dce-1b83-f931-abe3-286f4f2faa29"; // Punishment texture +string TEXTURE_LEADERBOARD = "5748decc-f629-461c-9a36-a35a221fe21f"; // Leaderboard texture + +// Status textures - REPLACE WITH YOUR ACTUAL TEXTURE UUIDs +string TEXTURE_PERIL = "c5676fec-0c85-5567-3dd8-f939234e21d9"; // Elimination texture +string TEXTURE_PERIL_SELECTED = "a53ff601-3c8a-e312-9e0e-f6fa76f6773a"; // Peril Selected texture +string TEXTURE_VICTORY = "ec5bf10e-4970-fb63-e7bf-751e1dc27a8d"; // Victory texture +string TEXTURE_PUNISHMENT = "acfabed0-84ad-bfd1-cdfc-2ada0aeeaa2f"; // Punishment texture (fallback) +string TEXTURE_DIRECT_HIT = "ecd2dba2-3969-6c39-ad59-319747307f55"; // Direct Hit texture +string TEXTURE_NO_SHIELD = "2440174f-e385-44e2-8016-ac34934f11f5"; // No Shield texture +string TEXTURE_PLOT_TWIST = "ec533379-4f7f-8183-e877-e68af703dcce"; // Plot Twist texture +string TEXTURE_TITLE = "624bb7a7-e856-965c-bae8-94d75226c1bc"; // Title texture + +// Player data - used for BOTH current game display AND leaderboard +list playerNames = []; // Current game players for display +list playerLives = []; // Current game player lives +list playerProfiles = []; // Maps to profile texture UUIDs + +// Leaderboard data (persistent - stored in linkset data) +list leaderboardNames = []; // Loaded from linkset data at startup +list leaderboardWins = []; // Loaded from linkset data at startup + +// HTTP tracking +list profileRequests = []; // Track which profile requests belong to which player +list httpRequests = []; // Track HTTP requests for profile pictures + +// Profile picture extraction constants +string profile_key_prefix = "; + + // Profile prim (left side) + vector profilePos = basePos + <-PRIM_OFFSET, 0.0, 0.0>; + integer profilePrimIndex = FIRST_PLAYER_PRIM + (i * 2); // Prims 4, 6, 8, 10... + + // Hearts prim (right side) + vector heartsPos = basePos + ; + integer heartsPrimIndex = FIRST_PLAYER_PRIM + (i * 2) + 1; // Prims 5, 7, 9, 11... + + // Set profile prim properties + if (profilePrimIndex <= totalPrims) { + llOwnerSay("DEBUG: Setting profile prim " + (string)profilePrimIndex + " for player " + (string)i); + llSetLinkPrimitiveParamsFast(profilePrimIndex, [ + PRIM_POSITION, profilePos, + PRIM_ROTATION, <-0.707107, 0.0, 0.0, 0.707107>, // Back to upright portrait rotation + PRIM_SIZE, , // Swap width/height for landscape + PRIM_TEXTURE, ALL_SIDES, TEXTURE_DEFAULT_PROFILE, <1,1,0>, <0,0,0>, 0.0, // All faces + PRIM_COLOR, ALL_SIDES, <1.0, 1.0, 1.0>, 1.0, // Fully opaque white + PRIM_TEXT, "", <1,1,1>, 0.0 + ]); + } else { + llOwnerSay("DEBUG: Skipping profile prim " + (string)profilePrimIndex + " (exceeds total prims)"); + } + + // Set hearts prim properties + if (heartsPrimIndex <= totalPrims) { + llSetLinkPrimitiveParamsFast(heartsPrimIndex, [ + PRIM_POSITION, heartsPos, + PRIM_ROTATION, <-0.7, 0.0, 0.0, 0.7>, // Back to upright portrait rotation + PRIM_SIZE, , // Swap width/height for landscape + PRIM_TEXTURE, ALL_SIDES, TEXTURE_3_HEARTS, <1,1,0>, <0,0,0>, 0.0, // All faces + PRIM_COLOR, ALL_SIDES, <1.0, 1.0, 1.0>, 1.0, // Fully opaque white + PRIM_TEXT, "", <1,1,1>, 0.0 + ]); + } + } +} + +setupUIPrims() { + // Background prim - centered behind the player grid + vector bgPosition = <0.0, 0.04, -1.6>; + llOwnerSay("DEBUG: Setting background prim " + (string)BACKGROUND_PRIM + " to position " + (string)bgPosition); + llSetLinkPrimitiveParamsFast(BACKGROUND_PRIM, [ + PRIM_POSITION, bgPosition, // Behind the board and higher up + PRIM_ROTATION, <0.0, 0.0, -0.04, 1.0>, // No rotation + PRIM_SIZE, , + 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, // Black background color + PRIM_TEXT, "", <1,1,1>, 0.0 + ]); + + // Actions/status prim - rectangle above the player grid + llSetLinkPrimitiveParamsFast(ACTIONS_PRIM, [ + PRIM_POSITION, <0.0, 0.01, -0.3>, // Even higher above the grid + PRIM_ROTATION, <0.0, 0.0, -0.04, 1.0>, // No rotation + PRIM_SIZE, , + PRIM_TEXTURE, ALL_SIDES, TEXTURE_TITLE, <1,1,0>, <0,0,0>, 0.0, // Start with title texture + PRIM_COLOR, ALL_SIDES, <1.0, 1.0, 1.0>, 1.0, // White for texture visibility + PRIM_TEXT, "", <0,0,0>, 0.0 // No floating text + ]); + + // Note: Leaderboard is now a separate object + + llOwnerSay("DEBUG: UI prims setup complete"); +} + +// Update actions prim with appropriate status texture +updateActionsPrim(string status) { + string textureToUse = ""; + + 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 { + llOwnerSay("DEBUG: Unrecognized status '" + status + "' - keeping current texture"); + return; // Don't change texture for unrecognized statuses + } + + 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 + ]); +} + +// XyzzyText Communication Constants +integer DISPLAY_STRING = 204000; +integer DISPLAY_EXTENDED = 204001; + +// Generate leaderboard text for triple XyzzyText display (15 characters per line split across 3 prims) +generateLeaderboardText() { + list sortedData = getSortedLeaderboard(); + integer maxDisplay = llGetListLength(sortedData); + if (maxDisplay > 11) maxDisplay = 11; // Show top 11 entries to fit in display + + // Triple prim approach - split lines between left, middle, and right prims + string leftText = "WINS:"; + string middleText = " "; // 5 spaces to align with left prim + string rightText = " "; // 5 spaces to align with left prim + + integer i; + for (i = 0; i < maxDisplay; i++) { + list playerData = llParseString2List(llList2String(sortedData, i), [":"], []); + string playerName = llList2String(playerData, 0); + string winsText = llList2String(playerData, 1); + + // Create 15-character line: "PlayerNameHere Wins" (player name + padding + wins) + // Truncate name to max 12 characters to leave room for wins + if (llStringLength(playerName) > 12) { + playerName = llGetSubString(playerName, 0, 11); + } + + // Format the complete line (15 chars total) + string fullLine = playerName; + + // Add padding spaces + integer padding = 13 - llStringLength(playerName); // Leave 2 chars for wins + integer j; + for (j = 0; j < padding; j++) { + fullLine += " "; + } + + // Add wins (max 2 digits) + if (llStringLength(winsText) > 2) { + winsText = llGetSubString(winsText, 0, 1); // Truncate to 2 digits + } + fullLine += winsText; + + // Ensure exactly 15 characters + if (llStringLength(fullLine) > 15) { + fullLine = llGetSubString(fullLine, 0, 14); + } else if (llStringLength(fullLine) < 15) { + // Pad with spaces if needed + while (llStringLength(fullLine) < 15) { + fullLine += " "; + } + } + + // Split the 15-character line: 5 chars each to left, middle, right prims + string leftPart = llGetSubString(fullLine, 0, 4); // Characters 0-4 + string middlePart = llGetSubString(fullLine, 5, 9); // Characters 5-9 + string rightPart = llGetSubString(fullLine, 10, 14); // Characters 10-14 + + leftText += "\n" + leftPart; + middleText += "\n" + middlePart; + rightText += "\n" + rightPart; + } + + // If no data, show empty message + if (maxDisplay == 0) { + leftText = "NO "; + middleText = "GAMES"; + rightText = " "; + } + + // Send to separate leaderboard object via chat + llOwnerSay("DEBUG: Sending to LEFT XyzzyText: " + leftText); + llOwnerSay("DEBUG: Sending to MIDDLE XyzzyText: " + middleText); + llOwnerSay("DEBUG: Sending to RIGHT XyzzyText: " + rightText); + + // Send to leaderboard bridge in separate object + llRegionSay(LEADERBOARD_CHANNEL, "LEFT_TEXT|" + leftText); + llRegionSay(LEADERBOARD_CHANNEL, "MIDDLE_TEXT|" + middleText); + llRegionSay(LEADERBOARD_CHANNEL, "RIGHT_TEXT|" + rightText); + + // Also create combined floating text as backup display method + string combinedText = "WINS:\n"; + for (i = 0; i < maxDisplay; i++) { + list playerData = llParseString2List(llList2String(sortedData, i), [":"], []); + string playerName = llList2String(playerData, 0); + string winsText = llList2String(playerData, 1); + + if (llStringLength(playerName) > 12) { + playerName = llGetSubString(playerName, 0, 11); + } + + combinedText += playerName + ": " + winsText + "\n"; + } + + if (maxDisplay == 0) { + combinedText = "No games played yet"; + } + + // Note: Leaderboard is now in separate object - no prim to update here +} + +// Update leaderboard prim with current standings and update persistent data +updateLeaderboard(list topPlayers) { + generateLeaderboardText(); +} + +// Handle game won message +handleGameWon(string winnerName) { + integer playerIndex = llListFindList(leaderboardNames, [winnerName]); + + if (playerIndex == -1) { + leaderboardNames += [winnerName]; + leaderboardWins += [1]; + } else { + integer currentWins = llList2Integer(leaderboardWins, playerIndex); + leaderboardWins = llListReplaceList(leaderboardWins, [currentWins + 1], playerIndex, playerIndex); + } + + saveLeaderboardData(); + updateLeaderboard(leaderboardNames); +} + +// Sort leaderboard by wins +list getSortedLeaderboard() { + list combined = []; + integer i; + + for (i = 0; i < llGetListLength(leaderboardNames); i++) { + string playerName = llList2String(leaderboardNames, i); + integer wins = llList2Integer(leaderboardWins, i); + combined += [playerName + ":" + (string)wins]; + } + + integer n = llGetListLength(combined); + integer swapped = TRUE; + + while (swapped) { + swapped = FALSE; + for (i = 0; i < n - 1; i++) { + list data1 = llParseString2List(llList2String(combined, i), [":"], []); + list data2 = llParseString2List(llList2String(combined, i + 1), [":"], []); + + integer wins1 = (integer)llList2String(data1, 1); + integer wins2 = (integer)llList2String(data2, 1); + + if (wins1 < wins2) { + string temp = llList2String(combined, i); + combined = llListReplaceList(combined, [llList2String(combined, i + 1)], i, i); + combined = llListReplaceList(combined, [temp], i + 1, i + 1); + swapped = TRUE; + } + } + n--; + } + + return combined; +} + +// Save leaderboard data +saveLeaderboardData() { + integer i; + llLinksetDataWrite("lb_count", (string)llGetListLength(leaderboardNames)); + for (i = 0; i < llGetListLength(leaderboardNames); i++) { + string playerName = llList2String(leaderboardNames, i); + integer wins = llList2Integer(leaderboardWins, i); + llLinksetDataWrite("lb_player_" + (string)i, playerName); + llLinksetDataWrite("lb_wins_" + (string)i, (string)wins); + } +} + +// Load leaderboard data +loadLeaderboardData() { + string countStr = llLinksetDataRead("lb_count"); + if (countStr == "") return; + + integer count = (integer)countStr; + leaderboardNames = []; + leaderboardWins = []; + integer i; + for (i = 0; i < count; i++) { + string playerName = llLinksetDataRead("lb_player_" + (string)i); + string winsStr = llLinksetDataRead("lb_wins_" + (string)i); + if (playerName != "" && winsStr != "") { + leaderboardNames += [playerName]; + leaderboardWins += [(integer)winsStr]; + } + } +} + +// Reset leaderboard data +resetLeaderboard() { + leaderboardNames = []; + leaderboardWins = []; + integer i; + for (i = 0; i < 100; i++) { + llLinksetDataDelete("lb_player_" + (string)i); + llLinksetDataDelete("lb_wins_" + (string)i); + } + llLinksetDataDelete("lb_count"); + updateLeaderboard([]); +} + +// Clear all current players from the scoreboard +clearAllPlayers() { + llOwnerSay("DEBUG: Clearing all players from scoreboard"); + + // Clear ONLY current game player data lists (NOT leaderboard data) + playerNames = []; + playerLives = []; + playerProfiles = []; + + // NOTE: leaderboardNames and leaderboardWins are NOT cleared - they persist + + // Clear any pending HTTP requests + httpRequests = []; + profileRequests = []; + + // Reset all player prims to default state + integer i; + for (i = 0; i < 10; i++) { + integer profilePrimIndex = FIRST_PLAYER_PRIM + (i * 2); + integer heartsPrimIndex = FIRST_PLAYER_PRIM + (i * 2) + 1; + + // Reset profile prim to default + llSetLinkPrimitiveParamsFast(profilePrimIndex, [ + 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>, 1.0, + PRIM_TEXT, "", <1,1,1>, 0.0 + ]); + + // Reset hearts prim to 3 hearts + llSetLinkPrimitiveParamsFast(heartsPrimIndex, [ + 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>, 1.0, + PRIM_TEXT, "", <1,1,1>, 0.0 + ]); + } +} + +// Check if a player name indicates it's a bot +integer isBot(string playerName) { + return (llSubStringIndex(playerName, "TestBot") == 0); +} + +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 +} + +// Update player display on the grid +updatePlayerDisplay(string playerName, integer lives, string profileUUID) { + // Find existing player or add new one + integer playerIndex = llListFindList(playerNames, [playerName]); + + if (playerIndex == -1) { + // New player - find next available slot + playerIndex = llGetListLength(playerNames); + if (playerIndex >= 10) { + llOwnerSay("WARNING: Maximum 10 players supported, ignoring: " + playerName); + return; + } + + // Add to tracking lists + playerNames += [playerName]; + playerLives += [lives]; + playerProfiles += [profileUUID]; + + llOwnerSay("DEBUG: Added new player " + playerName + " at slot " + (string)playerIndex); + } else { + // Update existing player + playerLives = llListReplaceList(playerLives, [lives], playerIndex, playerIndex); + // Don't overwrite cached profile texture with original UUID on updates + // playerProfiles = llListReplaceList(playerProfiles, [profileUUID], playerIndex, playerIndex); + + llOwnerSay("DEBUG: Updated player " + playerName + " at slot " + (string)playerIndex + " with " + (string)lives + " lives"); + } + + // Update the physical prims for this player + integer profilePrimIndex = FIRST_PLAYER_PRIM + (playerIndex * 2); + integer heartsPrimIndex = FIRST_PLAYER_PRIM + (playerIndex * 2) + 1; + + // Update profile prim texture + string profileTexture = profileUUID; + + // Check if we already have a cached profile texture for this player + if (playerIndex < llGetListLength(playerProfiles)) { + string cachedTexture = llList2String(playerProfiles, playerIndex); + if (cachedTexture != "" && cachedTexture != profileUUID && + cachedTexture != TEXTURE_DEFAULT_PROFILE && cachedTexture != TEXTURE_BOT_PROFILE) { + // We have a previously fetched profile picture, use it + profileTexture = cachedTexture; + llOwnerSay("DEBUG: Using cached profile texture for " + playerName); + } + } + + // If no cached texture or using original UUID, determine what to use + if (profileTexture == profileUUID) { + if (isBot(playerName)) { + profileTexture = TEXTURE_BOT_PROFILE; + llOwnerSay("DEBUG: Using bot profile texture for " + playerName); + } else if (profileTexture == "" || profileTexture == "00000000-0000-0000-0000-000000000000") { + profileTexture = TEXTURE_DEFAULT_PROFILE; + llOwnerSay("DEBUG: Using default profile texture for " + playerName); + } else { + // Request profile picture via HTTP (the correct method!) + llOwnerSay("DEBUG: Requesting profile picture via HTTP for avatar UUID: " + profileUUID); + string 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]; + + // For now, use the avatar UUID directly as texture + llOwnerSay("DEBUG: Setting temporary profile texture: " + profileTexture); + } + } + + llSetLinkPrimitiveParamsFast(profilePrimIndex, [ + PRIM_TEXTURE, ALL_SIDES, 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 // Remove player name text + ]); + + // Update hearts prim texture + string heartTexture = getHeartTexture(lives); + llSetLinkPrimitiveParamsFast(heartsPrimIndex, [ + 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,0,0>, 0.0 // Remove lives text + ]); +} + +default { + state_entry() { + // Clear any old floating text on root prim + llSetText("", <1,1,1>, 0.0); + + // Initialize profile picture extraction constants + profile_key_prefix_length = llStringLength(profile_key_prefix); + profile_img_prefix_length = llStringLength(profile_img_prefix); + + llListen(SCOREBOARD_CHANNEL, "", "", ""); + llListen(LEADERBOARD_CHANNEL, "", "", ""); + loadLeaderboardData(); + setupPlayerGrid(); + + // Generate initial leaderboard texture + llSetTimerEvent(1.0); // Short delay to ensure prims are set up + + llOwnerSay("Game Scoreboard Manager ready - Grid layout initialized"); + llOwnerSay("Touch me to re-run setup."); + } + + timer() { + llSetTimerEvent(0.0); // Stop timer + generateLeaderboardText(); // Generate initial leaderboard + } + + listen(integer channel, string senderName, key id, string message) { + if (channel == SCOREBOARD_CHANNEL) { + if (llSubStringIndex(message, "PLAYER_UPDATE|") == 0) { + // Handle player updates here if needed + // Format: PLAYER_UPDATE|PlayerName|Lives|ProfileUUID + llOwnerSay("DEBUG: Received player update: " + message); + + list parts = llParseString2List(message, ["|"], []); + if (llGetListLength(parts) >= 4) { + string playerName = llList2String(parts, 1); + integer lives = (integer)llList2String(parts, 2); + string profileUUID = llList2String(parts, 3); + + updatePlayerDisplay(playerName, lives, profileUUID); + } + } + else if (message == "CLEAR_GAME") { + // Reset to default state + clearAllPlayers(); // Clear all current players + updateActionsPrim("Title"); // Reset to title instead of "Game Cleared" + generateLeaderboardText(); // Refresh leaderboard display + } + else if (llSubStringIndex(message, "GAME_START|") == 0) { + updateActionsPrim("Title"); + } + else if (llSubStringIndex(message, "GAME_STATUS|") == 0) { + // Format: GAME_STATUS|StatusText + list parts = llParseString2List(message, ["|"], []); + if (llGetListLength(parts) >= 2) { + string status = llList2String(parts, 1); + updateActionsPrim(status); + } + } + else if (llSubStringIndex(message, "LEADERBOARD|") == 0) { + // Format: LEADERBOARD|Player1:Score1|Player2:Score2|... + list parts = llParseString2List(message, ["|"], []); + list playerData = llDeleteSubList(parts, 0, 0); // Remove "LEADERBOARD" + updateLeaderboard(playerData); + } else if (llSubStringIndex(message, "GAME_WON|") == 0) { + list parts = llParseString2List(message, ["|"], []); + if (llGetListLength(parts) >= 2) { + string winnerName = llList2String(parts, 1); + handleGameWon(winnerName); + } + } else if (llSubStringIndex(message, "RESET_LEADERBOARD") == 0) { + resetLeaderboard(); + } + } + } + + touch_start(integer total_number) { + // Owner can touch to manually re-run setup + if (llDetectedKey(0) == llGetOwner()) { + llOwnerSay("Owner touched - re-running setup..."); + setupPlayerGrid(); + } + } + + dataserver(key query_id, string data) { + // Handle profile picture data responses + integer requestIndex = llListFindList(profileRequests, [query_id]); + if (requestIndex != -1) { + // Found the request - get the corresponding player index and data type + integer playerIndex = llList2Integer(profileRequests, requestIndex + 1); + integer dataType = llList2Integer(profileRequests, requestIndex + 2); + + llOwnerSay("DEBUG: Data type " + (string)dataType + " returned: '" + data + "' for player at index " + (string)playerIndex); + + // Check if this looks like a valid texture UUID (36 characters, proper format) + if (llStringLength(data) == 36 && llSubStringIndex(data, "-") != -1) { + llOwnerSay("*** FOUND IT! Data type " + (string)dataType + " returned valid UUID: " + data + " ***"); + + // Update the profile texture for this player + integer profilePrimIndex = FIRST_PLAYER_PRIM + (playerIndex * 2); + + llSetLinkPrimitiveParamsFast(profilePrimIndex, [ + PRIM_TEXTURE, ALL_SIDES, data, <1,1,0>, <0,0,0>, 0.0, + PRIM_COLOR, ALL_SIDES, <1.0, 1.0, 1.0>, 1.0 + ]); + + llOwnerSay("Profile picture updated successfully for " + llList2String(playerNames, playerIndex)); + } else { + // Not a valid UUID, continue testing other data types + llOwnerSay("Data type " + (string)dataType + " did not return valid UUID"); + } + + // Clean up the completed request (3 elements: requestID, playerIndex, dataType) + profileRequests = llDeleteSubList(profileRequests, requestIndex, requestIndex + 2); + } + } + + http_response(key request_id, integer status, list metadata, string body) { + // Handle HTTP response for profile picture requests + integer requestIndex = llListFindList(httpRequests, [request_id]); + if (requestIndex != -1) { + integer playerIndex = llList2Integer(httpRequests, requestIndex + 1); + + llOwnerSay("DEBUG: HTTP response received for player at index " + (string)playerIndex + ", status: " + (string)status); + + if (status == 200) { + // Try to extract profile picture UUID from HTML + integer s1 = llSubStringIndex(body, profile_key_prefix); + integer s1l = profile_key_prefix_length; + + if (s1 == -1) { + // Second try with img tag + s1 = llSubStringIndex(body, profile_img_prefix); + s1l = profile_img_prefix_length; + llOwnerSay("DEBUG: Trying img tag method for profile picture extraction"); + } else { + llOwnerSay("DEBUG: Found meta tag with profile image"); + } + + if (s1 == -1) { + // Still no match - use default texture + llOwnerSay("DEBUG: No profile picture found in HTML for player " + llList2String(playerNames, playerIndex)); + + integer profilePrimIndex = FIRST_PLAYER_PRIM + (playerIndex * 2); + llSetLinkPrimitiveParamsFast(profilePrimIndex, [ + 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>, 1.0 + ]); + } else { + // Extract the UUID + s1 += s1l; + key profileUUID = llGetSubString(body, s1, s1 + 35); + + if (profileUUID == NULL_KEY) { + llOwnerSay("DEBUG: Extracted NULL_KEY, using default texture"); + integer profilePrimIndex = FIRST_PLAYER_PRIM + (playerIndex * 2); + llSetLinkPrimitiveParamsFast(profilePrimIndex, [ + 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>, 1.0 + ]); + } else { + llOwnerSay("*** SUCCESS! Found profile picture UUID: " + (string)profileUUID + " for player " + llList2String(playerNames, playerIndex) + " ***"); + + // Cache the profile picture UUID + playerProfiles = llListReplaceList(playerProfiles, [(string)profileUUID], playerIndex, playerIndex); + + // Update the profile texture + integer profilePrimIndex = FIRST_PLAYER_PRIM + (playerIndex * 2); + llSetLinkPrimitiveParamsFast(profilePrimIndex, [ + PRIM_TEXTURE, ALL_SIDES, (string)profileUUID, <1,1,0>, <0,0,0>, 0.0, + PRIM_COLOR, ALL_SIDES, <1.0, 1.0, 1.0>, 1.0 + ]); + } + } + } else { + llOwnerSay("DEBUG: HTTP request failed with status " + (string)status + " for player " + llList2String(playerNames, playerIndex)); + + // Use default texture on HTTP failure + integer profilePrimIndex = FIRST_PLAYER_PRIM + (playerIndex * 2); + llSetLinkPrimitiveParamsFast(profilePrimIndex, [ + 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>, 1.0 + ]); + } + + // Clean up the completed HTTP request (2 elements: requestID, playerIndex) + httpRequests = llDeleteSubList(httpRequests, requestIndex, requestIndex + 1); + } + } +} diff --git a/Helper.lsl b/Helper.lsl index 9b4b9b6..e90f49b 100644 --- a/Helper.lsl +++ b/Helper.lsl @@ -122,6 +122,18 @@ default { } link_message(integer sender, integer num, string str, key id) { + // Handle full reset from main controller + if (num == -99999 && str == "FULL_RESET") { + // Reset helper state + lives = []; + picksData = []; + perilPlayer = ""; + names = []; + pickQueue = []; + llOwnerSay("🧘 Helper Module reset!"); + return; + } + if (num == MSG_SYNC_GAME_STATE) { list parts = llParseString2List(str, ["~"], []); lives = llCSV2List(llList2String(parts, 0)); diff --git a/Leaderboard_Bridge.lsl b/Leaderboard_Bridge.lsl new file mode 100644 index 0000000..fe7e8af --- /dev/null +++ b/Leaderboard_Bridge.lsl @@ -0,0 +1,56 @@ +// Leaderboard Bridge - Receives text from Game Scoreboard Manager and sends to XyzzyText +// This script goes in the root prim of your leaderboard object (alongside XyzzyText master) + +// Communication channels +integer LEADERBOARD_CHANNEL = -12346; // Listen for messages from scoreboard + +// XyzzyText Communication Constants +integer DISPLAY_STRING = 204000; +integer DISPLAY_EXTENDED = 204001; + +default { + state_entry() { + llListen(LEADERBOARD_CHANNEL, "", "", ""); + llOwnerSay("Leaderboard Bridge ready - listening on channel " + (string)LEADERBOARD_CHANNEL); + + // Send initial blank display to all banks + llMessageLinked(LINK_SET, DISPLAY_STRING, " ", "0"); // Bank 0 (left) + llMessageLinked(LINK_SET, DISPLAY_STRING, " ", "1"); // Bank 1 (middle) + llMessageLinked(LINK_SET, DISPLAY_STRING, " ", "2"); // Bank 2 (right) + } + + listen(integer channel, string name, key id, string message) { + if (channel == LEADERBOARD_CHANNEL) { + llOwnerSay("DEBUG: Received leaderboard message: " + message); + + if (llSubStringIndex(message, "LEFT_TEXT|") == 0) { + // Extract left column text and send to bank 0 + string leftText = llGetSubString(message, 10, -1); // Remove "LEFT_TEXT|" prefix + llOwnerSay("DEBUG: Sending to LEFT bank (0): " + leftText); + llMessageLinked(LINK_SET, DISPLAY_STRING, leftText, "0"); + + } else if (llSubStringIndex(message, "MIDDLE_TEXT|") == 0) { + // Extract middle column text and send to bank 1 + string middleText = llGetSubString(message, 12, -1); // Remove "MIDDLE_TEXT|" prefix + llOwnerSay("DEBUG: Sending to MIDDLE bank (1): " + middleText); + llMessageLinked(LINK_SET, DISPLAY_STRING, middleText, "1"); + + } else if (llSubStringIndex(message, "RIGHT_TEXT|") == 0) { + // Extract right column text and send to bank 2 + string rightText = llGetSubString(message, 11, -1); // Remove "RIGHT_TEXT|" prefix + llOwnerSay("DEBUG: Sending to RIGHT bank (2): " + rightText); + llMessageLinked(LINK_SET, DISPLAY_STRING, rightText, "2"); + + } else if (message == "CLEAR_LEADERBOARD") { + // Clear all displays + llOwnerSay("DEBUG: Clearing all leaderboard displays"); + llMessageLinked(LINK_SET, DISPLAY_STRING, " ", "0"); + llMessageLinked(LINK_SET, DISPLAY_STRING, " ", "1"); + llMessageLinked(LINK_SET, DISPLAY_STRING, " ", "2"); + + } else { + llOwnerSay("DEBUG: Unrecognized leaderboard message: " + message); + } + } + } +} diff --git a/Leaderboard_Manager.lsl b/Leaderboard_Manager.lsl new file mode 100644 index 0000000..1f72245 --- /dev/null +++ b/Leaderboard_Manager.lsl @@ -0,0 +1,253 @@ +// Leaderboard Manager - Shows top 10 players ranked by total wins +// Displays as text on a prim face with persistent win tracking + +// Communication +integer LEADERBOARD_CHANNEL = -12346; +integer MAIN_CHANNEL = -12340; + +// Data storage +list playerNames = []; +list playerWins = []; + +// Display settings +integer DISPLAY_FACE = 0; // Which face to show leaderboard text on +vector TEXT_COLOR = <1.0, 1.0, 0.0>; // Yellow text +float TEXT_ALPHA = 1.0; + +default { + state_entry() { + // Listen for game results and commands + llListen(LEADERBOARD_CHANNEL, "", "", ""); + llListen(MAIN_CHANNEL, "", "", ""); + + // Load persistent data + loadLeaderboardData(); + + // Display current leaderboard + updateLeaderboardDisplay(); + + llOwnerSay("Leaderboard Manager ready - Win tracking active"); + } + + listen(integer channel, string name, key id, string message) { + if (channel == LEADERBOARD_CHANNEL) { + if (llSubStringIndex(message, "GAME_WON|") == 0) { + handleGameWon(message); + } + else if (message == "RESET_LEADERBOARD") { + resetLeaderboard(); + } + else if (message == "SHOW_LEADERBOARD") { + updateLeaderboardDisplay(); + } + } + else if (channel == MAIN_CHANNEL) { + // Listen for game end messages from main controller + if (llSubStringIndex(message, "GAME_ENDED|") == 0) { + handleGameEnded(message); + } + } + } + + touch_start(integer total_number) { + // Allow touching to refresh leaderboard display + updateLeaderboardDisplay(); + llOwnerSay("Leaderboard refreshed"); + } +} + +handleGameWon(string message) { + // Format: "GAME_WON|PlayerName" + list parts = llParseString2List(message, ["|"], []); + if (llGetListLength(parts) < 2) return; + + string winnerName = llList2String(parts, 1); + addWinToPlayer(winnerName); + + llOwnerSay("Win recorded for: " + winnerName); +} + +handleGameEnded(string message) { + // Format: "GAME_ENDED|WinnerName|PlayerList" + list parts = llParseString2List(message, ["|"], []); + if (llGetListLength(parts) >= 2) { + string winnerName = llList2String(parts, 1); + if (winnerName != "NONE" && winnerName != "") { + addWinToPlayer(winnerName); + llOwnerSay("Game ended - Win recorded for: " + winnerName); + } + } +} + +addWinToPlayer(string playerName) { + // Check if player already exists in leaderboard + integer playerIndex = llListFindList(playerNames, [playerName]); + + if (playerIndex == -1) { + // New player - add to lists + playerNames += [playerName]; + playerWins += [1]; + } else { + // Existing player - increment wins + integer currentWins = llList2Integer(playerWins, playerIndex); + playerWins = llListReplaceList(playerWins, [currentWins + 1], playerIndex, playerIndex); + } + + // Save data and update display + saveLeaderboardData(); + updateLeaderboardDisplay(); +} + +updateLeaderboardDisplay() { + // Create sorted list of top 10 players + list sortedData = getSortedLeaderboard(); + + // Build display text + string displayText = "═══ LEADERBOARD ═══\n"; + + integer i; + integer maxDisplay = llGetListLength(sortedData); + if (maxDisplay > 10) maxDisplay = 10; // Show only top 10 + + if (maxDisplay == 0) { + displayText += "No games played yet\n"; + } else { + for (i = 0; i < maxDisplay; i++) { + list playerData = llParseString2List(llList2String(sortedData, i), [":"], []); + string name = llList2String(playerData, 0); + string wins = llList2String(playerData, 1); + + displayText += (string)(i + 1) + ". " + name + " - " + wins + " wins\n"; + } + } + + displayText += "═══════════════════"; + + // Set text on prim face + llSetText("", <0,0,0>, 0.0); // Clear floating text + llSetLinkPrimitiveParamsFast(LINK_THIS, [ + PRIM_TEXT, displayText, TEXT_COLOR, TEXT_ALPHA + ]); + + // Also set as texture text if supported + // Note: This would require creating text textures dynamically + // For now, we use floating text above the prim +} + +list getSortedLeaderboard() { + // Create combined list of "PlayerName:Wins" for sorting + list combined = []; + integer i; + + for (i = 0; i < llGetListLength(playerNames); i++) { + string playerName = llList2String(playerNames, i); + integer wins = llList2Integer(playerWins, i); + combined += [playerName + ":" + (string)wins]; + } + + // Sort by wins (descending) - bubble sort + integer n = llGetListLength(combined); + integer swapped = TRUE; + + while (swapped) { + swapped = FALSE; + for (i = 0; i < n - 1; i++) { + list data1 = llParseString2List(llList2String(combined, i), [":"], []); + list data2 = llParseString2List(llList2String(combined, i + 1), [":"], []); + + integer wins1 = (integer)llList2String(data1, 1); + integer wins2 = (integer)llList2String(data2, 1); + + if (wins1 < wins2) { + // Swap positions + string temp = llList2String(combined, i); + combined = llListReplaceList(combined, [llList2String(combined, i + 1)], i, i); + combined = llListReplaceList(combined, [temp], i + 1, i + 1); + swapped = TRUE; + } + } + n--; + } + + return combined; +} + +saveLeaderboardData() { + // Save to linkset data for persistence + integer i; + + // Save player count + llLinksetDataWrite("lb_count", (string)llGetListLength(playerNames)); + + // Save each player's data + for (i = 0; i < llGetListLength(playerNames); i++) { + string playerName = llList2String(playerNames, i); + integer wins = llList2Integer(playerWins, i); + + llLinksetDataWrite("lb_player_" + (string)i, playerName); + llLinksetDataWrite("lb_wins_" + (string)i, (string)wins); + } +} + +loadLeaderboardData() { + // Load from linkset data + string countStr = llLinksetDataRead("lb_count"); + if (countStr == "") return; // No saved data + + integer count = (integer)countStr; + playerNames = []; + playerWins = []; + + integer i; + for (i = 0; i < count; i++) { + string playerName = llLinksetDataRead("lb_player_" + (string)i); + string winsStr = llLinksetDataRead("lb_wins_" + (string)i); + + if (playerName != "" && winsStr != "") { + playerNames += [playerName]; + playerWins += [(integer)winsStr]; + } + } + + llOwnerSay("Loaded leaderboard data: " + (string)llGetListLength(playerNames) + " players"); +} + +resetLeaderboard() { + // Clear all data + playerNames = []; + playerWins = []; + + // Clear persistent storage + integer i; + for (i = 0; i < 100; i++) { // Clear up to 100 possible entries + llLinksetDataDelete("lb_player_" + (string)i); + llLinksetDataDelete("lb_wins_" + (string)i); + } + llLinksetDataDelete("lb_count"); + + // Update display + updateLeaderboardDisplay(); + + llOwnerSay("Leaderboard reset - all win data cleared"); +} + +// Public function to get current standings (for other scripts) +string getLeaderboardString() { + list sortedData = getSortedLeaderboard(); + string result = ""; + + integer i; + integer maxDisplay = llGetListLength(sortedData); + if (maxDisplay > 10) maxDisplay = 10; + + for (i = 0; i < maxDisplay; i++) { + list playerData = llParseString2List(llList2String(sortedData, i), [":"], []); + string name = llList2String(playerData, 0); + string wins = llList2String(playerData, 1); + + if (i > 0) result += ", "; + result += name + "(" + wins + ")"; + } + + return result; +} diff --git a/Main.lsl b/Main.lsl index 3a5cb0a..3648cd6 100644 --- a/Main.lsl +++ b/Main.lsl @@ -13,6 +13,17 @@ integer numberPickChannel = -77888; integer rollDialogChannel = -77999; integer DIALOG_CHANNEL = -88888; +// Game timing settings to prevent dialog system overload +float BOT_PICK_DELAY = 2.0; // Delay after bot picks before next action +float HUMAN_PICK_DELAY = 1.0; // Delay after human picks before next action +float DIALOG_DELAY = 1.5; // Delay before showing dialogs +integer gameTimer = 0; // Timer for game flow delays + +// Status message timing +float STATUS_DISPLAY_TIME = 8.0; // How long to show status messages on scoreboard +integer statusTimer = 0; // Track when status messages were sent +string lastStatus = ""; // Track last status sent + list players = []; list names = []; list lives = []; @@ -39,6 +50,14 @@ integer MSG_QUERY_READY_STATE = 210; integer MSG_READY_STATE_RESULT = 211; integer MSG_CLEANUP_ALL_FLOATERS = 212; integer MSG_SHOW_ROLL_DIALOG = 301; +integer MSG_GET_CURRENT_DIALOG = 302; +integer MSG_PLAYER_WON = 551; +integer MSG_RESET_LEADERBOARD = 553; // Custom message to reset leaderboard + +// Display configuration +integer CONTROLLER_FACE = 1; // Face to display start image on (front face) +string TEXTURE_START = "title_start"; // Start image texture +string TEXTURE_GAME_ACTIVE = "game_active"; // Optional: different texture during game integer MSG_GET_DICE_TYPE = 1001; integer MSG_GET_PICKS_REQUIRED = 1002; @@ -61,6 +80,17 @@ integer lastWarning = 0; key currentPicker; +// Send status message to scoreboard with timing +sendStatusMessage(string status) { + statusTimer = llGetUnixTime(); + lastStatus = status; + llRegionSay(-12345, "GAME_STATUS|" + status); + llOwnerSay("šŸ“¢ Status: " + status + " (showing for " + (string)STATUS_DISPLAY_TIME + "s)"); + + // Start timer to automatically clear status after display time + llSetTimerEvent(STATUS_DISPLAY_TIME + 1.0); // Add 1 second buffer +} + // Forward game state to helpers when it changes updateHelpers() { // Don't send peril player name if game hasn't started yet @@ -111,6 +141,26 @@ updateHelpers() { "~" + perilForSync + "~" + llList2CSV(names); llMessageLinked(LINK_SET, MSG_SYNC_GAME_STATE, serialized, NULL_KEY); llMessageLinked(LINK_SET, MSG_SYNC_PICKQUEUE, llList2CSV(pickQueue), NULL_KEY); + + // Send a test message first to verify scoreboard communication + llRegionSay(-12345, "GAME_STATUS|Testing communication..."); + llOwnerSay("DEBUG: Sent test message to scoreboard"); + + // Send player updates to scoreboard + integer j; + for (j = 0; j < llGetListLength(names); j++) { + string playerName = llList2String(names, j); + integer playerLives = llList2Integer(lives, j); + key playerKey = llList2Key(players, j); + + // Use the actual player key as the profile UUID + string profileUUID = (string)playerKey; + + // Send PLAYER_UPDATE message to scoreboard + string updateMsg = "PLAYER_UPDATE|" + playerName + "|" + (string)playerLives + "|" + profileUUID; + llOwnerSay("DEBUG: Sending to scoreboard: " + updateMsg); + llRegionSay(-12345, updateMsg); + } } // Request helper-calculated values @@ -148,15 +198,36 @@ resetGame() { llMessageLinked(LINK_SET, MSG_CLEANUP_FLOAT, (string)ch, NULL_KEY); } } + + // Reset all game state variables players = names = lives = picksData = globalPickedNumbers = readyPlayers = []; floaterChannels = []; // Clear the tracked channels perilPlayer = ""; pickQueue = []; currentPickerIdx = 0; roundStarted = FALSE; // Reset round flag + currentPicker = NULL_KEY; // Reset current picker + timeoutTimer = 0; // Reset timeout timer + lastWarning = 0; // Reset warning timer + diceType = 6; // Reset dice type to default + + // Send reset message to all other scripts to clear their state + llMessageLinked(LINK_SET, -99999, "FULL_RESET", NULL_KEY); llSay(syncChannel, "RESET"); - llOwnerSay(" Game reset!"); - llSleep(0.2); + + // Send CLEAR_GAME message to scoreboard to reset display + llRegionSay(-12345, "CLEAR_GAME"); + llOwnerSay("šŸŽ® Game reset! All state cleared (including scoreboard)."); + + // Clear status tracking + statusTimer = 0; + lastStatus = ""; + + // Reset controller display to start image + llSetTexture(TEXTURE_START, CONTROLLER_FACE); + llSetText("šŸŽ® PERIL DICE GAME\nTouch to play!", <1.0, 1.0, 0.0>, 1.0); + + llSleep(0.5); // Give other scripts time to process reset llSetTimerEvent(0); updateHelpers(); } @@ -249,9 +320,17 @@ startNextRound() { return; } // If somehow called with exactly one player, treat them as the winner and reset. - if (llGetListLength(names) == 1) { - llSay(0, "✨ ULTIMATE VICTORY! " + llList2String(names, 0) + " is the Ultimate Survivor!"); - resetGame(); +if (llGetListLength(names) == 1) { +string winner = llList2String(names, 0); +llSay(0, "✨ ULTIMATE VICTORY! " + winner + " is the Ultimate Survivor!"); +// Trigger victory confetti and record win +llMessageLinked(LINK_SET, MSG_PLAYER_WON, winner, NULL_KEY); +llMessageLinked(LINK_SET, 995, "VICTORY_CONFETTI", NULL_KEY); +llRegionSay(-12345, "GAME_WON|" + winner); // Send winner info to scoreboard +sendStatusMessage("Victory"); // Show victory status + // Wait for victory status to display before reset + llSleep(STATUS_DISPLAY_TIME + 1.0); +resetGame(); return; } // DON'T reset round flag during continuation rounds - only reset for brand new games @@ -263,7 +342,12 @@ startNextRound() { integer randomIdx = (integer)llFrand(llGetListLength(names)); perilPlayer = llList2String(names, randomIdx); llSay(0, "šŸŽÆ " + perilPlayer + " has been randomly selected and is now in peril!"); + sendStatusMessage("Peril Selected"); // Show peril selected status when peril player is chosen } else { + // Continue showing peril selected status for existing peril player + sendStatusMessage("Peril Selected"); + // Add delay to allow this status to display before next phase + llSleep(STATUS_DISPLAY_TIME * 0.6); // Wait 60% of status display time (~5 seconds) } picksData = []; globalPickedNumbers = []; @@ -292,6 +376,9 @@ showNextPickerDialog() { string firstName = llList2String(pickQueue, currentPickerIdx); currentPicker = llList2Key(players, llListFindList(names, [firstName])); + // Add delay before showing dialog to prevent system overload + llSleep(DIALOG_DELAY); + // Check if this is a bot (TestBot names) if (llSubStringIndex(firstName, "TestBot") == 0) { // Get number of picks needed based on peril player's lives @@ -335,6 +422,11 @@ default { llOwnerSay("šŸŽ® Main Controller ready!"); llOwnerSay("šŸŽ® Main Controller key: " + (string)llGetKey()); llOwnerSay("šŸŽ® Main Controller position: " + (string)llGetPos()); + + // Set the start image on the controller prim + llSetTexture(TEXTURE_START, CONTROLLER_FACE); + llSetText("šŸŽ® PERIL DICE GAME\nTouch to play!", <1.0, 1.0, 0.0>, 1.0); + llListen(DIALOG_CHANNEL, "", NULL_KEY, ""); llListen(-9999, "", NULL_KEY, ""); // Listen for bot responses llListen(rollDialogChannel, "", NULL_KEY, ""); // Listen for roll dialog responses @@ -344,6 +436,56 @@ default { llOwnerSay("Touched by: " + (string)llDetectedKey(0)); key toucher = llDetectedKey(0); integer idx = llListFindList(players, [toucher]); + +// Check if player has a current dialog they can recover +if (idx != -1 && toucher != llGetOwner() && roundStarted) { + string playerName = llList2String(names, idx); + // Check if this player is the current picker waiting for a dialog + if (currentPicker == toucher) { + llRegionSayTo(toucher, 0, "šŸ”„ Restoring your number picking dialog..."); + llMessageLinked(LINK_SET, MSG_GET_CURRENT_DIALOG, playerName, toucher); + return; + } + // Check if this player is the peril player who might need a roll dialog + else if (playerName == perilPlayer && currentPickerIdx >= llGetListLength(pickQueue)) { + llRegionSayTo(toucher, 0, "šŸ”„ Restoring your roll dialog..."); + llMessageLinked(LINK_SET, MSG_SHOW_ROLL_DIALOG, perilPlayer, toucher); + return; + } +} + +// Special handling for owner during gameplay +if (toucher == llGetOwner() && roundStarted) { + string ownerName = llKey2Name(toucher); + list options = []; + string menuText = "Game in progress. What would you like to do?"; + + // Check if owner has active dialogs they can recover + integer hasActiveDialog = FALSE; + if (currentPicker == toucher) { + options += ["šŸ”„ Recover Pick Dialog"]; + hasActiveDialog = TRUE; + } + if (ownerName == perilPlayer && currentPickerIdx >= llGetListLength(pickQueue)) { + options += ["šŸ”„ Recover Roll Dialog"]; + hasActiveDialog = TRUE; + } + + // Always offer admin menu + options += ["šŸ”§ Admin Menu"]; + + // If no active dialogs, just show admin menu directly + if (!hasActiveDialog) { + llOwnerSay("DEBUG: Sending MSG_SHOW_MENU for owner during gameplay"); + llMessageLinked(LINK_SET, MSG_SHOW_MENU, "owner|0", toucher); + return; + } + + // Show choice dialog + llDialog(toucher, menuText, options, DIALOG_CHANNEL); + return; +} + if (toucher == llGetOwner()) { // Determine if this will be the first registered player (starter) before registration integer isStarter = FALSE; @@ -364,7 +506,7 @@ default { llMessageLinked(LINK_SET, MSG_REGISTER_PLAYER, ownerName + "|" + (string)toucher, NULL_KEY); } else { // Already registered; check if owner is the first human player - string ownerName = llList2String(names, idx); + // Owner is starter if they are at index 0 OR if all players before them are bots isStarter = TRUE; // Default to TRUE for owner integer k; for (k = 0; k < idx && isStarter; k++) { @@ -372,11 +514,17 @@ default { // If there's a human player before the owner, owner is not starter if (llSubStringIndex(existingName, "TestBot") != 0) { isStarter = FALSE; - llOwnerSay("āŒ Found human player before owner, not starter"); + llOwnerSay("DEBUG: Found human player before owner at index " + (string)k + ": " + existingName); } } + // Special case: if owner is at index 0, they are definitely the starter + if (idx == 0) { + isStarter = TRUE; + llOwnerSay("DEBUG: Owner is at index 0, definitely starter"); + } } // Show the owner menu with the appropriate starter flag + llOwnerSay("DEBUG: Sending MSG_SHOW_MENU for owner, isStarter=" + (string)isStarter + ", idx=" + (string)idx); llMessageLinked(LINK_SET, MSG_SHOW_MENU, "owner|" + (string)isStarter, toucher); } else if (idx != -1) { // For non-owner players, check if they're the first human player @@ -390,6 +538,27 @@ default { } } llMessageLinked(LINK_SET, MSG_SHOW_MENU, "player|" + (string)isStarter, toucher); + } else { + // Unregistered non-owner player - register them and show menu + string playerName = llKey2Name(toucher); + if (playerName != "") { + // Send a registration request; Main.lsl will handle adding them and rezzing a float + llMessageLinked(LINK_SET, MSG_REGISTER_PLAYER, playerName + "|" + (string)toucher, NULL_KEY); + // Determine if this will be the first human player (starter) + integer isStarter = TRUE; // Default to TRUE + integer n; + for (n = 0; n < llGetListLength(names) && isStarter; n++) { + string existingName = llList2String(names, n); + // If there's already a human player (not a bot), this player is not starter + if (llSubStringIndex(existingName, "TestBot") != 0) { + isStarter = FALSE; + } + } + // Show the player menu with the appropriate starter flag + llMessageLinked(LINK_SET, MSG_SHOW_MENU, "player|" + (string)isStarter, toucher); + } else { + llOwnerSay("āš ļø Could not get name for toucher: " + (string)toucher); + } } } @@ -523,7 +692,7 @@ default { readyPlayers += [newName]; llSay(0, "šŸ¤– " + newName + " boots up with deadly precision - ready to play! šŸ¤–"); } else { - // Check if this is the first human player (starter - automatically ready) + // Check if this is the first human player (starter - automatically ready only if owner) integer humanCount = 0; integer i; for (i = 0; i < llGetListLength(names); i++) { @@ -533,7 +702,14 @@ default { } } if (humanCount == 1) { // This is the first human player - llSay(0, "šŸ‘‘ " + newName + " steps forward as the game master - automatically ready for the deadly challenge! šŸ‘‘"); + if (newKey == llGetOwner()) { + // Owner is auto-ready as starter + readyPlayers += [newName]; + llSay(0, "šŸ‘‘ " + newName + " steps forward as the game master - automatically ready for the deadly challenge! šŸ‘‘"); + } else { + // Non-owner starter can toggle ready state + llSay(0, "šŸ‘‘ " + newName + " steps forward as the game master! Touch to set your ready status."); + } } } // The float will be rezzed by the Floater Manager after it processes @@ -566,17 +742,30 @@ default { llOwnerSay("šŸ—‘ļø Eliminated " + eliminatedPlayer + ". Remaining players: " + (string)llGetListLength(names)); + // Show elimination status when someone is actually eliminated + sendStatusMessage("Elimination"); + // Update all scripts with the new player lists after elimination updateHelpers(); // Check if game should end (1 or fewer players remaining) if (llGetListLength(names) <= 1) { - if (llGetListLength(names) == 1) { - llSay(0, "✨ ULTIMATE VICTORY! " + llList2String(names, 0) + " is the Ultimate Survivor!"); - // Trigger victory confetti - llMessageLinked(LINK_SET, 995, "VICTORY_CONFETTI", NULL_KEY); +if (llGetListLength(names) == 1) { +string winner = llList2String(names, 0); +llSay(0, "✨ ULTIMATE VICTORY! " + winner + " is the Ultimate Survivor!"); +// Trigger victory confetti and record win +llMessageLinked(LINK_SET, MSG_PLAYER_WON, winner, NULL_KEY); +llMessageLinked(LINK_SET, 995, "VICTORY_CONFETTI", NULL_KEY); +llRegionSay(-12345, "GAME_WON|" + winner); // Send winner info to scoreboard + // Wait for elimination status to display before showing victory + llSleep(STATUS_DISPLAY_TIME * 0.8); // Wait 80% of status display time (~6.4 seconds) +sendStatusMessage("Victory"); // Show victory status + // Wait for victory status to display before reset + llSleep(STATUS_DISPLAY_TIME + 1.0); } else { llSay(0, "šŸ’€ DESPAIR WINS! No Ultimate Survivors remain!"); + // Wait for elimination status to display before reset (elimination already sent above) + llSleep(STATUS_DISPLAY_TIME + 1.0); } resetGame(); return; @@ -625,9 +814,10 @@ default { if (num == 998) { list parts = llParseString2List(str, ["|"], []); if (llList2String(parts, 0) == "GAME_WON") { - string winner = llList2String(parts, 1); - llSleep(2.0); // Let celebration message display - resetGame(); +string winner = llList2String(parts, 1); +llRegionSay(-12346, "GAME_WON|" + winner); // Send winner info to scoreboard +llSleep(2.0); // Let celebration message display +resetGame(); } return; } @@ -758,13 +948,17 @@ default { updateHelpers(); llOwnerSay("šŸ‘‹ " + leavingName + " left the game"); // Check if game should end (less than 2 players) - if (llGetListLength(names) == 1) { - llSay(0, "✨ ULTIMATE VICTORY! " + llList2String(names, 0) + " survives the killing game!"); - // Trigger victory confetti - llMessageLinked(LINK_SET, 995, "VICTORY_CONFETTI", NULL_KEY); - resetGame(); - } else if (llGetListLength(names) == 0) { - resetGame(); +if (llGetListLength(names) == 1) { +string winner = llList2String(names, 0); +llSay(0, "✨ ULTIMATE VICTORY! " + winner + " is the Ultimate Survivor!"); +// Trigger victory confetti and record win +llMessageLinked(LINK_SET, MSG_PLAYER_WON, winner, NULL_KEY); +llMessageLinked(LINK_SET, 995, "VICTORY_CONFETTI", NULL_KEY); +llRegionSay(-12345, "GAME_WON|" + winner); // Send winner info to scoreboard +sendStatusMessage("Victory"); // Show victory status + // Wait for victory status to display before reset + llSleep(STATUS_DISPLAY_TIME + 1.0); +resetGame(); } } } @@ -854,6 +1048,9 @@ default { updateHelpers(); llSay(0, "šŸŽÆ " + playerName + " stakes their life on numbers: " + picksStr + " šŸŽ²"); + // Add delay before moving to next picker to prevent dialog system overload + llSleep(HUMAN_PICK_DELAY); + // Move to next picker currentPickerIdx++; if (currentPickerIdx < llGetListLength(pickQueue)) { @@ -912,6 +1109,9 @@ default { updateHelpers(); llSay(0, "šŸŽÆ " + playerName + " (bot) stakes their digital life on numbers: " + picksStr + " šŸŽ²"); + // Add delay after bot picks to prevent dialog system overload + llSleep(BOT_PICK_DELAY); + // Move to next picker currentPickerIdx++; if (currentPickerIdx < llGetListLength(pickQueue)) { @@ -946,23 +1146,45 @@ default { } if (channel == DIALOG_CHANNEL) { + // Handle owner choice dialog responses during gameplay + if (id == llGetOwner() && roundStarted) { + if (msg == "šŸ”„ Recover Pick Dialog") { + string ownerName = llKey2Name(id); + llRegionSayTo(id, 0, "šŸ”„ Restoring your number picking dialog..."); + llMessageLinked(LINK_SET, MSG_GET_CURRENT_DIALOG, ownerName, id); + return; + } + else if (msg == "šŸ”„ Recover Roll Dialog") { + llRegionSayTo(id, 0, "šŸ”„ Restoring your roll dialog..."); + llMessageLinked(LINK_SET, MSG_SHOW_ROLL_DIALOG, perilPlayer, id); + return; + } + else if (msg == "šŸ”§ Admin Menu") { + llOwnerSay("DEBUG: Sending MSG_SHOW_MENU for owner admin during gameplay"); + llMessageLinked(LINK_SET, MSG_SHOW_MENU, "owner|0", id); + return; + } + } + // Owner-specific commands: only owner messages in the dialog should be processed here. if (id == llGetOwner()) { - if (msg == "Reset Game") { - resetGame(); +if (msg == "Reset Game") { +resetGame(); // Only reset current game, keep leaderboard + return; + } + if (msg == "Reset Leaderboard") { +llRegionSay(-12345, "RESET_LEADERBOARD"); // Reset only leaderboard scores + llOwnerSay("šŸ† Leaderboard scores reset - game wins cleared!"); + return; + } + if (msg == "Reset All") { + resetGame(); // Reset current game +llRegionSay(-12345, "RESET_LEADERBOARD"); // Reset leaderboard scores + llOwnerSay("šŸ”„ Complete reset - game and leaderboard cleared!"); return; } // Note: do not handle "Start Game" here; allow the generic start logic below to apply, // so that minimum player checks are enforced and non-owner starters can initiate the game. - if (msg == "Dump Players") { - llOwnerSay(" Players: " + llList2CSV(names)); - return; - } - if (msg == "Manage Picks") { - llOwnerSay(" Fetching list of players for pick management..."); - llMessageLinked(LINK_SET, 202, "REQUEST_PLAYER_LIST", llGetOwner()); - return; - } if (msg == "Add Test Player") { // Ensure we do not exceed the maximum allowed players if (llGetListLength(players) >= MAX_PLAYERS) { @@ -1034,6 +1256,11 @@ default { // All checks passed, start the game llSay(0, "⚔ ALL PARTICIPANTS READY! THE DEADLY PERIL DICE GAME BEGINS! ⚔"); + + // Change controller display to show game is active + llSetText("šŸŽ® GAME IN PROGRESS\nRound " + (string)(llGetListLength(names)) + " players", <1.0, 0.2, 0.2>, 1.0); + + sendStatusMessage("Title"); // Show title status at game start startNextRound(); requestDiceType(); } @@ -1043,4 +1270,38 @@ default { } } } -} \ No newline at end of file + + timer() { + // Check if we need to clear status message + if (statusTimer > 0 && (llGetUnixTime() - statusTimer) >= STATUS_DISPLAY_TIME) { + // Clear the status and revert to title + statusTimer = 0; + lastStatus = ""; + llRegionSay(-12345, "GAME_STATUS|Title"); + llOwnerSay("šŸ“¢ Status cleared - reverted to Title"); + llSetTimerEvent(0); // Stop timer if no other timing needed + } + + // Handle dialog timeout warnings (existing code) + if (timeoutTimer > 0) { + integer elapsed = llGetUnixTime() - timeoutTimer; + if (elapsed >= TIMEOUT_SECONDS) { + llOwnerSay("ā° Dialog timeout reached!"); + llSetTimerEvent(0); + timeoutTimer = 0; + } else { + integer remaining = TIMEOUT_SECONDS - elapsed; + if (remaining <= warning2min && lastWarning < warning2min) { + llOwnerSay("āš ļø " + (string)(remaining / 60) + " minutes remaining for dialog response"); + lastWarning = warning2min; + } else if (remaining <= warning5min && lastWarning < warning5min) { + llOwnerSay("āš ļø " + (string)(remaining / 60) + " minutes remaining for dialog response"); + lastWarning = warning5min; + } else if (remaining <= warning9min && lastWarning < warning9min) { + llOwnerSay("āš ļø " + (string)(remaining / 60) + " minutes remaining for dialog response"); + lastWarning = warning9min; + } + } + } + } +} diff --git a/Number Picker Dialog Handler.lsl b/Number Picker Dialog Handler.lsl index 520fd49..755067f 100644 --- a/Number Picker Dialog Handler.lsl +++ b/Number Picker Dialog Handler.lsl @@ -1,6 +1,7 @@ // === Dialog Handler (Updated for Peril Dice - Paginated Multiple Picks) === integer MSG_SHOW_DIALOG = 101; +integer MSG_GET_CURRENT_DIALOG = 302; integer numberPickChannel = -77888; // Pagination and picking state @@ -66,11 +67,19 @@ showPickDialog(string name, key id, integer diceType, integer picks) { // Show number selection with pagination options = getNumbersForPage(currentPage, diceType); + // If current page has no numbers, go back to page 0 + if (llGetListLength(options) == 0 && currentPage > 0) { + currentPage = 0; + options = getNumbersForPage(currentPage, diceType); + } + // Calculate total pages based on available numbers, not all numbers integer availableCount = diceType - llGetListLength(globallyPickedNumbers) - llGetListLength(currentPicks); totalPages = (availableCount + NUMBERS_PER_PAGE - 1) / NUMBERS_PER_PAGE; if (totalPages < 1) totalPages = 1; - if (totalPages > 1) { + + // Only show navigation if there are multiple pages AND current page has numbers + if (totalPages > 1 && llGetListLength(options) > 0) { if (currentPage > 0) options += ["ā—€ Prev"]; if (currentPage < totalPages - 1) options += ["Next ā–¶"]; } @@ -107,8 +116,22 @@ default { } link_message(integer sender, integer num, string str, key id) { + // Handle full reset from main controller + if (num == -99999 && str == "FULL_RESET") { + // Reset all dialog state + currentPlayer = ""; + currentPlayerKey = NULL_KEY; + currentDiceType = 0; + picksNeeded = 1; + currentPicks = []; + globallyPickedNumbers = []; + currentPage = 0; + llOwnerSay("šŸŽ® Number Picker Dialog Handler reset!"); + return; + } + if (num == MSG_SHOW_DIALOG) { - list parts = llParseString2List(str, ["|"], []); + list parts = llParseString2List(str, ["|"], []); if (llGetListLength(parts) < 3) { llOwnerSay("āš ļø Malformed MSG_SHOW_DIALOG input: " + str); return; @@ -132,6 +155,18 @@ default { showPickDialog(name, id, diceType, picks); } + + // Handle dialog recovery requests + else if (num == MSG_GET_CURRENT_DIALOG) { + string playerName = str; + // Only show dialog if this player is the current picker + if (currentPlayer == playerName && currentPlayerKey == id) { + llOwnerSay("šŸ”„ Restoring dialog for " + playerName); + showPickDialog(currentPlayer, currentPlayerKey, currentDiceType, picksNeeded); + } else { + llRegionSayTo(id, 0, "āŒ You don't have an active dialog to restore."); + } + } } listen(integer channel, string name, key id, string message) { @@ -145,7 +180,11 @@ default { return; } if (message == "Next ā–¶") { - integer totalPages = (currentDiceType + NUMBERS_PER_PAGE - 1) / NUMBERS_PER_PAGE; + // Calculate total pages based on AVAILABLE numbers, not all dice numbers + integer availableCount = currentDiceType - llGetListLength(globallyPickedNumbers) - llGetListLength(currentPicks); + integer totalPages = (availableCount + NUMBERS_PER_PAGE - 1) / NUMBERS_PER_PAGE; + if (totalPages < 1) totalPages = 1; + if (currentPage < totalPages - 1) { currentPage++; showPickDialog(currentPlayer, currentPlayerKey, currentDiceType, picksNeeded); diff --git a/Owner and Player Dialog Handler.lsl b/Owner and Player Dialog Handler.lsl index c0ca1b5..5a7ff89 100644 --- a/Owner and Player Dialog Handler.lsl +++ b/Owner and Player Dialog Handler.lsl @@ -15,18 +15,42 @@ integer MSG_TOGGLE_READY = 202; integer MSG_QUERY_READY_STATE = 210; integer MSG_READY_STATE_RESULT = 211; integer MSG_CLEANUP_ALL_FLOATERS = 212; +integer MSG_GET_CURRENT_DIALOG = 302; -// Owner-specific options shown when pressing the "Owner" button -list ownerOptions = ["Join Game", "Leave Game", "Add Test Player", "Manage Picks", "Start Game", "Reset Game", "Dump Players", "Cleanup Floaters"]; +// Categorized owner menu system +list mainOwnerOptions = ["šŸŽ® Game Participation", "šŸ‘„ Player Management", "šŸ”„ Reset Options", "šŸ› ļø Troubleshooting", "ā¬…ļø Back to Game"]; + +// Sub-menu options for each category +list gameParticipationOptions = ["Join Game", "Leave Game", "ā¬…ļø Back to Main"]; +list playerManagementOptions = ["Add Test Player", "ā¬…ļø Back to Main"]; +list resetOptions = ["Reset Game", "Reset Leaderboard", "Reset All", "ā¬…ļø Back to Main"]; +list troubleshootingOptions = ["Cleanup Floaters", "ā¬…ļø Back to Main"]; // State tracking for dynamic ready menu key pendingMenuPlayer = NULL_KEY; integer pendingMenuIsStarter = FALSE; integer pendingMenuIsOwner = FALSE; -// Display the debug/owner menu +// Display the main categorized owner menu showOwnerMenu(key id) { - llDialog(id, "šŸ”§ Owner Menu", ownerOptions, DIALOG_CHANNEL); + llDialog(id, "šŸ”§ Owner Menu - Select Category", mainOwnerOptions, DIALOG_CHANNEL); +} + +// Display category sub-menus +showGameParticipationMenu(key id) { + llDialog(id, "šŸŽ® Game Participation", gameParticipationOptions, DIALOG_CHANNEL); +} + +showPlayerManagementMenu(key id) { + llDialog(id, "šŸ‘„ Player Management", playerManagementOptions, DIALOG_CHANNEL); +} + +showResetOptionsMenu(key id) { + llDialog(id, "šŸ”„ Reset Options", resetOptions, DIALOG_CHANNEL); +} + +showTroubleshootingMenu(key id) { + llDialog(id, "šŸ› ļø Troubleshooting", troubleshootingOptions, DIALOG_CHANNEL); } // Display the combined Ready/Leave menu for both players and owners @@ -114,6 +138,19 @@ default { } link_message(integer sender, integer num, string str, key id) { + // Handle full reset from main controller + if (num == -99999 && str == "FULL_RESET") { + // Reset dialog state + pendingMenuPlayer = NULL_KEY; + pendingMenuIsStarter = FALSE; + pendingMenuIsOwner = FALSE; + currentPickList = []; + currentPickTarget = ""; + currentPickLimit = 3; + llOwnerSay("šŸŽ­ Owner and Player Dialog Handler reset!"); + return; + } + if (num == MSG_SHOW_MENU) { llOwnerSay("šŸ“Ø Received MSG_SHOW_MENU with: " + str); list args = llParseString2List(str, ["|"], []); @@ -124,8 +161,11 @@ default { string targetType = llList2String(args, 0); integer isStarter = (integer)llList2String(args, 1); if (targetType == "owner") { + // Both starter and non-starter owners get ready/leave menu with Owner button + llOwnerSay("DEBUG: Owner menu request - isStarter=" + (string)isStarter + ", calling showReadyLeaveMenu"); showReadyLeaveMenu(id, isStarter, TRUE); } else if (targetType == "player") { + llOwnerSay("DEBUG: Player menu request - isStarter=" + (string)isStarter + ", calling showReadyLeaveMenu"); showReadyLeaveMenu(id, isStarter, FALSE); } } @@ -137,11 +177,17 @@ default { integer isReady = (integer)llList2String(parts, 1); integer isBot = (integer)llList2String(parts, 2); + llOwnerSay("DEBUG: Ready state result for " + playerName + ": ready=" + (string)isReady + ", bot=" + (string)isBot); + llOwnerSay("DEBUG: Pending menu - starter=" + (string)pendingMenuIsStarter + ", owner=" + (string)pendingMenuIsOwner); + // Show menu with the current ready state if (pendingMenuPlayer == id) { + llOwnerSay("DEBUG: Calling showReadyLeaveMenuWithState"); showReadyLeaveMenuWithState(pendingMenuPlayer, pendingMenuIsStarter, pendingMenuIsOwner, isReady, isBot); // Reset pending state pendingMenuPlayer = NULL_KEY; + } else { + llOwnerSay("DEBUG: Pending menu player mismatch: expected " + (string)pendingMenuPlayer + ", got " + (string)id); } } return; @@ -177,6 +223,27 @@ default { if (msg == "Owner") { showOwnerMenu(id); } + // Handle category menu navigation + else if (msg == "šŸŽ® Game Participation") { + showGameParticipationMenu(id); + } + else if (msg == "šŸ‘„ Player Management") { + showPlayerManagementMenu(id); + } + else if (msg == "šŸ”„ Reset Options") { + showResetOptionsMenu(id); + } + else if (msg == "šŸ› ļø Troubleshooting") { + showTroubleshootingMenu(id); + } + else if (msg == "ā¬…ļø Back to Main") { + showOwnerMenu(id); + } + else if (msg == "ā¬…ļø Back to Game") { + // Return to the owner's ready/leave menu + // Need to determine if owner is starter + llMessageLinked(LINK_SET, MSG_SHOW_MENU, "owner|1", id); // Assume starter for now, will be corrected by main controller + } // Owner joins the game: register and rez a float for them else if (msg == "Join Game") { // Use the avatar's key as the identifier diff --git a/README.md b/README.md index 4e6049a..1768fbb 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,23 @@ Peril Dice is a multiplayer elimination game where each player selects numbers b ## Key Features +### šŸ† Visual Scoreboard System +- **Real-Time Player Grid**: Visual scoreboard showing all players with profile pictures and hearts +- **Profile Picture Integration**: Automatic avatar profile fetching from Second Life +- **Heart Texture Display**: Visual life representation (3, 2, 1, 0 hearts) +- **Instant Updates**: Hearts change immediately when lives are lost + +### šŸŽ­ Enhanced Status Display +- **Visual Status Actions**: Large status prim with custom textures for each game event +- **Specific Status Types**: Direct Hit, No Shield, Plot Twist, Elimination, Victory, etc. +- **Perfect Timing**: 8-second display time with protective delays to prevent overwriting + +### šŸŽÆ Comprehensive Leaderboard +- **Persistent Win Tracking**: Player victories saved across sessions +- **XyzzyText Display**: Professional 3-prim text system for leaderboard +- **Automatic Sorting**: Top players by win count + +### šŸŽ® Core Game Features - **šŸŽÆ Dynamic Player Management**: Players can join at runtime (owner and other avatars) - **šŸ¤– Bot Support**: Add AI bots for testing and gameplay variety - **šŸ“± Floating HUD Display**: Real-time stats for each player @@ -69,18 +86,34 @@ Dice type is automatically chosen to ensure at least 3 picks per player: | 5–6 | d20 | | 7–10 | d30 | -## Recent Improvements (v2.2.0) +## Recent Improvements (v2.3.0) -- **šŸŽ­ Dramatic Messaging**: All game events now use thematic, immersive language -- **šŸ“¢ Public Announcements**: Players can see all major game events in public chat -- **šŸŽ² Enhanced Dice Messages**: Rolls show both dice type (d6, d8, etc.) and result -- **šŸ”§ Bug Fixes**: Resolved StatFloat duplication and duplicate message handling issues +### šŸ† Visual Scoreboard Revolution +- **Real-Time Visual Display**: Complete visual overhaul with player grid showing profile pictures and hearts +- **Instant Heart Updates**: Hearts change immediately when lives are lost, before any dialogs appear +- **Profile Picture Fetching**: Automatic HTTP requests to get actual Second Life avatar pictures +- **0-Hearts Display**: Shows elimination sequence visually before player removal + +### šŸŽ­ Enhanced Status System +- **Specific Status Types**: Separate textures for Direct Hit, No Shield, Plot Twist, Elimination, Victory +- **Perfect Timing**: 8-second display with 2-second protective delays prevent status overwriting +- **Visual Impact**: Large action prim displays current game status with custom textures + +### šŸŽÆ Comprehensive Leaderboard +- **Persistent Win Tracking**: Player victories saved across game sessions using linkset data +- **Professional Display**: XyzzyText 3-prim system for clean leaderboard presentation +- **Flexible Reset Options**: Separate commands for game reset, leaderboard reset, or complete reset + +### šŸ”§ Critical Fixes +- **Heart Update Timing**: Fixed hearts not updating until after next-turn dialog +- **Status Conflicts**: All status messages now have protective delays to prevent overwriting +- **Victory/Elimination Flow**: Proper 6.4-second delay between elimination and victory status ## Version -**Current Version**: 2.2.0 -**Last Updated**: August 2025 -**Status**: Stable - Enhanced UX with dramatic messaging system +**Current Version**: 2.3.0 +**Last Updated**: August 5, 2025 +**Status**: Stable - Complete visual scoreboard system with enhanced status display --- diff --git a/Roll Confetti Module.lsl b/Roll Confetti Module.lsl index bceb9f3..9cf173b 100644 --- a/Roll Confetti Module.lsl +++ b/Roll Confetti Module.lsl @@ -68,6 +68,19 @@ default { } link_message(integer sender, integer num, string str, key id) { + // Handle full reset from main controller + if (num == -99999 && str == "FULL_RESET") { + // Reset roll confetti module state + names = []; + lives = []; + picksData = []; + perilPlayer = ""; + // Stop any active particles + llParticleSystem([]); + llOwnerSay("šŸŽ† Roll Confetti Module reset!"); + return; + } + // Handle game state sync from main controller if (num == MSG_SYNC_GAME_STATE) { list parts = llParseString2List(str, ["~"], []); @@ -155,6 +168,12 @@ default { if (matched && newPeril != "") { llSay(0, "⚔ PLOT TWIST! " + newPeril + " picked " + resultStr + " (rolled on d" + (string)diceType + ") and is now in ULTIMATE PERIL! ⚔"); perilPlayer = newPeril; + + // Send Plot Twist status to scoreboard + llRegionSay(-12345, "GAME_STATUS|Plot Twist"); + // Add delay to let status display before next phase + llSleep(2.0); + // Update floaters immediately to show correct peril player before sync llMessageLinked(LINK_SET, MSG_UPDATE_FLOAT, newPeril, NULL_KEY); } else { @@ -163,19 +182,42 @@ default { integer currentLives = llList2Integer(lives, pidx); lives = llListReplaceList(lives, [currentLives - 1], pidx, pidx); + // Immediately send updated heart count to scoreboard + integer newLives = currentLives - 1; + key perilKey = NULL_KEY; // We don't have player keys in this module, use NULL_KEY + if (pidx < llGetListLength(names)) { + string updateMsg = "PLAYER_UPDATE|" + perilPlayer + "|" + (string)newLives + "|" + (string)perilKey; + llRegionSay(-12345, updateMsg); + llOwnerSay("šŸ’— Immediate scoreboard update: " + perilPlayer + " now has " + (string)newLives + " lives"); + } + // Check if peril player picked the rolled number list perilPicks = getPicksFor(perilPlayer); integer perilPickedIt = (llListFindList(perilPicks, [resultStr]) != -1); if (perilPickedIt) { llSay(0, "🩸 DIRECT HIT! " + perilPlayer + " picked their own doom - the d" + (string)diceType + " landed on " + resultStr + "! 🩸"); + // Send Direct Hit status to scoreboard + llRegionSay(-12345, "GAME_STATUS|Direct Hit"); + // Add delay to let status display before next phase + llSleep(2.0); } else { llSay(0, "🩸 NO SHIELD! Nobody picked " + resultStr + " - " + perilPlayer + " takes the hit from the d" + (string)diceType + "! 🩸"); + // Send No Shield status to scoreboard + llRegionSay(-12345, "GAME_STATUS|No Shield"); + // Add delay to let status display before next phase + llSleep(2.0); } + llMessageLinked(LINK_SET, MSG_UPDATE_FLOAT, perilPlayer, NULL_KEY); // Check for elimination if (currentLives - 1 <= 0) { + // Show 0 hearts on scoreboard before elimination message + string eliminationUpdateMsg = "PLAYER_UPDATE|" + perilPlayer + "|0|" + (string)NULL_KEY; + llRegionSay(-12345, eliminationUpdateMsg); + llOwnerSay("šŸ’€ Elimination update: " + perilPlayer + " now shows 0 hearts"); + llSay(0, "🐻 PUNISHMENT TIME! " + perilPlayer + " has been ELIMINATED!"); // Remove eliminated player (send message to main controller) llMessageLinked(LINK_SET, 999, "ELIMINATE_PLAYER|" + perilPlayer, NULL_KEY); diff --git a/StatFloat.lsl b/StatFloat.lsl index 4588478..fe6fafe 100644 --- a/StatFloat.lsl +++ b/StatFloat.lsl @@ -40,8 +40,13 @@ default { on_rez(integer start_param) { llListen(start_param, "", NULL_KEY, ""); + // Wait a moment for the description to be set + llSleep(0.1); myName = llGetObjectDesc(); - llOwnerSay("šŸ“± Listening on channel " + (string)start_param + " for " + myName); + if (myName == "" || myName == "(No Description)") { + myName = "UnknownPlayer" + (string)start_param; + } +// StatFloat ready and listening } listen(integer channel, string name, key id, string message) { @@ -50,6 +55,40 @@ default { if (llGetListLength(parts) >= 2) { target = (key)llGetSubString(llList2String(parts, 0), 6, -1); displayText = llList2String(parts, 1); + + // Extract life count from the display text to set heart texture + // Look for "Lives: X" pattern in the text + integer livesPos = llSubStringIndex(displayText, "Lives: "); + if (livesPos != -1) { + string livesStr = llGetSubString(displayText, livesPos + 7, livesPos + 7); + integer lifeCount = (integer)livesStr; + string heartTexture = (string)lifeCount + "_hearts"; + // Set heart texture based on current life count + llSetTexture(heartTexture, 1); // Face 1 (right side) + llSetTexture(heartTexture, 2); // Face 2 (back) + llSetTexture(heartTexture, 3); // Face 3 (left side) + llSetTexture(heartTexture, 4); // Face 4 (front) + // Set white background for all faces to eliminate gaps without tinting + llSetColor(<1.0, 1.0, 1.0>, 1); // White background for right side + llSetColor(<1.0, 1.0, 1.0>, 2); // White background for back + llSetColor(<1.0, 1.0, 1.0>, 3); // White background for left side + llSetColor(<1.0, 1.0, 1.0>, 4); // White background for front + llSetColor(<0.2, 0.2, 0.2>, 0); // Dark gray for top + llSetColor(<0.2, 0.2, 0.2>, 5); // Dark gray for bottom + + // Remove "Lives: X" from display text since hearts show it + list lines = llParseString2List(displayText, ["\n"], []); + list filteredLines = []; + integer i; + for (i = 0; i < llGetListLength(lines); i++) { + string line = llList2String(lines, i); + if (llSubStringIndex(line, "Lives: ") != 0) { + filteredLines += [line]; + } + } + displayText = llDumpList2String(filteredLines, "\n"); + } + llSetText(displayText, <1,1,1>, 1.0); } } @@ -57,6 +96,12 @@ default { llOwnerSay("🪟 Cleaning up..."); llDie(); } + else if (llSubStringIndex(message, "SET_NAME:") == 0) { + myName = llGetSubString(message, 9, -1); + // Name successfully received and set + // Update the object description as well for consistency + llSetObjectDesc(myName); + } } link_message(integer sender, integer num, string str, key id) { @@ -101,7 +146,10 @@ default { string picksDisplay = llList2CSV(picks); - string txt = "šŸŽ² Peril Dice\nšŸ‘¤ " + myName + "\nā¤ļø Lives: " + (string)lifeCount + "\nšŸ¢ Peril: " + perilDisplay + "\nšŸ”¢ Picks: " + picksDisplay; + // Note: Heart texture is set via listen() path to avoid conflicts + + // Update text display (lives now shown by heart textures) + string txt = "šŸŽ² Peril Dice\nšŸ‘¤ " + myName + "\nšŸ¢ Peril: " + perilDisplay + "\nšŸ”¢ Picks: " + picksDisplay; llSetText(txt, <1,1,1>, 1.0); } } diff --git a/XyzzyText_Debug.lsl b/XyzzyText_Debug.lsl new file mode 100644 index 0000000..ea2ab38 --- /dev/null +++ b/XyzzyText_Debug.lsl @@ -0,0 +1,88 @@ +// XyzzyText Debug Helper +// This script helps debug issues with XyzzyText display + +// Constants for XyzzyText communication +integer DISPLAY_STRING = 204000; +integer DISPLAY_EXTENDED = 204001; +integer RESCAN_LINKSET = 204008; + +// Communication channel from scoreboard +integer LEADERBOARD_CHANNEL = -12346; + +default { + state_entry() { + // Listen for messages from scoreboard + llListen(LEADERBOARD_CHANNEL, "", "", ""); + llOwnerSay("XyzzyText Debug Helper started"); + + // Check linkset info + integer numPrims = llGetNumberOfPrims(); + llOwnerSay("DEBUG: Linkset has " + (string)numPrims + " prims"); + + // List all prim names to verify XyzzyText setup + integer i; + for (i = 1; i <= numPrims; i++) { + string primName = llGetLinkName(i); + llOwnerSay("DEBUG: Prim " + (string)i + " name: '" + primName + "'"); + } + + // Trigger a rescan of the linkset for XyzzyText + llOwnerSay("DEBUG: Triggering XyzzyText rescan"); + llMessageLinked(LINK_SET, RESCAN_LINKSET, "", ""); + + // Send test text to each bank + llSleep(1.0); // Wait for rescan to complete + llOwnerSay("DEBUG: Sending test text to banks"); + llMessageLinked(LINK_SET, DISPLAY_STRING, "TEST0", "0"); // Bank 0 (left) + llMessageLinked(LINK_SET, DISPLAY_STRING, "TEST1", "1"); // Bank 1 (middle) + llMessageLinked(LINK_SET, DISPLAY_STRING, "TEST2", "2"); // Bank 2 (right) + } + + listen(integer channel, string name, key id, string message) { + if (channel == LEADERBOARD_CHANNEL) { + llOwnerSay("DEBUG: Received message on channel " + (string)channel + ": " + message); + + if (llSubStringIndex(message, "LEFT_TEXT|") == 0) { + string leftText = llGetSubString(message, 10, -1); + llOwnerSay("DEBUG: Got LEFT text: " + leftText); + llMessageLinked(LINK_SET, DISPLAY_STRING, leftText, "0"); + } + else if (llSubStringIndex(message, "MIDDLE_TEXT|") == 0) { + string middleText = llGetSubString(message, 12, -1); + llOwnerSay("DEBUG: Got MIDDLE text: " + middleText); + llMessageLinked(LINK_SET, DISPLAY_STRING, middleText, "1"); + } + else if (llSubStringIndex(message, "RIGHT_TEXT|") == 0) { + string rightText = llGetSubString(message, 11, -1); + llOwnerSay("DEBUG: Got RIGHT text: " + rightText); + llMessageLinked(LINK_SET, DISPLAY_STRING, rightText, "2"); + } + } + } + + touch_start(integer total_number) { + // When touched, refresh debug info and test displays + llOwnerSay("--- XyzzyText Debug Refresh ---"); + + // Check linkset info + integer numPrims = llGetNumberOfPrims(); + llOwnerSay("DEBUG: Linkset has " + (string)numPrims + " prims"); + + // List all prim names + integer i; + for (i = 1; i <= numPrims; i++) { + string primName = llGetLinkName(i); + llOwnerSay("DEBUG: Prim " + (string)i + " name: '" + primName + "'"); + } + + // Trigger rescan and test displays + llOwnerSay("DEBUG: Triggering XyzzyText rescan"); + llMessageLinked(LINK_SET, RESCAN_LINKSET, "", ""); + + llSleep(1.0); + llOwnerSay("DEBUG: Sending test text to banks"); + llMessageLinked(LINK_SET, DISPLAY_STRING, "TEST0", "0"); + llMessageLinked(LINK_SET, DISPLAY_STRING, "TEST1", "1"); + llMessageLinked(LINK_SET, DISPLAY_STRING, "TEST2", "2"); + } +} diff --git a/images/title.png b/images/title.png new file mode 100644 index 0000000..d802db9 Binary files /dev/null and b/images/title.png differ diff --git a/xytext_master.lsl b/xytext_master.lsl new file mode 100644 index 0000000..42a1f9e --- /dev/null +++ b/xytext_master.lsl @@ -0,0 +1,611 @@ +//////////////////////////////////////////// +// XyzzyText v2.1 Script (Set Line Color) by Huney Jewell +// XyzzyText v2.0 Script (5 Face, Single Texture) +// +// +// Edited trivially by Joel Cloquet on 1/2011 to use the (relatively) +// new llSetLinkPrimitiveParamsFast function, thereby removing the need in +// some cases to use the slave script. +// +// Heavily Modified by Thraxis Epsilon, Gigs Taggart 5/2007 and Strife Onizuka 8/2007 +// Rewrite to allow one-script-per-object operation w/ optional slaves +// Enable prim-label functionality +// Enabled Banking +// +// Modified by Kermitt Quirk 19/01/2006 +// To add support for 5 face prim instead of 3 +// +// Core XyText Originally Written by Xylor Baysklef +// +// +//////////////////////////////////////////// + +/////////////// CONSTANTS /////////////////// +// XyText Message Map. +integer DISPLAY_STRING = 204000; +integer DISPLAY_EXTENDED = 204001; +integer REMAP_INDICES = 204002; +integer RESET_INDICES = 204003; +integer SET_FADE_OPTIONS = 204004; +integer SET_FONT_TEXTURE = 204005; +integer SET_LINE_COLOR = 204006; +integer SET_COLOR = 204007; +integer RESCAN_LINKSET = 204008; + +//internal API +integer REGISTER_SLAVE = 205000; +integer SLAVE_RECOGNIZED = 205001; +integer SLAVE_DISPLAY = 205003; +integer SLAVE_DISPLAY_EXTENDED = 205004; +integer SLAVE_RESET = 205005; + +// This is an extended character escape sequence. +string ESCAPE_SEQUENCE = "\e"; + +// This is used to get an index for the extended character. +string EXTENDED_INDEX = "12345"; + +// Face numbers. +integer FACE_1 = 3; +integer FACE_2 = 7; +integer FACE_3 = 4; +integer FACE_4 = 6; +integer FACE_5 = 1; + +// Used to hide the text after a fade-out. +key TRANSPARENT = "701917a8-d614-471f-13dd-5f4644e36e3c"; +key null_key = NULL_KEY; +///////////// END CONSTANTS //////////////// + +///////////// GLOBAL VARIABLES /////////////// +// This is the key of the font we are displaying. +key gFontTexture = "b2e7394f-5e54-aa12-6e1c-ef327b6bed9e"; +// All displayable characters. Default to ASCII order. +string gCharIndex; + +// This is whether or not to use the fade in/out special effect. +integer gCellUseFading = FALSE; +// This is how long to display the text before fading out (if using +// fading special effect). +// Note: < 0 means don't fade out. +float gCellHoldDelay = 1.0; + +integer gSlaveRegistered; +list gSlaveNames; + +integer BANK_STRIDE=3; //offset, length, highest_dirty +list gBankingData; + +/////////// END GLOBAL VARIABLES //////////// + +ResetCharIndex() { + gCharIndex = " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`"; + gCharIndex += "abcdefghijklmnopqrstuvwxyz{|}~"; + gCharIndex += "\n\n\n\n\n"; +} + +vector GetGridOffset(integer index) { + // Calculate the offset needed to display this character. + integer Row = index / 10; + integer Col = index % 10; + + // Return the offset in the texture. + return <-0.45 + 0.1 * Col, 0.45 - 0.1 * Row, 0.0>; +} + +ShowChars(integer link,vector grid_offset1, vector grid_offset2, vector grid_offset3, vector grid_offset4, vector grid_offset5) { + // Set the primitive textures directly. + + // <-0.256, 0, 0> + // <0, 0, 0> + // <0.130, 0, 0> + // <0, 0, 0> + // <-0.74, 0, 0> + + llSetLinkPrimitiveParamsFast( link,[ + PRIM_TEXTURE, FACE_1, (string)gFontTexture, <0.126, 0.1, 0>, grid_offset1 + <0.037, 0, 0>, 0.0, + PRIM_TEXTURE, FACE_2, (string)gFontTexture, <0.05, 0.1, 0>, grid_offset2, 0.0, + PRIM_TEXTURE, FACE_3, (string)gFontTexture, <-0.74, 0.1, 0>, grid_offset3 - <0.244, 0, 0>, 0.0, + PRIM_TEXTURE, FACE_4, (string)gFontTexture, <0.05, 0.1, 0>, grid_offset4, 0.0, + PRIM_TEXTURE, FACE_5, (string)gFontTexture, <0.126, 0.1, 0>, grid_offset5 - <0.037, 0, 0>, 0.0 + ]); +} + +RenderString(integer link, string str) { + // Get the grid positions for each pair of characters. + vector GridOffset1 = GetGridOffset( llSubStringIndex(gCharIndex, llGetSubString(str, 0, 0)) ); + vector GridOffset2 = GetGridOffset( llSubStringIndex(gCharIndex, llGetSubString(str, 1, 1)) ); + vector GridOffset3 = GetGridOffset( llSubStringIndex(gCharIndex, llGetSubString(str, 2, 2)) ); + vector GridOffset4 = GetGridOffset( llSubStringIndex(gCharIndex, llGetSubString(str, 3, 3)) ); + vector GridOffset5 = GetGridOffset( llSubStringIndex(gCharIndex, llGetSubString(str, 4, 4)) ); + + // Use these grid positions to display the correct textures/offsets. + ShowChars(link,GridOffset1, GridOffset2, GridOffset3, GridOffset4, GridOffset5); +} + +RenderWithEffects(integer link, string str) { + // Get the grid positions for each pair of characters. + vector GridOffset1 = GetGridOffset( llSubStringIndex(gCharIndex, llGetSubString(str, 0, 0)) ); + vector GridOffset2 = GetGridOffset( llSubStringIndex(gCharIndex, llGetSubString(str, 1, 1)) ); + vector GridOffset3 = GetGridOffset( llSubStringIndex(gCharIndex, llGetSubString(str, 2, 2)) ); + vector GridOffset4 = GetGridOffset( llSubStringIndex(gCharIndex, llGetSubString(str, 3, 3)) ); + vector GridOffset5 = GetGridOffset( llSubStringIndex(gCharIndex, llGetSubString(str, 4, 4)) ); + + // First set the alpha to the lowest possible. + llSetLinkAlpha(link,0.05, ALL_SIDES); + + // Use these grid positions to display the correct textures/offsets. + ShowChars(link,GridOffset1, GridOffset2, GridOffset3, GridOffset4, GridOffset5); // Now turn up the alpha until it is at full strength. + float Alpha = 0.10; + for (; Alpha <= 1.0; Alpha += 0.05) + llSetLinkAlpha(link,Alpha, ALL_SIDES); + // See if we want to fade out as well. + if (gCellHoldDelay < 0.0) + // No, bail out. (Just keep showing the string at full strength). + return; + // Hold the text for a while. + llSleep(gCellHoldDelay); + // Now fade out. + for (Alpha = 0.95; Alpha >= 0.05; Alpha -= 0.05) + llSetLinkAlpha(link,Alpha, ALL_SIDES); + // Make the text transparent to fully hide it. + llSetLinkTexture(link,TRANSPARENT, ALL_SIDES); +} + +integer RenderExtended(integer link, string str, integer render) { + // Look for escape sequences. + integer length = 0; + list Parsed = llParseString2List(str, [], (list)ESCAPE_SEQUENCE); + integer ParsedLen = llGetListLength(Parsed); + + // Create a list of index values to work with. + list Indices; + // We start with room for 5 indices. + integer IndicesLeft = 5; + + string Token; + integer Clipped; + integer LastWasEscapeSequence = FALSE; + // Work from left to right. + integer i = 0; + for (; i < ParsedLen && IndicesLeft > 0; ++i) { + Token = llList2String(Parsed, i); + + // If this is an escape sequence, just set the flag and move on. + if (Token == ESCAPE_SEQUENCE) { + LastWasEscapeSequence = TRUE; + } + else { // Token != ESCAPE_SEQUENCE + // Otherwise this is a normal token. Check its length. + Clipped = FALSE; + integer TokenLength = llStringLength(Token); + // Clip if necessary. + if (TokenLength > IndicesLeft) { + TokenLength = llStringLength(Token = llGetSubString(Token, 0, IndicesLeft - 1)); + IndicesLeft = 0; + Clipped = TRUE; + } + else + IndicesLeft -= TokenLength; + + // Was the previous token an escape sequence? + if (LastWasEscapeSequence) { + // Yes, the first character is an escape character, the rest are normal. + length += 2 + TokenLength; + if(render) + { + // This is the extended character. + Indices += (llSubStringIndex(EXTENDED_INDEX, llGetSubString(Token, 0, 0)) + 95); + + // These are the normal characters. + integer j = 1; + for (; j < TokenLength; ++j) + { + Indices += llSubStringIndex(gCharIndex, llGetSubString(Token, j, j)); + } + } + } + else { // Normal string. + // Just add the characters normally. + length += TokenLength; + if(render) + { + integer j = 0; + for (; j < TokenLength; ++j) + { + Indices += llSubStringIndex(gCharIndex, llGetSubString(Token, j, j)); + } + } + } + + // Unset this flag, since this was not an escape sequence. + LastWasEscapeSequence = FALSE; + } + } + + // Use the indices to create grid positions. + if(render) + { + vector GridOffset1 = GetGridOffset( llList2Integer(Indices, 0) ); + vector GridOffset2 = GetGridOffset( llList2Integer(Indices, 1) ); + vector GridOffset3 = GetGridOffset( llList2Integer(Indices, 2) ); + vector GridOffset4 = GetGridOffset( llList2Integer(Indices, 3) ); + vector GridOffset5 = GetGridOffset( llList2Integer(Indices, 4) ); + + // Use these grid positions to display the correct textures/offsets. + ShowChars(link,GridOffset1, GridOffset2, GridOffset3, GridOffset4, GridOffset5); + } + return length; +} + +integer ConvertIndex(integer index) { + // This converts from an ASCII based index to our indexing scheme. + if (index >= 32) // ' ' or higher + index -= 32; + else { // index < 32 + // Quick bounds check. + if (index > 15) + index = 15; + + index += 94; // extended characters + } + + return index; +} + + + +PassToRender(integer render,string message, integer bank) +{ +// float time; + integer extendedlen = 0; + integer link; + integer i = 0; + integer msgLen = llStringLength(message); + string TextToRender; + integer num_slaves=llGetListLength(gSlaveNames); + string slave_name; //avoids unnecessary casts, keeping it as a string + + + //get the bank offset and length + integer bank_offset=llList2Integer(gBankingData, (bank * BANK_STRIDE)); + integer bank_length=llList2Integer(gBankingData, (bank * BANK_STRIDE) + 1); + integer bank_highest_dirty=llList2Integer(gBankingData, (bank * BANK_STRIDE) + 2); + + integer x = 0; + for (;x < msgLen;x = x + 5) + { + + if (i >= bank_length) //we don't want to run off the end of the bank + { + //set the dirty to max, and bail out, we're done + gBankingData=llListReplaceList(gBankingData, (list)bank_length, (bank * BANK_STRIDE) + 2, (bank * BANK_STRIDE) + 2); + return; + } + + link = unpack(gXyTextPrims,(i + bank_offset)); + TextToRender = llGetSubString(message, x, x + 15); + + if(gSlaveRegistered && (link % (num_slaves + 1))) + { + slave_name=llList2String(gSlaveNames, (link % (num_slaves + 1)) - 1); + if (render == 1) + llMessageLinked(LINK_THIS, SLAVE_DISPLAY, TextToRender, (key)((string)link + "," + slave_name)); + if (render == 2) + { + //time = llGetAndResetTime(); + if(llSubStringIndex(TextToRender,"\e") > x) + extendedlen = 5; + else + extendedlen = RenderExtended(link,TextToRender,0); + if (extendedlen > 5) + { + x += extendedlen - 5; + } + llMessageLinked(LINK_THIS, SLAVE_DISPLAY_EXTENDED, TextToRender, (key)((string)link + "," + slave_name)); + // llOwnerSay((string)llGetAndResetTime()); + } + //sorry, no fade effect with slave + } + else + { + if (render == 1) + RenderString(link,TextToRender); + if (render == 2) + { + extendedlen = RenderExtended(link,TextToRender,1); + if (extendedlen > 5) + { + x += extendedlen - 5; + } + + } + if (render == 3) + RenderWithEffects(link,TextToRender); + } + ++i; + } + + if (bank_highest_dirty==0) + bank_highest_dirty=bank_length; + + integer current_highest_dirty=i; + while (i < bank_highest_dirty) + { + link = unpack(gXyTextPrims,(i + bank_offset)); + + if(gSlaveRegistered && (link % (num_slaves+1) != 0)) + { + slave_name=llList2String(gSlaveNames, (link % (num_slaves + 1)) - 1); + llMessageLinked(LINK_THIS, SLAVE_DISPLAY, " ", (key)((string)link + "," + slave_name)); + //sorry, no fade effect with slave + } + else + { + RenderString(link," "); + } + ++i; + } + gBankingData=llListReplaceList(gBankingData, (list)current_highest_dirty, (bank * BANK_STRIDE) + 2, (bank * BANK_STRIDE) + 2); +} + +// Bitwise Voodoo by Gigs Taggart and optimized by Strife Onizuka +list gXyTextPrims; + + +integer get_number_of_prims() +{//ignores avatars. + integer a = llGetNumberOfPrims(); + //Mono tweak + vector size = llGetAgentSize(llGetLinkKey(a)); + while(size.z > 0) + { + --a; + size = llGetAgentSize(llGetLinkKey(a)); + } + return a; +} + +//functions to pack 8-bit shorts into ints +list pack_and_insert(list in_list, integer pos, integer value) +{ +// //figure out the bitpack position +// integer pack = pos & 3; //4 bytes per int +// pos=pos >> 2; +// integer shifted = value << (pack << 3); +// integer old_value = llList2Integer(in_list, pos); +// shifted = old_value | shifted; +// in_list = llListReplaceList(in_list, (list)shifted, pos, pos); +// return in_list; + //Safe optimized version + integer index = pos >> 2; + return llListReplaceList(in_list, (list)(llList2Integer(in_list, index) | (value << ((pos & 3) << 3))), index, index); +} + +integer unpack(list in_list, integer pos) +{ + return (llList2Integer(in_list, pos >> 2) >> ((pos & 3) << 3)) & 0x000000FF;//unsigned +// return (llList2Integer(in_list, pos >> 2) << (((~pos) & 3) << 3)) >> 24;//signed +} + +change_color(vector color) +{ + integer num_prims=llGetListLength(gXyTextPrims) << 2; + + integer i = 0; + + for (; i<=num_prims; ++i) + { + integer link = unpack(gXyTextPrims,i); + if (!link) + return; + + llSetLinkPrimitiveParamsFast( link,[ + PRIM_COLOR, FACE_1, color, 1.0, + PRIM_COLOR, FACE_2, color, 1.0, + PRIM_COLOR, FACE_3, color, 1.0, + PRIM_COLOR, FACE_4, color, 1.0, + PRIM_COLOR, FACE_5, color, 1.0 + ]); + } +} + +change_line_color(integer bank, vector color) +{ + + //get the bank offset and length + integer i = llList2Integer(gBankingData, (bank * BANK_STRIDE)); + integer bank_end = i + llList2Integer(gBankingData, (bank * BANK_STRIDE) + 1); + + for (; i < bank_end; ++i) + { + integer link = unpack(gXyTextPrims,i); + if (!link) + return; + + llSetLinkPrimitiveParamsFast( link,[ + PRIM_COLOR, FACE_1, color, 1.0, + PRIM_COLOR, FACE_2, color, 1.0, + PRIM_COLOR, FACE_3, color, 1.0, + PRIM_COLOR, FACE_4, color, 1.0, + PRIM_COLOR, FACE_5, color, 1.0 + ]); + } +} + + +init() +{ + integer num_prims=get_number_of_prims(); + string link_name; + integer bank=0; + integer prims_pointer=0; //"pointer" to the next entry to be used in the gXyTextPrims list. + + list temp_bank = []; + integer temp_bank_stride=2; + + //FIXME: font texture might should be per-bank + llMessageLinked(LINK_THIS, SET_FONT_TEXTURE, "", gFontTexture); + + // moving this before the prim scan so that the slaves properly configure themseves before + // any requests to display + llMessageLinked(LINK_THIS, SLAVE_RESET, "" , null_key); + + gXyTextPrims=[]; + integer x=0; + for (;x<64;++x) + { + gXyTextPrims = (gXyTextPrims = []) + gXyTextPrims + 0; //we need to pad out the list to make it easier to add things in any order later + } + + gBankingData = []; + @loop; + { + //loop over all prims, looking for ones in the current bank + for(x=0;x<=num_prims;++x) + { + link_name=llGetLinkName(x); + list tmp = llParseString2List(link_name, (list)"-", []); + if(llList2String(tmp,0) == "xyzzytext") + { + if (llList2Integer(tmp,1) == bank) + { + temp_bank += llList2Integer(tmp,2) + (list)x; + } + } + + } + + if (temp_bank != []) + { + //sort the current bank + temp_bank = llListSort(temp_bank, temp_bank_stride, TRUE); + + integer temp_len = llGetListLength(temp_bank); + + //store metadata + gBankingData += [prims_pointer, temp_len/temp_bank_stride, 0]; + + //repack the bank into the prim list + for (x = 0; x < temp_len; x += temp_bank_stride) + { + gXyTextPrims = pack_and_insert(gXyTextPrims, prims_pointer, llList2Integer(temp_bank, x + 1)); + ++prims_pointer; + } + ++bank; + temp_bank=[]; + jump loop; + } + } + + //llOwnerSay((string)llGetFreeMemory()); +} + + +default { + state_entry() { + // Initialize the character index. + ResetCharIndex(); + init(); + } + + on_rez(integer num) + { + llResetScript(); + } + + link_message(integer sender, integer channel, string data, key id) { + if (id==null_key) + id="0"; + + if (channel == DISPLAY_STRING) { + PassToRender(1,data, (integer)((string)id)); + } + else if (channel == DISPLAY_EXTENDED) { + PassToRender(2,data, (integer)((string)id)); + } + else if (channel == REMAP_INDICES) { + // Parse the message, splitting it up into index values. + list Parsed = llCSV2List(data); + integer i = 0; + // Go through the list and swap each pair of indices. + for (; i < llGetListLength(Parsed); i += 2) { + integer Index1 = ConvertIndex( llList2Integer(Parsed, i) ); + integer Index2 = ConvertIndex( llList2Integer(Parsed, i + 1) ); + + // Swap these index values. + string Value1 = llGetSubString(gCharIndex, Index1, Index1); + string Value2 = llGetSubString(gCharIndex, Index2, Index2); + + gCharIndex = llDeleteSubString(gCharIndex, Index1, Index1); + gCharIndex = llInsertString(gCharIndex, Index1, Value2); + + gCharIndex = llDeleteSubString(gCharIndex, Index2, Index2); + gCharIndex = llInsertString(gCharIndex, Index2, Value1); + } + } + else if (channel == RESCAN_LINKSET) + { + init(); + } + else if (channel == RESET_INDICES) { + // Restore the character index back to default settings. + ResetCharIndex(); + } + else if (channel == SET_FADE_OPTIONS) { + // Change the channel we listen to for cell commands, the + // starting character position to extract from, and + // special effect attributes. + list Parsed = llCSV2List(data); + gCellUseFading = (integer) llList2String(Parsed, 0); + gCellHoldDelay = (float) llList2String(Parsed, 1); + } + else if (channel == SET_FONT_TEXTURE) { + // Use the new texture instead of the current one. + gFontTexture = id; + } + else if (channel == SET_COLOR) { + change_color((vector)data); + } + else if (channel == SET_LINE_COLOR) { + change_line_color((integer)((string)id), (vector)data); + } + else if (channel == REGISTER_SLAVE) + { + if(!~llListFindList(gSlaveNames, (list)data)) + {//isn't registered yet + gSlaveNames += data; + gSlaveRegistered=TRUE; + //llOwnerSay((string)llGetListLength(gSlaveNames) + " Slave(s) Recognized: " + data); + } +// else +// {//it already exists +// llOwnerSay((string)llGetListLength(gSlaveNames) + " Slave, Existing Slave Recognized: " + data); +// } + llMessageLinked(LINK_THIS, SLAVE_RECOGNIZED, data , null_key); + } + } + + + changed(integer change) + { + if(change&CHANGED_INVENTORY) + { + if(gSlaveRegistered) + { + //by using negative indexes they don't need to be adjusted when an entry is deleted. + integer x = ~llGetListLength(gSlaveNames); + while(++x) + { + if (!~llGetInventoryType(llList2String(gSlaveNames, x))) + { + //llOwnerSay("Slave Removed: " + llList2String(gSlaveNames, x)); + gSlaveNames = llDeleteSubList(gSlaveNames, x, x); + } + } + gSlaveRegistered = !(gSlaveNames == []); + } + } + } +} + diff --git a/xytext_master_clean.lsl b/xytext_master_clean.lsl new file mode 100644 index 0000000..5c1c9a6 --- /dev/null +++ b/xytext_master_clean.lsl @@ -0,0 +1,521 @@ +//////////////////////////////////////////// +// XyzzyText v2.1 Script (Set Line Color) by Huney Jewell +// Modified to listen directly to scoreboard channel +// +//////////////////////////////////////////// + +/////////////// CONSTANTS /////////////////// +// XyText Message Map. +integer DISPLAY_STRING = 204000; +integer DISPLAY_EXTENDED = 204001; +integer REMAP_INDICES = 204002; +integer RESET_INDICES = 204003; +integer SET_FADE_OPTIONS = 204004; +integer SET_FONT_TEXTURE = 204005; +integer SET_LINE_COLOR = 204006; +integer SET_COLOR = 204007; +integer RESCAN_LINKSET = 204008; + +//internal API +integer REGISTER_SLAVE = 205000; +integer SLAVE_RECOGNIZED = 205001; +integer SLAVE_DISPLAY = 205003; +integer SLAVE_DISPLAY_EXTENDED = 205004; +integer SLAVE_RESET = 205005; + +// Communication channel from scoreboard +integer LEADERBOARD_CHANNEL = -12346; + +// This is an extended character escape sequence. +string ESCAPE_SEQUENCE = "\\e"; + +// This is used to get an index for the extended character. +string EXTENDED_INDEX = "12345"; + +// Face numbers. +integer FACE_1 = 3; +integer FACE_2 = 7; +integer FACE_3 = 4; +integer FACE_4 = 6; +integer FACE_5 = 1; + +// Used to hide the text after a fade-out. +key TRANSPARENT = "701917a8-d614-471f-13dd-5f4644e36e3c"; +key null_key = NULL_KEY; +///////////// END CONSTANTS //////////////// + +///////////// GLOBAL VARIABLES /////////////// +// This is the key of the font we are displaying. +key gFontTexture = "b2e7394f-5e54-aa12-6e1c-ef327b6bed9e"; +// All displayable characters. Default to ASCII order. +string gCharIndex; + +// This is whether or not to use the fade in/out special effect. +integer gCellUseFading = FALSE; +// This is how long to display the text before fading out (if using +// fading special effect). +// Note: < 0 means don't fade out. +float gCellHoldDelay = 1.0; + +integer gSlaveRegistered; +list gSlaveNames; + +integer BANK_STRIDE=3; //offset, length, highest_dirty +list gBankingData; + +list gXyTextPrims; //rather than make it generic, we just reuse this from the original script + +////////////////////////////////////////////////////////////// +// *** IMPORTANT: SET THIS FOR EACH XYZZYTEXT OBJECT *** +// +// Bank 0 = LEFT display (set MY_BANK = 0;) +// Bank 1 = MIDDLE display (set MY_BANK = 1;) +// Bank 2 = RIGHT display (set MY_BANK = 2;) +// +////////////////////////////////////////////////////////////// +integer MY_BANK = 1; // ← CHANGE THIS NUMBER FOR EACH OBJECT! + +/////////// END GLOBAL VARIABLES //////////// + +ResetCharIndex() { + gCharIndex = " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`"; + gCharIndex += "abcdefghijklmnopqrstuvwxyz{|}~"; + gCharIndex += "\\n\\n\\n\\n\\n"; +} + +vector GetGridOffset(integer index) { + // Calculate the offset needed to display this character. + integer Row = index / 10; + integer Col = index % 10; + + // Return the offset in the texture. + return <-0.45 + 0.1 * Col, 0.45 - 0.1 * Row, 0.0>; +} + +ShowChars(integer link,vector grid_offset1, vector grid_offset2, vector grid_offset3, vector grid_offset4, vector grid_offset5) { + // Set the primitive textures directly. + + llSetLinkPrimitiveParamsFast( link,[ + PRIM_TEXTURE, FACE_1, (string)gFontTexture, <0.126, 0.1, 0>, grid_offset1 + <0.037, 0, 0>, 0.0, + PRIM_TEXTURE, FACE_2, (string)gFontTexture, <0.05, 0.1, 0>, grid_offset2, 0.0, + PRIM_TEXTURE, FACE_3, (string)gFontTexture, <-0.74, 0.1, 0>, grid_offset3 - <0.244, 0, 0>, 0.0, + PRIM_TEXTURE, FACE_4, (string)gFontTexture, <0.05, 0.1, 0>, grid_offset4, 0.0, + PRIM_TEXTURE, FACE_5, (string)gFontTexture, <0.126, 0.1, 0>, grid_offset5 - <0.037, 0, 0>, 0.0 + ]); +} + +RenderString(integer link, string str) { + // Get the grid positions for each pair of characters. + vector GridOffset1 = GetGridOffset( llSubStringIndex(gCharIndex, llGetSubString(str, 0, 0)) ); + vector GridOffset2 = GetGridOffset( llSubStringIndex(gCharIndex, llGetSubString(str, 1, 1)) ); + vector GridOffset3 = GetGridOffset( llSubStringIndex(gCharIndex, llGetSubString(str, 2, 2)) ); + vector GridOffset4 = GetGridOffset( llSubStringIndex(gCharIndex, llGetSubString(str, 3, 3)) ); + vector GridOffset5 = GetGridOffset( llSubStringIndex(gCharIndex, llGetSubString(str, 4, 4)) ); + + // Use these grid positions to display the correct textures/offsets. + ShowChars(link,GridOffset1, GridOffset2, GridOffset3, GridOffset4, GridOffset5); +} + +RenderWithEffects(integer link, string str) { + // Get the grid positions for each pair of characters. + vector GridOffset1 = GetGridOffset( llSubStringIndex(gCharIndex, llGetSubString(str, 0, 0)) ); + vector GridOffset2 = GetGridOffset( llSubStringIndex(gCharIndex, llGetSubString(str, 1, 1)) ); + vector GridOffset3 = GetGridOffset( llSubStringIndex(gCharIndex, llGetSubString(str, 2, 2)) ); + vector GridOffset4 = GetGridOffset( llSubStringIndex(gCharIndex, llGetSubString(str, 3, 3)) ); + vector GridOffset5 = GetGridOffset( llSubStringIndex(gCharIndex, llGetSubString(str, 4, 4)) ); + + // First set the alpha to the lowest possible. + llSetLinkAlpha(link,0.05, ALL_SIDES); + + // Use these grid positions to display the correct textures/offsets. + ShowChars(link,GridOffset1, GridOffset2, GridOffset3, GridOffset4, GridOffset5); // Now turn up the alpha until it is at full strength. + float Alpha = 0.10; + for (; Alpha <= 1.0; Alpha += 0.05) + llSetLinkAlpha(link,Alpha, ALL_SIDES); + // See if we want to fade out as well. + if (gCellHoldDelay < 0.0) + // No, bail out. (Just keep showing the string at full strength). + return; + // Hold the text for a while. + llSleep(gCellHoldDelay); + // Now fade out. + for (Alpha = 0.95; Alpha >= 0.05; Alpha -= 0.05) + llSetLinkAlpha(link,Alpha, ALL_SIDES); + // Make the text transparent to fully hide it. + llSetLinkTexture(link,TRANSPARENT, ALL_SIDES); +} + +integer RenderExtended(integer link, string str, integer render) { + // Look for escape sequences. + integer length = 0; + list Parsed = llParseString2List(str, [], (list)ESCAPE_SEQUENCE); + integer ParsedLen = llGetListLength(Parsed); + + // Create a list of index values to work with. + list Indices; + // We start with room for 5 indices. + integer IndicesLeft = 5; + + string Token; + integer Clipped; + integer LastWasEscapeSequence = FALSE; + // Work from left to right. + integer i = 0; + for (; i < ParsedLen && IndicesLeft > 0; ++i) { + Token = llList2String(Parsed, i); + + // If this is an escape sequence, just set the flag and move on. + if (Token == ESCAPE_SEQUENCE) { + LastWasEscapeSequence = TRUE; + } + else { // Token != ESCAPE_SEQUENCE + // Otherwise this is a normal token. Check its length. + Clipped = FALSE; + integer TokenLength = llStringLength(Token); + // Clip if necessary. + if (TokenLength > IndicesLeft) { + TokenLength = llStringLength(Token = llGetSubString(Token, 0, IndicesLeft - 1)); + IndicesLeft = 0; + Clipped = TRUE; + } + else + IndicesLeft -= TokenLength; + + // Was the previous token an escape sequence? + if (LastWasEscapeSequence) { + // Yes, the first character is an escape character, the rest are normal. + length += 2 + TokenLength; + if(render) + { + // This is the extended character. + Indices += (llSubStringIndex(EXTENDED_INDEX, llGetSubString(Token, 0, 0)) + 95); + + // These are the normal characters. + integer j = 1; + for(; j < TokenLength; ++j) { + Indices += llSubStringIndex(gCharIndex, llGetSubString(Token, j, j)); + } + } + LastWasEscapeSequence = FALSE; + } + else { + // Nope, just normal characters. + length += TokenLength; + if(render) + { + // Just add the characters normally. + integer j = 0; + for(; j < TokenLength; ++j) { + Indices += llSubStringIndex(gCharIndex, llGetSubString(Token, j, j)); + } + } + } + + // Bail out if this was a clipped token. + if (Clipped) + i = ParsedLen; + } + } + + if(render) { + // Make sure we have 5 indices. + integer IndicesCount = llGetListLength(Indices); + for (; IndicesCount < 5; ++IndicesCount) { + Indices += (llSubStringIndex(gCharIndex, " ")); + } + + // Use the indices to create grid positions. + vector GridOffset1 = GetGridOffset( llList2Integer(Indices, 0) ); + vector GridOffset2 = GetGridOffset( llList2Integer(Indices, 1) ); + vector GridOffset3 = GetGridOffset( llList2Integer(Indices, 2) ); + vector GridOffset4 = GetGridOffset( llList2Integer(Indices, 3) ); + vector GridOffset5 = GetGridOffset( llList2Integer(Indices, 4) ); + + // Use these grid positions to display the correct textures/offsets. + ShowChars(link,GridOffset1, GridOffset2, GridOffset3, GridOffset4, GridOffset5); + } + return length; +} + +integer ConvertIndex(integer index) { + // This function is used to convert from an ASCII based index to our + // internal representation. + if (index >= 32) + // ASCII character, subtract 32. + return index - 32; + else if (index == 10) + // Newline character. + return 94; + else + // Everything else is a space. + return 0; +} + +PassToRender(integer render_type, string message, integer bank) { + if(bank != MY_BANK) { + // This message is not for our bank, ignore it + return; + } + + list lines = llParseString2List(message, ["\n"], []); + integer line_count = llGetListLength(lines); + + //get the bank offset and length + integer i = llList2Integer(gBankingData, (bank * BANK_STRIDE)); + integer bank_end = i + llList2Integer(gBankingData, (bank * BANK_STRIDE) + 1); + + for(; i < bank_end && (i - llList2Integer(gBankingData, (bank * BANK_STRIDE))) < line_count; ++i) { + integer link = unpack(gXyTextPrims, i); + if(link) { + string line = llList2String(lines, i - llList2Integer(gBankingData, (bank * BANK_STRIDE))); + + if(render_type == 1) { + RenderString(link, line); + } else if(render_type == 2) { + RenderExtended(link, line, TRUE); + } + } + } +} + +integer get_number_of_prims() +{//ignores avatars. + integer a = llGetNumberOfPrims(); + //Mono tweak + vector size = llGetAgentSize(llGetLinkKey(a)); + while(size.z > 0) + { + --a; + size = llGetAgentSize(llGetLinkKey(a)); + } + return a; +} + +//functions to pack 8-bit shorts into ints +list pack_and_insert(list in_list, integer pos, integer value) +{ + //Safe optimized version + integer index = pos >> 2; + return llListReplaceList(in_list, (list)(llList2Integer(in_list, index) | (value << ((pos & 3) << 3))), index, index); +} + +integer unpack(list in_list, integer pos) +{ + return (llList2Integer(in_list, pos >> 2) >> ((pos & 3) << 3)) & 0x000000FF;//unsigned +} + +change_color(vector color) +{ + integer num_prims=llGetListLength(gXyTextPrims) << 2; + + integer i = 0; + + for (; i<=num_prims; ++i) + { + integer link = unpack(gXyTextPrims,i); + if (!link) + return; + + llSetLinkPrimitiveParamsFast( link,[ + PRIM_COLOR, FACE_1, color, 1.0, + PRIM_COLOR, FACE_2, color, 1.0, + PRIM_COLOR, FACE_3, color, 1.0, + PRIM_COLOR, FACE_4, color, 1.0, + PRIM_COLOR, FACE_5, color, 1.0 + ]); + } +} + +change_line_color(integer bank, vector color) +{ + + //get the bank offset and length + integer i = llList2Integer(gBankingData, (bank * BANK_STRIDE)); + integer bank_end = i + llList2Integer(gBankingData, (bank * BANK_STRIDE) + 1); + + for (; i < bank_end; ++i) + { + integer link = unpack(gXyTextPrims,i); + if (!link) + return; + + llSetLinkPrimitiveParamsFast( link,[ + PRIM_COLOR, FACE_1, color, 1.0, + PRIM_COLOR, FACE_2, color, 1.0, + PRIM_COLOR, FACE_3, color, 1.0, + PRIM_COLOR, FACE_4, color, 1.0, + PRIM_COLOR, FACE_5, color, 1.0 + ]); + } +} + + +init() +{ + integer num_prims=get_number_of_prims(); + string link_name; + integer prims_pointer=0; + + list temp_bank = []; + integer temp_bank_stride=2; + + //FIXME: font texture might should be per-bank + llMessageLinked(LINK_THIS, SET_FONT_TEXTURE, "", gFontTexture); + + // moving this before the prim scan so that the slaves properly configure themseves before + // any requests to display + llMessageLinked(LINK_THIS, SLAVE_RESET, "" , null_key); + + gXyTextPrims=[]; + integer x=0; + for (;x<64;++x) + { + gXyTextPrims = (gXyTextPrims = []) + gXyTextPrims + 0; //we need to pad out the list to make it easier to add things in any order later + } + + gBankingData = []; + + // Only look for prims that match MY_BANK + for(x=0;x<=num_prims;++x) + { + link_name=llGetLinkName(x); + + list tmp = llParseString2List(link_name, (list)"-", []); + if(llList2String(tmp,0) == "xyzzytext") + { + integer prim_bank = llList2Integer(tmp,1); + integer prim_position = llList2Integer(tmp,2); + + if (prim_bank == MY_BANK) + { + temp_bank += prim_position + (list)x; + } + } + } + + if (temp_bank != []) + { + //sort the current bank by position + temp_bank = llListSort(temp_bank, temp_bank_stride, TRUE); + + integer temp_len = llGetListLength(temp_bank); + + //store metadata for MY_BANK at position MY_BANK in the banking data + while(llGetListLength(gBankingData) <= (MY_BANK * BANK_STRIDE + 2)) { + gBankingData += [0, 0, 0]; // pad the list + } + + gBankingData = llListReplaceList(gBankingData, [prims_pointer, temp_len/temp_bank_stride, 0], + MY_BANK * BANK_STRIDE, MY_BANK * BANK_STRIDE + 2); + + //repack the bank into the prim list + for (x = 0; x < temp_len; x += temp_bank_stride) + { + integer link_num = llList2Integer(temp_bank, x + 1); + gXyTextPrims = pack_and_insert(gXyTextPrims, prims_pointer, link_num); + ++prims_pointer; + } + } + + llOwnerSay("XyzzyText Bank " + (string)MY_BANK + " ready with " + (string)prims_pointer + " prims"); +} + + +default { + state_entry() { + // Initialize the character index. + ResetCharIndex(); + + // Listen for messages from scoreboard + llListen(LEADERBOARD_CHANNEL, "", "", ""); + + init(); + } + + on_rez(integer num) + { + llResetScript(); + } + + listen(integer channel, string name, key id, string message) { + if (channel == LEADERBOARD_CHANNEL) { + if (MY_BANK == 0 && llSubStringIndex(message, "LEFT_TEXT|") == 0) { + // This is bank 0 (left), process LEFT_TEXT messages + string leftText = llGetSubString(message, 10, -1); + PassToRender(1, leftText, 0); + + } else if (MY_BANK == 1 && llSubStringIndex(message, "MIDDLE_TEXT|") == 0) { + // This is bank 1 (middle), process MIDDLE_TEXT messages + string middleText = llGetSubString(message, 12, -1); + PassToRender(1, middleText, 1); + + } else if (MY_BANK == 2 && llSubStringIndex(message, "RIGHT_TEXT|") == 0) { + // This is bank 2 (right), process RIGHT_TEXT messages + string rightText = llGetSubString(message, 11, -1); + PassToRender(1, rightText, 2); + + } else if (message == "CLEAR_LEADERBOARD") { + // Clear display for this bank + PassToRender(1, " ", MY_BANK); + } + } + } + + link_message(integer sender, integer channel, string data, key id) { + if (id==null_key) + id="0"; + + if (channel == DISPLAY_STRING) { + PassToRender(1,data, (integer)((string)id)); + } + else if (channel == DISPLAY_EXTENDED) { + PassToRender(2,data, (integer)((string)id)); + } + else if (channel == REMAP_INDICES) { + // Parse the message, splitting it up into index values. + list Parsed = llCSV2List(data); + integer i = 0; + // Go through the list and swap each pair of indices. + for (; i < llGetListLength(Parsed); i += 2) { + integer Index1 = ConvertIndex( llList2Integer(Parsed, i) ); + integer Index2 = ConvertIndex( llList2Integer(Parsed, i + 1) ); + + // Swap these index values. + string Value1 = llGetSubString(gCharIndex, Index1, Index1); + string Value2 = llGetSubString(gCharIndex, Index2, Index2); + + gCharIndex = llDeleteSubString(gCharIndex, Index1, Index1); + gCharIndex = llInsertString(gCharIndex, Index1, Value2); + + gCharIndex = llDeleteSubString(gCharIndex, Index2, Index2); + gCharIndex = llInsertString(gCharIndex, Index2, Value1); + } + } + else if (channel == RESCAN_LINKSET) + { + init(); + } + else if (channel == RESET_INDICES) { + // Restore the character index back to default settings. + ResetCharIndex(); + } + else if (channel == SET_FADE_OPTIONS) { + list Parsed = llCSV2List(data); + gCellUseFading = llList2Integer(Parsed, 0); + gCellHoldDelay = llList2Float(Parsed, 1); + } + else if (channel == SET_FONT_TEXTURE) { + gFontTexture = id; + } + else if (channel == SET_LINE_COLOR) { + change_line_color((integer)data, (vector)((string)id)); + } + else if (channel == SET_COLOR) { + change_color((vector)data); + } + else if (channel == REGISTER_SLAVE) { + gSlaveNames += data; + llMessageLinked(sender, SLAVE_RECOGNIZED, "", ""); + } + } +} diff --git a/xytext_master_with_listener.lsl b/xytext_master_with_listener.lsl new file mode 100644 index 0000000..b682774 --- /dev/null +++ b/xytext_master_with_listener.lsl @@ -0,0 +1,540 @@ +//////////////////////////////////////////// +// XyzzyText v2.1 Script (Set Line Color) by Huney Jewell +// Modified to listen directly to scoreboard channel +// +//////////////////////////////////////////// + +/////////////// CONSTANTS /////////////////// +// XyText Message Map. +integer DISPLAY_STRING = 204000; +integer DISPLAY_EXTENDED = 204001; +integer REMAP_INDICES = 204002; +integer RESET_INDICES = 204003; +integer SET_FADE_OPTIONS = 204004; +integer SET_FONT_TEXTURE = 204005; +integer SET_LINE_COLOR = 204006; +integer SET_COLOR = 204007; +integer RESCAN_LINKSET = 204008; + +//internal API +integer REGISTER_SLAVE = 205000; +integer SLAVE_RECOGNIZED = 205001; +integer SLAVE_DISPLAY = 205003; +integer SLAVE_DISPLAY_EXTENDED = 205004; +integer SLAVE_RESET = 205005; + +// Communication channel from scoreboard +integer LEADERBOARD_CHANNEL = -12346; + +// This is an extended character escape sequence. +string ESCAPE_SEQUENCE = "\\e"; + +// This is used to get an index for the extended character. +string EXTENDED_INDEX = "12345"; + +// Face numbers. +integer FACE_1 = 3; +integer FACE_2 = 7; +integer FACE_3 = 4; +integer FACE_4 = 6; +integer FACE_5 = 1; + +// Used to hide the text after a fade-out. +key TRANSPARENT = "701917a8-d614-471f-13dd-5f4644e36e3c"; +key null_key = NULL_KEY; +///////////// END CONSTANTS //////////////// + +///////////// GLOBAL VARIABLES /////////////// +// This is the key of the font we are displaying. +key gFontTexture = "b2e7394f-5e54-aa12-6e1c-ef327b6bed9e"; +// All displayable characters. Default to ASCII order. +string gCharIndex; + +// This is whether or not to use the fade in/out special effect. +integer gCellUseFading = FALSE; +// This is how long to display the text before fading out (if using +// fading special effect). +// Note: < 0 means don't fade out. +float gCellHoldDelay = 1.0; + +integer gSlaveRegistered; +list gSlaveNames; + +integer BANK_STRIDE=3; //offset, length, highest_dirty +list gBankingData; + +list gXyTextPrims; //rather than make it generic, we just reuse this from the original script + +// Bank configuration - SET THIS FOR EACH OBJECT +integer MY_BANK = 1; // Change this: 0 for left, 1 for middle, 2 for right + +/////////// END GLOBAL VARIABLES //////////// + +ResetCharIndex() { + gCharIndex = " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`"; + gCharIndex += "abcdefghijklmnopqrstuvwxyz{|}~"; + gCharIndex += "\\n\\n\\n\\n\\n"; +} + +vector GetGridOffset(integer index) { + // Calculate the offset needed to display this character. + integer Row = index / 10; + integer Col = index % 10; + + // Return the offset in the texture. + return <-0.45 + 0.1 * Col, 0.45 - 0.1 * Row, 0.0>; +} + +ShowChars(integer link,vector grid_offset1, vector grid_offset2, vector grid_offset3, vector grid_offset4, vector grid_offset5) { + // Set the primitive textures directly. + + // <-0.256, 0, 0> + // <0, 0, 0> + // <0.130, 0, 0> + // <0, 0, 0> + // <-0.74, 0, 0> + + llSetLinkPrimitiveParamsFast( link,[ + PRIM_TEXTURE, FACE_1, (string)gFontTexture, <0.126, 0.1, 0>, grid_offset1 + <0.037, 0, 0>, 0.0, + PRIM_TEXTURE, FACE_2, (string)gFontTexture, <0.05, 0.1, 0>, grid_offset2, 0.0, + PRIM_TEXTURE, FACE_3, (string)gFontTexture, <-0.74, 0.1, 0>, grid_offset3 - <0.244, 0, 0>, 0.0, + PRIM_TEXTURE, FACE_4, (string)gFontTexture, <0.05, 0.1, 0>, grid_offset4, 0.0, + PRIM_TEXTURE, FACE_5, (string)gFontTexture, <0.126, 0.1, 0>, grid_offset5 - <0.037, 0, 0>, 0.0 + ]); +} + +RenderString(integer link, string str) { + // Get the grid positions for each pair of characters. + vector GridOffset1 = GetGridOffset( llSubStringIndex(gCharIndex, llGetSubString(str, 0, 0)) ); + vector GridOffset2 = GetGridOffset( llSubStringIndex(gCharIndex, llGetSubString(str, 1, 1)) ); + vector GridOffset3 = GetGridOffset( llSubStringIndex(gCharIndex, llGetSubString(str, 2, 2)) ); + vector GridOffset4 = GetGridOffset( llSubStringIndex(gCharIndex, llGetSubString(str, 3, 3)) ); + vector GridOffset5 = GetGridOffset( llSubStringIndex(gCharIndex, llGetSubString(str, 4, 4)) ); + + // Use these grid positions to display the correct textures/offsets. + ShowChars(link,GridOffset1, GridOffset2, GridOffset3, GridOffset4, GridOffset5); +} + +RenderWithEffects(integer link, string str) { + // Get the grid positions for each pair of characters. + vector GridOffset1 = GetGridOffset( llSubStringIndex(gCharIndex, llGetSubString(str, 0, 0)) ); + vector GridOffset2 = GetGridOffset( llSubStringIndex(gCharIndex, llGetSubString(str, 1, 1)) ); + vector GridOffset3 = GetGridOffset( llSubStringIndex(gCharIndex, llGetSubString(str, 2, 2)) ); + vector GridOffset4 = GetGridOffset( llSubStringIndex(gCharIndex, llGetSubString(str, 3, 3)) ); + vector GridOffset5 = GetGridOffset( llSubStringIndex(gCharIndex, llGetSubString(str, 4, 4)) ); + + // First set the alpha to the lowest possible. + llSetLinkAlpha(link,0.05, ALL_SIDES); + + // Use these grid positions to display the correct textures/offsets. + ShowChars(link,GridOffset1, GridOffset2, GridOffset3, GridOffset4, GridOffset5); // Now turn up the alpha until it is at full strength. + float Alpha = 0.10; + for (; Alpha <= 1.0; Alpha += 0.05) + llSetLinkAlpha(link,Alpha, ALL_SIDES); + // See if we want to fade out as well. + if (gCellHoldDelay < 0.0) + // No, bail out. (Just keep showing the string at full strength). + return; + // Hold the text for a while. + llSleep(gCellHoldDelay); + // Now fade out. + for (Alpha = 0.95; Alpha >= 0.05; Alpha -= 0.05) + llSetLinkAlpha(link,Alpha, ALL_SIDES); + // Make the text transparent to fully hide it. + llSetLinkTexture(link,TRANSPARENT, ALL_SIDES); +} + +integer RenderExtended(integer link, string str, integer render) { + // Look for escape sequences. + integer length = 0; + list Parsed = llParseString2List(str, [], (list)ESCAPE_SEQUENCE); + integer ParsedLen = llGetListLength(Parsed); + + // Create a list of index values to work with. + list Indices; + // We start with room for 5 indices. + integer IndicesLeft = 5; + + string Token; + integer Clipped; + integer LastWasEscapeSequence = FALSE; + // Work from left to right. + integer i = 0; + for (; i < ParsedLen && IndicesLeft > 0; ++i) { + Token = llList2String(Parsed, i); + + // If this is an escape sequence, just set the flag and move on. + if (Token == ESCAPE_SEQUENCE) { + LastWasEscapeSequence = TRUE; + } + else { // Token != ESCAPE_SEQUENCE + // Otherwise this is a normal token. Check its length. + Clipped = FALSE; + integer TokenLength = llStringLength(Token); + // Clip if necessary. + if (TokenLength > IndicesLeft) { + TokenLength = llStringLength(Token = llGetSubString(Token, 0, IndicesLeft - 1)); + IndicesLeft = 0; + Clipped = TRUE; + } + else + IndicesLeft -= TokenLength; + + // Was the previous token an escape sequence? + if (LastWasEscapeSequence) { + // Yes, the first character is an escape character, the rest are normal. + length += 2 + TokenLength; + if(render) + { + // This is the extended character. + Indices += (llSubStringIndex(EXTENDED_INDEX, llGetSubString(Token, 0, 0)) + 95); + + // These are the normal characters. + integer j = 1; + for(; j < TokenLength; ++j) { + Indices += llSubStringIndex(gCharIndex, llGetSubString(Token, j, j)); + } + } + LastWasEscapeSequence = FALSE; + } + else { + // Nope, just normal characters. + length += TokenLength; + if(render) + { + // Just add the characters normally. + integer j = 0; + for(; j < TokenLength; ++j) { + Indices += llSubStringIndex(gCharIndex, llGetSubString(Token, j, j)); + } + } + } + + // Bail out if this was a clipped token. + if (Clipped) + i = ParsedLen; + } + } + + if(render) { + // Make sure we have 5 indices. + integer IndicesCount = llGetListLength(Indices); + for (; IndicesCount < 5; ++IndicesCount) { + Indices += (llSubStringIndex(gCharIndex, " ")); + } + + // Use the indices to create grid positions. + vector GridOffset1 = GetGridOffset( llList2Integer(Indices, 0) ); + vector GridOffset2 = GetGridOffset( llList2Integer(Indices, 1) ); + vector GridOffset3 = GetGridOffset( llList2Integer(Indices, 2) ); + vector GridOffset4 = GetGridOffset( llList2Integer(Indices, 3) ); + vector GridOffset5 = GetGridOffset( llList2Integer(Indices, 4) ); + + // Use these grid positions to display the correct textures/offsets. + ShowChars(link,GridOffset1, GridOffset2, GridOffset3, GridOffset4, GridOffset5); + } + return length; +} + +integer ConvertIndex(integer index) { + // This function is used to convert from an ASCII based index to our + // internal representation. + if (index >= 32) + // ASCII character, subtract 32. + return index - 32; + else if (index == 10) + // Newline character. + return 94; + else + // Everything else is a space. + return 0; +} + +PassToRender(integer render_type, string message, integer bank) { + if(bank != MY_BANK) { + // This message is not for our bank, ignore it + return; + } + + llOwnerSay("DEBUG: Bank " + (string)MY_BANK + " received message: " + message); + + list lines = llParseString2List(message, ["\n"], []); + integer line_count = llGetListLength(lines); + + //get the bank offset and length + integer i = llList2Integer(gBankingData, (bank * BANK_STRIDE)); + integer bank_end = i + llList2Integer(gBankingData, (bank * BANK_STRIDE) + 1); + + for(; i < bank_end && (i - llList2Integer(gBankingData, (bank * BANK_STRIDE))) < line_count; ++i) { + integer link = unpack(gXyTextPrims, i); + if(link) { + string line = llList2String(lines, i - llList2Integer(gBankingData, (bank * BANK_STRIDE))); + + if(render_type == 1) { + RenderString(link, line); + } else if(render_type == 2) { + RenderExtended(link, line, TRUE); + } + } + } +} + +integer get_number_of_prims() +{//ignores avatars. + integer a = llGetNumberOfPrims(); + //Mono tweak + vector size = llGetAgentSize(llGetLinkKey(a)); + while(size.z > 0) + { + --a; + size = llGetAgentSize(llGetLinkKey(a)); + } + return a; +} + +//functions to pack 8-bit shorts into ints +list pack_and_insert(list in_list, integer pos, integer value) +{ + //Safe optimized version + integer index = pos >> 2; + return llListReplaceList(in_list, (list)(llList2Integer(in_list, index) | (value << ((pos & 3) << 3))), index, index); +} + +integer unpack(list in_list, integer pos) +{ + return (llList2Integer(in_list, pos >> 2) >> ((pos & 3) << 3)) & 0x000000FF;//unsigned +} + +change_color(vector color) +{ + integer num_prims=llGetListLength(gXyTextPrims) << 2; + + integer i = 0; + + for (; i<=num_prims; ++i) + { + integer link = unpack(gXyTextPrims,i); + if (!link) + return; + + llSetLinkPrimitiveParamsFast( link,[ + PRIM_COLOR, FACE_1, color, 1.0, + PRIM_COLOR, FACE_2, color, 1.0, + PRIM_COLOR, FACE_3, color, 1.0, + PRIM_COLOR, FACE_4, color, 1.0, + PRIM_COLOR, FACE_5, color, 1.0 + ]); + } +} + +change_line_color(integer bank, vector color) +{ + + //get the bank offset and length + integer i = llList2Integer(gBankingData, (bank * BANK_STRIDE)); + integer bank_end = i + llList2Integer(gBankingData, (bank * BANK_STRIDE) + 1); + + for (; i < bank_end; ++i) + { + integer link = unpack(gXyTextPrims,i); + if (!link) + return; + + llSetLinkPrimitiveParamsFast( link,[ + PRIM_COLOR, FACE_1, color, 1.0, + PRIM_COLOR, FACE_2, color, 1.0, + PRIM_COLOR, FACE_3, color, 1.0, + PRIM_COLOR, FACE_4, color, 1.0, + PRIM_COLOR, FACE_5, color, 1.0 + ]); + } +} + + +init() +{ + integer num_prims=get_number_of_prims(); + string link_name; + integer prims_pointer=0; + + list temp_bank = []; + integer temp_bank_stride=2; + + //FIXME: font texture might should be per-bank + llMessageLinked(LINK_THIS, SET_FONT_TEXTURE, "", gFontTexture); + + // moving this before the prim scan so that the slaves properly configure themseves before + // any requests to display + llMessageLinked(LINK_THIS, SLAVE_RESET, "" , null_key); + + gXyTextPrims=[]; + integer x=0; + for (;x<64;++x) + { + gXyTextPrims = (gXyTextPrims = []) + gXyTextPrims + 0; //we need to pad out the list to make it easier to add things in any order later + } + + gBankingData = []; + + // Only look for prims that match MY_BANK + llOwnerSay("DEBUG: Scanning " + (string)num_prims + " prims for bank " + (string)MY_BANK); + + for(x=0;x<=num_prims;++x) + { + link_name=llGetLinkName(x); + llOwnerSay("DEBUG: Prim " + (string)x + " name: '" + link_name + "'"); + + list tmp = llParseString2List(link_name, (list)"-", []); + if(llList2String(tmp,0) == "xyzzytext") + { + integer prim_bank = llList2Integer(tmp,1); + integer prim_position = llList2Integer(tmp,2); + + llOwnerSay("DEBUG: Found xyzzytext prim - bank: " + (string)prim_bank + ", position: " + (string)prim_position); + + if (prim_bank == MY_BANK) + { + llOwnerSay("DEBUG: Adding prim " + (string)x + " to bank " + (string)MY_BANK); + temp_bank += prim_position + (list)x; + } + } + } + + if (temp_bank != []) + { + llOwnerSay("DEBUG: Found " + (string)(llGetListLength(temp_bank)/temp_bank_stride) + " prims for bank " + (string)MY_BANK); + + //sort the current bank by position + temp_bank = llListSort(temp_bank, temp_bank_stride, TRUE); + + integer temp_len = llGetListLength(temp_bank); + + //store metadata for MY_BANK at position MY_BANK in the banking data + while(llGetListLength(gBankingData) <= (MY_BANK * BANK_STRIDE + 2)) { + gBankingData += [0, 0, 0]; // pad the list + } + + gBankingData = llListReplaceList(gBankingData, [prims_pointer, temp_len/temp_bank_stride, 0], + MY_BANK * BANK_STRIDE, MY_BANK * BANK_STRIDE + 2); + + //repack the bank into the prim list + for (x = 0; x < temp_len; x += temp_bank_stride) + { + integer link_num = llList2Integer(temp_bank, x + 1); + llOwnerSay("DEBUG: Packing prim " + (string)link_num + " at position " + (string)prims_pointer); + gXyTextPrims = pack_and_insert(gXyTextPrims, prims_pointer, link_num); + ++prims_pointer; + } + } else { + llOwnerSay("DEBUG: No prims found for bank " + (string)MY_BANK); + } + + llOwnerSay("XyzzyText initialized for bank " + (string)MY_BANK + " with " + (string)prims_pointer + " prims"); +} + + +default { + state_entry() { + // Initialize the character index. + ResetCharIndex(); + + // Listen for messages from scoreboard + llListen(LEADERBOARD_CHANNEL, "", "", ""); + llOwnerSay("XyzzyText Bank " + (string)MY_BANK + " ready - listening on channel " + (string)LEADERBOARD_CHANNEL); + + init(); + } + + on_rez(integer num) + { + llResetScript(); + } + + listen(integer channel, string name, key id, string message) { + if (channel == LEADERBOARD_CHANNEL) { + llOwnerSay("DEBUG: Bank " + (string)MY_BANK + " heard: " + message); + + if (MY_BANK == 0 && llSubStringIndex(message, "LEFT_TEXT|") == 0) { + // This is bank 0 (left), process LEFT_TEXT messages + string leftText = llGetSubString(message, 10, -1); + llOwnerSay("DEBUG: Bank 0 processing LEFT text: " + leftText); + PassToRender(1, leftText, 0); + + } else if (MY_BANK == 1 && llSubStringIndex(message, "MIDDLE_TEXT|") == 0) { + // This is bank 1 (middle), process MIDDLE_TEXT messages + string middleText = llGetSubString(message, 12, -1); + llOwnerSay("DEBUG: Bank 1 processing MIDDLE text: " + middleText); + PassToRender(1, middleText, 1); + + } else if (MY_BANK == 2 && llSubStringIndex(message, "RIGHT_TEXT|") == 0) { + // This is bank 2 (right), process RIGHT_TEXT messages + string rightText = llGetSubString(message, 11, -1); + llOwnerSay("DEBUG: Bank 2 processing RIGHT text: " + rightText); + PassToRender(1, rightText, 2); + + } else if (message == "CLEAR_LEADERBOARD") { + // Clear display for this bank + llOwnerSay("DEBUG: Bank " + (string)MY_BANK + " clearing display"); + PassToRender(1, " ", MY_BANK); + } + } + } + + link_message(integer sender, integer channel, string data, key id) { + if (id==null_key) + id="0"; + + if (channel == DISPLAY_STRING) { + PassToRender(1,data, (integer)((string)id)); + } + else if (channel == DISPLAY_EXTENDED) { + PassToRender(2,data, (integer)((string)id)); + } + else if (channel == REMAP_INDICES) { + // Parse the message, splitting it up into index values. + list Parsed = llCSV2List(data); + integer i = 0; + // Go through the list and swap each pair of indices. + for (; i < llGetListLength(Parsed); i += 2) { + integer Index1 = ConvertIndex( llList2Integer(Parsed, i) ); + integer Index2 = ConvertIndex( llList2Integer(Parsed, i + 1) ); + + // Swap these index values. + string Value1 = llGetSubString(gCharIndex, Index1, Index1); + string Value2 = llGetSubString(gCharIndex, Index2, Index2); + + gCharIndex = llDeleteSubString(gCharIndex, Index1, Index1); + gCharIndex = llInsertString(gCharIndex, Index1, Value2); + + gCharIndex = llDeleteSubString(gCharIndex, Index2, Index2); + gCharIndex = llInsertString(gCharIndex, Index2, Value1); + } + } + else if (channel == RESCAN_LINKSET) + { + init(); + } + else if (channel == RESET_INDICES) { + // Restore the character index back to default settings. + ResetCharIndex(); + } + else if (channel == SET_FADE_OPTIONS) { + list Parsed = llCSV2List(data); + gCellUseFading = llList2Integer(Parsed, 0); + gCellHoldDelay = llList2Float(Parsed, 1); + } + else if (channel == SET_FONT_TEXTURE) { + gFontTexture = id; + } + else if (channel == SET_LINE_COLOR) { + change_line_color((integer)data, (vector)((string)id)); + } + else if (channel == SET_COLOR) { + change_color((vector)data); + } + else if (channel == REGISTER_SLAVE) { + gSlaveNames += data; + llMessageLinked(sender, SLAVE_RECOGNIZED, "", ""); + } + } +} diff --git a/xytext_slave.lsl b/xytext_slave.lsl new file mode 100644 index 0000000..92e7b0c --- /dev/null +++ b/xytext_slave.lsl @@ -0,0 +1,281 @@ +//////////////////////////////////////////// +// XyText v2.0 SLAVE Script (5 Face, Single Texture) +// +// Modified by Thraxis Epsilon and Gigs Taggart 5/2007 +// Rewrite to allow one-script-per-object operation +// +// Modified by Kermitt Quirk 19/01/2006 +// To add support for 5 face prim instead of 3 +// +// Originally Written by Xylor Baysklef +// +// +//////////////////////////////////////////// + + +integer REMAP_INDICES = 204002; +integer RESET_INDICES = 204003; + +//internal API +integer REGISTER_SLAVE = 205000; +integer SLAVE_RECOGNIZED = 205001; +integer SLAVE_DISPLAY = 205003; +integer SET_FONT_TEXTURE = 204005; + +integer SLAVE_DISPLAY_EXTENDED = 205004; +integer SLAVE_RESET = 205005; + + +// This is an extended character escape sequence. +string ESCAPE_SEQUENCE = "\e"; + +// This is used to get an index for the extended character. +string EXTENDED_INDEX = "12345"; + +// Face numbers. +integer FACE_1 = 3; +integer FACE_2 = 7; +integer FACE_3 = 4; +integer FACE_4 = 6; +integer FACE_5 = 1; + + +///////////// GLOBAL VARIABLES /////////////// +// This is the key of the font we are displaying. +key gFontTexture = "b2e7394f-5e54-aa12-6e1c-ef327b6bed9e"; +// All displayable characters. Default to ASCII order. +string gCharIndex; + +integer gActive; //if we are recognized, this is true +/////////// END GLOBAL VARIABLES //////////// + +ResetCharIndex() { + gCharIndex = " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`"; + gCharIndex += "abcdefghijklmnopqrstuvwxyz{|}~"; + gCharIndex += "\n\n\n\n\n"; +} + +vector GetGridOffset(integer index) { + // Calculate the offset needed to display this character. + integer Row = index / 10; + integer Col = index % 10; + + // Return the offset in the texture. + return <-0.45 + 0.1 * Col, 0.45 - 0.1 * Row, 0.0>; +} + +ShowChars(integer link,vector grid_offset1, vector grid_offset2, vector grid_offset3, vector grid_offset4, vector grid_offset5) { + // Set the primitive textures directly. + + // <-0.256, 0, 0> + // <0, 0, 0> + // <0.130, 0, 0> + // <0, 0, 0> + // <-0.74, 0, 0> + + llSetLinkPrimitiveParamsFast( link,[ + PRIM_TEXTURE, FACE_1, (string)gFontTexture, <0.126, 0.1, 0>, grid_offset1 + <0.037, 0, 0>, 0.0, + PRIM_TEXTURE, FACE_2, (string)gFontTexture, <0.05, 0.1, 0>, grid_offset2, 0.0, + PRIM_TEXTURE, FACE_3, (string)gFontTexture, <-0.74, 0.1, 0>, grid_offset3 - <0.244, 0, 0>, 0.0, + PRIM_TEXTURE, FACE_4, (string)gFontTexture, <0.05, 0.1, 0>, grid_offset4, 0.0, + PRIM_TEXTURE, FACE_5, (string)gFontTexture, <0.126, 0.1, 0>, grid_offset5 - <0.037, 0, 0>, 0.0 + ]); +} + +RenderString(integer link, string str) { + // Get the grid positions for each pair of characters. + vector GridOffset1 = GetGridOffset( llSubStringIndex(gCharIndex, llGetSubString(str, 0, 0)) ); + vector GridOffset2 = GetGridOffset( llSubStringIndex(gCharIndex, llGetSubString(str, 1, 1)) ); + vector GridOffset3 = GetGridOffset( llSubStringIndex(gCharIndex, llGetSubString(str, 2, 2)) ); + vector GridOffset4 = GetGridOffset( llSubStringIndex(gCharIndex, llGetSubString(str, 3, 3)) ); + vector GridOffset5 = GetGridOffset( llSubStringIndex(gCharIndex, llGetSubString(str, 4, 4)) ); + + // Use these grid positions to display the correct textures/offsets. + ShowChars(link,GridOffset1, GridOffset2, GridOffset3, GridOffset4, GridOffset5); +} + +RenderExtended(integer link, string str) { + // Look for escape sequences. + list Parsed = llParseString2List(str, [], [ESCAPE_SEQUENCE]); + integer ParsedLen = llGetListLength(Parsed); + + // Create a list of index values to work with. + list Indices; + // We start with room for 5 indices. + integer IndicesLeft = 5; + + integer i = 0; + string Token; + integer Clipped; + integer LastWasEscapeSequence = FALSE; + // Work from left to right. + for (; i < ParsedLen && IndicesLeft > 0; ++i) { + Token = llList2String(Parsed, i); + + // If this is an escape sequence, just set the flag and move on. + if (Token == ESCAPE_SEQUENCE) { + LastWasEscapeSequence = TRUE; + } + else { // Token != ESCAPE_SEQUENCE + // Otherwise this is a normal token. Check its length. + Clipped = FALSE; + integer TokenLength = llStringLength(Token); + // Clip if necessary. + if (TokenLength > IndicesLeft) { + Token = llGetSubString(Token, 0, IndicesLeft - 1); + TokenLength = llStringLength(Token); + IndicesLeft = 0; + Clipped = TRUE; + } + else + IndicesLeft -= TokenLength; + + // Was the previous token an escape sequence? + if (LastWasEscapeSequence) { + // Yes, the first character is an escape character, the rest are normal. + + // This is the extended character. + Indices += (llSubStringIndex(EXTENDED_INDEX, llGetSubString(Token, 0, 0)) + 95); + + // These are the normal characters. + integer j = 1; + for (; j < TokenLength; ++j) + Indices += llSubStringIndex(gCharIndex, llGetSubString(Token, j, j)); + } + else { // Normal string. + // Just add the characters normally. + integer j; + for (j = 0; j < TokenLength; ++j) + Indices += llSubStringIndex(gCharIndex, llGetSubString(Token, j, j)); + } + + // Unset this flag, since this was not an escape sequence. + LastWasEscapeSequence = FALSE; + } + } + + // Use the indices to create grid positions. + vector GridOffset1 = GetGridOffset( llList2Integer(Indices, 0) ); + vector GridOffset2 = GetGridOffset( llList2Integer(Indices, 1) ); + vector GridOffset3 = GetGridOffset( llList2Integer(Indices, 2) ); + vector GridOffset4 = GetGridOffset( llList2Integer(Indices, 3) ); + vector GridOffset5 = GetGridOffset( llList2Integer(Indices, 4) ); + + // Use these grid positions to display the correct textures/offsets. + ShowChars(link,GridOffset1, GridOffset2, GridOffset3, GridOffset4, GridOffset5); +} + +integer ConvertIndex(integer index) { + // This converts from an ASCII based index to our indexing scheme. + if (index >= 32) // ' ' or higher + index -= 32; + else { // index < 32 + // Quick bounds check. + if (index > 15) + index = 15; + + index += 94; // extended characters + } + + return index; +} + + +default +{ + state_entry() + { + // Initialize the character index. + ResetCharIndex(); + llMessageLinked(LINK_THIS, REGISTER_SLAVE, llGetScriptName() , NULL_KEY); + } + + on_rez(integer num) + { + llResetScript(); + } + + link_message(integer sender, integer channel, string data, key id) + { + + if (channel == SLAVE_RECOGNIZED) + { + if (data == llGetScriptName()) + { + gActive=TRUE; + } + return; + } + + if (channel == SLAVE_DISPLAY) + { + if (!gActive) + return; + + list params=llCSV2List((string)id); + if (llList2String(params, 1) != llGetScriptName()) + return; + + + RenderString(llList2Integer(params, 0),data); + return; + } + + if (channel == SLAVE_DISPLAY_EXTENDED) + { + if (!gActive) + return; + + list params=llCSV2List((string)id); + if (llList2String(params, 1) != llGetScriptName()) + return; + + RenderExtended(llList2Integer(params, 0),data); + } + + if (channel == SET_FONT_TEXTURE) + { + gFontTexture = id; + return; + } + + if (channel == REMAP_INDICES) { + // Parse the message, splitting it up into index values. + list Parsed = llCSV2List(data); + integer i; + // Go through the list and swap each pair of indices. + for (i = 0; i < llGetListLength(Parsed); i += 2) { + integer Index1 = ConvertIndex( llList2Integer(Parsed, i) ); + integer Index2 = ConvertIndex( llList2Integer(Parsed, i + 1) ); + + // Swap these index values. + string Value1 = llGetSubString(gCharIndex, Index1, Index1); + string Value2 = llGetSubString(gCharIndex, Index2, Index2); + + gCharIndex = llDeleteSubString(gCharIndex, Index1, Index1); + gCharIndex = llInsertString(gCharIndex, Index1, Value2); + + gCharIndex = llDeleteSubString(gCharIndex, Index2, Index2); + gCharIndex = llInsertString(gCharIndex, Index2, Value1); + } + return; + } + if (channel == RESET_INDICES) { + // Restore the character index back to default settings. + ResetCharIndex(); + return; + } + + + if (channel == SLAVE_RESET) + { + ResetCharIndex(); + gActive=FALSE; + llMessageLinked(LINK_THIS, REGISTER_SLAVE, llGetScriptName() , NULL_KEY); + } + + } + + +} +