## [2.3.0] - 2025-08-05

This commit is contained in:
RebeccaNod1 2025-08-05 21:58:20 -05:00
parent 0eceef2227
commit f8d711d3c1
20 changed files with 3736 additions and 59 deletions

View file

@ -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, ["~"], []);

View file

@ -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

View file

@ -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) {

View file

@ -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) {

727
Game_Scoreboard_Manager.lsl Normal file
View file

@ -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 = "<meta name=\"imageid\" content=\"";
string profile_img_prefix = "<img alt=\"profile image\" src=\"http://secondlife.com/app/image/";
integer profile_key_prefix_length;
integer profile_img_prefix_length;
// Grid layout settings - dual prim approach
float PRIM_WIDTH = 0.2; // Smaller width for individual prims
float PRIM_HEIGHT = 0.3;
float PRIM_DEPTH = 0;
float SPACING_X = 0.7; // Space between player pairs
float SPACING_Y = 0.25;
float PRIM_OFFSET = 0.16; // Distance between profile and heart prims
integer GRID_COLS = 2; // 2 columns, 5 rows for 10 players
// Additional prims settings
float BACKGROUND_WIDTH = 4.0;
float BACKGROUND_HEIGHT = 3;
float BACKGROUND_DEPTH = 0.01;
float ACTIONS_WIDTH = 4;
float ACTIONS_HEIGHT = 0.4;
float ACTIONS_DEPTH = 0.02;
float LEADERBOARD_WIDTH = 0.8;
float LEADERBOARD_HEIGHT = 1.2;
float LEADERBOARD_DEPTH = 0.02;
// Prim indices (after root prim = 1)
integer BACKGROUND_PRIM = 2;
integer ACTIONS_PRIM = 3;
integer FIRST_PLAYER_PRIM = 4; // Player prims start from index 4 (no leaderboard prim)
// Communication
integer SCOREBOARD_CHANNEL = -12345;
setupPlayerGrid() {
// Create/position prims: 3 UI prims + 10 player slots (20 prims) = 23 prims total
integer totalPrims = llGetNumberOfPrims();
llOwnerSay("DEBUG: Total prims detected: " + (string)totalPrims);
// Need 23 prims total: 1 root + 2 UI (background, actions) + 20 player prims (2 per player)
// Note: Leaderboard is now a separate object
if (totalPrims < 23) {
llOwnerSay("Need 23 prims total: 1 root + 2 UI + 20 player prims (2 per player)");
llOwnerSay("Current prims: " + (string)totalPrims + ", needed: 23");
llOwnerSay("Note: Leaderboard is separate object now");
return;
}
// Setup UI prims first
setupUIPrims();
llOwnerSay("DEBUG: Starting to position " + (string)totalPrims + " prims...");
// Position prims for each player (2 prims per player)
integer i;
for (i = 0; i < 10; i++) {
integer row = i / GRID_COLS;
integer col = i % GRID_COLS;
// Calculate base position for this player slot (relative to root prim)
vector basePos = <col * SPACING_X - (GRID_COLS-1) * SPACING_X * 1.8,
0.0,
row * SPACING_Y - 1.0 * SPACING_Y + -1.5>;
// 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 + <PRIM_OFFSET, 0.0, 0.0>;
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, <PRIM_HEIGHT, PRIM_WIDTH, PRIM_DEPTH>, // 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, <PRIM_HEIGHT, PRIM_WIDTH, PRIM_DEPTH>, // 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, <BACKGROUND_WIDTH, BACKGROUND_DEPTH, BACKGROUND_HEIGHT>,
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, <ACTIONS_WIDTH, ACTIONS_DEPTH, ACTIONS_HEIGHT>,
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);
}
}
}

View file

@ -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));

56
Leaderboard_Bridge.lsl Normal file
View file

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

253
Leaderboard_Manager.lsl Normal file
View file

@ -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;
}

331
Main.lsl
View file

@ -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 {
}
}
}
}
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;
}
}
}
}
}

View file

@ -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);

View file

@ -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

View file

@ -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
---

View file

@ -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);

View file

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

88
XyzzyText_Debug.lsl Normal file
View file

@ -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");
}
}

BIN
images/title.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

611
xytext_master.lsl Normal file
View file

@ -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 == []);
}
}
}
}

521
xytext_master_clean.lsl Normal file
View file

@ -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, "", "");
}
}
}

View file

@ -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, "", "");
}
}
}

281
xytext_slave.lsl Normal file
View file

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