Files
TrinityCore/araxiaonline/lua_scripts/ad_admin_handlers.lua
James Huston e3746aa50e feat(agent-chat): Add Agent Chat Panel with MCP integration
- Add AgentChatPanel UI with agent selector, chat history, message input
- Implement message send/receive flow via AMS and MCP bridge
- Add 'Sending...' / 'Delivered' status indicator for message confirmation
- Add message history persistence via SavedVariables
- Persist subscriptions in ElunaSharedData (survives .reload eluna)
- Add online/offline status display in agent dropdown
- Fix JSON content parsing for escaped quotes
- Add server-side handlers for agent messaging (ad_admin_handlers.lua)
- Add AMS_Server for client-server communication
- Update AGENTS.md with 'never defer documentation' policy
- Update file editing locations documentation
2025-12-31 01:54:46 +00:00

1476 lines
54 KiB
Lua
Executable File

--[[
AraxiaTrinityAdmin Server Handlers
Provides server-side data access for the AraxiaTrinityAdmin addon.
Uses AMS (Araxia Messaging System) for client-server communication.
]]
print("[Admin Handlers] Loading...")
SetSharedData("debug_admin_handlers", "step1_loading")
-- Check if AMS is available
if not AMS then
print("[Admin Handlers] AMS not available yet (will be loaded via init.lua dofile)")
SetSharedData("debug_admin_handlers", "step2_no_ams_returning")
return
end
print("[Admin Handlers] AMS found, registering handlers...")
SetSharedData("debug_admin_handlers", "step3_ams_found")
-- Load Smallfolk for serialization (used by shared data)
local Smallfolk = require("aa_Smallfolk")
-- ============================================================================
-- Configurable Display IDs (can be changed via shared data without recompile)
-- ============================================================================
-- Default display IDs - these can be overridden via SetSharedData
local DEFAULT_DISPLAYS = {
waypoint_marker = 1824, -- Elven Wisp (works in 11.2.5)
waypoint_highlight = 1824, -- Same as marker but scaled up (highlight via size)
spawn_marker = 31366, -- Green targeting circle
}
-- Get a display ID, checking shared data first, then falling back to default
local function GetDisplayId(key)
local sharedKey = "config_display_" .. key
local value = GetSharedData(sharedKey)
if value and value ~= "" then
local num = tonumber(value)
if num then
return num
end
end
return DEFAULT_DISPLAYS[key] or 17188
end
-- Set a display ID in shared data (persists until server restart)
local function SetDisplayIdConfig(key, displayId)
local sharedKey = "config_display_" .. key
SetSharedData(sharedKey, tostring(displayId))
print("[Admin Handlers] Display config updated: " .. key .. " = " .. displayId)
end
-- Initialize display configs from defaults (only if not already set)
local function InitDisplayConfigs()
for key, defaultValue in pairs(DEFAULT_DISPLAYS) do
local sharedKey = "config_display_" .. key
if not HasSharedData(sharedKey) then
SetSharedData(sharedKey, tostring(defaultValue))
end
end
print("[Admin Handlers] Display configs initialized")
end
-- Initialize on load
InitDisplayConfigs()
-- ============================================================================
-- Helper Functions
-- ============================================================================
-- Convert copper to formatted gold string
local function FormatGold(copper)
if not copper or copper == 0 then
return "0 copper"
end
local gold = math.floor(copper / 10000)
local silver = math.floor((copper % 10000) / 100)
local copperLeft = copper % 100
local parts = {}
if gold > 0 then table.insert(parts, gold .. "g") end
if silver > 0 then table.insert(parts, silver .. "s") end
if copperLeft > 0 then table.insert(parts, copperLeft .. "c") end
return table.concat(parts, " ")
end
-- Get spell school name
local function GetSpellSchoolName(school)
local schools = {
[0] = "Physical",
[1] = "Holy",
[2] = "Fire",
[3] = "Nature",
[4] = "Frost",
[5] = "Shadow",
[6] = "Arcane"
}
return schools[school] or "Unknown"
end
-- Get stat name
local function GetStatName(stat)
local stats = {
[0] = "Strength",
[1] = "Agility",
[2] = "Stamina",
[3] = "Intellect",
[4] = "Spirit"
}
return stats[stat] or "Unknown"
end
-- Get rank name
local function GetRankName(rank)
local ranks = {
[0] = "Normal",
[1] = "Elite",
[2] = "Rare Elite",
[3] = "Boss",
[4] = "Rare"
}
return ranks[rank] or "Unknown"
end
-- ============================================================================
-- NPC Data Handler
-- ============================================================================
AMS.RegisterHandler("GET_NPC_DATA", function(player, data)
local npcGUID = data.npcGUID
if not npcGUID then
print("[Admin Handlers] GET_NPC_DATA: No GUID provided")
AMS.Send(player, "NPC_DATA_RESPONSE", {
success = false,
error = "No NPC GUID provided"
})
return
end
print("[Admin Handlers] GET_NPC_DATA: Fetching data for GUID:", npcGUID)
local creature = player:GetSelection()
if not creature then
print("[Admin Handlers] GET_NPC_DATA: No creature selected")
AMS.Send(player, "NPC_DATA_RESPONSE", {
success = false,
error = "No creature selected",
guid = npcGUID
})
return
end
-- Verify it's a creature (not a player or gameobject)
creature = creature:ToCreature()
if not creature then
print("[Admin Handlers] GET_NPC_DATA: Selected object is not a creature")
AMS.Send(player, "NPC_DATA_RESPONSE", {
success = false,
error = "Selected object is not a creature",
guid = npcGUID
})
return
end
-- Helper to safely call creature methods (converts userdata to string)
local function SafeGet(fn, default)
local success, result = pcall(fn)
if success then
-- Convert userdata to string to avoid serialization issues
if type(result) == "userdata" then
return tostring(result)
end
return result
else
print("[Admin Handlers] SafeGet failed:", result)
return default
end
end
print("[Admin Handlers] Building response data...")
-- Build response data using available Eluna methods
local response = {
success = true,
guid = npcGUID,
timestamp = os.time(),
-- Basic Information
basic = {
entry = SafeGet(function() return creature:GetEntry() end, 0),
name = SafeGet(function() return creature:GetName() end, "Unknown"),
level = SafeGet(function() return creature:GetLevel() end, 0),
displayId = SafeGet(function() return creature:GetDisplayId() end, 0),
nativeDisplayId = SafeGet(function() return creature:GetNativeDisplayId() end, 0),
scale = SafeGet(function() return creature:GetScale() end, 1.0),
faction = SafeGet(function() return creature:GetFaction() end, 0),
creatureType = SafeGet(function() return creature:GetCreatureType() end, 0),
rank = SafeGet(function() return creature:GetRank() end, 0),
rankName = SafeGet(function() return GetRankName(creature:GetRank()) end, "Normal")
},
-- Health & Power
vitals = {
health = SafeGet(function() return creature:GetHealth() end, 0),
maxHealth = SafeGet(function() return creature:GetMaxHealth() end, 1),
healthPercent = SafeGet(function() return (creature:GetHealth() / creature:GetMaxHealth()) * 100 end, 0),
power = SafeGet(function() return creature:GetPower(0) end, 0),
maxPower = SafeGet(function() return creature:GetMaxPower(0) end, 0),
powerType = SafeGet(function() return creature:GetPowerType() end, 0)
},
-- Base Stats (STR, AGI, STA, INT, SPI)
stats = {},
-- Movement Speeds
speeds = {
walk = SafeGet(function() return creature:GetSpeed(0) end, 0),
run = SafeGet(function() return creature:GetSpeed(1) end, 0),
runBack = SafeGet(function() return creature:GetSpeed(2) end, 0),
swim = SafeGet(function() return creature:GetSpeed(3) end, 0),
swimBack = SafeGet(function() return creature:GetSpeed(4) end, 0),
fly = SafeGet(function() return creature:GetSpeed(6) end, 0),
flyBack = SafeGet(function() return creature:GetSpeed(7) end, 0)
},
-- Spell Power per school
spellPower = {},
-- AI & Scripts
scripts = {
aiName = SafeGet(function() return creature:GetAIName() end, ""),
scriptName = SafeGet(function() return creature:GetScriptName() end, ""),
scriptId = SafeGet(function() return creature:GetScriptId() end, 0)
},
-- Behavior
behavior = {
respawnDelay = SafeGet(function() return creature:GetRespawnDelay() end, 0),
wanderRadius = SafeGet(function() return creature:GetWanderRadius() end, 0),
isInCombat = SafeGet(function() return creature:IsInCombat() end, false),
isRegeneratingHealth = SafeGet(function() return creature:IsRegeneratingHealth() end, false),
isElite = SafeGet(function() return creature:IsElite() end, false),
isWorldBoss = SafeGet(function() return creature:IsWorldBoss() end, false),
isCivilian = SafeGet(function() return creature:IsCivilian() end, false)
},
-- Movement info (0=Idle, 1=Random, 2=Waypoint)
movement = {
defaultType = SafeGet(function() return creature:GetDefaultMovementType() end, 0),
currentType = SafeGet(function() return creature:GetMovementType() end, 0),
currentWaypointId = SafeGet(function() return creature:GetCurrentWaypointId() end, 0),
respawnTime = SafeGet(function() return creature:GetRespawnDelay() end, 0),
waypointPath = SafeGet(function() return creature:GetWaypointPathData() end, nil)
}
}
print("[Admin Handlers] Response data built successfully")
-- Use new safe C++ methods for combat stats (Phase 2)
print("[Admin Handlers] Getting combat stats via safe C++ methods...")
-- Get armor using safe C++ method
response.combat = {
armor = SafeGet(function() return creature:GetArmor() end, 0),
baseAttackTime = SafeGet(function() return creature:GetBaseAttackTime(0) end, 0),
offhandAttackTime = SafeGet(function() return creature:GetBaseAttackTime(1) end, 0),
rangedAttackTime = SafeGet(function() return creature:GetBaseAttackTime(2) end, 0)
}
-- Get resistances using safe C++ method (0=Physical/Armor, 1=Holy, 2=Fire, 3=Nature, 4=Frost, 5=Shadow, 6=Arcane)
response.resistances = {}
for i = 0, 6 do
response.resistances[GetSpellSchoolName(i)] = SafeGet(function() return creature:GetResistance(i) end, 0)
end
-- Get creature template data using safe C++ method
-- Note: This can be large for some creatures (several KB), contributing to payload size
response.template = SafeGet(function() return creature:GetCreatureTemplateData() end, nil)
-- Stats - using safe C++ GetStat method
for i = 0, 4 do
response.stats[GetStatName(i)] = SafeGet(function() return creature:GetStat(i) end, 0)
end
-- Spell power - still skip as it crashes on creatures
for i = 0, 6 do
response.spellPower[GetSpellSchoolName(i)] = 0
end
-- Add note about available data
response.notes = {
"Phase 2: Stats, armor, resistances, attack times, template data available",
"Spell power disabled - crashes on creatures"
}
local creatureName = SafeGet(function() return creature:GetName() end, "Unknown")
print("[Admin Handlers] GET_NPC_DATA: Sending response for", creatureName)
-- Send response back to client
print("[Admin Handlers] Calling AMS.Send...")
AMS.Send(player, "NPC_DATA_RESPONSE", response)
print("[Admin Handlers] AMS.Send completed")
end)
-- ============================================================================
-- Waypoint Visualization Handlers
-- ============================================================================
-- Show waypoint markers in 3D space
AMS.RegisterHandler("SHOW_WAYPOINTS", function(player, data)
print("[Admin Handlers] SHOW_WAYPOINTS request received")
-- Get creature from player's current selection
local creature = player:GetSelection()
if not creature then
print("[Admin Handlers] SHOW_WAYPOINTS: No target selected")
AMS.Send(player, "WAYPOINTS_RESPONSE", { success = false, error = "No target selected" })
return
end
creature = creature:ToCreature()
if not creature then
print("[Admin Handlers] SHOW_WAYPOINTS: Target is not a creature")
AMS.Send(player, "WAYPOINTS_RESPONSE", { success = false, error = "Target is not a creature" })
return
end
-- Check if creature has waypoints
local pathId = creature:GetWaypointPath()
if not pathId or pathId == 0 then
print("[Admin Handlers] SHOW_WAYPOINTS: Creature has no waypoint path")
AMS.Send(player, "WAYPOINTS_RESPONSE", { success = false, error = "No waypoint path" })
return
end
-- Clear any existing visualization first (fixes state after Clear All)
pcall(function() creature:DevisualizeWaypointPath() end)
-- Get configurable display ID for waypoint markers
local displayId = GetDisplayId("waypoint_marker")
print("[Admin Handlers] SHOW_WAYPOINTS: Using displayId " .. tostring(displayId))
-- Visualize the path (spawns marker creatures at each waypoint)
-- Pass player to inherit their phase, and displayId for marker appearance
local success, err = pcall(function() return creature:VisualizeWaypointPath(player, displayId) end)
if not success then
print("[Admin Handlers] SHOW_WAYPOINTS: Error calling VisualizeWaypointPath:", err)
end
if success then
print("[Admin Handlers] SHOW_WAYPOINTS: Visualization created for path", pathId)
AMS.Send(player, "WAYPOINTS_RESPONSE", {
success = true,
pathId = pathId,
message = "Waypoint markers spawned"
})
else
print("[Admin Handlers] SHOW_WAYPOINTS: Failed to create visualization")
AMS.Send(player, "WAYPOINTS_RESPONSE", { success = false, error = "Failed to visualize (C++ method not available - rebuild required)" })
end
end)
-- Hide waypoint markers
AMS.RegisterHandler("HIDE_WAYPOINTS", function(player, data)
print("[Admin Handlers] HIDE_WAYPOINTS request received")
-- Get creature from player's current selection
local creature = player:GetSelection()
if not creature then
print("[Admin Handlers] HIDE_WAYPOINTS: No target selected")
AMS.Send(player, "WAYPOINTS_RESPONSE", { success = false, error = "No target selected" })
return
end
creature = creature:ToCreature()
if not creature then
print("[Admin Handlers] HIDE_WAYPOINTS: Target is not a creature")
AMS.Send(player, "WAYPOINTS_RESPONSE", { success = false, error = "Target is not a creature" })
return
end
-- Devisualize the path (removes marker creatures)
local success, err = pcall(function() return creature:DevisualizeWaypointPath() end)
if not success then
print("[Admin Handlers] HIDE_WAYPOINTS: Error calling DevisualizeWaypointPath:", err)
end
if success then
print("[Admin Handlers] HIDE_WAYPOINTS: Visualization removed")
AMS.Send(player, "WAYPOINTS_RESPONSE", {
success = true,
message = "Waypoint markers removed"
})
else
print("[Admin Handlers] HIDE_WAYPOINTS: Failed to remove visualization")
AMS.Send(player, "WAYPOINTS_RESPONSE", { success = false, error = "Failed to hide" })
end
end)
-- Hide waypoints by GUID (for Clear All / Paths tab)
-- Note: Markers are tied to creatures - they'll despawn when creature respawns or server restarts
-- This handler logs the request for debugging
AMS.RegisterHandler("HIDE_WAYPOINTS_BY_GUID", function(player, data)
if not data or not data.guid then
print("[Admin Handlers] HIDE_WAYPOINTS_BY_GUID: No GUID provided")
return
end
print("[Admin Handlers] HIDE_WAYPOINTS_BY_GUID: Acknowledged clear request for GUID:", data.guid)
-- Note: If the creature is not the player's current target, we can't easily devisualize
-- The client tracker is the source of truth; markers will despawn naturally
end)
-- Clear ALL waypoint markers and reset WaypointManager tracking state
AMS.RegisterHandler("CLEAR_ALL_WAYPOINT_MARKERS", function(player, data)
print("[Admin Handlers] CLEAR_ALL_WAYPOINT_MARKERS: Clearing all waypoint visualizations")
-- Use the C++ method to properly clear tracking state and despawn markers
player:ClearAllWaypointVisualizations()
print("[Admin Handlers] CLEAR_ALL_WAYPOINT_MARKERS: All visualizations cleared")
AMS.Send(player, "CLEAR_WAYPOINTS_RESPONSE", { success = true })
end)
-- ============================================================================
-- Spawn Point Marker Operations
-- ============================================================================
--[[
SHOW_SPAWN_MARKER - Show a marker at the selected creature's spawn point
Request: { displayId = optional number }
Response: { success = bool, x, y, z, o }
]]
AMS.RegisterHandler("SHOW_SPAWN_MARKER", function(player, data)
print("[Admin Handlers] SHOW_SPAWN_MARKER request received")
local creature = player:GetSelection()
if not creature then
AMS.Send(player, "SPAWN_MARKER_RESPONSE", { success = false, error = "No target selected" })
return
end
creature = creature:ToCreature()
if not creature then
AMS.Send(player, "SPAWN_MARKER_RESPONSE", { success = false, error = "Target is not a creature" })
return
end
-- Get home position for response
local x, y, z, o = creature:GetHomePosition()
-- Show spawn marker (pass player for phase, optional displayId)
local displayId = data and data.displayId or nil
local success, marker = pcall(function()
if displayId then
return creature:ShowSpawnPointMarker(player, displayId)
else
return creature:ShowSpawnPointMarker(player)
end
end)
if success and marker then
print("[Admin Handlers] SHOW_SPAWN_MARKER: Marker created at " .. x .. ", " .. y .. ", " .. z)
AMS.Send(player, "SPAWN_MARKER_RESPONSE", {
success = true,
x = x, y = y, z = z, o = o,
message = "Spawn marker shown"
})
else
print("[Admin Handlers] SHOW_SPAWN_MARKER: Failed to create marker")
AMS.Send(player, "SPAWN_MARKER_RESPONSE", {
success = false,
error = "Failed to show marker (rebuild server required)"
})
end
end)
--[[
HIDE_SPAWN_MARKER - Hide the spawn point marker for the selected creature
Request: {}
Response: { success = bool }
]]
AMS.RegisterHandler("HIDE_SPAWN_MARKER", function(player, data)
print("[Admin Handlers] HIDE_SPAWN_MARKER request received")
local creature = player:GetSelection()
if not creature then
AMS.Send(player, "SPAWN_MARKER_RESPONSE", { success = false, error = "No target selected" })
return
end
creature = creature:ToCreature()
if not creature then
AMS.Send(player, "SPAWN_MARKER_RESPONSE", { success = false, error = "Target is not a creature" })
return
end
local success = pcall(function() return creature:HideSpawnPointMarker() end)
if success then
print("[Admin Handlers] HIDE_SPAWN_MARKER: Marker hidden")
AMS.Send(player, "SPAWN_MARKER_RESPONSE", { success = true, message = "Spawn marker hidden" })
else
print("[Admin Handlers] HIDE_SPAWN_MARKER: Failed to hide marker")
AMS.Send(player, "SPAWN_MARKER_RESPONSE", { success = false, error = "Failed to hide marker" })
end
end)
-- Local cache for wander radius markers (for same-session cleanup)
-- Also stored in ElunaSharedData for visibility, but Lua object refs only work same-session
local wanderRadiusMarkers = {}
--[[
SHOW_WANDER_RADIUS - Show visual markers in a circle at the creature's wander radius
Request: { segments = optional number (default 12) }
Response: { success = bool, radius = number, x, y, z }
]]
AMS.RegisterHandler("SHOW_WANDER_RADIUS", function(player, data)
print("[Admin Handlers] SHOW_WANDER_RADIUS request received")
local creature = player:GetSelection()
if not creature then
AMS.Send(player, "WANDER_RADIUS_RESPONSE", { success = false, error = "No target selected" })
return
end
creature = creature:ToCreature()
if not creature then
AMS.Send(player, "WANDER_RADIUS_RESPONSE", { success = false, error = "Target is not a creature" })
return
end
local spawnId = creature:GetDBTableGUIDLow()
local spawnIdStr = tostring(spawnId) -- Convert to string for table key
local wanderRadius = creature:GetWanderRadius()
if wanderRadius <= 0 then
AMS.Send(player, "WANDER_RADIUS_RESPONSE", { success = false, error = "Creature has no wander radius (0)" })
return
end
-- Get spawn position
local homeX, homeY, homeZ, homeO = creature:GetHomePosition()
local markerKey = "wander_markers_" .. spawnIdStr
-- Clear any existing markers for this creature (from local cache)
if wanderRadiusMarkers[spawnIdStr] then
for _, marker in ipairs(wanderRadiusMarkers[spawnIdStr]) do
if marker and marker:IsInWorld() then
marker:DespawnOrUnsummon()
end
end
wanderRadiusMarkers[spawnIdStr] = nil
end
ClearSharedData(markerKey) -- Also clear shared data
-- Number of markers around the circle (default 12 = every 30 degrees)
local segments = (data and data.segments) or 12
if segments < 4 then segments = 4 end
if segments > 36 then segments = 36 end
local markers = {}
local markerGuids = {}
local angleStep = (2 * math.pi) / segments
-- Spawn markers around the circle perimeter
for i = 0, segments - 1 do
local angle = i * angleStep
local markerX = homeX + wanderRadius * math.cos(angle)
local markerY = homeY + wanderRadius * math.sin(angle)
local markerZ = homeZ -- Keep at spawn height
-- Spawn a visual marker at this position
-- Using entry 1 (VISUAL_WAYPOINT) with a distinct display
local marker = creature:SpawnCreature(1, markerX, markerY, markerZ, 0, 8) -- 8 = TEMPSUMMON_MANUAL_DESPAWN
if marker then
marker:SetDisplayId(31366) -- Green circle like spawn marker
marker:SetScale(0.3) -- Smaller for radius markers
table.insert(markers, marker)
table.insert(markerGuids, tostring(marker:GetGUIDLow()))
end
end
-- Also spawn a marker at the center (spawn point)
local centerMarker = creature:SpawnCreature(1, homeX, homeY, homeZ, homeO, 8)
if centerMarker then
centerMarker:SetDisplayId(31366) -- Green circle
centerMarker:SetScale(1.0) -- Larger for center
table.insert(markers, centerMarker)
table.insert(markerGuids, tostring(centerMarker:GetGUIDLow()))
end
-- Store in local cache for same-session cleanup (use string key)
wanderRadiusMarkers[spawnIdStr] = markers
-- Also store GUIDs in shared data (for debugging/tracking)
SetSharedData(markerKey, Smallfolk.dumps(markerGuids))
print("[Admin Handlers] SHOW_WANDER_RADIUS: Created " .. #markers .. " markers for radius " .. wanderRadius)
AMS.Send(player, "WANDER_RADIUS_RESPONSE", {
success = true,
radius = wanderRadius,
segments = segments,
markerCount = #markers,
x = homeX, y = homeY, z = homeZ,
message = "Wander radius shown (" .. wanderRadius .. " yards)"
})
end)
--[[
HIDE_WANDER_RADIUS - Hide the wander radius markers for the selected creature
Request: {}
Response: { success = bool }
]]
AMS.RegisterHandler("HIDE_WANDER_RADIUS", function(player, data)
print("[Admin Handlers] HIDE_WANDER_RADIUS request received")
local creature = player:GetSelection()
if not creature then
AMS.Send(player, "WANDER_RADIUS_RESPONSE", { success = false, error = "No target selected" })
return
end
creature = creature:ToCreature()
if not creature then
AMS.Send(player, "WANDER_RADIUS_RESPONSE", { success = false, error = "Target is not a creature" })
return
end
local spawnId = creature:GetDBTableGUIDLow()
local spawnIdStr = tostring(spawnId)
local markerKey = "wander_markers_" .. spawnIdStr
if wanderRadiusMarkers[spawnIdStr] then
local count = #wanderRadiusMarkers[spawnIdStr]
for _, marker in ipairs(wanderRadiusMarkers[spawnIdStr]) do
if marker and marker:IsInWorld() then
marker:DespawnOrUnsummon()
end
end
wanderRadiusMarkers[spawnIdStr] = nil
ClearSharedData(markerKey)
print("[Admin Handlers] HIDE_WANDER_RADIUS: Removed " .. count .. " markers")
AMS.Send(player, "WANDER_RADIUS_RESPONSE", { success = true, message = "Wander radius hidden" })
else
-- No local cache - markers may have been lost on reload
-- They'll despawn naturally, just clear any tracking data
ClearSharedData(markerKey)
AMS.Send(player, "WANDER_RADIUS_RESPONSE", { success = true, message = "Wander radius markers cleared (may already be despawned)" })
end
end)
--[[
CLEAR_WANDER_MARKERS - Clear both spawn marker and wander radius markers
Request: {}
Response: { success = bool }
]]
AMS.RegisterHandler("CLEAR_WANDER_MARKERS", function(player, data)
print("[Admin Handlers] CLEAR_WANDER_MARKERS request received")
local creature = player:GetSelection()
if not creature then
AMS.Send(player, "CLEAR_WANDER_MARKERS_RESPONSE", { success = false, error = "No target selected" })
return
end
creature = creature:ToCreature()
if not creature then
AMS.Send(player, "CLEAR_WANDER_MARKERS_RESPONSE", { success = false, error = "Target is not a creature" })
return
end
local spawnId = creature:GetDBTableGUIDLow()
local spawnIdStr = tostring(spawnId)
local cleared = 0
local messages = {}
-- Clear spawn marker (via C++ method if available)
local spawnSuccess, spawnResult = pcall(function() return creature:HideSpawnPointMarker() end)
if spawnSuccess and spawnResult then
cleared = cleared + 1
table.insert(messages, "spawn marker")
end
-- Clear wander radius markers (from local cache, use string key)
if wanderRadiusMarkers[spawnIdStr] then
local count = #wanderRadiusMarkers[spawnIdStr]
for _, marker in ipairs(wanderRadiusMarkers[spawnIdStr]) do
if marker and marker:IsInWorld() then
marker:DespawnOrUnsummon()
cleared = cleared + 1
end
end
wanderRadiusMarkers[spawnIdStr] = nil
table.insert(messages, count .. " radius markers")
end
-- Clear shared data
ClearSharedData("wander_markers_" .. spawnIdStr)
local msg = cleared > 0 and ("Cleared: " .. table.concat(messages, ", ")) or "No active markers in cache (may have been lost on reload)"
print("[Admin Handlers] CLEAR_WANDER_MARKERS: " .. msg .. " for creature " .. tostring(spawnId))
AMS.Send(player, "CLEAR_WANDER_MARKERS_RESPONSE", { success = true, cleared = cleared, message = msg })
end)
--[[
CLEAR_NEARBY_MARKERS - Emergency clear of all VISUAL_WAYPOINT creatures near player
This clears orphaned markers from before tracking was implemented
Request: { range = optional number (default 100) }
Response: { success = bool, cleared = number }
]]
AMS.RegisterHandler("CLEAR_NEARBY_MARKERS", function(player, data)
print("[Admin Handlers] CLEAR_NEARBY_MARKERS request received")
local range = (data and data.range) or 100
local cleared = 0
-- Get all creatures near player with entry 1 (VISUAL_WAYPOINT)
local creatures = player:GetCreaturesInRange(range, 1) -- entry 1 = VISUAL_WAYPOINT
if creatures then
for _, creature in ipairs(creatures) do
if creature and creature:IsInWorld() then
creature:DespawnOrUnsummon()
cleared = cleared + 1
end
end
end
print("[Admin Handlers] CLEAR_NEARBY_MARKERS: Cleared " .. cleared .. " markers within " .. range .. " yards")
AMS.Send(player, "CLEAR_NEARBY_MARKERS_RESPONSE", { success = true, cleared = cleared, message = "Cleared " .. cleared .. " orphaned markers" })
end)
-- Get detailed waypoint data for a path
AMS.RegisterHandler("GET_WAYPOINT_DETAILS", function(player, data)
if not data or not data.pathId then
print("[Admin Handlers] GET_WAYPOINT_DETAILS: No pathId provided")
AMS.Send(player, "WAYPOINT_DETAILS_RESPONSE", { success = false, error = "No pathId" })
return
end
local pathId = data.pathId
print("[Admin Handlers] GET_WAYPOINT_DETAILS: Fetching details for path " .. pathId)
-- Get the waypoint path using the C++ binding
local path = GetWaypointPath(pathId)
if not path or not path.nodes then
print("[Admin Handlers] GET_WAYPOINT_DETAILS: Path not found")
AMS.Send(player, "WAYPOINT_DETAILS_RESPONSE", { success = false, error = "Path not found" })
return
end
print("[Admin Handlers] GET_WAYPOINT_DETAILS: Sending " .. #path.nodes .. " nodes")
AMS.Send(player, "WAYPOINT_DETAILS_RESPONSE", {
success = true,
pathId = pathId,
nodes = path.nodes
})
end)
-- Highlight a specific waypoint in the world
AMS.RegisterHandler("SELECT_WAYPOINT", function(player, data)
if not data or not data.pathId or not data.nodeId then
print("[Admin Handlers] SELECT_WAYPOINT: Missing pathId or nodeId")
return
end
local pathId = data.pathId
local nodeId = data.nodeId
print("[Admin Handlers] SELECT_WAYPOINT: Highlighting path " .. pathId .. " node " .. nodeId)
-- Get the waypoint path
local path = GetWaypointPath(pathId)
if not path or not path.nodes then
print("[Admin Handlers] SELECT_WAYPOINT: Path not found")
return
end
-- Find the node
local node = nil
for _, n in ipairs(path.nodes) do
if n.id == nodeId then
node = n
break
end
end
if not node then
print("[Admin Handlers] SELECT_WAYPOINT: Node not found")
return
end
-- Notify player of selection
print("[Admin Handlers] SELECT_WAYPOINT: Node " .. nodeId .. " at " .. node.x .. ", " .. node.y .. ", " .. node.z)
-- Clear previous highlight if any (stored in shared data per player)
local playerKey = "waypoint_highlight_" .. tostring(player:GetGUIDLow())
local prevData = GetSharedData(playerKey)
if prevData then
local prev = Smallfolk.loads(prevData)
if prev and prev.pathId and prev.nodeId then
ClearWaypointMarkerAuras(player, prev.pathId, prev.nodeId)
end
end
-- Highlight the new waypoint marker by changing its display
-- Display ID is configurable via shared data (key: config_display_waypoint_highlight)
local highlightDisplayId = GetDisplayId("waypoint_highlight")
local success = HighlightWaypointMarker(player, pathId, nodeId, highlightDisplayId)
if success then
print("[Admin Handlers] SELECT_WAYPOINT: Highlighted marker with displayId " .. highlightDisplayId)
-- Store current selection for later cleanup
SetSharedData(playerKey, Smallfolk.dumps({ pathId = pathId, nodeId = nodeId }))
else
print("[Admin Handlers] SELECT_WAYPOINT: Could not find marker to highlight")
end
player:SendBroadcastMessage("Waypoint " .. nodeId .. " selected")
-- Send response with node position
AMS.Send(player, "WAYPOINT_SELECTED_RESPONSE", {
success = true,
pathId = pathId,
nodeId = nodeId,
x = node.x,
y = node.y,
z = node.z
})
end)
-- Get waypoint node info from a visual waypoint GUID (when targeting a waypoint marker)
AMS.RegisterHandler("GET_WAYPOINT_FOR_GUID", function(player, data)
if not data or not data.guid then
print("[Admin Handlers] GET_WAYPOINT_FOR_GUID: No GUID provided")
return
end
local pathId, nodeId = GetWaypointNodeForVisualGUID(data.guid)
if pathId == 0 or nodeId == 0 then
print("[Admin Handlers] GET_WAYPOINT_FOR_GUID: GUID not found in waypoint system")
AMS.Send(player, "WAYPOINT_FOR_GUID_RESPONSE", { success = false })
return
end
print("[Admin Handlers] GET_WAYPOINT_FOR_GUID: Found path " .. pathId .. " node " .. nodeId)
AMS.Send(player, "WAYPOINT_FOR_GUID_RESPONSE", {
success = true,
pathId = pathId,
nodeId = nodeId
})
end)
-- Get player data (GM state, etc.)
AMS.RegisterHandler("GET_PLAYER_DATA", function(player, data)
if not player then
return
end
-- Send player data including GM state (IsGM is the correct Eluna method)
AMS.Send(player, "PLAYER_DATA_RESPONSE", {
success = true,
isGM = player:IsGM()
})
end)
-- Set display config (for MCP to change waypoint marker appearance)
-- Keys: waypoint_marker, waypoint_highlight, spawn_marker
AMS.RegisterHandler("SET_DISPLAY_CONFIG", function(player, data)
if not data or not data.key or not data.displayId then
print("[Admin Handlers] SET_DISPLAY_CONFIG: Missing key or displayId")
return
end
local key = data.key
local displayId = tonumber(data.displayId)
if not displayId then
print("[Admin Handlers] SET_DISPLAY_CONFIG: Invalid displayId")
return
end
SetDisplayIdConfig(key, displayId)
print("[Admin Handlers] SET_DISPLAY_CONFIG: Set " .. key .. " = " .. displayId)
AMS.Send(player, "DISPLAY_CONFIG_RESPONSE", {
success = true,
key = key,
displayId = displayId,
message = "Display config updated. Re-show waypoints to see changes."
})
end)
-- Get current display configs
AMS.RegisterHandler("GET_DISPLAY_CONFIGS", function(player, data)
local configs = {}
for key, _ in pairs(DEFAULT_DISPLAYS) do
configs[key] = GetDisplayId(key)
end
AMS.Send(player, "DISPLAY_CONFIGS_RESPONSE", {
success = true,
configs = configs
})
end)
-- Teleport player to a waypoint location
AMS.RegisterHandler("TELEPORT_TO_WAYPOINT", function(player, data)
if not data or not data.x or not data.y or not data.z then
print("[Admin Handlers] TELEPORT_TO_WAYPOINT: Missing coordinates")
return
end
local x = data.x
local y = data.y
local z = data.z
local orientation = data.orientation or 0
print("[Admin Handlers] TELEPORT_TO_WAYPOINT: Teleporting to " .. x .. ", " .. y .. ", " .. z .. " facing " .. orientation)
-- Teleport player to the waypoint location with correct orientation
player:Teleport(player:GetMapId(), x, y, z, orientation)
player:SendBroadcastMessage("Teleported to waypoint")
end)
-- ============================================================================
-- Creature Edit Operations (Araxia Write Operations)
-- ============================================================================
--[[
SET_WANDER_DISTANCE - Set the wander distance for the selected creature
Request: { distance = 10.0 }
Response: { success = bool, message = string, newDistance = number }
Note: Creature must be selected (same as GET_NPC_DATA)
]]
AMS.RegisterHandler("SET_WANDER_DISTANCE", function(player, data)
if not data or data.distance == nil then
print("[Admin Handlers] SET_WANDER_DISTANCE: Missing distance")
AMS.Send(player, "SET_WANDER_DISTANCE_RESPONSE", {
success = false,
message = "Missing distance parameter"
})
return
end
print("[Admin Handlers] SET_WANDER_DISTANCE: Setting distance to " .. data.distance)
-- Use player's selection (same pattern as GET_NPC_DATA)
local creature = player:GetSelection()
if not creature then
AMS.Send(player, "SET_WANDER_DISTANCE_RESPONSE", {
success = false,
message = "No creature selected"
})
return
end
creature = creature:ToCreature()
if not creature then
AMS.Send(player, "SET_WANDER_DISTANCE_RESPONSE", {
success = false,
message = "Selected object is not a creature"
})
return
end
local spawnId = creature:GetDBTableGUIDLow()
local spawnIdStr = tostring(spawnId) -- Convert to string for table key
local homeX, homeY, homeZ, homeO = creature:GetHomePosition()
-- Check if markers were visible and hide them
local hadSpawnMarker = false
local hadRadiusMarkers = false
-- Try to hide spawn marker (via C++) - check if it returns true
local spawnHideSuccess, spawnHideResult = pcall(function() return creature:HideSpawnPointMarker() end)
if spawnHideSuccess and spawnHideResult then
hadSpawnMarker = true
print("[Admin Handlers] SET_WANDER_DISTANCE: Hid spawn marker")
end
-- Hide wander radius markers if they exist (use string key)
if wanderRadiusMarkers[spawnIdStr] then
hadRadiusMarkers = true
for _, marker in ipairs(wanderRadiusMarkers[spawnIdStr]) do
if marker and marker:IsInWorld() then
marker:DespawnOrUnsummon()
end
end
wanderRadiusMarkers[spawnIdStr] = nil
ClearSharedData("wander_markers_" .. spawnIdStr)
print("[Admin Handlers] SET_WANDER_DISTANCE: Hid radius markers")
end
-- Use the Araxia writer method to save to database
local success, message = creature:SaveWanderDistance(data.distance, player)
print("[Admin Handlers] SET_WANDER_DISTANCE: Result - " .. tostring(success) .. ", " .. message)
-- Despawn and respawn the creature so the new settings take effect immediately
if success then
-- Set a very short respawn delay (1 second), then despawn
creature:SetRespawnDelay(1)
creature:Respawn() -- Mark for respawn
creature:DespawnOrUnsummon(0) -- Remove from world
print("[Admin Handlers] SET_WANDER_DISTANCE: Creature despawned, respawning in 1 second")
-- Tell client markers need to be re-shown (client will handle the delay)
if hadSpawnMarker or hadRadiusMarkers then
print("[Admin Handlers] SET_WANDER_DISTANCE: Markers will need to be re-shown after respawn")
end
end
AMS.Send(player, "SET_WANDER_DISTANCE_RESPONSE", {
success = success,
message = message,
newDistance = data.distance,
markersCleared = hadSpawnMarker or hadRadiusMarkers,
hadSpawnMarker = hadSpawnMarker,
hadRadiusMarkers = hadRadiusMarkers
})
end)
--[[
SET_MOVEMENT_TYPE - Set the movement type for the selected creature
Request: { movementType = 0|1|2 }
Response: { success = bool, message = string, newMovementType = number }
Note: Creature must be selected
]]
AMS.RegisterHandler("SET_MOVEMENT_TYPE", function(player, data)
if not data or data.movementType == nil then
print("[Admin Handlers] SET_MOVEMENT_TYPE: Missing movementType")
AMS.Send(player, "SET_MOVEMENT_TYPE_RESPONSE", {
success = false,
message = "Missing movementType parameter"
})
return
end
print("[Admin Handlers] SET_MOVEMENT_TYPE: Setting type to " .. data.movementType)
local creature = player:GetSelection()
if not creature then
AMS.Send(player, "SET_MOVEMENT_TYPE_RESPONSE", {
success = false,
message = "No creature selected"
})
return
end
creature = creature:ToCreature()
if not creature then
AMS.Send(player, "SET_MOVEMENT_TYPE_RESPONSE", {
success = false,
message = "Selected object is not a creature"
})
return
end
local success, message = creature:SaveMovementType(data.movementType, player)
print("[Admin Handlers] SET_MOVEMENT_TYPE: Result - " .. tostring(success) .. ", " .. message)
AMS.Send(player, "SET_MOVEMENT_TYPE_RESPONSE", {
success = success,
message = message,
newMovementType = data.movementType
})
end)
-- ============================================================================
-- Agent Chat Handlers
-- ============================================================================
-- Subscribed players for agent response delivery (guid -> player_name)
-- NOTE: Stored in ElunaSharedData to persist across .reload eluna
local SUBSCRIBERS_KEY = "agent_chat_subscribers"
local function getSubscribers()
local data = GetSharedData(SUBSCRIBERS_KEY)
if not data or data == "" then return {} end
-- Parse simple format: "guid1:name1,guid2:name2"
local subs = {}
for pair in data:gmatch("([^,]+)") do
local guid, name = pair:match("(%d+):(.+)")
if guid and name then
subs[tonumber(guid)] = name
end
end
return subs
end
local function saveSubscribers(subs)
local parts = {}
for guid, name in pairs(subs) do
table.insert(parts, tostring(guid) .. ":" .. name)
end
SetSharedData(SUBSCRIBERS_KEY, table.concat(parts, ","))
end
-- Response delivery timer (checks every 2 seconds)
local RESPONSE_CHECK_INTERVAL = 2000 -- milliseconds
--[[
AGENT_LIST_REQUEST - Get list of registered AI agents
Request: {}
Response: { success = bool, agents = [{name, online, owner, description}] }
]]
-- Simple JSON helpers for agent chat (MCP uses JSON, not Smallfolk)
local function parseJsonAgentRegistry(jsonStr)
-- Parse: {"AgentName":{"owner":"X","description":"Y","last_poll":123,...},...}
local agents = {}
if not jsonStr or jsonStr == "" or jsonStr == "{}" then return agents end
-- Extract each agent entry: "Name":{...}
for name, content in jsonStr:gmatch('"([^"]+)":%s*{([^}]*)}') do
local agent = { name = name }
agent.owner = content:match('"owner":"([^"]*)"') or "Unknown"
agent.description = content:match('"description":"([^"]*)"') or ""
agent.last_poll = tonumber(content:match('"last_poll":(%d+)')) or 0
agent.status = content:match('"status":"([^"]*)"') or "offline"
table.insert(agents, agent)
end
return agents
end
local function escapeJsonString(s)
if not s then return "" end
s = tostring(s)
s = s:gsub('\\', '\\\\')
s = s:gsub('"', '\\"')
s = s:gsub('\n', '\\n')
s = s:gsub('\r', '\\r')
s = s:gsub('\t', '\\t')
return s
end
local function encodeJsonMessage(msg)
-- Encode a message as JSON object
local parts = {}
table.insert(parts, '"message_id":"' .. escapeJsonString(msg.message_id) .. '"')
table.insert(parts, '"from_player_guid":' .. tostring(msg.from_player_guid or 0))
table.insert(parts, '"from_player_name":"' .. escapeJsonString(msg.from_player_name) .. '"')
table.insert(parts, '"content":"' .. escapeJsonString(msg.content) .. '"')
table.insert(parts, '"timestamp":' .. (msg.timestamp or 0))
if msg.context then
-- Simple context encoding
local ctxParts = {}
for k, v in pairs(msg.context) do
if type(v) == "string" then
table.insert(ctxParts, '"' .. escapeJsonString(k) .. '":"' .. escapeJsonString(v) .. '"')
elseif type(v) == "number" then
table.insert(ctxParts, '"' .. escapeJsonString(k) .. '":' .. v)
end
end
table.insert(parts, '"context":{' .. table.concat(ctxParts, ",") .. '}')
end
return "{" .. table.concat(parts, ",") .. "}"
end
local function parseJsonArray(jsonStr)
-- Parse JSON array of messages: [{...},{...}]
local messages = {}
if not jsonStr or jsonStr == "" or jsonStr == "[]" then return messages end
-- Extract content field more carefully (handles escaped quotes)
-- Pattern explanation: match "content":" then capture everything until unescaped quote
local function extractContent(json)
local start = json:find('"content":"')
if not start then return nil end
local contentStart = start + 11 -- length of '"content":"'
local result = {}
local i = contentStart
while i <= #json do
local c = json:sub(i, i)
if c == '\\' and i < #json then
-- Escaped character - include the next char
local nextC = json:sub(i+1, i+1)
if nextC == '"' then
table.insert(result, '"')
elseif nextC == 'n' then
table.insert(result, '\n')
elseif nextC == '\\' then
table.insert(result, '\\')
else
table.insert(result, nextC)
end
i = i + 2
elseif c == '"' then
-- End of content string
break
else
table.insert(result, c)
i = i + 1
end
end
return table.concat(result)
end
-- Simple extraction of message objects (handles both player->agent and agent->player formats)
for msgJson in jsonStr:gmatch('{[^{}]+}') do
local msg = {}
msg.message_id = msgJson:match('"message_id":"([^"]*)"')
msg.from_player_guid = tonumber(msgJson:match('"from_player_guid":(%d+)'))
msg.from_player_name = msgJson:match('"from_player_name":"([^"]*)"')
msg.from_agent = msgJson:match('"from_agent":"([^"]*)"') -- For agent->player messages
msg.content = extractContent(msgJson) -- Use proper extraction for content
msg.timestamp = tonumber(msgJson:match('"timestamp":(%d+)'))
table.insert(messages, msg)
end
return messages
end
local function encodeJsonArray(messages)
local parts = {}
for _, msg in ipairs(messages) do
table.insert(parts, encodeJsonMessage(msg))
end
return "[" .. table.concat(parts, ",") .. "]"
end
AMS.RegisterHandler("AGENT_LIST_REQUEST", function(player, data)
print("[Admin Handlers] AGENT_LIST_REQUEST received")
local agents = {}
local registryData = GetSharedData("agent_registry")
if registryData and registryData ~= "" then
agents = parseJsonAgentRegistry(registryData)
-- Update online status based on last_poll
local now = os.time()
for _, agent in ipairs(agents) do
agent.online = (now - (agent.last_poll or 0)) < 60
end
print("[Admin Handlers] AGENT_LIST_REQUEST: Found " .. #agents .. " agents")
else
print("[Admin Handlers] AGENT_LIST_REQUEST: No agent registry found")
end
AMS.Send(player, "AGENT_LIST_RESPONSE", {
success = true,
agents = agents
})
end)
--[[
AGENT_SEND_MESSAGE - Player sends message to an AI agent
Request: { agent_name = string, content = string, context = optional table }
Response: { success = bool, message_id = string, error = string }
]]
AMS.RegisterHandler("AGENT_SEND_MESSAGE", function(player, data)
SetSharedData("debug_agent_send", "step1")
if not data or not data.agent_name or not data.content then
SetSharedData("debug_agent_send", "step2_missing_data")
AMS.Send(player, "AGENT_SEND_MESSAGE_RESPONSE", {
success = false,
error = "Missing agent_name or content"
})
return
end
SetSharedData("debug_agent_send", "step3_" .. data.agent_name)
local agentName = data.agent_name
local content = data.content
local context = data.context
-- Check if agent exists (parse JSON registry)
local registryData = GetSharedData("agent_registry")
SetSharedData("debug_agent_send", "step4_reg")
if not registryData or registryData == "" then
SetSharedData("debug_agent_send", "step4_no_registry")
AMS.Send(player, "AGENT_SEND_MESSAGE_RESPONSE", {
success = false,
error = "No agents registered"
})
return
end
SetSharedData("debug_agent_send", "step5_parsing")
local agents = parseJsonAgentRegistry(registryData)
SetSharedData("debug_agent_send", "step6_found_" .. #agents)
-- Case-insensitive agent lookup
local foundName = nil
for _, agent in ipairs(agents) do
if agent.name:lower() == agentName:lower() then
foundName = agent.name
break
end
end
SetSharedData("debug_agent_send", "step7_lookup_" .. tostring(foundName))
if not foundName then
AMS.Send(player, "AGENT_SEND_MESSAGE_RESPONSE", {
success = false,
error = "Agent '" .. agentName .. "' not found"
})
return
end
-- Generate message ID
local messageId = "msg_" .. tostring(os.time()) .. "_" .. tostring(player:GetGUIDLow())
SetSharedData("debug_agent_send", "step8_" .. messageId)
-- Create message
local message = {
message_id = messageId,
from_player_guid = player:GetGUIDLow(),
from_player_name = player:GetName(),
content = content,
timestamp = os.time(),
context = context
}
-- Add to agent's inbox (using JSON format)
local inboxKey = "agent_inbox_" .. foundName
local inboxData = GetSharedData(inboxKey)
local inbox = {}
if inboxData and inboxData ~= "" then
inbox = parseJsonArray(inboxData)
end
table.insert(inbox, message)
SetSharedData("debug_agent_send", "step9_queued_" .. #inbox)
-- Cap inbox size (100 messages max)
while #inbox > 100 do
table.remove(inbox, 1)
end
SetSharedData("debug_agent_send", "step9b_encoding")
local ok, encoded = pcall(encodeJsonArray, inbox)
if not ok then
SetSharedData("debug_agent_send", "step9b_error_" .. tostring(encoded))
return
end
SetSharedData("debug_agent_send", "step9c_encoded_len_" .. #encoded)
SetSharedData(inboxKey, encoded)
SetSharedData("debug_agent_send", "step10_saved_" .. inboxKey)
AMS.Send(player, "AGENT_SEND_MESSAGE_RESPONSE", {
success = true,
message_id = messageId
})
end)
--[[
AGENT_CHAT_SUBSCRIBE - Subscribe to agent response delivery
Request: {}
Response: (none - just tracks subscription)
]]
AMS.RegisterHandler("AGENT_CHAT_SUBSCRIBE", function(player, data)
local guid = player:GetGUIDLow()
local subs = getSubscribers()
subs[guid] = player:GetName() -- Store name for lookup
saveSubscribers(subs)
print("[Admin Handlers] AGENT_CHAT_SUBSCRIBE: Player " .. player:GetName() .. " subscribed")
end)
--[[
AGENT_CHAT_UNSUBSCRIBE - Unsubscribe from agent response delivery
Request: {}
Response: (none)
]]
AMS.RegisterHandler("AGENT_CHAT_UNSUBSCRIBE", function(player, data)
local guid = player:GetGUIDLow()
local subs = getSubscribers()
subs[guid] = nil
saveSubscribers(subs)
print("[Admin Handlers] AGENT_CHAT_UNSUBSCRIBE: Player " .. player:GetName() .. " unsubscribed")
end)
--[[
AGENT_POLL_RESPONSES - Manual poll for agent responses (fallback)
Request: {}
Response: { success = bool, messages = [...] }
]]
AMS.RegisterHandler("AGENT_POLL_RESPONSES", function(player, data)
local guid = player:GetGUIDLow()
local inboxKey = "player_inbox_" .. tostring(guid)
local inboxData = GetSharedData(inboxKey)
local messages = {}
if inboxData and inboxData ~= "" then
-- Parse JSON array from MCP
messages = parseJsonArray(inboxData)
-- Clear after reading
ClearSharedData(inboxKey)
end
AMS.Send(player, "AGENT_POLL_RESPONSES_RESULT", {
success = true,
messages = messages
})
end)
-- Timer to check for and deliver agent responses to subscribed players
local function CheckAndDeliverAgentResponses()
local agentChatSubscribers = getSubscribers()
local subsChanged = false
for guid, playerName in pairs(agentChatSubscribers) do
local inboxKey = "player_inbox_" .. tostring(guid)
local inboxData = GetSharedData(inboxKey)
if inboxData and inboxData ~= "" then
-- Parse JSON array from MCP
local messages = parseJsonArray(inboxData)
if #messages > 0 then
-- Find the player by name
local player = GetPlayerByName(playerName)
if player then
print("[Admin Handlers] Delivering " .. #messages .. " agent responses to " .. playerName)
AMS.Send(player, "AGENT_MESSAGE_RESPONSE", {
success = true,
messages = messages
})
-- Clear delivered messages
ClearSharedData(inboxKey)
else
-- Player offline, remove from subscribers
agentChatSubscribers[guid] = nil
subsChanged = true
end
end
end
end
if subsChanged then
saveSubscribers(agentChatSubscribers)
end
end
-- Start response delivery timer
CreateLuaEvent(CheckAndDeliverAgentResponses, RESPONSE_CHECK_INTERVAL, 0) -- 0 = repeat forever
print("[Admin Handlers] Agent response delivery timer started (every " .. (RESPONSE_CHECK_INTERVAL/1000) .. "s)")
-- ============================================================================
-- Initialization
-- ============================================================================
print("[Admin Handlers] Loaded successfully!")
print("[Admin Handlers] Registered handlers:")
print(" - GET_NPC_DATA")
print(" - SHOW_WAYPOINTS")
print(" - HIDE_WAYPOINTS")
print(" - HIDE_WAYPOINTS_BY_GUID")
print(" - CLEAR_ALL_WAYPOINT_MARKERS")
print(" - SHOW_SPAWN_MARKER")
print(" - HIDE_SPAWN_MARKER")
print(" - SHOW_WANDER_RADIUS")
print(" - HIDE_WANDER_RADIUS")
print(" - CLEAR_WANDER_MARKERS")
print(" - CLEAR_NEARBY_MARKERS")
print(" - GET_WAYPOINT_DETAILS")
print(" - SELECT_WAYPOINT")
print(" - GET_WAYPOINT_FOR_GUID")
print(" - GET_PLAYER_DATA")
print(" - TELEPORT_TO_WAYPOINT")
print(" - SET_WANDER_DISTANCE")
print(" - SET_MOVEMENT_TYPE")
print(" - AGENT_LIST_REQUEST")
print(" - AGENT_SEND_MESSAGE")
print(" - AGENT_CHAT_SUBSCRIBE")
print(" - AGENT_CHAT_UNSUBSCRIBE")
print(" - AGENT_POLL_RESPONSES")