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);
+ }
+
+ }
+
+
+}
+