Peril_Dice/Bot_Manager.lsl

348 lines
14 KiB
Text
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "Peril_Constants.lsl"
// Handles logic for test players (bots): picking numbers and rolling dice
// =============================================================================
// DYNAMIC CHANNEL CONFIGURATION
// =============================================================================
// Base channel offset must match Main.lsl
#define CHANNEL_BASE -77000
// Calculate channels dynamically to avoid hardcoded conflicts
integer calculateChannel(integer offset) {
// Use BOTH owner's key AND object's key to make channels unique per game instance
// This prevents interference when same owner has multiple game tables
string ownerStr = (string)llGetOwner();
string objectStr = (string)llGetKey();
string combinedStr = ownerStr + objectStr;
// Create a more unique hash using both keys
string hashStr = llMD5String(combinedStr, 0);
integer hash1 = llSubStringIndex("0123456789abcdef", llGetSubString(hashStr, 0, 0));
integer hash2 = llSubStringIndex("0123456789abcdef", llGetSubString(hashStr, 1, 1));
integer combinedHash = hash1 * 16 + hash2; // Creates 0-255 range
return CHANNEL_BASE - (offset * 1000) - combinedHash;
}
// Dynamic channel variables
integer BOT_COMMAND_CHANNEL;
// Channel initialization function
initializeChannels() {
BOT_COMMAND_CHANNEL = calculateChannel(5); // ~-77500 range to match Main.lsl
}
// Configuration
integer LISTEN_CHANNEL; // Will be set dynamically
// Listen handle management
integer listenHandle = -1;
// Bot timing configuration to prevent dialog system overload
// BOT_RESPONSE_DELAY defined in Peril_Constants.lsl
// Memory monitoring
#define MEMORY_WARNING_THRESHOLD 0.8 // Warn when using >80% of memory
// Track game state to validate bot commands
list players = [];
list names = [];
list lives = [];
string perilPlayer = "";
// Track processed bot commands to prevent duplicates
list processedBotCommands = [];
// Track sent BOT_PICKED messages to prevent duplicate sending
list sentBotMessages = [];
// Helper function to check and report memory usage
checkMemoryUsage(string context) {
integer usedMemory = llGetUsedMemory();
float memoryPercentage = (float)usedMemory / 65536.0; // LSL scripts have 64KB limit
if (memoryPercentage > MEMORY_WARNING_THRESHOLD) {
dbg("🤖 [Bot Manager] ⚠️ High memory usage in " + context + ": " +
(string)usedMemory + " bytes (" +
(string)llRound(memoryPercentage * 100.0) + "% of 64KB limit)");
}
}
// Helper to parse and respond to pick commands
doBotPick(string botName, integer count, integer diceMax, list avoidNumbers) {
checkMemoryUsage("doBotPick start");
// Validate bot exists and is still alive
integer botIdx = llListFindList(names, [botName]);
if (botIdx == -1) {
dbg("🤖 [Bot Manager] ❌ Bot '" + botName + "' not found in game - ignoring command");
return;
}
integer botLives = llList2Integer(lives, botIdx);
if (botLives <= 0) {
dbg("🤖 [Bot Manager] ❌ Bot '" + botName + "' is eliminated (lives=" + (string)botLives + ") - ignoring command");
return;
}
// IMPROVED ALGORITHM: Calculate available numbers first
list availableNumbers = [];
integer i;
for (i = 1; i <= diceMax; i++) {
string numStr = (string)i;
if (llListFindList(avoidNumbers, [numStr]) == -1) {
availableNumbers += [numStr];
}
}
integer availableCount = llGetListLength(availableNumbers);
integer targetCount = count;
// Graceful degradation: adjust target if not enough numbers available
if (availableCount < count) {
if (availableCount > 0) {
dbg("🤖 [Bot Manager] ⚠️ " + botName + " requested " + (string)count + " picks, but only " + (string)availableCount + " numbers available - picking all available");
targetCount = availableCount;
} else {
dbg("🤖 [Bot Manager] ❌ " + botName + " cannot pick any numbers - all " + (string)diceMax + " numbers are taken!");
// Still send empty response to prevent game hanging
string emptyResponse = "BOT_PICKED:" + botName + ":";
llMessageLinked(LINK_SET, MSG_BOT_PICKED, emptyResponse, NULL_KEY);
return;
}
}
// IMPROVED PICKING: Use shuffled selection instead of random attempts
list picks = [];
list workingList = availableNumbers; // Copy to modify
integer pickCount = 0;
while (pickCount < targetCount && llGetListLength(workingList) > 0) {
// Pick random index from remaining available numbers
integer randomIdx = (integer)(llFrand((float)llGetListLength(workingList)));
string selectedNumber = llList2String(workingList, randomIdx);
// Add to picks and remove from working list
picks += [selectedNumber];
workingList = llDeleteSubList(workingList, randomIdx, randomIdx);
pickCount++;
}
// Final validation
if (llGetListLength(picks) < count) {
dbg("🤖 [Bot Manager] ℹ️ " + botName + " picked " + (string)llGetListLength(picks) + "/" + (string)count + " numbers (limited by availability)");
} else {
dbg("🤖 [Bot Manager] ✅ " + botName + " successfully picked " + (string)llGetListLength(picks) + " numbers");
}
string pickString = llDumpList2String(picks, ";");
string response = "BOT_PICKED:" + botName + ":" + pickString;
// Check if we've already sent this exact message to prevent duplicates
if (llListFindList(sentBotMessages, [response]) != -1) {
dbg("🤖 [Bot Manager] ⚠️ DUPLICATE SEND - Already sent message: " + response);
return;
}
// Prepend to list for more efficient check later
sentBotMessages = [response] + llList2List(sentBotMessages, 0, 19); // Keep last 20
dbg("🤖 [Bot Manager] 🤖 " + botName + " picked: " + llList2CSV(picks));
dbg("🤖 [Bot Manager] 📬 SENDING BOT_PICKED: " + response);
// Send message immediately to avoid timing issues
llMessageLinked(LINK_SET, MSG_BOT_PICKED, response, NULL_KEY);
dbg("🤖 [Bot Manager] 📬 BOT_PICKED message sent via llMessageLinked(LINK_SET, MSG_BOT_PICKED, ...)");
// Do NOT sleep after sending messages - this causes LSL message delivery loops!
}
// Helper to respond to peril roll commands
doBotRoll(string botName, integer diceMax) {
// Validate bot exists and is still alive
integer rollBotIdx = llListFindList(names, [botName]);
if (rollBotIdx == -1) {
dbg("🤖 [Bot Manager] ❌ Bot '" + botName + "' not found in game - ignoring roll command");
return;
}
integer rollBotLives = llList2Integer(lives, rollBotIdx);
if (rollBotLives <= 0) {
dbg("🤖 [Bot Manager] ❌ Bot '" + botName + "' is eliminated (lives=" + (string)rollBotLives + ") - ignoring roll command");
return;
}
if (botName != perilPlayer) {
dbg("🤖 [Bot Manager] ❌ Bot '" + botName + "' is not the peril player (current peril: '" + perilPlayer + "') - ignoring roll command");
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);
dbg("🤖 [Bot Manager] 🎲 " + botName + " rolled: " + (string)roll);
}
default {
state_entry() {
REPORT_MEMORY();
initializeChannels();
LISTEN_CHANNEL = BOT_COMMAND_CHANNEL;
if (listenHandle != -1) {
llListenRemove(listenHandle);
}
// Initialize/reset all state variables
names = [];
lives = [];
perilPlayer = "";
processedBotCommands = [];
sentBotMessages = [];
listenHandle = llListen(LISTEN_CHANNEL, "", NULL_KEY, "");
dbg("🤖 [Bot Manager] Bot Manager ready!");
}
on_rez(integer start_param) {
REPORT_MEMORY();
dbg("🤖 [Bot Manager] 🔄 Bot Manager rezzed - reinitializing...");
// Re-initialize dynamic channels
initializeChannels();
LISTEN_CHANNEL = BOT_COMMAND_CHANNEL;
if (listenHandle != -1) {
llListenRemove(listenHandle);
}
// Reset all state variables on rez
names = [];
lives = [];
perilPlayer = "";
processedBotCommands = [];
sentBotMessages = [];
listenHandle = llListen(LISTEN_CHANNEL, "", NULL_KEY, "");
dbg("🤖 [Bot Manager] ✅ Bot Manager reset complete after rez!");
}
link_message(integer sender, integer num, string str, key id) {
// Handle full reset from main controller
if (num == MSG_RESET_ALL && str == "FULL_RESET") {
// Reset bot manager state
players = [];
names = [];
lives = [];
perilPlayer = "";
processedBotCommands = []; // Clear processed commands for new game
sentBotMessages = []; // Clear sent messages for new game
dbg("🤖 [Bot Manager] Reset complete!");
return;
}
// Handle game state sync to track player eliminations
if (num == MSG_SYNC_GAME_STATE) {
list parts = llParseStringKeepNulls(str, ["~"], []);
if (llGetListLength(parts) >= 4) {
lives = llCSV2List(llList2String(parts, 0));
// Don't need picks data for validation, skip parts[1]
string receivedPeril = llList2String(parts, 2);
string oldPerilPlayer = perilPlayer;
if (receivedPeril == "NONE") {
perilPlayer = "";
} else {
perilPlayer = receivedPeril;
}
names = llCSV2List(llList2String(parts, 3));
// Clear processed commands and sent messages when new round starts
// Detect new round by: peril player change OR lives change (someone got hit)
string currentLivesStr = llList2CSV(lives);
string oldLivesStr = "";
if (llGetListLength(lives) > 0) {
// Build previous lives string for comparison
oldLivesStr = currentLivesStr; // This will be different if lives changed
}
if (oldPerilPlayer != perilPlayer && perilPlayer != "") {
processedBotCommands = [];
sentBotMessages = [];
dbg("🤖 [Bot Manager] New round detected (peril change), cleared processed commands and sent messages");
} else {
// Standardized ~EMPTY~ token logic
string picksStr = llList2String(parts, 1);
if (picksStr == "~EMPTY~" && perilPlayer != "") {
processedBotCommands = [];
sentBotMessages = [];
dbg("🤖 [Bot Manager] Turn reset detected (picks cleared), cleared processed commands and sent messages");
}
}
}
return;
}
if (num == MSG_BOT_COMMAND) {
if (llSubStringIndex(str, "BOT_PICK:") == 0) {
// Format: BOT_PICK:bot_1:3:20:1,2,3 (last part is optional already picked numbers)
dbg("🤖 [Bot Manager] 📬 RECEIVED BOT_PICK: " + str);
list parts = llParseStringKeepNulls(str, [":"], []);
if (llGetListLength(parts) >= 4) {
string botName = llList2String(parts, 1);
integer count = (integer)llList2String(parts, 2);
integer diceMax = (integer)llList2String(parts, 3);
// Check for duplicate command - prevent processing same bot command multiple times
// Include more details to make signature more specific
string commandSignature = "PICK:" + botName + ":" + (string)count + ":" + (string)diceMax + ":" + perilPlayer;
if (llListFindList(processedBotCommands, [commandSignature]) != -1) {
dbg("🤖 [Bot Manager] ⚠️ DUPLICATE - Ignoring identical command for " + botName + " (already processed this exact pick request)");
return;
}
// Mark this command as processed BEFORE doing the work to prevent race conditions
processedBotCommands += [commandSignature];
dbg("🤖 [Bot Manager] ✅ Processing " + botName + " pick command");
list avoidNumbers = [];
if (llGetListLength(parts) >= 5) {
string avoidStr = llList2String(parts, 4);
if (avoidStr != "") {
list rawList = llCSV2List(avoidStr);
// Trim whitespace from each number
avoidNumbers = [];
integer i;
for (i = 0; i < llGetListLength(rawList); i++) {
string trimmed = llStringTrim(llList2String(rawList, i), STRING_TRIM);
if (trimmed != "") {
avoidNumbers += [trimmed];
}
}
}
}
doBotPick(botName, count, diceMax, avoidNumbers);
}
}
else if (llSubStringIndex(str, "BOT_PERIL_ROLL:") == 0) {
// Format: BOT_PERIL_ROLL:bot_1:20
list parts = llParseStringKeepNulls(str, [":"], []);
if (llGetListLength(parts) == 3) {
string botName = llList2String(parts, 1);
integer diceMax = (integer)llList2String(parts, 2);
doBotRoll(botName, diceMax);
}
}
}
}
listen(integer channel, string name, key id, string message) {
// Listen handler removed - all bot commands now come via link messages only
// This prevents duplicate processing of bot commands
}
}