mirror of
https://github.com/araxiaonline/TrinityCore.git
synced 2026-06-13 03:32:28 -04:00
Add of first attempt to get client talking to LLM
This commit is contained in:
@@ -9,7 +9,9 @@
|
||||
Core.lua
|
||||
ServerDataModule.lua
|
||||
AMSTestClient.lua
|
||||
MCPBridge.lua
|
||||
UI/MainWindow.lua
|
||||
UI/MinimapButton.lua
|
||||
UI/Panels/NPCInfoPanel.lua
|
||||
UI/Panels/AddNPCPanel.lua
|
||||
UI/Panels/AgentChatPanel.lua
|
||||
|
||||
@@ -222,6 +222,36 @@ UIParent
|
||||
- `|cFF0070DD` - Blue (rare)
|
||||
- `|cFFFF00FF` - Purple (rare elite)
|
||||
|
||||
### Agent Chat Panel (`UI/Panels/AgentChatPanel.lua`)
|
||||
Bidirectional chat interface for communicating with AI agents (like Cascade).
|
||||
|
||||
**Features:**
|
||||
- Agent selector dropdown with online/offline status
|
||||
- Chat history scrollframe with timestamps
|
||||
- Message input with Enter-to-send
|
||||
- Auto-subscribe to push updates when panel opens
|
||||
- Context attachment (target info) with messages
|
||||
|
||||
**Data Flow:**
|
||||
1. Player sends message via AMS `AGENT_SEND_MESSAGE`
|
||||
2. Server queues in ElunaSharedData (`agent_inbox_<name>`)
|
||||
3. AI agent polls via MCP `mcp_agent_poll_messages`
|
||||
4. AI agent responds via MCP `mcp_agent_send_message`
|
||||
5. Server pushes to player via AMS `AGENT_MESSAGE_RESPONSE`
|
||||
|
||||
**AMS Handlers (Client):**
|
||||
- `AGENT_LIST_RESPONSE` - Updates agent dropdown
|
||||
- `AGENT_SEND_MESSAGE_RESPONSE` - Confirms message sent
|
||||
- `AGENT_MESSAGE_RESPONSE` - Receives agent responses
|
||||
|
||||
**For AI Agents (MCP Tools):**
|
||||
```
|
||||
1. mcp_agent_register({name: "Scarlet", owner: "Cascade"})
|
||||
2. mcp_agent_poll_messages({name: "Scarlet"}) - returns pending messages
|
||||
3. mcp_agent_send_message({name: "Scarlet", to_player_guid: X, content: "..."})
|
||||
4. mcp_agent_unregister({name: "Scarlet"}) - when done
|
||||
```
|
||||
|
||||
## Future Expansion Ideas
|
||||
- Item Info Panel - View item details, stats, sources
|
||||
- Quest Info Panel - Quest chains, requirements, rewards
|
||||
@@ -232,4 +262,5 @@ UIParent
|
||||
- Macro Builder - Generate TrinityCore command macros
|
||||
|
||||
## Version History
|
||||
- v1.1.0 - Added Agent Chat panel for AI assistant communication
|
||||
- v1.0.0 - Initial release with NPC Info panel, tab navigation, 3D model viewer
|
||||
|
||||
@@ -97,6 +97,20 @@ mainWindow:SetScript("OnHide", function(self)
|
||||
AraxiaTrinityAdminDB.windowShown = false
|
||||
end)
|
||||
|
||||
-- Movement-based opacity: 70% opaque when moving, 100% when stopped
|
||||
local MOVING_ALPHA = 0.3 -- 70% opaque = 30% alpha
|
||||
local STOPPED_ALPHA = 1.0
|
||||
local movementFrame = CreateFrame("Frame")
|
||||
movementFrame:RegisterEvent("PLAYER_STARTED_MOVING")
|
||||
movementFrame:RegisterEvent("PLAYER_STOPPED_MOVING")
|
||||
movementFrame:SetScript("OnEvent", function(self, event)
|
||||
if event == "PLAYER_STARTED_MOVING" then
|
||||
mainWindow:SetAlpha(MOVING_ALPHA)
|
||||
elseif event == "PLAYER_STOPPED_MOVING" then
|
||||
mainWindow:SetAlpha(STOPPED_ALPHA)
|
||||
end
|
||||
end)
|
||||
|
||||
-- Determine content area (Inset or fallback to mainWindow with margins)
|
||||
local contentArea = mainWindow.Inset or mainWindow
|
||||
local topOffset = mainWindow.Inset and -4 or -30 -- Account for title bar if no Inset
|
||||
|
||||
@@ -0,0 +1,434 @@
|
||||
-- AraxiaTrinityAdmin Agent Chat Panel
|
||||
-- Chat interface for communicating with AI agents
|
||||
|
||||
local addonName = "AraxiaTrinityAdmin"
|
||||
|
||||
-- Wait for addon to load
|
||||
local initFrame = CreateFrame("Frame")
|
||||
initFrame:RegisterEvent("ADDON_LOADED")
|
||||
initFrame:SetScript("OnEvent", function(self, event, loadedAddon)
|
||||
if loadedAddon ~= addonName then return end
|
||||
self:UnregisterEvent("ADDON_LOADED")
|
||||
|
||||
local ATA = AraxiaTrinityAdmin
|
||||
if not ATA then return end
|
||||
|
||||
-- Create panel frame
|
||||
local chatPanel = CreateFrame("Frame", "AraxiaTrinityAdminAgentChatPanel", UIParent)
|
||||
chatPanel:Hide()
|
||||
|
||||
-- Chat history storage
|
||||
local chatHistory = {} -- { {from="player"|"agent", agent_name, content, timestamp, message_id}, ... }
|
||||
local MAX_HISTORY = 200
|
||||
|
||||
-- Current selected agent
|
||||
local selectedAgent = nil
|
||||
local availableAgents = {}
|
||||
|
||||
-- Polling state
|
||||
local isSubscribed = false
|
||||
|
||||
-- ============================================================================
|
||||
-- Header Section
|
||||
-- ============================================================================
|
||||
|
||||
local title = chatPanel:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
|
||||
title:SetPoint("TOPLEFT", chatPanel, "TOPLEFT", 10, -10)
|
||||
title:SetText("Agent Chat")
|
||||
|
||||
-- Agent selector dropdown
|
||||
local agentDropdown = CreateFrame("Frame", "AgentChatAgentDropdown", chatPanel, "UIDropDownMenuTemplate")
|
||||
agentDropdown:SetPoint("LEFT", title, "RIGHT", 10, -2)
|
||||
|
||||
local function AgentDropdown_OnClick(self, arg1, arg2, checked)
|
||||
selectedAgent = arg1
|
||||
UIDropDownMenu_SetText(agentDropdown, arg1 or "Select Agent")
|
||||
end
|
||||
|
||||
local function AgentDropdown_Initialize(self, level)
|
||||
local info = UIDropDownMenu_CreateInfo()
|
||||
|
||||
if #availableAgents == 0 then
|
||||
info.text = "No agents available"
|
||||
info.disabled = true
|
||||
info.notCheckable = true
|
||||
UIDropDownMenu_AddButton(info)
|
||||
return
|
||||
end
|
||||
|
||||
for _, agent in ipairs(availableAgents) do
|
||||
info.text = agent.name .. (agent.online and " |cFF00FF00(online)|r" or " |cFF888888(offline)|r")
|
||||
info.arg1 = agent.name
|
||||
info.func = AgentDropdown_OnClick
|
||||
info.checked = (selectedAgent == agent.name)
|
||||
info.notCheckable = false
|
||||
UIDropDownMenu_AddButton(info)
|
||||
end
|
||||
end
|
||||
|
||||
UIDropDownMenu_SetWidth(agentDropdown, 150)
|
||||
UIDropDownMenu_SetText(agentDropdown, "Select Agent")
|
||||
UIDropDownMenu_Initialize(agentDropdown, AgentDropdown_Initialize)
|
||||
|
||||
-- Refresh agents button
|
||||
local refreshAgentsBtn = CreateFrame("Button", nil, chatPanel, "UIPanelButtonTemplate")
|
||||
refreshAgentsBtn:SetSize(24, 24)
|
||||
refreshAgentsBtn:SetPoint("LEFT", agentDropdown, "RIGHT", -10, 2)
|
||||
refreshAgentsBtn:SetText("R")
|
||||
refreshAgentsBtn:SetScript("OnClick", function()
|
||||
if AMS then
|
||||
AMS.Send("AGENT_LIST_REQUEST", {})
|
||||
end
|
||||
end)
|
||||
refreshAgentsBtn:SetScript("OnEnter", function(self)
|
||||
GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
|
||||
GameTooltip:AddLine("Refresh agent list", 1, 1, 1)
|
||||
GameTooltip:Show()
|
||||
end)
|
||||
refreshAgentsBtn:SetScript("OnLeave", function() GameTooltip:Hide() end)
|
||||
|
||||
-- Status indicator
|
||||
local statusText = chatPanel:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
|
||||
statusText:SetPoint("TOPRIGHT", chatPanel, "TOPRIGHT", -10, -14)
|
||||
statusText:SetText("|cFF888888Disconnected|r")
|
||||
|
||||
-- ============================================================================
|
||||
-- Chat History Display
|
||||
-- ============================================================================
|
||||
|
||||
local chatContainer = CreateFrame("Frame", nil, chatPanel, "BackdropTemplate")
|
||||
chatContainer:SetPoint("TOPLEFT", chatPanel, "TOPLEFT", 10, -45)
|
||||
chatContainer:SetPoint("BOTTOMRIGHT", chatPanel, "BOTTOMRIGHT", -10, 50)
|
||||
chatContainer:SetBackdrop({
|
||||
bgFile = "Interface/Tooltips/UI-Tooltip-Background",
|
||||
edgeFile = "Interface/Tooltips/UI-Tooltip-Border",
|
||||
tile = true, tileSize = 16, edgeSize = 16,
|
||||
insets = { left = 4, right = 4, top = 4, bottom = 4 }
|
||||
})
|
||||
chatContainer:SetBackdropColor(0.05, 0.05, 0.05, 0.9)
|
||||
chatContainer:SetBackdropBorderColor(0.4, 0.4, 0.4, 1)
|
||||
|
||||
local chatScroll = CreateFrame("ScrollFrame", "AgentChatScrollFrame", chatContainer, "UIPanelScrollFrameTemplate")
|
||||
chatScroll:SetPoint("TOPLEFT", chatContainer, "TOPLEFT", 8, -8)
|
||||
chatScroll:SetPoint("BOTTOMRIGHT", chatContainer, "BOTTOMRIGHT", -28, 8)
|
||||
|
||||
local chatScrollChild = CreateFrame("Frame", nil, chatScroll)
|
||||
chatScrollChild:SetWidth(chatScroll:GetWidth() - 10)
|
||||
chatScrollChild:SetHeight(1)
|
||||
chatScroll:SetScrollChild(chatScrollChild)
|
||||
|
||||
-- Message display elements
|
||||
local messageFrames = {}
|
||||
|
||||
local function FormatTimestamp(ts)
|
||||
if not ts then return "" end
|
||||
return date("%H:%M", ts)
|
||||
end
|
||||
|
||||
local function AddMessageToDisplay(from, agentName, content, timestamp, isFromPlayer)
|
||||
local frame = CreateFrame("Frame", nil, chatScrollChild)
|
||||
frame:SetWidth(chatScrollChild:GetWidth() - 10)
|
||||
|
||||
-- Header line (name + timestamp)
|
||||
local header = frame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
|
||||
header:SetPoint("TOPLEFT", frame, "TOPLEFT", 5, -5)
|
||||
|
||||
if isFromPlayer then
|
||||
header:SetText("|cFF00CCFF" .. UnitName("player") .. "|r |cFF888888" .. FormatTimestamp(timestamp) .. "|r")
|
||||
else
|
||||
header:SetText("|cFF00FF00" .. (agentName or "Agent") .. "|r |cFF888888" .. FormatTimestamp(timestamp) .. "|r")
|
||||
end
|
||||
|
||||
-- Content
|
||||
local contentText = frame:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
|
||||
contentText:SetPoint("TOPLEFT", header, "BOTTOMLEFT", 0, -3)
|
||||
contentText:SetPoint("RIGHT", frame, "RIGHT", -5, 0)
|
||||
contentText:SetJustifyH("LEFT")
|
||||
contentText:SetText(content)
|
||||
contentText:SetWordWrap(true)
|
||||
|
||||
-- Calculate height
|
||||
local contentHeight = contentText:GetStringHeight()
|
||||
frame:SetHeight(header:GetStringHeight() + contentHeight + 15)
|
||||
|
||||
-- Position frame
|
||||
local yOffset = 0
|
||||
for _, f in ipairs(messageFrames) do
|
||||
yOffset = yOffset + f:GetHeight() + 5
|
||||
end
|
||||
frame:SetPoint("TOPLEFT", chatScrollChild, "TOPLEFT", 0, -yOffset)
|
||||
|
||||
table.insert(messageFrames, frame)
|
||||
|
||||
-- Update scroll child height
|
||||
chatScrollChild:SetHeight(yOffset + frame:GetHeight() + 10)
|
||||
|
||||
-- Scroll to bottom
|
||||
C_Timer.After(0.01, function()
|
||||
chatScroll:SetVerticalScroll(chatScroll:GetVerticalScrollRange())
|
||||
end)
|
||||
|
||||
return frame
|
||||
end
|
||||
|
||||
local function ClearMessageDisplay()
|
||||
for _, frame in ipairs(messageFrames) do
|
||||
frame:Hide()
|
||||
frame:SetParent(nil)
|
||||
end
|
||||
messageFrames = {}
|
||||
chatScrollChild:SetHeight(1)
|
||||
end
|
||||
|
||||
local function RefreshChatDisplay()
|
||||
ClearMessageDisplay()
|
||||
|
||||
for _, msg in ipairs(chatHistory) do
|
||||
local isFromPlayer = (msg.from == "player")
|
||||
AddMessageToDisplay(msg.from, msg.agent_name, msg.content, msg.timestamp, isFromPlayer)
|
||||
end
|
||||
end
|
||||
|
||||
-- ============================================================================
|
||||
-- Message Input
|
||||
-- ============================================================================
|
||||
|
||||
local inputContainer = CreateFrame("Frame", nil, chatPanel, "BackdropTemplate")
|
||||
inputContainer:SetPoint("BOTTOMLEFT", chatPanel, "BOTTOMLEFT", 10, 10)
|
||||
inputContainer:SetPoint("BOTTOMRIGHT", chatPanel, "BOTTOMRIGHT", -80, 10)
|
||||
inputContainer:SetHeight(30)
|
||||
inputContainer:SetBackdrop({
|
||||
bgFile = "Interface/Tooltips/UI-Tooltip-Background",
|
||||
edgeFile = "Interface/Tooltips/UI-Tooltip-Border",
|
||||
tile = true, tileSize = 16, edgeSize = 12,
|
||||
insets = { left = 3, right = 3, top = 3, bottom = 3 }
|
||||
})
|
||||
inputContainer:SetBackdropColor(0.1, 0.1, 0.1, 0.8)
|
||||
inputContainer:SetBackdropBorderColor(0.3, 0.3, 0.3, 1)
|
||||
|
||||
local inputBox = CreateFrame("EditBox", "AgentChatInputBox", inputContainer)
|
||||
inputBox:SetPoint("TOPLEFT", inputContainer, "TOPLEFT", 8, -6)
|
||||
inputBox:SetPoint("BOTTOMRIGHT", inputContainer, "BOTTOMRIGHT", -8, 6)
|
||||
inputBox:SetFontObject("ChatFontNormal")
|
||||
inputBox:SetAutoFocus(false)
|
||||
inputBox:SetMaxLetters(1000)
|
||||
|
||||
local function SendMessage()
|
||||
local text = inputBox:GetText()
|
||||
if not text or text == "" then return end
|
||||
|
||||
if not selectedAgent then
|
||||
print("|cFFFF0000[Agent Chat]|r Please select an agent first")
|
||||
return
|
||||
end
|
||||
|
||||
if not AMS then
|
||||
print("|cFFFF0000[Agent Chat]|r AMS not available")
|
||||
return
|
||||
end
|
||||
|
||||
-- Get context (current target info)
|
||||
local context = nil
|
||||
if UnitExists("target") then
|
||||
local guid = UnitGUID("target")
|
||||
context = {
|
||||
target_guid = guid,
|
||||
target_name = UnitName("target"),
|
||||
target_level = UnitLevel("target")
|
||||
}
|
||||
end
|
||||
|
||||
-- Send message
|
||||
AMS.Send("AGENT_SEND_MESSAGE", {
|
||||
agent_name = selectedAgent,
|
||||
content = text,
|
||||
context = context
|
||||
})
|
||||
|
||||
-- Add to local history immediately
|
||||
local msg = {
|
||||
from = "player",
|
||||
agent_name = selectedAgent,
|
||||
content = text,
|
||||
timestamp = time(),
|
||||
message_id = "local_" .. time()
|
||||
}
|
||||
table.insert(chatHistory, msg)
|
||||
|
||||
-- Cap history
|
||||
while #chatHistory > MAX_HISTORY do
|
||||
table.remove(chatHistory, 1)
|
||||
end
|
||||
|
||||
-- Refresh display
|
||||
AddMessageToDisplay("player", selectedAgent, text, msg.timestamp, true)
|
||||
|
||||
-- Clear input
|
||||
inputBox:SetText("")
|
||||
end
|
||||
|
||||
inputBox:SetScript("OnEnterPressed", function()
|
||||
SendMessage()
|
||||
end)
|
||||
|
||||
inputBox:SetScript("OnEscapePressed", function(self)
|
||||
self:ClearFocus()
|
||||
end)
|
||||
|
||||
-- Send button
|
||||
local sendBtn = CreateFrame("Button", nil, chatPanel, "UIPanelButtonTemplate")
|
||||
sendBtn:SetSize(60, 30)
|
||||
sendBtn:SetPoint("LEFT", inputContainer, "RIGHT", 5, 0)
|
||||
sendBtn:SetText("Send")
|
||||
sendBtn:SetScript("OnClick", SendMessage)
|
||||
|
||||
-- ============================================================================
|
||||
-- AMS Response Handlers
|
||||
-- ============================================================================
|
||||
|
||||
local function InitAMSHandlers()
|
||||
if not AMS then
|
||||
C_Timer.After(0.5, InitAMSHandlers)
|
||||
return
|
||||
end
|
||||
|
||||
-- Handle agent list response
|
||||
AMS.RegisterHandler("AGENT_LIST_RESPONSE", function(data)
|
||||
if data.success then
|
||||
availableAgents = data.agents or {}
|
||||
UIDropDownMenu_Initialize(agentDropdown, AgentDropdown_Initialize)
|
||||
|
||||
-- Auto-select first agent if none selected
|
||||
if not selectedAgent and #availableAgents > 0 then
|
||||
selectedAgent = availableAgents[1].name
|
||||
UIDropDownMenu_SetText(agentDropdown, selectedAgent)
|
||||
end
|
||||
|
||||
print("|cFF00FF00[Agent Chat]|r Found " .. #availableAgents .. " agent(s)")
|
||||
end
|
||||
end)
|
||||
|
||||
-- Handle send confirmation
|
||||
AMS.RegisterHandler("AGENT_SEND_MESSAGE_RESPONSE", function(data)
|
||||
if not data.success then
|
||||
print("|cFFFF0000[Agent Chat]|r Failed to send: " .. (data.error or "Unknown error"))
|
||||
end
|
||||
end)
|
||||
|
||||
-- Handle agent responses (push delivery)
|
||||
AMS.RegisterHandler("AGENT_MESSAGE_RESPONSE", function(data)
|
||||
if data.success and data.messages then
|
||||
for _, msg in ipairs(data.messages) do
|
||||
-- Add to history
|
||||
local historyEntry = {
|
||||
from = "agent",
|
||||
agent_name = msg.from_agent,
|
||||
content = msg.content,
|
||||
timestamp = msg.timestamp,
|
||||
message_id = msg.message_id,
|
||||
reply_to_id = msg.reply_to_id
|
||||
}
|
||||
table.insert(chatHistory, historyEntry)
|
||||
|
||||
-- Add to display
|
||||
AddMessageToDisplay("agent", msg.from_agent, msg.content, msg.timestamp, false)
|
||||
|
||||
-- Show notification if panel not visible
|
||||
if not chatPanel:IsVisible() then
|
||||
print("|cFF00FF00[" .. msg.from_agent .. "]|r " .. msg.content:sub(1, 100) .. (msg.content:len() > 100 and "..." or ""))
|
||||
end
|
||||
end
|
||||
|
||||
-- Cap history
|
||||
while #chatHistory > MAX_HISTORY do
|
||||
table.remove(chatHistory, 1)
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
-- Handle poll responses (manual polling fallback)
|
||||
AMS.RegisterHandler("AGENT_POLL_RESPONSES_RESULT", function(data)
|
||||
if data.success and data.messages then
|
||||
for _, msg in ipairs(data.messages) do
|
||||
local historyEntry = {
|
||||
from = "agent",
|
||||
agent_name = msg.from_agent,
|
||||
content = msg.content,
|
||||
timestamp = msg.timestamp,
|
||||
message_id = msg.message_id
|
||||
}
|
||||
table.insert(chatHistory, historyEntry)
|
||||
AddMessageToDisplay("agent", msg.from_agent, msg.content, msg.timestamp, false)
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
print("[Agent Chat] AMS handlers registered")
|
||||
end
|
||||
|
||||
-- ============================================================================
|
||||
-- Panel Lifecycle
|
||||
-- ============================================================================
|
||||
|
||||
-- Subscribe/unsubscribe when panel shows/hides
|
||||
chatPanel:SetScript("OnShow", function()
|
||||
if AMS then
|
||||
AMS.Send("AGENT_CHAT_SUBSCRIBE", {})
|
||||
AMS.Send("AGENT_LIST_REQUEST", {})
|
||||
isSubscribed = true
|
||||
statusText:SetText("|cFF00FF00Connected|r")
|
||||
end
|
||||
inputBox:SetFocus()
|
||||
end)
|
||||
|
||||
chatPanel:SetScript("OnHide", function()
|
||||
if AMS and isSubscribed then
|
||||
AMS.Send("AGENT_CHAT_UNSUBSCRIBE", {})
|
||||
isSubscribed = false
|
||||
statusText:SetText("|cFF888888Disconnected|r")
|
||||
end
|
||||
inputBox:ClearFocus()
|
||||
end)
|
||||
|
||||
-- Manual poll button (hidden, for debugging)
|
||||
local pollBtn = CreateFrame("Button", nil, chatPanel, "UIPanelButtonTemplate")
|
||||
pollBtn:SetSize(60, 22)
|
||||
pollBtn:SetPoint("BOTTOMRIGHT", chatPanel, "BOTTOMRIGHT", -10, 45)
|
||||
pollBtn:SetText("Poll")
|
||||
pollBtn:Hide() -- Hidden by default
|
||||
pollBtn:SetScript("OnClick", function()
|
||||
if AMS then
|
||||
AMS.Send("AGENT_POLL_RESPONSES", {})
|
||||
end
|
||||
end)
|
||||
|
||||
-- Clear chat button
|
||||
local clearBtn = CreateFrame("Button", nil, chatPanel, "UIPanelButtonTemplate")
|
||||
clearBtn:SetSize(50, 22)
|
||||
clearBtn:SetPoint("TOPRIGHT", chatPanel, "TOPRIGHT", -10, -35)
|
||||
clearBtn:SetText("Clear")
|
||||
clearBtn:SetScript("OnClick", function()
|
||||
chatHistory = {}
|
||||
ClearMessageDisplay()
|
||||
end)
|
||||
|
||||
-- ============================================================================
|
||||
-- Register with MainWindow
|
||||
-- ============================================================================
|
||||
|
||||
local function InitPanel()
|
||||
if ATA.MainWindow then
|
||||
ATA.MainWindow:RegisterPanel("AgentChat", "Agent Chat", chatPanel)
|
||||
InitAMSHandlers()
|
||||
else
|
||||
C_Timer.After(0.1, InitPanel)
|
||||
end
|
||||
end
|
||||
|
||||
C_Timer.After(0.1, InitPanel)
|
||||
|
||||
-- Make available globally for debugging
|
||||
ATA.AgentChatPanel = chatPanel
|
||||
ATA.AgentChatHistory = chatHistory
|
||||
|
||||
end) -- End ADDON_LOADED handler
|
||||
@@ -46,9 +46,46 @@ deleteButton:SetSize(80, 22)
|
||||
deleteButton:SetPoint("LEFT", refreshButton, "RIGHT", 5, 0)
|
||||
deleteButton:SetText("Delete")
|
||||
|
||||
local respawnButton = CreateFrame("Button", nil, npcPanel, "UIPanelButtonTemplate")
|
||||
respawnButton:SetSize(80, 22)
|
||||
respawnButton:SetPoint("LEFT", deleteButton, "RIGHT", 5, 0)
|
||||
respawnButton:SetText("Respawn")
|
||||
|
||||
respawnButton:SetScript("OnClick", function()
|
||||
if AMS then
|
||||
print("|cFF00FF00[ATA]|r Requesting respawn...")
|
||||
AMS.Send("RESPAWN_TARGET", {})
|
||||
else
|
||||
print("|cFFFF0000[ATA]|r AMS not available")
|
||||
end
|
||||
end)
|
||||
|
||||
respawnButton:SetScript("OnEnter", function(self)
|
||||
GameTooltip:SetOwner(self, "ANCHOR_BOTTOM")
|
||||
GameTooltip:AddLine("Respawn Target", 1, 1, 1)
|
||||
GameTooltip:AddLine("Force despawn and respawn the targeted creature", 0.7, 0.7, 0.7)
|
||||
GameTooltip:AddLine("Useful after changing creature template data", 0.7, 0.7, 0.7)
|
||||
GameTooltip:Show()
|
||||
end)
|
||||
|
||||
respawnButton:SetScript("OnLeave", function()
|
||||
GameTooltip:Hide()
|
||||
end)
|
||||
|
||||
-- Register response handler for respawn
|
||||
if AMS then
|
||||
AMS.RegisterHandler("RESPAWN_TARGET_RESPONSE", function(data)
|
||||
if data.success then
|
||||
print("|cFF00FF00[ATA]|r " .. data.message .. " (" .. data.creature .. ")")
|
||||
else
|
||||
print("|cFFFF0000[ATA]|r Respawn failed: " .. (data.error or "Unknown error"))
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
local waypointButton = CreateFrame("Button", nil, npcPanel, "UIPanelButtonTemplate")
|
||||
waypointButton:SetSize(110, 22)
|
||||
waypointButton:SetPoint("LEFT", deleteButton, "RIGHT", 5, 0)
|
||||
waypointButton:SetPoint("LEFT", respawnButton, "RIGHT", 5, 0)
|
||||
waypointButton:SetText("Show Waypoints")
|
||||
waypointButton:Disable() -- Disabled until we have a creature with waypoints
|
||||
|
||||
@@ -1296,7 +1333,7 @@ end
|
||||
-- Content Formatters
|
||||
-- ============================================================================
|
||||
|
||||
local function FormatBasicTab(npcData)
|
||||
local function FormatBasicTab(npcData, sData)
|
||||
if not npcData then
|
||||
return "No valid NPC target found.\n\nPlease target a creature or NPC."
|
||||
end
|
||||
@@ -1306,6 +1343,9 @@ local function FormatBasicTab(npcData)
|
||||
table.insert(lines, string.format(" |cFF00FF00Name:|r %s", npcData.name or "Unknown"))
|
||||
table.insert(lines, string.format(" |cFF00FF00Entry ID:|r %s", npcData.npcID or "Unknown"))
|
||||
table.insert(lines, string.format(" |cFF00FF00GUID:|r %s", npcData.guid or "Unknown"))
|
||||
-- Spawn ID from server data (database guid)
|
||||
local spawnId = (sData and sData.success and sData.basic and sData.basic.spawnId) or "Unknown"
|
||||
table.insert(lines, string.format(" |cFF00FF00Spawn ID:|r %s", spawnId))
|
||||
table.insert(lines, string.format(" |cFF00FF00Level:|r %s", npcData.level == -1 and "??" or npcData.level))
|
||||
table.insert(lines, "")
|
||||
|
||||
@@ -1334,6 +1374,62 @@ local function FormatBasicTab(npcData)
|
||||
table.insert(lines, string.format(" |cFF00FF00Faction:|r %s", npcData.faction or "Unknown"))
|
||||
table.insert(lines, "")
|
||||
|
||||
-- Position data (from server)
|
||||
if sData and sData.success and sData.position then
|
||||
table.insert(lines, "|cFFFFD700Location|r")
|
||||
|
||||
-- Spawn/Home position
|
||||
if sData.position.spawn then
|
||||
local sp = sData.position.spawn
|
||||
table.insert(lines, string.format(" |cFF00FF00Spawn:|r %.1f, %.1f, %.1f", sp.x or 0, sp.y or 0, sp.z or 0))
|
||||
end
|
||||
|
||||
-- Current position
|
||||
if sData.position.current then
|
||||
local cp = sData.position.current
|
||||
table.insert(lines, string.format(" |cFF00FF00Current:|r %.1f, %.1f, %.1f", cp.x or 0, cp.y or 0, cp.z or 0))
|
||||
if cp.mapId then
|
||||
table.insert(lines, string.format(" |cFF00FF00Map/Zone/Area:|r %d / %d / %d", cp.mapId or 0, cp.zoneId or 0, cp.areaId or 0))
|
||||
end
|
||||
end
|
||||
table.insert(lines, "")
|
||||
end
|
||||
|
||||
-- Behavior flags (from server)
|
||||
if sData and sData.success and sData.flags then
|
||||
table.insert(lines, "|cFFFFD700Behavior Flags|r")
|
||||
|
||||
-- NPC Flags
|
||||
if sData.flags.npcFlags and sData.flags.npcFlags > 0 then
|
||||
local flagStr = ""
|
||||
if sData.flags.npcFlagNames and #sData.flags.npcFlagNames > 0 then
|
||||
flagStr = table.concat(sData.flags.npcFlagNames, ", ")
|
||||
else
|
||||
flagStr = tostring(sData.flags.npcFlags)
|
||||
end
|
||||
table.insert(lines, string.format(" |cFF00FF00NPC Flags:|r %s", flagStr))
|
||||
else
|
||||
table.insert(lines, " |cFF00FF00NPC Flags:|r None")
|
||||
end
|
||||
|
||||
-- Unit Flags
|
||||
if sData.flags.unitFlags and sData.flags.unitFlags > 0 then
|
||||
local flagStr = ""
|
||||
if sData.flags.unitFlagNames and #sData.flags.unitFlagNames > 0 then
|
||||
flagStr = table.concat(sData.flags.unitFlagNames, ", ")
|
||||
else
|
||||
flagStr = tostring(sData.flags.unitFlags)
|
||||
end
|
||||
table.insert(lines, string.format(" |cFF00FF00Unit Flags:|r %s", flagStr))
|
||||
else
|
||||
table.insert(lines, " |cFF00FF00Unit Flags:|r None")
|
||||
end
|
||||
table.insert(lines, "")
|
||||
elseif not sData or not sData.success then
|
||||
table.insert(lines, "|cFF888888Click Refresh to load location/flags data|r")
|
||||
table.insert(lines, "")
|
||||
end
|
||||
|
||||
table.insert(lines, "|cFFFFD700GM Commands|r")
|
||||
table.insert(lines, string.format(" |cFFFFFF00.npc info|r"))
|
||||
table.insert(lines, string.format(" |cFFFFFF00.lookup creature %s|r", npcData.name or ""))
|
||||
@@ -1661,7 +1757,7 @@ function npcPanel:Update(requestServerData, forceRefresh)
|
||||
local npcData = ATA:GetTargetNPCInfo()
|
||||
|
||||
-- Update Basic tab
|
||||
contentFrames["Basic"].text:SetText(FormatBasicTab(npcData))
|
||||
contentFrames["Basic"].text:SetText(FormatBasicTab(npcData, serverData))
|
||||
UpdateContentSize(contentFrames["Basic"])
|
||||
|
||||
-- Update Stats tab
|
||||
@@ -1682,6 +1778,7 @@ function npcPanel:Update(requestServerData, forceRefresh)
|
||||
serverData = nil
|
||||
|
||||
-- Update displays to show loading
|
||||
contentFrames["Basic"].text:SetText(FormatBasicTab(npcData, nil))
|
||||
contentFrames["Stats"].text:SetText(FormatStatsTab(npcData, nil))
|
||||
contentFrames["AI"].text:SetText(FormatAITab(npcData, nil))
|
||||
contentFrames["Raw"].text:SetText(FormatRawTab(npcData, nil))
|
||||
|
||||
500
src/araxiaonline/mcp/AgentTools.cpp
Normal file
500
src/araxiaonline/mcp/AgentTools.cpp
Normal file
@@ -0,0 +1,500 @@
|
||||
/*
|
||||
* Araxia MCP Server - Agent Tools
|
||||
*
|
||||
* Tools for AI agent registration and bidirectional chat with players.
|
||||
* Agents register with friendly names (e.g., "Scarlet") and can receive
|
||||
* messages from players and send responses.
|
||||
*
|
||||
* Message flow:
|
||||
* Player (WoW) -> AMS -> ElunaSharedData -> MCP poll -> AI Agent
|
||||
* AI Agent -> MCP send -> ElunaSharedData -> AMS push -> Player (WoW)
|
||||
*
|
||||
* Data stored in ElunaSharedData:
|
||||
* - agent_registry: JSON object mapping agent names to info
|
||||
* - agent_inbox_<name>: JSON array of pending messages for agent
|
||||
* - player_inbox_<guid>: JSON array of pending responses for player
|
||||
*/
|
||||
|
||||
#include "AraxiaMCPServer.h"
|
||||
#include "Log.h"
|
||||
#include "GameTime.h"
|
||||
#include "LuaEngine/ElunaSharedData.h"
|
||||
#include <sstream>
|
||||
#include <random>
|
||||
#include <iomanip>
|
||||
|
||||
namespace Araxia
|
||||
{
|
||||
|
||||
// Helper: Generate a unique message ID
|
||||
static std::string GenerateMessageId()
|
||||
{
|
||||
static std::random_device rd;
|
||||
static std::mt19937 gen(rd());
|
||||
static std::uniform_int_distribution<uint64_t> dis;
|
||||
|
||||
std::stringstream ss;
|
||||
ss << "msg_" << std::hex << dis(gen);
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
// Helper: Get current timestamp
|
||||
static uint64_t GetTimestamp()
|
||||
{
|
||||
return static_cast<uint64_t>(GameTime::GetGameTime());
|
||||
}
|
||||
|
||||
// Helper: Get agent registry from shared data
|
||||
static json GetAgentRegistry()
|
||||
{
|
||||
std::string registryStr;
|
||||
if (!sElunaSharedData->Get("agent_registry", registryStr) || registryStr.empty())
|
||||
return json::object();
|
||||
|
||||
try {
|
||||
return json::parse(registryStr);
|
||||
} catch (...) {
|
||||
return json::object();
|
||||
}
|
||||
}
|
||||
|
||||
// Helper: Save agent registry to shared data
|
||||
static void SaveAgentRegistry(const json& registry)
|
||||
{
|
||||
sElunaSharedData->Set("agent_registry", registry.dump());
|
||||
}
|
||||
|
||||
// Helper: Get agent inbox
|
||||
static json GetAgentInbox(const std::string& agentName)
|
||||
{
|
||||
std::string key = "agent_inbox_" + agentName;
|
||||
std::string inboxStr;
|
||||
if (!sElunaSharedData->Get(key, inboxStr) || inboxStr.empty())
|
||||
return json::array();
|
||||
|
||||
try {
|
||||
return json::parse(inboxStr);
|
||||
} catch (...) {
|
||||
return json::array();
|
||||
}
|
||||
}
|
||||
|
||||
// Helper: Save agent inbox
|
||||
static void SaveAgentInbox(const std::string& agentName, const json& inbox)
|
||||
{
|
||||
std::string key = "agent_inbox_" + agentName;
|
||||
sElunaSharedData->Set(key, inbox.dump());
|
||||
}
|
||||
|
||||
// Helper: Get player inbox
|
||||
static json GetPlayerInbox(uint64_t playerGuid)
|
||||
{
|
||||
std::string key = "player_inbox_" + std::to_string(playerGuid);
|
||||
std::string inboxStr;
|
||||
if (!sElunaSharedData->Get(key, inboxStr) || inboxStr.empty())
|
||||
return json::array();
|
||||
|
||||
try {
|
||||
return json::parse(inboxStr);
|
||||
} catch (...) {
|
||||
return json::array();
|
||||
}
|
||||
}
|
||||
|
||||
// Helper: Save player inbox
|
||||
static void SavePlayerInbox(uint64_t playerGuid, const json& inbox)
|
||||
{
|
||||
std::string key = "player_inbox_" + std::to_string(playerGuid);
|
||||
sElunaSharedData->Set(key, inbox.dump());
|
||||
}
|
||||
|
||||
// Helper: Normalize agent name (lowercase for comparison)
|
||||
static std::string NormalizeAgentName(const std::string& name)
|
||||
{
|
||||
std::string normalized = name;
|
||||
std::transform(normalized.begin(), normalized.end(), normalized.begin(), ::tolower);
|
||||
return normalized;
|
||||
}
|
||||
|
||||
void RegisterAgentTools()
|
||||
{
|
||||
TC_LOG_INFO("araxia.mcp", "[MCP] Registering Agent Chat tools...");
|
||||
|
||||
// ========================================================================
|
||||
// mcp_agent_register - Register an AI agent with a friendly name
|
||||
// ========================================================================
|
||||
sMCPServer->RegisterTool(
|
||||
"mcp_agent_register",
|
||||
"Register an AI agent with a friendly name. Players can then send messages to this agent. "
|
||||
"Agent names are case-insensitive and must be unique.",
|
||||
{
|
||||
{"type", "object"},
|
||||
{"properties", {
|
||||
{"name", {
|
||||
{"type", "string"},
|
||||
{"description", "Friendly name for the agent (e.g., 'Scarlet', 'Helper')"}
|
||||
}},
|
||||
{"owner", {
|
||||
{"type", "string"},
|
||||
{"description", "Identifier for the LLM/system (e.g., 'Cascade', 'Claude')"}
|
||||
}},
|
||||
{"description", {
|
||||
{"type", "string"},
|
||||
{"description", "Optional description of what this agent does"}
|
||||
}}
|
||||
}},
|
||||
{"required", {"name", "owner"}}
|
||||
},
|
||||
[](const json& params) -> json {
|
||||
std::string name = params["name"];
|
||||
std::string owner = params["owner"];
|
||||
std::string description = params.value("description", "");
|
||||
|
||||
if (name.empty()) {
|
||||
return {{"success", false}, {"error", "Agent name cannot be empty"}};
|
||||
}
|
||||
|
||||
std::string normalizedName = NormalizeAgentName(name);
|
||||
|
||||
// Check if name already taken
|
||||
json registry = GetAgentRegistry();
|
||||
for (auto& [key, value] : registry.items()) {
|
||||
if (NormalizeAgentName(key) == normalizedName) {
|
||||
return {
|
||||
{"success", false},
|
||||
{"error", "Agent name already registered"},
|
||||
{"existing_owner", value.value("owner", "")}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Register the agent
|
||||
registry[name] = {
|
||||
{"owner", owner},
|
||||
{"description", description},
|
||||
{"registered_at", GetTimestamp()},
|
||||
{"last_poll", GetTimestamp()},
|
||||
{"status", "online"}
|
||||
};
|
||||
|
||||
SaveAgentRegistry(registry);
|
||||
|
||||
TC_LOG_INFO("araxia.mcp", "[MCP] Agent '{}' registered by {}", name, owner);
|
||||
|
||||
return {
|
||||
{"success", true},
|
||||
{"agent_name", name},
|
||||
{"owner", owner},
|
||||
{"message", "Agent registered successfully. Use mcp_agent_poll_messages to receive player messages."}
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// ========================================================================
|
||||
// mcp_agent_unregister - Unregister an agent
|
||||
// ========================================================================
|
||||
sMCPServer->RegisterTool(
|
||||
"mcp_agent_unregister",
|
||||
"Unregister an AI agent. Pending messages will be discarded.",
|
||||
{
|
||||
{"type", "object"},
|
||||
{"properties", {
|
||||
{"name", {
|
||||
{"type", "string"},
|
||||
{"description", "Name of the agent to unregister"}
|
||||
}}
|
||||
}},
|
||||
{"required", {"name"}}
|
||||
},
|
||||
[](const json& params) -> json {
|
||||
std::string name = params["name"];
|
||||
std::string normalizedName = NormalizeAgentName(name);
|
||||
|
||||
json registry = GetAgentRegistry();
|
||||
std::string foundKey;
|
||||
|
||||
for (auto& [key, value] : registry.items()) {
|
||||
if (NormalizeAgentName(key) == normalizedName) {
|
||||
foundKey = key;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (foundKey.empty()) {
|
||||
return {{"success", false}, {"error", "Agent not found"}};
|
||||
}
|
||||
|
||||
// Remove from registry
|
||||
registry.erase(foundKey);
|
||||
SaveAgentRegistry(registry);
|
||||
|
||||
// Clear inbox
|
||||
std::string inboxKey = "agent_inbox_" + foundKey;
|
||||
sElunaSharedData->Clear(inboxKey);
|
||||
|
||||
TC_LOG_INFO("araxia.mcp", "[MCP] Agent '{}' unregistered", foundKey);
|
||||
|
||||
return {
|
||||
{"success", true},
|
||||
{"agent_name", foundKey},
|
||||
{"message", "Agent unregistered"}
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// ========================================================================
|
||||
// mcp_agent_list - List all registered agents
|
||||
// ========================================================================
|
||||
sMCPServer->RegisterTool(
|
||||
"mcp_agent_list",
|
||||
"List all registered AI agents with their status.",
|
||||
{
|
||||
{"type", "object"},
|
||||
{"properties", json::object()}
|
||||
},
|
||||
[](const json& /*params*/) -> json {
|
||||
json registry = GetAgentRegistry();
|
||||
json agents = json::array();
|
||||
|
||||
uint64_t now = GetTimestamp();
|
||||
|
||||
for (auto& [name, info] : registry.items()) {
|
||||
// Mark as offline if no poll in 60 seconds
|
||||
uint64_t lastPoll = info.value("last_poll", 0ULL);
|
||||
bool isOnline = (now - lastPoll) < 60;
|
||||
|
||||
// Get pending message count
|
||||
json inbox = GetAgentInbox(name);
|
||||
|
||||
agents.push_back({
|
||||
{"name", name},
|
||||
{"owner", info.value("owner", "")},
|
||||
{"description", info.value("description", "")},
|
||||
{"online", isOnline},
|
||||
{"last_poll", lastPoll},
|
||||
{"pending_messages", inbox.size()}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
{"success", true},
|
||||
{"agent_count", agents.size()},
|
||||
{"agents", agents}
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// ========================================================================
|
||||
// mcp_agent_poll_messages - Get pending messages for an agent
|
||||
// ========================================================================
|
||||
sMCPServer->RegisterTool(
|
||||
"mcp_agent_poll_messages",
|
||||
"Poll for pending messages sent to this agent by players. "
|
||||
"By default, messages are acknowledged (removed from queue) after retrieval.",
|
||||
{
|
||||
{"type", "object"},
|
||||
{"properties", {
|
||||
{"name", {
|
||||
{"type", "string"},
|
||||
{"description", "Agent name to poll messages for"}
|
||||
}},
|
||||
{"limit", {
|
||||
{"type", "integer"},
|
||||
{"description", "Maximum messages to return (default: 10)"}
|
||||
}},
|
||||
{"acknowledge", {
|
||||
{"type", "boolean"},
|
||||
{"description", "Remove messages from queue after returning (default: true)"}
|
||||
}}
|
||||
}},
|
||||
{"required", {"name"}}
|
||||
},
|
||||
[](const json& params) -> json {
|
||||
std::string name = params["name"];
|
||||
int limit = params.value("limit", 10);
|
||||
bool acknowledge = params.value("acknowledge", true);
|
||||
|
||||
std::string normalizedName = NormalizeAgentName(name);
|
||||
|
||||
// Find agent in registry
|
||||
json registry = GetAgentRegistry();
|
||||
std::string foundKey;
|
||||
|
||||
for (auto& [key, value] : registry.items()) {
|
||||
if (NormalizeAgentName(key) == normalizedName) {
|
||||
foundKey = key;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (foundKey.empty()) {
|
||||
return {{"success", false}, {"error", "Agent not registered"}};
|
||||
}
|
||||
|
||||
// Update last poll time
|
||||
registry[foundKey]["last_poll"] = GetTimestamp();
|
||||
registry[foundKey]["status"] = "online";
|
||||
SaveAgentRegistry(registry);
|
||||
|
||||
// Get messages from inbox
|
||||
json inbox = GetAgentInbox(foundKey);
|
||||
json messages = json::array();
|
||||
|
||||
int count = 0;
|
||||
for (auto& msg : inbox) {
|
||||
if (count >= limit) break;
|
||||
messages.push_back(msg);
|
||||
count++;
|
||||
}
|
||||
|
||||
// Remove acknowledged messages
|
||||
if (acknowledge && count > 0) {
|
||||
json remainingInbox = json::array();
|
||||
for (size_t i = count; i < inbox.size(); i++) {
|
||||
remainingInbox.push_back(inbox[i]);
|
||||
}
|
||||
SaveAgentInbox(foundKey, remainingInbox);
|
||||
}
|
||||
|
||||
return {
|
||||
{"success", true},
|
||||
{"agent_name", foundKey},
|
||||
{"message_count", messages.size()},
|
||||
{"messages", messages},
|
||||
{"remaining", inbox.size() - count}
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// ========================================================================
|
||||
// mcp_agent_send_message - Send a response to a player
|
||||
// ========================================================================
|
||||
sMCPServer->RegisterTool(
|
||||
"mcp_agent_send_message",
|
||||
"Send a message/response from this agent to a player. "
|
||||
"The message will be queued and delivered to the player via AMS.",
|
||||
{
|
||||
{"type", "object"},
|
||||
{"properties", {
|
||||
{"name", {
|
||||
{"type", "string"},
|
||||
{"description", "Agent name sending the message"}
|
||||
}},
|
||||
{"to_player_guid", {
|
||||
{"type", "integer"},
|
||||
{"description", "Target player GUID (from the original message)"}
|
||||
}},
|
||||
{"content", {
|
||||
{"type", "string"},
|
||||
{"description", "Message content to send"}
|
||||
}},
|
||||
{"reply_to_id", {
|
||||
{"type", "string"},
|
||||
{"description", "ID of the message being replied to (optional)"}
|
||||
}}
|
||||
}},
|
||||
{"required", {"name", "to_player_guid", "content"}}
|
||||
},
|
||||
[](const json& params) -> json {
|
||||
std::string name = params["name"];
|
||||
uint64_t toPlayerGuid = params["to_player_guid"];
|
||||
std::string content = params["content"];
|
||||
std::string replyToId = params.value("reply_to_id", "");
|
||||
|
||||
std::string normalizedName = NormalizeAgentName(name);
|
||||
|
||||
// Verify agent is registered
|
||||
json registry = GetAgentRegistry();
|
||||
std::string foundKey;
|
||||
|
||||
for (auto& [key, value] : registry.items()) {
|
||||
if (NormalizeAgentName(key) == normalizedName) {
|
||||
foundKey = key;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (foundKey.empty()) {
|
||||
return {{"success", false}, {"error", "Agent not registered"}};
|
||||
}
|
||||
|
||||
// Create the response message
|
||||
std::string messageId = GenerateMessageId();
|
||||
json message = {
|
||||
{"message_id", messageId},
|
||||
{"from_agent", foundKey},
|
||||
{"content", content},
|
||||
{"timestamp", GetTimestamp()}
|
||||
};
|
||||
|
||||
if (!replyToId.empty()) {
|
||||
message["reply_to_id"] = replyToId;
|
||||
}
|
||||
|
||||
// Add to player's inbox
|
||||
json playerInbox = GetPlayerInbox(toPlayerGuid);
|
||||
playerInbox.push_back(message);
|
||||
|
||||
// Cap inbox size at 100 messages
|
||||
while (playerInbox.size() > 100) {
|
||||
playerInbox.erase(playerInbox.begin());
|
||||
}
|
||||
|
||||
SavePlayerInbox(toPlayerGuid, playerInbox);
|
||||
|
||||
TC_LOG_DEBUG("araxia.mcp", "[MCP] Agent '{}' sent message to player {}", foundKey, toPlayerGuid);
|
||||
|
||||
return {
|
||||
{"success", true},
|
||||
{"message_id", messageId},
|
||||
{"to_player_guid", toPlayerGuid},
|
||||
{"queued", true},
|
||||
{"note", "Message queued. Will be delivered when player's client polls for responses."}
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// ========================================================================
|
||||
// mcp_agent_get_player_responses - Get pending responses for a player (used by Lua)
|
||||
// ========================================================================
|
||||
sMCPServer->RegisterTool(
|
||||
"mcp_agent_get_player_responses",
|
||||
"Get pending agent responses for a specific player. Used internally by server scripts.",
|
||||
{
|
||||
{"type", "object"},
|
||||
{"properties", {
|
||||
{"player_guid", {
|
||||
{"type", "integer"},
|
||||
{"description", "Player GUID to get responses for"}
|
||||
}},
|
||||
{"acknowledge", {
|
||||
{"type", "boolean"},
|
||||
{"description", "Remove messages from queue after returning (default: true)"}
|
||||
}}
|
||||
}},
|
||||
{"required", {"player_guid"}}
|
||||
},
|
||||
[](const json& params) -> json {
|
||||
uint64_t playerGuid = params["player_guid"];
|
||||
bool acknowledge = params.value("acknowledge", true);
|
||||
|
||||
json inbox = GetPlayerInbox(playerGuid);
|
||||
|
||||
if (acknowledge && !inbox.empty()) {
|
||||
// Clear the inbox
|
||||
SavePlayerInbox(playerGuid, json::array());
|
||||
}
|
||||
|
||||
return {
|
||||
{"success", true},
|
||||
{"player_guid", playerGuid},
|
||||
{"message_count", inbox.size()},
|
||||
{"messages", inbox}
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
TC_LOG_INFO("araxia.mcp", "[MCP] Registered 6 Agent Chat tools");
|
||||
}
|
||||
|
||||
} // namespace Araxia
|
||||
@@ -77,6 +77,7 @@ bool MCPServer::Initialize()
|
||||
RegisterDatabaseTools();
|
||||
RegisterWorldScanTools(); // LIDAR-style spatial awareness
|
||||
RegisterSpawnTools(); // Headless spawn management
|
||||
RegisterAgentTools(); // Agent chat - bidirectional player↔AI messaging
|
||||
|
||||
// Initialize AraxiaCore (provides World::Update hook for all Araxia systems)
|
||||
sAraxiaCore->Initialize();
|
||||
|
||||
@@ -108,6 +108,7 @@ void RegisterWorldTools();
|
||||
void RegisterWorldScanTools(); // LIDAR-style spatial awareness
|
||||
void RegisterSpawnTools(); // Headless spawn management (no player required)
|
||||
void RegisterMCPPlayerTools(); // AI player session management
|
||||
void RegisterAgentTools(); // Agent chat - bidirectional player↔AI messaging
|
||||
|
||||
} // namespace Araxia
|
||||
|
||||
|
||||
@@ -874,7 +874,7 @@ void RegisterServerTools()
|
||||
if (!itemTemplate) continue;
|
||||
|
||||
// Quality filter - check bucket's quality mask
|
||||
if (qualityFilter >= 0 && itemTemplate->GetQuality() < qualityFilter)
|
||||
if (qualityFilter >= 0 && static_cast<int>(itemTemplate->GetQuality()) < qualityFilter)
|
||||
continue;
|
||||
|
||||
// Name filter using bucket's FullName (same as server's BuildListBuckets)
|
||||
|
||||
Reference in New Issue
Block a user