mirror of
https://github.com/araxiaonline/TrinityCore.git
synced 2026-06-13 03:32:28 -04:00
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
This commit is contained in:
18
AGENTS.md
18
AGENTS.md
@@ -18,6 +18,24 @@ Documentation with status can be found in [ELUNA_INTEGRATION_COMPLETE.md](ELUNA_
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ NEVER Defer Documentation
|
||||
|
||||
**Documentation is CRITICAL and must never be skipped or deferred.**
|
||||
|
||||
Why:
|
||||
- AI assistants have limited context windows
|
||||
- Documentation is how future sessions understand past decisions
|
||||
- Without docs, work gets duplicated or misunderstood
|
||||
- Code comments, READMEs, and wiki pages are essential deliverables
|
||||
|
||||
**Every feature/change must include:**
|
||||
1. Code comments explaining non-obvious decisions
|
||||
2. Updated README if user-facing
|
||||
3. Wiki/AGENTS.md updates for AI context
|
||||
4. Inline examples where helpful
|
||||
|
||||
---
|
||||
|
||||
## Code Commenting Guidelines
|
||||
|
||||
**ALWAYS add comments when modifying C++ code in this project.**
|
||||
|
||||
@@ -17,10 +17,13 @@ initFrame:SetScript("OnEvent", function(self, event, loadedAddon)
|
||||
local chatPanel = CreateFrame("Frame", "AraxiaTrinityAdminAgentChatPanel", UIParent)
|
||||
chatPanel:Hide()
|
||||
|
||||
-- Chat history storage
|
||||
-- Chat history storage (persisted via SavedVariables)
|
||||
local chatHistory = {} -- { {from="player"|"agent", agent_name, content, timestamp, message_id}, ... }
|
||||
local MAX_HISTORY = 200
|
||||
|
||||
-- Message send status tracking
|
||||
local pendingMessageId = nil
|
||||
|
||||
-- Current selected agent
|
||||
local selectedAgent = nil
|
||||
local availableAgents = {}
|
||||
@@ -28,6 +31,9 @@ local availableAgents = {}
|
||||
-- Polling state
|
||||
local isSubscribed = false
|
||||
|
||||
-- Status indicator for message sending
|
||||
local sendStatusText = nil -- Will be created after chatPanel is set up
|
||||
|
||||
-- ============================================================================
|
||||
-- Header Section
|
||||
-- ============================================================================
|
||||
@@ -40,9 +46,18 @@ title:SetText("Agent Chat")
|
||||
local agentDropdown = CreateFrame("Frame", "AgentChatAgentDropdown", chatPanel, "UIDropDownMenuTemplate")
|
||||
agentDropdown:SetPoint("LEFT", title, "RIGHT", 10, -2)
|
||||
|
||||
local function GetAgentStatusText(agentName)
|
||||
for _, agent in ipairs(availableAgents) do
|
||||
if agent.name == agentName then
|
||||
return agentName .. (agent.online and " |cFF00FF00(online)|r" or " |cFF888888(offline)|r")
|
||||
end
|
||||
end
|
||||
return agentName or "Select Agent"
|
||||
end
|
||||
|
||||
local function AgentDropdown_OnClick(self, arg1, arg2, checked)
|
||||
selectedAgent = arg1
|
||||
UIDropDownMenu_SetText(agentDropdown, arg1 or "Select Agent")
|
||||
UIDropDownMenu_SetText(agentDropdown, GetAgentStatusText(arg1))
|
||||
end
|
||||
|
||||
local function AgentDropdown_Initialize(self, level)
|
||||
@@ -72,9 +87,9 @@ UIDropDownMenu_Initialize(agentDropdown, AgentDropdown_Initialize)
|
||||
|
||||
-- Refresh agents button
|
||||
local refreshAgentsBtn = CreateFrame("Button", nil, chatPanel, "UIPanelButtonTemplate")
|
||||
refreshAgentsBtn:SetSize(24, 24)
|
||||
refreshAgentsBtn:SetSize(60, 24)
|
||||
refreshAgentsBtn:SetPoint("LEFT", agentDropdown, "RIGHT", -10, 2)
|
||||
refreshAgentsBtn:SetText("R")
|
||||
refreshAgentsBtn:SetText("Reload")
|
||||
refreshAgentsBtn:SetScript("OnClick", function()
|
||||
if AMS then
|
||||
AMS.Send("AGENT_LIST_REQUEST", {})
|
||||
@@ -87,11 +102,17 @@ refreshAgentsBtn:SetScript("OnEnter", function(self)
|
||||
end)
|
||||
refreshAgentsBtn:SetScript("OnLeave", function() GameTooltip:Hide() end)
|
||||
|
||||
-- Status indicator
|
||||
-- Status indicator (connection status)
|
||||
local statusText = chatPanel:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
|
||||
statusText:SetPoint("TOPRIGHT", chatPanel, "TOPRIGHT", -10, -14)
|
||||
statusText:SetText("|cFF888888Disconnected|r")
|
||||
|
||||
-- Message send status indicator (shows Sending.../Delivered)
|
||||
sendStatusText = chatPanel:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
|
||||
sendStatusText:SetPoint("TOPRIGHT", chatPanel, "TOPRIGHT", -10, -28)
|
||||
sendStatusText:SetText("")
|
||||
sendStatusText:Hide()
|
||||
|
||||
-- ============================================================================
|
||||
-- Chat History Display
|
||||
-- ============================================================================
|
||||
@@ -109,7 +130,7 @@ 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("TOPLEFT", chatContainer, "TOPLEFT", 5, -8)
|
||||
chatScroll:SetPoint("BOTTOMRIGHT", chatContainer, "BOTTOMRIGHT", -28, 8)
|
||||
|
||||
local chatScrollChild = CreateFrame("Frame", nil, chatScroll)
|
||||
@@ -127,11 +148,13 @@ end
|
||||
|
||||
local function AddMessageToDisplay(from, agentName, content, timestamp, isFromPlayer)
|
||||
local frame = CreateFrame("Frame", nil, chatScrollChild)
|
||||
frame:SetWidth(chatScrollChild:GetWidth() - 10)
|
||||
local frameWidth = chatScrollChild:GetWidth() - 20
|
||||
if frameWidth < 100 then frameWidth = 400 end -- Fallback width
|
||||
frame:SetWidth(frameWidth)
|
||||
|
||||
-- Header line (name + timestamp)
|
||||
local header = frame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
|
||||
header:SetPoint("TOPLEFT", frame, "TOPLEFT", 5, -5)
|
||||
header:SetPoint("TOPLEFT", frame, "TOPLEFT", 15, -5)
|
||||
|
||||
if isFromPlayer then
|
||||
header:SetText("|cFF00CCFF" .. UnitName("player") .. "|r |cFF888888" .. FormatTimestamp(timestamp) .. "|r")
|
||||
@@ -139,13 +162,15 @@ local function AddMessageToDisplay(from, agentName, content, timestamp, isFromPl
|
||||
header:SetText("|cFF00FF00" .. (agentName or "Agent") .. "|r |cFF888888" .. FormatTimestamp(timestamp) .. "|r")
|
||||
end
|
||||
|
||||
-- Content
|
||||
-- Content - displayed below header
|
||||
local contentText = frame:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
|
||||
contentText:SetPoint("TOPLEFT", header, "BOTTOMLEFT", 0, -3)
|
||||
contentText:SetPoint("RIGHT", frame, "RIGHT", -5, 0)
|
||||
contentText:SetPoint("TOPLEFT", header, "BOTTOMLEFT", 0, -2)
|
||||
contentText:SetWidth(frameWidth - 20)
|
||||
contentText:SetJustifyH("LEFT")
|
||||
contentText:SetText(content)
|
||||
contentText:SetWordWrap(true)
|
||||
local displayContent = content or ""
|
||||
if displayContent == "" then displayContent = "[empty]" end
|
||||
contentText:SetText(displayContent)
|
||||
|
||||
-- Calculate height
|
||||
local contentHeight = contentText:GetStringHeight()
|
||||
@@ -159,6 +184,7 @@ local function AddMessageToDisplay(from, agentName, content, timestamp, isFromPl
|
||||
frame:SetPoint("TOPLEFT", chatScrollChild, "TOPLEFT", 0, -yOffset)
|
||||
|
||||
table.insert(messageFrames, frame)
|
||||
frame:Show()
|
||||
|
||||
-- Update scroll child height
|
||||
chatScrollChild:SetHeight(yOffset + frame:GetHeight() + 10)
|
||||
@@ -238,6 +264,11 @@ local function SendMessage()
|
||||
}
|
||||
end
|
||||
|
||||
-- Show sending status
|
||||
pendingMessageId = "local_" .. time()
|
||||
sendStatusText:SetText("|cFFFFFF00Sending...|r")
|
||||
sendStatusText:Show()
|
||||
|
||||
-- Send message
|
||||
AMS.Send("AGENT_SEND_MESSAGE", {
|
||||
agent_name = selectedAgent,
|
||||
@@ -251,7 +282,7 @@ local function SendMessage()
|
||||
agent_name = selectedAgent,
|
||||
content = text,
|
||||
timestamp = time(),
|
||||
message_id = "local_" .. time()
|
||||
message_id = pendingMessageId
|
||||
}
|
||||
table.insert(chatHistory, msg)
|
||||
|
||||
@@ -298,10 +329,12 @@ local function InitAMSHandlers()
|
||||
availableAgents = data.agents or {}
|
||||
UIDropDownMenu_Initialize(agentDropdown, AgentDropdown_Initialize)
|
||||
|
||||
-- Auto-select first agent if none selected
|
||||
-- Auto-select first agent if none selected, or update status display
|
||||
if not selectedAgent and #availableAgents > 0 then
|
||||
selectedAgent = availableAgents[1].name
|
||||
UIDropDownMenu_SetText(agentDropdown, selectedAgent)
|
||||
end
|
||||
if selectedAgent then
|
||||
UIDropDownMenu_SetText(agentDropdown, GetAgentStatusText(selectedAgent))
|
||||
end
|
||||
|
||||
print("|cFF00FF00[Agent Chat]|r Found " .. #availableAgents .. " agent(s)")
|
||||
@@ -310,9 +343,22 @@ local function InitAMSHandlers()
|
||||
|
||||
-- Handle send confirmation
|
||||
AMS.RegisterHandler("AGENT_SEND_MESSAGE_RESPONSE", function(data)
|
||||
if not data.success then
|
||||
if data.success then
|
||||
-- Show delivered status briefly
|
||||
sendStatusText:SetText("|cFF00FF00Delivered ✓|r")
|
||||
C_Timer.After(3, function()
|
||||
sendStatusText:SetText("")
|
||||
sendStatusText:Hide()
|
||||
end)
|
||||
else
|
||||
sendStatusText:SetText("|cFFFF0000Failed|r")
|
||||
print("|cFFFF0000[Agent Chat]|r Failed to send: " .. (data.error or "Unknown error"))
|
||||
C_Timer.After(5, function()
|
||||
sendStatusText:SetText("")
|
||||
sendStatusText:Hide()
|
||||
end)
|
||||
end
|
||||
pendingMessageId = nil
|
||||
end)
|
||||
|
||||
-- Handle agent responses (push delivery)
|
||||
@@ -402,16 +448,48 @@ pollBtn:SetScript("OnClick", function()
|
||||
end
|
||||
end)
|
||||
|
||||
-- Clear chat button
|
||||
-- Clear chat button (positioned left of status text)
|
||||
local clearBtn = CreateFrame("Button", nil, chatPanel, "UIPanelButtonTemplate")
|
||||
clearBtn:SetSize(50, 22)
|
||||
clearBtn:SetPoint("TOPRIGHT", chatPanel, "TOPRIGHT", -10, -35)
|
||||
clearBtn:SetPoint("RIGHT", statusText, "LEFT", -10, 0)
|
||||
clearBtn:SetText("Clear")
|
||||
clearBtn:SetScript("OnClick", function()
|
||||
chatHistory = {}
|
||||
ClearMessageDisplay()
|
||||
end)
|
||||
|
||||
-- ============================================================================
|
||||
-- SavedVariables Persistence
|
||||
-- ============================================================================
|
||||
|
||||
-- Load chat history from SavedVariables on login
|
||||
local function LoadChatHistory()
|
||||
if AraxiaTrinityAdminDB and AraxiaTrinityAdminDB.AgentChatHistory then
|
||||
chatHistory = AraxiaTrinityAdminDB.AgentChatHistory
|
||||
-- Refresh display with loaded history
|
||||
C_Timer.After(0.5, function()
|
||||
RefreshChatDisplay()
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
-- Save chat history to SavedVariables on logout
|
||||
local function SaveChatHistory()
|
||||
if not AraxiaTrinityAdminDB then
|
||||
AraxiaTrinityAdminDB = {}
|
||||
end
|
||||
AraxiaTrinityAdminDB.AgentChatHistory = chatHistory
|
||||
end
|
||||
|
||||
-- Register for logout event to save history
|
||||
local saveFrame = CreateFrame("Frame")
|
||||
saveFrame:RegisterEvent("PLAYER_LOGOUT")
|
||||
saveFrame:SetScript("OnEvent", function(self, event)
|
||||
if event == "PLAYER_LOGOUT" then
|
||||
SaveChatHistory()
|
||||
end
|
||||
end)
|
||||
|
||||
-- ============================================================================
|
||||
-- Register with MainWindow
|
||||
-- ============================================================================
|
||||
@@ -420,6 +498,7 @@ local function InitPanel()
|
||||
if ATA.MainWindow then
|
||||
ATA.MainWindow:RegisterPanel("AgentChat", "Agent Chat", chatPanel)
|
||||
InitAMSHandlers()
|
||||
LoadChatHistory() -- Load saved history after init
|
||||
else
|
||||
C_Timer.After(0.1, InitPanel)
|
||||
end
|
||||
@@ -430,5 +509,6 @@ C_Timer.After(0.1, InitPanel)
|
||||
-- Make available globally for debugging
|
||||
ATA.AgentChatPanel = chatPanel
|
||||
ATA.AgentChatHistory = chatHistory
|
||||
ATA.SaveAgentChatHistory = SaveChatHistory -- Allow manual save
|
||||
|
||||
end) -- End ADDON_LOADED handler
|
||||
|
||||
@@ -1,19 +1,46 @@
|
||||
# Eluna Lua Scripts - Agent Guidelines
|
||||
|
||||
## ⚠️ IMPORTANT: Commit Workflow
|
||||
## ⚠️ CRITICAL: File Editing Locations
|
||||
|
||||
**This directory (`/opt/trinitycore/server/lua_scripts/`) is NOT a git repo.**
|
||||
### Server-Side Lua Scripts
|
||||
- **EDIT HERE (running server):** `/opt/trinitycore/lua_scripts/`
|
||||
- **COMMIT FROM:** `/opt/trinitycore/TrinityCore/araxiaonline/lua_scripts/`
|
||||
|
||||
To commit changes to lua_scripts:
|
||||
1. Copy modified files to: `/opt/trinitycore/TrinityCore/araxiaonline/lua_scripts/`
|
||||
2. Commit from the TrinityCore repo
|
||||
### Client-Side Addons (WoW Client)
|
||||
- **EDIT HERE (running client):** `/opt/trinitycore/Interface/AddOns/`
|
||||
- **COMMIT FROM:** `/opt/trinitycore/TrinityCore/araxiaonline/client_addons/`
|
||||
|
||||
### Commit Workflow
|
||||
Before committing, sync files from running locations to repo locations:
|
||||
|
||||
```bash
|
||||
# Example sync command
|
||||
cp -r /opt/trinitycore/server/lua_scripts/* /opt/trinitycore/TrinityCore/araxiaonline/lua_scripts/
|
||||
# Sync lua_scripts
|
||||
cp -r /opt/trinitycore/lua_scripts/* /opt/trinitycore/TrinityCore/araxiaonline/lua_scripts/
|
||||
|
||||
# Sync client addons
|
||||
cp -r /opt/trinitycore/Interface/AddOns/AraxiaTrinityAdmin/* /opt/trinitycore/TrinityCore/araxiaonline/client_addons/AraxiaTrinityAdmin/
|
||||
cp -r /opt/trinitycore/Interface/AddOns/AMS_Client/* /opt/trinitycore/TrinityCore/araxiaonline/client_addons/AMS_Client/
|
||||
```
|
||||
|
||||
**AI assistants: Do this automatically when committing session work.**
|
||||
**AI assistants: ALWAYS edit the running locations, then sync before committing.**
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ NEVER Defer Documentation
|
||||
|
||||
**Documentation is CRITICAL and must never be skipped or deferred.**
|
||||
|
||||
Why:
|
||||
- AI assistants have limited context windows
|
||||
- Documentation is how future sessions understand past decisions
|
||||
- Without docs, work gets duplicated or misunderstood
|
||||
- Code comments, READMEs, and wiki pages are essential deliverables
|
||||
|
||||
**Every feature/change must include:**
|
||||
1. Code comments explaining non-obvious decisions
|
||||
2. Updated README if user-facing
|
||||
3. Wiki/AGENTS.md updates for AI context
|
||||
4. Inline examples where helpful
|
||||
|
||||
---
|
||||
|
||||
|
||||
207
araxiaonline/lua_scripts/Smallfolk.lua
Normal file
207
araxiaonline/lua_scripts/Smallfolk.lua
Normal file
@@ -0,0 +1,207 @@
|
||||
--[[
|
||||
Smallfolk - A Lua serialization library
|
||||
https://github.com/gvx/Smallfolk
|
||||
License: ISC
|
||||
]]
|
||||
|
||||
local Smallfolk = {}
|
||||
|
||||
local floor = math.floor
|
||||
local pairs = pairs
|
||||
local type = type
|
||||
local concat = table.concat
|
||||
local tostring = tostring
|
||||
local tonumber = tonumber
|
||||
local s_byte = string.byte
|
||||
local s_char = string.char
|
||||
local s_sub = string.sub
|
||||
local s_gsub = string.gsub
|
||||
local s_match = string.match
|
||||
|
||||
local function is_array(t)
|
||||
local n = #t
|
||||
for k in pairs(t) do
|
||||
if type(k) ~= "number" or k < 1 or k > n or floor(k) ~= k then
|
||||
return false
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
local function escape_string(s)
|
||||
return s_gsub(s, '[%c"\\]', function(c)
|
||||
local n = s_byte(c)
|
||||
if c == '"' then return '\\"'
|
||||
elseif c == '\\' then return '\\\\'
|
||||
elseif c == '\n' then return '\\n'
|
||||
elseif c == '\r' then return '\\r'
|
||||
elseif c == '\t' then return '\\t'
|
||||
else return '\\' .. n
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
local function dump_value(value, tables)
|
||||
local t = type(value)
|
||||
if t == "number" then
|
||||
if value ~= value then
|
||||
return "nan"
|
||||
elseif value == 1/0 then
|
||||
return "inf"
|
||||
elseif value == -1/0 then
|
||||
return "-inf"
|
||||
else
|
||||
return tostring(value)
|
||||
end
|
||||
elseif t == "string" then
|
||||
return '"' .. escape_string(value) .. '"'
|
||||
elseif t == "boolean" then
|
||||
return value and "true" or "false"
|
||||
elseif t == "nil" then
|
||||
return "nil"
|
||||
elseif t == "table" then
|
||||
if tables[value] then
|
||||
return "@" .. tables[value]
|
||||
end
|
||||
local n = (tables.n or 0) + 1
|
||||
tables[value] = n
|
||||
tables.n = n
|
||||
|
||||
local parts = {}
|
||||
if is_array(value) then
|
||||
for i = 1, #value do
|
||||
parts[i] = dump_value(value[i], tables)
|
||||
end
|
||||
return "[" .. concat(parts, ",") .. "]"
|
||||
else
|
||||
local i = 1
|
||||
for k, v in pairs(value) do
|
||||
parts[i] = dump_value(k, tables) .. ":" .. dump_value(v, tables)
|
||||
i = i + 1
|
||||
end
|
||||
return "{" .. concat(parts, ",") .. "}"
|
||||
end
|
||||
else
|
||||
error("cannot serialize type " .. t)
|
||||
end
|
||||
end
|
||||
|
||||
function Smallfolk.dumps(value)
|
||||
return dump_value(value, {})
|
||||
end
|
||||
|
||||
local function unescape_string(s)
|
||||
return s_gsub(s, '\\(.)', function(c)
|
||||
if c == 'n' then return '\n'
|
||||
elseif c == 'r' then return '\r'
|
||||
elseif c == 't' then return '\t'
|
||||
elseif c == '"' then return '"'
|
||||
elseif c == '\\' then return '\\'
|
||||
else
|
||||
local n = tonumber(c)
|
||||
if n then return s_char(n) end
|
||||
return c
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
local function parse_value(str, pos, tables)
|
||||
local c = s_sub(str, pos, pos)
|
||||
|
||||
if c == '"' then
|
||||
local end_pos = pos + 1
|
||||
while true do
|
||||
local ch = s_sub(str, end_pos, end_pos)
|
||||
if ch == '"' then break
|
||||
elseif ch == '\\' then end_pos = end_pos + 1
|
||||
elseif ch == '' then error("unterminated string")
|
||||
end
|
||||
end_pos = end_pos + 1
|
||||
end
|
||||
return unescape_string(s_sub(str, pos + 1, end_pos - 1)), end_pos + 1
|
||||
elseif c == '[' then
|
||||
local arr = {}
|
||||
local n = #tables + 1
|
||||
tables[n] = arr
|
||||
pos = pos + 1
|
||||
if s_sub(str, pos, pos) == ']' then
|
||||
return arr, pos + 1
|
||||
end
|
||||
local i = 1
|
||||
while true do
|
||||
local val
|
||||
val, pos = parse_value(str, pos, tables)
|
||||
arr[i] = val
|
||||
i = i + 1
|
||||
c = s_sub(str, pos, pos)
|
||||
if c == ']' then return arr, pos + 1
|
||||
elseif c == ',' then pos = pos + 1
|
||||
else error("expected ',' or ']'")
|
||||
end
|
||||
end
|
||||
elseif c == '{' then
|
||||
local obj = {}
|
||||
local n = #tables + 1
|
||||
tables[n] = obj
|
||||
pos = pos + 1
|
||||
if s_sub(str, pos, pos) == '}' then
|
||||
return obj, pos + 1
|
||||
end
|
||||
while true do
|
||||
local key, val
|
||||
key, pos = parse_value(str, pos, tables)
|
||||
if s_sub(str, pos, pos) ~= ':' then error("expected ':'") end
|
||||
val, pos = parse_value(str, pos + 1, tables)
|
||||
obj[key] = val
|
||||
c = s_sub(str, pos, pos)
|
||||
if c == '}' then return obj, pos + 1
|
||||
elseif c == ',' then pos = pos + 1
|
||||
else error("expected ',' or '}'")
|
||||
end
|
||||
end
|
||||
elseif c == '@' then
|
||||
local end_pos = pos + 1
|
||||
while s_match(s_sub(str, end_pos, end_pos), '%d') do
|
||||
end_pos = end_pos + 1
|
||||
end
|
||||
local ref = tonumber(s_sub(str, pos + 1, end_pos - 1))
|
||||
return tables[ref], end_pos
|
||||
elseif s_sub(str, pos, pos + 3) == "true" then
|
||||
return true, pos + 4
|
||||
elseif s_sub(str, pos, pos + 4) == "false" then
|
||||
return false, pos + 5
|
||||
elseif s_sub(str, pos, pos + 2) == "nil" then
|
||||
return nil, pos + 3
|
||||
elseif s_sub(str, pos, pos + 2) == "nan" then
|
||||
return 0/0, pos + 3
|
||||
elseif s_sub(str, pos, pos + 2) == "inf" then
|
||||
return 1/0, pos + 3
|
||||
elseif s_sub(str, pos, pos + 3) == "-inf" then
|
||||
return -1/0, pos + 4
|
||||
else
|
||||
local end_pos = pos
|
||||
if s_sub(str, end_pos, end_pos) == '-' then
|
||||
end_pos = end_pos + 1
|
||||
end
|
||||
while s_match(s_sub(str, end_pos, end_pos), '[%d%.eE%+%-]') do
|
||||
end_pos = end_pos + 1
|
||||
end
|
||||
local num_str = s_sub(str, pos, end_pos - 1)
|
||||
local num = tonumber(num_str)
|
||||
if num then
|
||||
return num, end_pos
|
||||
else
|
||||
error("unexpected character at position " .. pos .. ": " .. c)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function Smallfolk.loads(str)
|
||||
if str == nil or str == "" then
|
||||
return nil
|
||||
end
|
||||
local val, _ = parse_value(str, 1, {})
|
||||
return val
|
||||
end
|
||||
|
||||
return Smallfolk
|
||||
207
araxiaonline/lua_scripts/aa_Smallfolk.lua
Normal file
207
araxiaonline/lua_scripts/aa_Smallfolk.lua
Normal file
@@ -0,0 +1,207 @@
|
||||
--[[
|
||||
Smallfolk - A Lua serialization library
|
||||
https://github.com/gvx/Smallfolk
|
||||
License: ISC
|
||||
]]
|
||||
|
||||
local Smallfolk = {}
|
||||
|
||||
local floor = math.floor
|
||||
local pairs = pairs
|
||||
local type = type
|
||||
local concat = table.concat
|
||||
local tostring = tostring
|
||||
local tonumber = tonumber
|
||||
local s_byte = string.byte
|
||||
local s_char = string.char
|
||||
local s_sub = string.sub
|
||||
local s_gsub = string.gsub
|
||||
local s_match = string.match
|
||||
|
||||
local function is_array(t)
|
||||
local n = #t
|
||||
for k in pairs(t) do
|
||||
if type(k) ~= "number" or k < 1 or k > n or floor(k) ~= k then
|
||||
return false
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
local function escape_string(s)
|
||||
return s_gsub(s, '[%c"\\]', function(c)
|
||||
local n = s_byte(c)
|
||||
if c == '"' then return '\\"'
|
||||
elseif c == '\\' then return '\\\\'
|
||||
elseif c == '\n' then return '\\n'
|
||||
elseif c == '\r' then return '\\r'
|
||||
elseif c == '\t' then return '\\t'
|
||||
else return '\\' .. n
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
local function dump_value(value, tables)
|
||||
local t = type(value)
|
||||
if t == "number" then
|
||||
if value ~= value then
|
||||
return "nan"
|
||||
elseif value == 1/0 then
|
||||
return "inf"
|
||||
elseif value == -1/0 then
|
||||
return "-inf"
|
||||
else
|
||||
return tostring(value)
|
||||
end
|
||||
elseif t == "string" then
|
||||
return '"' .. escape_string(value) .. '"'
|
||||
elseif t == "boolean" then
|
||||
return value and "true" or "false"
|
||||
elseif t == "nil" then
|
||||
return "nil"
|
||||
elseif t == "table" then
|
||||
if tables[value] then
|
||||
return "@" .. tables[value]
|
||||
end
|
||||
local n = (tables.n or 0) + 1
|
||||
tables[value] = n
|
||||
tables.n = n
|
||||
|
||||
local parts = {}
|
||||
if is_array(value) then
|
||||
for i = 1, #value do
|
||||
parts[i] = dump_value(value[i], tables)
|
||||
end
|
||||
return "[" .. concat(parts, ",") .. "]"
|
||||
else
|
||||
local i = 1
|
||||
for k, v in pairs(value) do
|
||||
parts[i] = dump_value(k, tables) .. ":" .. dump_value(v, tables)
|
||||
i = i + 1
|
||||
end
|
||||
return "{" .. concat(parts, ",") .. "}"
|
||||
end
|
||||
else
|
||||
error("cannot serialize type " .. t)
|
||||
end
|
||||
end
|
||||
|
||||
function Smallfolk.dumps(value)
|
||||
return dump_value(value, {})
|
||||
end
|
||||
|
||||
local function unescape_string(s)
|
||||
return s_gsub(s, '\\(.)', function(c)
|
||||
if c == 'n' then return '\n'
|
||||
elseif c == 'r' then return '\r'
|
||||
elseif c == 't' then return '\t'
|
||||
elseif c == '"' then return '"'
|
||||
elseif c == '\\' then return '\\'
|
||||
else
|
||||
local n = tonumber(c)
|
||||
if n then return s_char(n) end
|
||||
return c
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
local function parse_value(str, pos, tables)
|
||||
local c = s_sub(str, pos, pos)
|
||||
|
||||
if c == '"' then
|
||||
local end_pos = pos + 1
|
||||
while true do
|
||||
local ch = s_sub(str, end_pos, end_pos)
|
||||
if ch == '"' then break
|
||||
elseif ch == '\\' then end_pos = end_pos + 1
|
||||
elseif ch == '' then error("unterminated string")
|
||||
end
|
||||
end_pos = end_pos + 1
|
||||
end
|
||||
return unescape_string(s_sub(str, pos + 1, end_pos - 1)), end_pos + 1
|
||||
elseif c == '[' then
|
||||
local arr = {}
|
||||
local n = #tables + 1
|
||||
tables[n] = arr
|
||||
pos = pos + 1
|
||||
if s_sub(str, pos, pos) == ']' then
|
||||
return arr, pos + 1
|
||||
end
|
||||
local i = 1
|
||||
while true do
|
||||
local val
|
||||
val, pos = parse_value(str, pos, tables)
|
||||
arr[i] = val
|
||||
i = i + 1
|
||||
c = s_sub(str, pos, pos)
|
||||
if c == ']' then return arr, pos + 1
|
||||
elseif c == ',' then pos = pos + 1
|
||||
else error("expected ',' or ']'")
|
||||
end
|
||||
end
|
||||
elseif c == '{' then
|
||||
local obj = {}
|
||||
local n = #tables + 1
|
||||
tables[n] = obj
|
||||
pos = pos + 1
|
||||
if s_sub(str, pos, pos) == '}' then
|
||||
return obj, pos + 1
|
||||
end
|
||||
while true do
|
||||
local key, val
|
||||
key, pos = parse_value(str, pos, tables)
|
||||
if s_sub(str, pos, pos) ~= ':' then error("expected ':'") end
|
||||
val, pos = parse_value(str, pos + 1, tables)
|
||||
obj[key] = val
|
||||
c = s_sub(str, pos, pos)
|
||||
if c == '}' then return obj, pos + 1
|
||||
elseif c == ',' then pos = pos + 1
|
||||
else error("expected ',' or '}'")
|
||||
end
|
||||
end
|
||||
elseif c == '@' then
|
||||
local end_pos = pos + 1
|
||||
while s_match(s_sub(str, end_pos, end_pos), '%d') do
|
||||
end_pos = end_pos + 1
|
||||
end
|
||||
local ref = tonumber(s_sub(str, pos + 1, end_pos - 1))
|
||||
return tables[ref], end_pos
|
||||
elseif s_sub(str, pos, pos + 3) == "true" then
|
||||
return true, pos + 4
|
||||
elseif s_sub(str, pos, pos + 4) == "false" then
|
||||
return false, pos + 5
|
||||
elseif s_sub(str, pos, pos + 2) == "nil" then
|
||||
return nil, pos + 3
|
||||
elseif s_sub(str, pos, pos + 2) == "nan" then
|
||||
return 0/0, pos + 3
|
||||
elseif s_sub(str, pos, pos + 2) == "inf" then
|
||||
return 1/0, pos + 3
|
||||
elseif s_sub(str, pos, pos + 3) == "-inf" then
|
||||
return -1/0, pos + 4
|
||||
else
|
||||
local end_pos = pos
|
||||
if s_sub(str, end_pos, end_pos) == '-' then
|
||||
end_pos = end_pos + 1
|
||||
end
|
||||
while s_match(s_sub(str, end_pos, end_pos), '[%d%.eE%+%-]') do
|
||||
end_pos = end_pos + 1
|
||||
end
|
||||
local num_str = s_sub(str, pos, end_pos - 1)
|
||||
local num = tonumber(num_str)
|
||||
if num then
|
||||
return num, end_pos
|
||||
else
|
||||
error("unexpected character at position " .. pos .. ": " .. c)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function Smallfolk.loads(str)
|
||||
if str == nil or str == "" then
|
||||
return nil
|
||||
end
|
||||
local val, _ = parse_value(str, 1, {})
|
||||
return val
|
||||
end
|
||||
|
||||
return Smallfolk
|
||||
446
araxiaonline/lua_scripts/ab_AMS_Server/AMS_Server.lua
Executable file
446
araxiaonline/lua_scripts/ab_AMS_Server/AMS_Server.lua
Executable file
@@ -0,0 +1,446 @@
|
||||
--[[
|
||||
Araxia Messaging System (AMS) - Server Side
|
||||
|
||||
A lightweight, modern client-server messaging library for TrinityCore 11.2.5
|
||||
Inspired by Rochet2's AIO but built for modern WoW and simplified for our needs.
|
||||
|
||||
Features:
|
||||
- Handler registration system
|
||||
- Smallfolk serialization
|
||||
- Message splitting for long messages
|
||||
- Request/response pattern
|
||||
- Error isolation via pcall
|
||||
|
||||
Usage:
|
||||
-- Register a handler
|
||||
AMS.RegisterHandler("NPC_SEARCH", function(player, data)
|
||||
local results = QueryDatabase(data.searchTerm)
|
||||
AMS.Send(player, "NPC_SEARCH_RESULT", results)
|
||||
end)
|
||||
|
||||
-- Send a message (fluent API)
|
||||
AMS.Msg():Add("UPDATE_NPC", {npcID = 1234, hp = 5000}):Send(player)
|
||||
]]
|
||||
|
||||
-- ============================================================================
|
||||
-- Configuration
|
||||
-- ============================================================================
|
||||
|
||||
local AMS_VERSION = "1.0.0-alpha"
|
||||
local AMS_PREFIX = "AMS"
|
||||
local AMS_DEBUG = true -- Set to true for debugging
|
||||
|
||||
-- Message limits (server can send larger messages than client)
|
||||
-- Client: 255 bytes max, Server: ~2560 bytes safe on most patches
|
||||
local AMS_MAX_MSG_LENGTH = 2500 - #AMS_PREFIX - 10 -- Reserve space for overhead
|
||||
|
||||
-- Message ID tracking
|
||||
local AMS_MSG_MIN_ID = 1
|
||||
local AMS_MSG_MAX_ID = 65535 -- 16-bit ID
|
||||
|
||||
-- ============================================================================
|
||||
-- Dependencies
|
||||
-- ============================================================================
|
||||
|
||||
-- Smallfolk for serialization
|
||||
local Smallfolk = require("ab_AMS_Server.smallfolk")
|
||||
|
||||
-- ============================================================================
|
||||
-- Core AMS Table
|
||||
-- ============================================================================
|
||||
|
||||
-- NOTE: Cross-state data persistence is now handled by C++ ElunaSharedData
|
||||
-- using SetSharedData()/GetSharedData() functions. The AMS table here is
|
||||
-- only for local state within this Eluna instance.
|
||||
|
||||
AMS = {
|
||||
version = AMS_VERSION,
|
||||
handlers = {},
|
||||
playerData = {}, -- Local cache (C++ shared data is authoritative)
|
||||
nextMessageID = {}, -- Per-player message ID counter
|
||||
}
|
||||
|
||||
-- ============================================================================
|
||||
-- Utility Functions
|
||||
-- ============================================================================
|
||||
|
||||
-- Debug logging (verbose)
|
||||
local function Debug(...)
|
||||
if AMS_DEBUG then
|
||||
print("[AMS Server]", ...)
|
||||
end
|
||||
end
|
||||
|
||||
-- Info logging (always shown)
|
||||
local function Info(...)
|
||||
print("[AMS Server]", ...)
|
||||
end
|
||||
|
||||
-- Error logging (always shown)
|
||||
local function Error(...)
|
||||
print("[AMS Server] ERROR:", ...)
|
||||
end
|
||||
|
||||
-- Encode number as 4-character hex string (text-safe)
|
||||
local function NumberToHex(num)
|
||||
return string.format("%04X", num)
|
||||
end
|
||||
|
||||
-- Decode hex string to number
|
||||
local function HexToNumber(str)
|
||||
if #str < 4 then return 0 end
|
||||
return tonumber(str:sub(1, 4), 16) or 0
|
||||
end
|
||||
|
||||
-- Get next message ID for a player
|
||||
local function GetNextMessageID(playerGUID)
|
||||
if not AMS.nextMessageID[playerGUID] then
|
||||
AMS.nextMessageID[playerGUID] = AMS_MSG_MIN_ID
|
||||
end
|
||||
|
||||
local msgID = AMS.nextMessageID[playerGUID]
|
||||
|
||||
-- Increment and wrap around if needed
|
||||
if msgID >= AMS_MSG_MAX_ID then
|
||||
AMS.nextMessageID[playerGUID] = AMS_MSG_MIN_ID
|
||||
else
|
||||
AMS.nextMessageID[playerGUID] = msgID + 1
|
||||
end
|
||||
|
||||
return msgID
|
||||
end
|
||||
|
||||
-- ============================================================================
|
||||
-- Message Sending
|
||||
-- ============================================================================
|
||||
|
||||
-- Send a raw addon message (handles splitting if needed)
|
||||
local function SendAddonMessage(player, message)
|
||||
local playerGUID = player:GetGUIDLow()
|
||||
|
||||
Debug("Sending message to", player:GetName(), "length:", #message)
|
||||
|
||||
-- Short message - send directly
|
||||
if #message <= AMS_MAX_MSG_LENGTH then
|
||||
-- Prefix with marker for short message (ID = 0000, parts = 0000, partID = 0000)
|
||||
local packet = NumberToHex(0) .. NumberToHex(0) .. NumberToHex(0) .. message
|
||||
player:SendAddonMessage(AMS_PREFIX, packet, 7, player)
|
||||
return
|
||||
end
|
||||
|
||||
-- Long message - split into chunks
|
||||
local msgID = GetNextMessageID(playerGUID)
|
||||
local chunkSize = AMS_MAX_MSG_LENGTH - 12 -- Reserve 12 bytes for hex header (3 * 4 chars)
|
||||
local totalParts = math.ceil(#message / chunkSize)
|
||||
|
||||
Debug("Splitting message ID", msgID, "into", totalParts, "parts")
|
||||
|
||||
for partID = 1, totalParts do
|
||||
local startPos = (partID - 1) * chunkSize + 1
|
||||
local endPos = math.min(partID * chunkSize, #message)
|
||||
local chunk = message:sub(startPos, endPos)
|
||||
|
||||
-- Header: msgID (4 hex) + totalParts (4 hex) + partID (4 hex) = 12 chars
|
||||
local header = NumberToHex(msgID) ..
|
||||
NumberToHex(totalParts) ..
|
||||
NumberToHex(partID)
|
||||
|
||||
local packet = header .. chunk
|
||||
player:SendAddonMessage(AMS_PREFIX, packet, 7, player)
|
||||
end
|
||||
end
|
||||
|
||||
-- Serialize and send data
|
||||
function AMS.Send(player, handlerName, data)
|
||||
if type(player) ~= 'userdata' then
|
||||
Debug("ERROR: Send requires a player object")
|
||||
return
|
||||
end
|
||||
|
||||
-- Create message block
|
||||
local messageBlock = {handlerName, data}
|
||||
|
||||
-- Serialize using Smallfolk
|
||||
local serialized = Smallfolk.dumps({messageBlock})
|
||||
|
||||
if not serialized then
|
||||
Debug("ERROR: Failed to serialize message for", handlerName)
|
||||
return
|
||||
end
|
||||
|
||||
SendAddonMessage(player, serialized)
|
||||
end
|
||||
|
||||
-- ============================================================================
|
||||
-- Message Class (Fluent API)
|
||||
-- ============================================================================
|
||||
|
||||
local MessageMT = {}
|
||||
MessageMT.__index = MessageMT
|
||||
|
||||
-- Add a handler call to the message
|
||||
function MessageMT:Add(handlerName, data)
|
||||
table.insert(self.blocks, {handlerName, data})
|
||||
return self -- Fluent API
|
||||
end
|
||||
|
||||
-- Send the message to player(s)
|
||||
function MessageMT:Send(player, ...)
|
||||
if #self.blocks == 0 then
|
||||
Error("Attempted to send empty message")
|
||||
return
|
||||
end
|
||||
|
||||
-- Serialize all blocks
|
||||
local serialized = Smallfolk.dumps(self.blocks)
|
||||
|
||||
if not serialized then
|
||||
Error("Failed to serialize message")
|
||||
return
|
||||
end
|
||||
|
||||
-- Send to primary player
|
||||
SendAddonMessage(player, serialized)
|
||||
|
||||
-- Send to additional players if provided
|
||||
for i = 1, select('#', ...) do
|
||||
local additionalPlayer = select(i, ...)
|
||||
SendAddonMessage(additionalPlayer, serialized)
|
||||
end
|
||||
end
|
||||
|
||||
-- Check if message has content
|
||||
function MessageMT:HasContent()
|
||||
return #self.blocks > 0
|
||||
end
|
||||
|
||||
-- Create a new message
|
||||
function AMS.Msg()
|
||||
local msg = {
|
||||
blocks = {}
|
||||
}
|
||||
setmetatable(msg, MessageMT)
|
||||
return msg
|
||||
end
|
||||
|
||||
-- ============================================================================
|
||||
-- Message Receiving & Reassembly
|
||||
-- ============================================================================
|
||||
|
||||
-- Handle incoming message part (reassemble if split)
|
||||
local function HandleIncomingMessage(player, rawMessage)
|
||||
local playerGUID = tostring(player:GetGUIDLow())
|
||||
local dataKey = "AMS_PLAYER_" .. playerGUID
|
||||
|
||||
-- Use C++ shared data for cross-state persistence
|
||||
-- C++ stores strings, so we serialize with Smallfolk
|
||||
local serializedData = GetSharedData(dataKey)
|
||||
local playerData
|
||||
if serializedData then
|
||||
local success, decoded = pcall(Smallfolk.loads, serializedData)
|
||||
if success and type(decoded) == 'table' then
|
||||
playerData = decoded
|
||||
else
|
||||
playerData = { pendingMessages = {} }
|
||||
end
|
||||
else
|
||||
playerData = { pendingMessages = {} }
|
||||
end
|
||||
|
||||
-- Parse header (12 chars hex: msgID + totalParts + partID)
|
||||
if #rawMessage < 12 then
|
||||
Error("Message too short for header, length:", #rawMessage)
|
||||
return nil
|
||||
end
|
||||
|
||||
local hexMsgID = rawMessage:sub(1, 4)
|
||||
local hexTotalParts = rawMessage:sub(5, 8)
|
||||
local hexPartID = rawMessage:sub(9, 12)
|
||||
|
||||
local msgID = HexToNumber(hexMsgID)
|
||||
local totalParts = HexToNumber(hexTotalParts)
|
||||
local partID = HexToNumber(hexPartID)
|
||||
local payload = rawMessage:sub(13)
|
||||
|
||||
Debug(string.format("Parsed: msgID=%d, totalParts=%d, partID=%d", msgID, totalParts, partID))
|
||||
Debug("Received part", partID, "of", totalParts, "for message ID", msgID)
|
||||
|
||||
-- Short message (msgID = 0, totalParts = 0, partID = 0)
|
||||
if msgID == 0 and totalParts == 0 and partID == 0 then
|
||||
Debug("Received short message, length:", #payload)
|
||||
return payload -- Return payload for processing
|
||||
end
|
||||
|
||||
-- Long message part
|
||||
Debug("Received part", partID, "of", totalParts, "for message ID", msgID)
|
||||
|
||||
-- Initialize message tracking
|
||||
if not playerData.pendingMessages[msgID] then
|
||||
Debug("Creating new pendingMessages entry for msgID", msgID)
|
||||
playerData.pendingMessages[msgID] = {
|
||||
parts = {},
|
||||
totalParts = totalParts,
|
||||
receivedParts = 0
|
||||
}
|
||||
end
|
||||
|
||||
local msgData = playerData.pendingMessages[msgID]
|
||||
|
||||
-- Store the part
|
||||
if not msgData.parts[partID] then
|
||||
msgData.parts[partID] = payload
|
||||
msgData.receivedParts = msgData.receivedParts + 1
|
||||
Debug(string.format("Stored part %d, now have %d of %d parts", partID, msgData.receivedParts, msgData.totalParts))
|
||||
else
|
||||
Debug(string.format("Duplicate part %d received, ignoring", partID))
|
||||
end
|
||||
|
||||
-- Save updated data back to C++ shared storage (serialize first)
|
||||
SetSharedData(dataKey, Smallfolk.dumps(playerData))
|
||||
|
||||
-- Check if we have all parts
|
||||
if msgData.receivedParts == msgData.totalParts then
|
||||
Debug("Message ID", msgID, "complete, reassembling...")
|
||||
|
||||
-- Reassemble message (parts are 1-indexed from client)
|
||||
local completeParts = {}
|
||||
for i = 1, msgData.totalParts do
|
||||
if msgData.parts[i] then
|
||||
table.insert(completeParts, msgData.parts[i])
|
||||
else
|
||||
Error("Missing part", i, "for message", msgID)
|
||||
playerData.pendingMessages[msgID] = nil
|
||||
SetSharedData(dataKey, Smallfolk.dumps(playerData))
|
||||
return nil
|
||||
end
|
||||
end
|
||||
local completeMessage = table.concat(completeParts)
|
||||
|
||||
Debug("Reassembled message length:", #completeMessage)
|
||||
|
||||
-- Clean up this message from pending
|
||||
playerData.pendingMessages[msgID] = nil
|
||||
SetSharedData(dataKey, Smallfolk.dumps(playerData))
|
||||
|
||||
return completeMessage -- Return complete message for processing
|
||||
end
|
||||
|
||||
-- Message incomplete, wait for more parts
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Process a complete message (deserialize and dispatch handlers)
|
||||
local function ProcessMessage(player, serializedMessage)
|
||||
-- Deserialize using Smallfolk
|
||||
local success, blocks = pcall(Smallfolk.loads, serializedMessage)
|
||||
|
||||
if not success or type(blocks) ~= 'table' then
|
||||
Error("Failed to deserialize message:", blocks)
|
||||
return
|
||||
end
|
||||
|
||||
Debug("Processing", #blocks, "message block(s)")
|
||||
|
||||
-- Process each block
|
||||
for i, block in ipairs(blocks) do
|
||||
if type(block) == 'table' and #block >= 1 then
|
||||
local handlerName = block[1]
|
||||
local data = block[2]
|
||||
|
||||
-- Find and call handler
|
||||
local handler = AMS.handlers[handlerName]
|
||||
if handler then
|
||||
Debug("Calling handler:", handlerName)
|
||||
|
||||
-- Use pcall to isolate errors
|
||||
local success, err = pcall(handler, player, data)
|
||||
if not success then
|
||||
Error("Handler", handlerName, "failed:", err)
|
||||
end
|
||||
else
|
||||
Error("No handler registered for", handlerName)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- ============================================================================
|
||||
-- Handler Registration
|
||||
-- ============================================================================
|
||||
|
||||
-- Register a message handler
|
||||
function AMS.RegisterHandler(handlerName, callback)
|
||||
if type(handlerName) ~= 'string' then
|
||||
Error("Handler name must be a string")
|
||||
return
|
||||
end
|
||||
|
||||
if type(callback) ~= 'function' then
|
||||
Error("Handler callback must be a function")
|
||||
return
|
||||
end
|
||||
|
||||
if AMS.handlers[handlerName] then
|
||||
Info("Overwriting handler:", handlerName)
|
||||
end
|
||||
|
||||
AMS.handlers[handlerName] = callback
|
||||
Info("Registered handler:", handlerName)
|
||||
end
|
||||
|
||||
-- ============================================================================
|
||||
-- Event Hooks (using shared global data across all states)
|
||||
-- ============================================================================
|
||||
|
||||
local stateMapId = GetStateMapId()
|
||||
print(string.format("[AMS Server] Registering event handlers in state %d", stateMapId))
|
||||
|
||||
-- Handle incoming addon messages from clients
|
||||
RegisterServerEvent(30, function(event, player, msgType, prefix, message, target)
|
||||
if prefix ~= AMS_PREFIX then
|
||||
return -- Not our message
|
||||
end
|
||||
|
||||
Debug(string.format("Received addon message from %s, length: %d", player:GetName(), #message))
|
||||
|
||||
-- Handle message reassembly (uses C++ ElunaSharedData)
|
||||
local completeMessage = HandleIncomingMessage(player, message)
|
||||
|
||||
if completeMessage then
|
||||
Debug("Message reassembled, length:", #completeMessage)
|
||||
-- Process the complete message
|
||||
ProcessMessage(player, completeMessage)
|
||||
else
|
||||
Debug("Waiting for more message parts...")
|
||||
end
|
||||
end)
|
||||
|
||||
-- Clean up player data on logout
|
||||
RegisterPlayerEvent(4, function(event, player)
|
||||
local playerGUID = tostring(player:GetGUIDLow())
|
||||
local dataKey = "AMS_PLAYER_" .. playerGUID
|
||||
|
||||
-- Clean up C++ shared data
|
||||
if HasSharedData(dataKey) then
|
||||
Debug("Cleaning up shared data for", player:GetName())
|
||||
ClearSharedData(dataKey)
|
||||
end
|
||||
|
||||
-- Also clean up local references (legacy)
|
||||
if AMS.playerData[playerGUID] then
|
||||
AMS.playerData[playerGUID] = nil
|
||||
AMS.nextMessageID[playerGUID] = nil
|
||||
end
|
||||
end)
|
||||
|
||||
print(string.format("[AMS Server] Event handlers registered successfully in state %d", stateMapId))
|
||||
|
||||
-- ============================================================================
|
||||
-- Initialization
|
||||
-- ============================================================================
|
||||
|
||||
print("[AMS Server] Initialization complete - AMS Server v" .. AMS_VERSION)
|
||||
Info("AMS Server v" .. AMS_VERSION .. " initialized")
|
||||
|
||||
-- Export for testing
|
||||
return AMS
|
||||
94
araxiaonline/lua_scripts/ab_AMS_Server/README.md
Executable file
94
araxiaonline/lua_scripts/ab_AMS_Server/README.md
Executable file
@@ -0,0 +1,94 @@
|
||||
# AMS Server - Araxia Messaging System (Server Side)
|
||||
|
||||
**Version:** 1.0.0-alpha
|
||||
|
||||
A lightweight, modern client-server messaging library for TrinityCore 11.2.5 with Eluna.
|
||||
|
||||
## Folder Structure
|
||||
|
||||
```
|
||||
AMS_Server/
|
||||
├── AMS_Server.lua - Main AMS server implementation
|
||||
├── smallfolk.lua - Serialization library dependency
|
||||
└── README.md - This file
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
The AMS_Server module is loaded automatically via:
|
||||
|
||||
```lua
|
||||
require("AMS_Server.AMS_Server") -- Loads AMS_Server/AMS_Server.lua
|
||||
```
|
||||
|
||||
Note: Named `AMS_Server.lua` instead of `init.lua` to avoid name collision with the main `init.lua` file.
|
||||
|
||||
## Components
|
||||
|
||||
### AMS_Server.lua
|
||||
Main AMS server implementation with:
|
||||
- Handler registration system
|
||||
- Message splitting/reassembly
|
||||
- Smallfolk serialization integration
|
||||
- Request/response patterns
|
||||
- Error isolation via pcall
|
||||
|
||||
### smallfolk.lua
|
||||
Lightweight Lua serialization library used for encoding/decoding messages between client and server.
|
||||
|
||||
## API
|
||||
|
||||
### Registering Handlers
|
||||
|
||||
```lua
|
||||
AMS.RegisterHandler("HANDLER_NAME", function(player, data)
|
||||
-- Process request
|
||||
local response = ProcessData(data)
|
||||
|
||||
-- Send response
|
||||
AMS.Send(player, "RESPONSE_NAME", response)
|
||||
end)
|
||||
```
|
||||
|
||||
### Sending Messages
|
||||
|
||||
**Simple send:**
|
||||
```lua
|
||||
AMS.Send(player, "UPDATE_NPC", {npcID = 1234, hp = 5000})
|
||||
```
|
||||
|
||||
**Fluent API (multiple handlers in one message):**
|
||||
```lua
|
||||
AMS.Msg()
|
||||
:Add("UPDATE_NPC", {npcID = 1234})
|
||||
:Add("UPDATE_QUEST", {questID = 5678})
|
||||
:Send(player)
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- ✅ Automatic message splitting for long payloads
|
||||
- ✅ Message reassembly on both ends
|
||||
- ✅ Handler registration system
|
||||
- ✅ Error isolation with pcall
|
||||
- ✅ Player disconnect cleanup
|
||||
- ✅ Debug logging (toggle via AMS_DEBUG)
|
||||
|
||||
## Dependencies
|
||||
|
||||
- **TrinityCore 11.2.5** with Eluna
|
||||
- **Smallfolk** - Included in this folder
|
||||
|
||||
## Related
|
||||
|
||||
- **Client:** `Interface/AddOns/AMS_Client/`
|
||||
- **Documentation:** `araxiaonline/araxia_docs/ams_system/`
|
||||
|
||||
## Version History
|
||||
|
||||
**1.0.0-alpha** (Current)
|
||||
- Initial release
|
||||
- Handler registration
|
||||
- Message splitting/reassembly
|
||||
- Smallfolk serialization
|
||||
- Error handling
|
||||
203
araxiaonline/lua_scripts/ab_AMS_Server/smallfolk.lua
Executable file
203
araxiaonline/lua_scripts/ab_AMS_Server/smallfolk.lua
Executable file
@@ -0,0 +1,203 @@
|
||||
-- Smallfolk serialization library
|
||||
-- Embedded dependency for AMS
|
||||
|
||||
local M = {}
|
||||
|
||||
local expect_object, dump_object
|
||||
local error, tostring, pairs, type, floor, huge, concat = error, tostring, pairs, type, math.floor, math.huge, table.concat
|
||||
|
||||
local dump_type = {}
|
||||
|
||||
function dump_type:string(nmemo, memo, acc)
|
||||
local nacc = #acc
|
||||
acc[nacc + 1] = '"'
|
||||
acc[nacc + 2] = self:gsub('"', '""')
|
||||
acc[nacc + 3] = '"'
|
||||
return nmemo
|
||||
end
|
||||
|
||||
function dump_type:number(nmemo, memo, acc)
|
||||
acc[#acc + 1] = ("%.17g"):format(self)
|
||||
return nmemo
|
||||
end
|
||||
|
||||
function dump_type:table(nmemo, memo, acc)
|
||||
memo[self] = nmemo
|
||||
acc[#acc + 1] = '{'
|
||||
local nself = #self
|
||||
for i = 1, nself do
|
||||
nmemo = dump_object(self[i], nmemo, memo, acc)
|
||||
acc[#acc + 1] = ','
|
||||
end
|
||||
for k, v in pairs(self) do
|
||||
if type(k) ~= 'number' or floor(k) ~= k or k < 1 or k > nself then
|
||||
nmemo = dump_object(k, nmemo, memo, acc)
|
||||
acc[#acc + 1] = ':'
|
||||
nmemo = dump_object(v, nmemo, memo, acc)
|
||||
acc[#acc + 1] = ','
|
||||
end
|
||||
end
|
||||
acc[#acc] = acc[#acc] == '{' and '{}' or '}'
|
||||
return nmemo
|
||||
end
|
||||
|
||||
function dump_object(object, nmemo, memo, acc)
|
||||
if object == true then
|
||||
acc[#acc + 1] = 't'
|
||||
elseif object == false then
|
||||
acc[#acc + 1] = 'f'
|
||||
elseif object == nil then
|
||||
acc[#acc + 1] = 'n'
|
||||
elseif object ~= object then
|
||||
if (''..object):sub(1,1) == '-' then
|
||||
acc[#acc + 1] = 'N'
|
||||
else
|
||||
acc[#acc + 1] = 'Q'
|
||||
end
|
||||
elseif object == huge then
|
||||
acc[#acc + 1] = 'I'
|
||||
elseif object == -huge then
|
||||
acc[#acc + 1] = 'i'
|
||||
else
|
||||
local t = type(object)
|
||||
if not dump_type[t] then
|
||||
error('cannot dump type ' .. t)
|
||||
end
|
||||
return dump_type[t](object, nmemo, memo, acc)
|
||||
end
|
||||
return nmemo
|
||||
end
|
||||
|
||||
function M.dumps(object)
|
||||
local nmemo = 0
|
||||
local memo = {}
|
||||
local acc = {}
|
||||
dump_object(object, nmemo, memo, acc)
|
||||
return concat(acc)
|
||||
end
|
||||
|
||||
local function invalid(i)
|
||||
error('invalid input at position ' .. i)
|
||||
end
|
||||
|
||||
local nonzero_digit = {['1'] = true, ['2'] = true, ['3'] = true, ['4'] = true, ['5'] = true, ['6'] = true, ['7'] = true, ['8'] = true, ['9'] = true}
|
||||
local is_digit = {['0'] = true, ['1'] = true, ['2'] = true, ['3'] = true, ['4'] = true, ['5'] = true, ['6'] = true, ['7'] = true, ['8'] = true, ['9'] = true}
|
||||
local function expect_number(string, start)
|
||||
local i = start
|
||||
local head = string:sub(i, i)
|
||||
if head == '-' then
|
||||
i = i + 1
|
||||
head = string:sub(i, i)
|
||||
end
|
||||
if nonzero_digit[head] then
|
||||
repeat
|
||||
i = i + 1
|
||||
head = string:sub(i, i)
|
||||
until not is_digit[head]
|
||||
elseif head == '0' then
|
||||
i = i + 1
|
||||
head = string:sub(i, i)
|
||||
else
|
||||
invalid(i)
|
||||
end
|
||||
if head == '.' then
|
||||
local oldi = i
|
||||
repeat
|
||||
i = i + 1
|
||||
head = string:sub(i, i)
|
||||
until not is_digit[head]
|
||||
if i == oldi + 1 then
|
||||
invalid(i)
|
||||
end
|
||||
end
|
||||
if head == 'e' or head == 'E' then
|
||||
i = i + 1
|
||||
head = string:sub(i, i)
|
||||
if head == '+' or head == '-' then
|
||||
i = i + 1
|
||||
head = string:sub(i, i)
|
||||
end
|
||||
if not is_digit[head] then
|
||||
invalid(i)
|
||||
end
|
||||
repeat
|
||||
i = i + 1
|
||||
head = string:sub(i, i)
|
||||
until not is_digit[head]
|
||||
end
|
||||
return tonumber(string:sub(start, i - 1)), i
|
||||
end
|
||||
|
||||
local expect_object_head = {
|
||||
t = function(string, i) return true, i end,
|
||||
f = function(string, i) return false, i end,
|
||||
n = function(string, i) return nil, i end,
|
||||
Q = function(string, i) return -(0/0), i end,
|
||||
N = function(string, i) return 0/0, i end,
|
||||
I = function(string, i) return 1/0, i end,
|
||||
i = function(string, i) return -1/0, i end,
|
||||
['"'] = function(string, i)
|
||||
local nexti = i - 1
|
||||
repeat
|
||||
nexti = string:find('"', nexti + 1, true) + 1
|
||||
until string:sub(nexti, nexti) ~= '"'
|
||||
return string:sub(i, nexti - 2):gsub('""', '"'), nexti
|
||||
end,
|
||||
['0'] = function(string, i)
|
||||
return expect_number(string, i - 1)
|
||||
end,
|
||||
['{'] = function(string, i, tables)
|
||||
local nt, k, v = {}
|
||||
local j = 1
|
||||
tables[#tables + 1] = nt
|
||||
if string:sub(i, i) == '}' then
|
||||
return nt, i + 1
|
||||
end
|
||||
while true do
|
||||
k, i = expect_object(string, i, tables)
|
||||
if string:sub(i, i) == ':' then
|
||||
v, i = expect_object(string, i + 1, tables)
|
||||
nt[k] = v
|
||||
else
|
||||
nt[j] = k
|
||||
j = j + 1
|
||||
end
|
||||
local head = string:sub(i, i)
|
||||
if head == ',' then
|
||||
i = i + 1
|
||||
elseif head == '}' then
|
||||
return nt, i + 1
|
||||
else
|
||||
invalid(i)
|
||||
end
|
||||
end
|
||||
end,
|
||||
}
|
||||
expect_object_head['1'] = expect_object_head['0']
|
||||
expect_object_head['2'] = expect_object_head['0']
|
||||
expect_object_head['3'] = expect_object_head['0']
|
||||
expect_object_head['4'] = expect_object_head['0']
|
||||
expect_object_head['5'] = expect_object_head['0']
|
||||
expect_object_head['6'] = expect_object_head['0']
|
||||
expect_object_head['7'] = expect_object_head['0']
|
||||
expect_object_head['8'] = expect_object_head['0']
|
||||
expect_object_head['9'] = expect_object_head['0']
|
||||
expect_object_head['-'] = expect_object_head['0']
|
||||
expect_object_head['.'] = expect_object_head['0']
|
||||
|
||||
expect_object = function(string, i, tables)
|
||||
local head = string:sub(i, i)
|
||||
if expect_object_head[head] then
|
||||
return expect_object_head[head](string, i + 1, tables)
|
||||
end
|
||||
invalid(i)
|
||||
end
|
||||
|
||||
function M.loads(string, maxsize)
|
||||
if #string > (maxsize or 10000) then
|
||||
error 'input too large'
|
||||
end
|
||||
return (expect_object(string, 1, {}))
|
||||
end
|
||||
|
||||
return M
|
||||
359
araxiaonline/lua_scripts/ac_ams_test_handlers.lua
Executable file
359
araxiaonline/lua_scripts/ac_ams_test_handlers.lua
Executable file
@@ -0,0 +1,359 @@
|
||||
--[[
|
||||
AMS Test Handlers - Server Side
|
||||
|
||||
Comprehensive test suite for validating AMS client-server messaging.
|
||||
Handlers for various test scenarios including echo, data types,
|
||||
performance, error handling, and server-push tests.
|
||||
|
||||
See: araxiaonline/araxia_docs/admin_npcdata/AMS_TESTSUITE.md
|
||||
]]
|
||||
|
||||
print("[AMS] Loading test handlers...")
|
||||
|
||||
-- Ensure AMS is loaded
|
||||
if not AMS then
|
||||
print("[AMS] ERROR: AMS not loaded! Test handlers will not work.")
|
||||
return
|
||||
end
|
||||
|
||||
-- Test statistics
|
||||
local testStats = {
|
||||
echoCount = 0,
|
||||
typeTestCount = 0,
|
||||
errorTestCount = 0,
|
||||
largePayloadCount = 0,
|
||||
totalMessagesReceived = 0,
|
||||
startTime = os.time()
|
||||
}
|
||||
|
||||
-- ============================================================================
|
||||
-- Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
-- Generate a large test payload
|
||||
local function GenerateLargePayload(size)
|
||||
local chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
||||
local payload = {}
|
||||
for i = 1, size do
|
||||
local idx = math.random(1, #chars)
|
||||
payload[i] = chars:sub(idx, idx)
|
||||
end
|
||||
return table.concat(payload)
|
||||
end
|
||||
|
||||
-- Calculate simple hash of string (for comparison)
|
||||
local function SimpleHash(str)
|
||||
local hash = 0
|
||||
for i = 1, #str do
|
||||
hash = (hash * 31 + string.byte(str, i)) % 0xFFFFFFFF
|
||||
end
|
||||
return hash
|
||||
end
|
||||
|
||||
-- Validate data types
|
||||
local function ValidateDataTypes(data)
|
||||
local results = {
|
||||
success = true,
|
||||
validations = {}
|
||||
}
|
||||
|
||||
if type(data.string) ~= "string" then
|
||||
results.success = false
|
||||
table.insert(results.validations, "string type failed")
|
||||
end
|
||||
|
||||
if type(data.number) ~= "number" then
|
||||
results.success = false
|
||||
table.insert(results.validations, "number type failed")
|
||||
end
|
||||
|
||||
if type(data.float) ~= "number" then
|
||||
results.success = false
|
||||
table.insert(results.validations, "float type failed")
|
||||
end
|
||||
|
||||
if type(data.boolean) ~= "boolean" then
|
||||
results.success = false
|
||||
table.insert(results.validations, "boolean type failed")
|
||||
end
|
||||
|
||||
if data.nilValue ~= nil then
|
||||
results.success = false
|
||||
table.insert(results.validations, "nil type failed")
|
||||
end
|
||||
|
||||
if type(data.table) ~= "table" then
|
||||
results.success = false
|
||||
table.insert(results.validations, "table type failed")
|
||||
end
|
||||
|
||||
if type(data.array) ~= "table" then
|
||||
results.success = false
|
||||
table.insert(results.validations, "array type failed")
|
||||
end
|
||||
|
||||
if results.success then
|
||||
table.insert(results.validations, "All data types validated successfully")
|
||||
end
|
||||
|
||||
return results
|
||||
end
|
||||
|
||||
-- ============================================================================
|
||||
-- Test Handlers
|
||||
-- ============================================================================
|
||||
|
||||
-- TEST_ECHO: Simple echo test (round-trip)
|
||||
AMS.RegisterHandler("TEST_ECHO", function(player, data)
|
||||
testStats.echoCount = testStats.echoCount + 1
|
||||
testStats.totalMessagesReceived = testStats.totalMessagesReceived + 1
|
||||
|
||||
print("[AMS] ECHO test from", player:GetName(), "- Message:", data.message)
|
||||
|
||||
-- Echo the data back with server timestamp
|
||||
local response = {
|
||||
message = data.message,
|
||||
clientTimestamp = data.timestamp,
|
||||
serverTimestamp = os.time(),
|
||||
echoCount = testStats.echoCount
|
||||
}
|
||||
|
||||
AMS.Send(player, "TEST_ECHO_RESPONSE", response)
|
||||
end)
|
||||
|
||||
-- TEST_TYPES: Test all Lua data types
|
||||
AMS.RegisterHandler("TEST_TYPES", function(player, data)
|
||||
testStats.typeTestCount = testStats.typeTestCount + 1
|
||||
testStats.totalMessagesReceived = testStats.totalMessagesReceived + 1
|
||||
|
||||
print("[AMS] TYPE test from", player:GetName())
|
||||
|
||||
-- Validate the received data types
|
||||
local validation = ValidateDataTypes(data)
|
||||
|
||||
-- Echo back the data with validation results
|
||||
local response = {
|
||||
receivedData = data,
|
||||
validation = validation,
|
||||
serverTimestamp = os.time()
|
||||
}
|
||||
|
||||
AMS.Send(player, "TEST_TYPES_RESPONSE", response)
|
||||
end)
|
||||
|
||||
-- TEST_LARGE_PAYLOAD: Test message splitting and reassembly
|
||||
AMS.RegisterHandler("TEST_LARGE_PAYLOAD", function(player, data)
|
||||
testStats.largePayloadCount = testStats.largePayloadCount + 1
|
||||
testStats.totalMessagesReceived = testStats.totalMessagesReceived + 1
|
||||
|
||||
print("[AMS] LARGE_PAYLOAD test from", player:GetName(), "- Size:", #data.payload, "bytes")
|
||||
|
||||
-- Calculate hash of received payload
|
||||
local receivedHash = SimpleHash(data.payload)
|
||||
|
||||
-- Generate a large response payload
|
||||
local responsePayload = GenerateLargePayload(data.responseSize or 3000)
|
||||
local responseHash = SimpleHash(responsePayload)
|
||||
|
||||
local response = {
|
||||
receivedSize = #data.payload,
|
||||
receivedHash = receivedHash,
|
||||
expectedHash = data.expectedHash,
|
||||
hashMatch = (receivedHash == data.expectedHash),
|
||||
responsePayload = responsePayload,
|
||||
responseHash = responseHash,
|
||||
serverTimestamp = os.time()
|
||||
}
|
||||
|
||||
AMS.Send(player, "TEST_LARGE_PAYLOAD_RESPONSE", response)
|
||||
end)
|
||||
|
||||
-- TEST_RAPID_FIRE: Receive many messages rapidly
|
||||
AMS.RegisterHandler("TEST_RAPID_FIRE", function(player, data)
|
||||
testStats.totalMessagesReceived = testStats.totalMessagesReceived + 1
|
||||
|
||||
-- Only log every 10th message to avoid spam
|
||||
if data.messageId % 10 == 0 then
|
||||
print("[AMS] RAPID_FIRE progress:", data.messageId, "of", data.totalMessages)
|
||||
end
|
||||
|
||||
-- Send acknowledgment back
|
||||
local response = {
|
||||
messageId = data.messageId,
|
||||
serverTimestamp = os.time()
|
||||
}
|
||||
|
||||
AMS.Send(player, "TEST_RAPID_FIRE_ACK", response)
|
||||
|
||||
-- If this was the last message, send completion
|
||||
if data.messageId == data.totalMessages then
|
||||
print("[AMS] RAPID_FIRE complete:", data.totalMessages, "messages received")
|
||||
AMS.Send(player, "TEST_RAPID_FIRE_COMPLETE", {
|
||||
totalMessages = data.totalMessages,
|
||||
totalReceived = testStats.totalMessagesReceived
|
||||
})
|
||||
end
|
||||
end)
|
||||
|
||||
-- TEST_REQUEST_PUSH: Client requests server to push data
|
||||
AMS.RegisterHandler("TEST_REQUEST_PUSH", function(player, data)
|
||||
testStats.totalMessagesReceived = testStats.totalMessagesReceived + 1
|
||||
|
||||
print("[AMS] REQUEST_PUSH from", player:GetName(), "- Count:", data.pushCount)
|
||||
|
||||
-- Send multiple server-initiated messages
|
||||
for i = 1, data.pushCount do
|
||||
local pushData = {
|
||||
pushNumber = i,
|
||||
totalPushes = data.pushCount,
|
||||
serverTimestamp = os.time(),
|
||||
randomData = math.random(1, 1000),
|
||||
message = "Server push #" .. i
|
||||
}
|
||||
|
||||
AMS.Send(player, "TEST_SERVER_PUSH", pushData)
|
||||
end
|
||||
|
||||
-- Send completion message
|
||||
AMS.Send(player, "TEST_REQUEST_PUSH_COMPLETE", {
|
||||
pushesSent = data.pushCount
|
||||
})
|
||||
end)
|
||||
|
||||
-- TEST_ERROR_HANDLING: Test various error scenarios
|
||||
AMS.RegisterHandler("TEST_ERROR_HANDLING", function(player, data)
|
||||
testStats.errorTestCount = testStats.errorTestCount + 1
|
||||
testStats.totalMessagesReceived = testStats.totalMessagesReceived + 1
|
||||
|
||||
print("[AMS] ERROR_HANDLING test from", player:GetName(), "- Type:", data.errorType)
|
||||
|
||||
if data.errorType == "throw" then
|
||||
-- This should be caught by AMS's pcall wrapper
|
||||
error("Intentional test error!")
|
||||
|
||||
elseif data.errorType == "invalid" then
|
||||
-- Send back invalid data (nil handler name)
|
||||
AMS.Send(player, nil, {error = "invalid handler name"})
|
||||
|
||||
elseif data.errorType == "timeout" then
|
||||
-- Simulate slow processing (don't send response immediately)
|
||||
print("[AMS] Simulating timeout (no response)")
|
||||
-- Don't send response
|
||||
|
||||
else
|
||||
-- Unknown error type - send response
|
||||
AMS.Send(player, "TEST_ERROR_HANDLING_RESPONSE", {
|
||||
success = false,
|
||||
error = "Unknown error type: " .. tostring(data.errorType)
|
||||
})
|
||||
end
|
||||
end)
|
||||
|
||||
-- TEST_PERFORMANCE: Measure round-trip performance
|
||||
AMS.RegisterHandler("TEST_PERFORMANCE", function(player, data)
|
||||
testStats.totalMessagesReceived = testStats.totalMessagesReceived + 1
|
||||
|
||||
local serverTime = os.time()
|
||||
|
||||
local response = {
|
||||
clientStartTime = data.startTime,
|
||||
serverReceiveTime = serverTime,
|
||||
testIteration = data.iteration,
|
||||
totalIterations = data.totalIterations
|
||||
}
|
||||
|
||||
AMS.Send(player, "TEST_PERFORMANCE_RESPONSE", response)
|
||||
|
||||
-- Log every 25th iteration
|
||||
if data.iteration % 25 == 0 then
|
||||
print("[AMS] PERFORMANCE test progress:", data.iteration, "of", data.totalIterations)
|
||||
end
|
||||
end)
|
||||
|
||||
-- TEST_NESTED_DATA: Test deeply nested table structures
|
||||
AMS.RegisterHandler("TEST_NESTED_DATA", function(player, data)
|
||||
testStats.totalMessagesReceived = testStats.totalMessagesReceived + 1
|
||||
|
||||
print("[AMS] NESTED_DATA test from", player:GetName(), "- Depth:", data.depth)
|
||||
|
||||
-- Validate nested structure
|
||||
local function countDepth(tbl, currentDepth)
|
||||
currentDepth = currentDepth or 0
|
||||
if type(tbl) ~= "table" then
|
||||
return currentDepth
|
||||
end
|
||||
local maxDepth = currentDepth
|
||||
for k, v in pairs(tbl) do
|
||||
if type(v) == "table" then
|
||||
local depth = countDepth(v, currentDepth + 1)
|
||||
if depth > maxDepth then
|
||||
maxDepth = depth
|
||||
end
|
||||
end
|
||||
end
|
||||
return maxDepth
|
||||
end
|
||||
|
||||
-- countDepth counts levels below root, add 1 to include root table
|
||||
local measuredDepth = countDepth(data.nestedData) + 1
|
||||
|
||||
local response = {
|
||||
expectedDepth = data.depth,
|
||||
measuredDepth = measuredDepth,
|
||||
depthMatch = (measuredDepth == data.depth),
|
||||
receivedData = data.nestedData
|
||||
}
|
||||
|
||||
AMS.Send(player, "TEST_NESTED_DATA_RESPONSE", response)
|
||||
end)
|
||||
|
||||
-- TEST_GET_STATS: Get test statistics
|
||||
AMS.RegisterHandler("TEST_GET_STATS", function(player, data)
|
||||
print("[AMS] GET_STATS request from", player:GetName())
|
||||
|
||||
local uptime = os.time() - testStats.startTime
|
||||
|
||||
local response = {
|
||||
stats = testStats,
|
||||
uptime = uptime,
|
||||
messagesPerMinute = uptime > 0 and (testStats.totalMessagesReceived / (uptime / 60)) or 0
|
||||
}
|
||||
|
||||
AMS.Send(player, "TEST_GET_STATS_RESPONSE", response)
|
||||
end)
|
||||
|
||||
-- TEST_RESET_STATS: Reset test statistics
|
||||
AMS.RegisterHandler("TEST_RESET_STATS", function(player, data)
|
||||
print("[AMS] RESET_STATS request from", player:GetName())
|
||||
|
||||
testStats = {
|
||||
echoCount = 0,
|
||||
typeTestCount = 0,
|
||||
errorTestCount = 0,
|
||||
largePayloadCount = 0,
|
||||
totalMessagesReceived = 0,
|
||||
startTime = os.time()
|
||||
}
|
||||
|
||||
AMS.Send(player, "TEST_RESET_STATS_RESPONSE", {
|
||||
success = true,
|
||||
message = "Test statistics reset"
|
||||
})
|
||||
end)
|
||||
|
||||
-- ============================================================================
|
||||
-- Initialization
|
||||
-- ============================================================================
|
||||
|
||||
print("[AMS] Registered test handlers:")
|
||||
print(" - TEST_ECHO")
|
||||
print(" - TEST_TYPES")
|
||||
print(" - TEST_LARGE_PAYLOAD")
|
||||
print(" - TEST_RAPID_FIRE")
|
||||
print(" - TEST_REQUEST_PUSH")
|
||||
print(" - TEST_ERROR_HANDLING")
|
||||
print(" - TEST_PERFORMANCE")
|
||||
print(" - TEST_NESTED_DATA")
|
||||
print(" - TEST_GET_STATS")
|
||||
print(" - TEST_RESET_STATS")
|
||||
print("[AMS] Test suite ready!")
|
||||
1475
araxiaonline/lua_scripts/ad_admin_handlers.lua
Executable file
1475
araxiaonline/lua_scripts/ad_admin_handlers.lua
Executable file
File diff suppressed because it is too large
Load Diff
196
araxiaonline/lua_scripts/ae_mcp_bridge.lua
Executable file
196
araxiaonline/lua_scripts/ae_mcp_bridge.lua
Executable file
@@ -0,0 +1,196 @@
|
||||
--[[
|
||||
MCP Bridge - Server Side
|
||||
|
||||
Provides a communication bridge between the WoW client and the MCP server.
|
||||
Client sends messages via AMS, server stores them in ElunaSharedData,
|
||||
MCP tools can read/write to ElunaSharedData.
|
||||
|
||||
Keys used:
|
||||
- mcp_client_chat: Last N chat messages from client
|
||||
- mcp_client_logs: Client-side addon errors/logs
|
||||
- mcp_to_client: Messages from MCP to display on client
|
||||
]]
|
||||
|
||||
local Smallfolk = require("aa_Smallfolk")
|
||||
|
||||
-- Configuration
|
||||
local MAX_CHAT_MESSAGES = 50
|
||||
local MAX_LOG_MESSAGES = 100
|
||||
|
||||
-- Initialize storage keys if not present
|
||||
local function InitSharedData()
|
||||
if not HasSharedData("mcp_client_chat") then
|
||||
SetSharedData("mcp_client_chat", Smallfolk.dumps({}))
|
||||
end
|
||||
if not HasSharedData("mcp_client_logs") then
|
||||
SetSharedData("mcp_client_logs", Smallfolk.dumps({}))
|
||||
end
|
||||
if not HasSharedData("mcp_to_client") then
|
||||
SetSharedData("mcp_to_client", Smallfolk.dumps({}))
|
||||
end
|
||||
end
|
||||
|
||||
-- Add a message to a shared data list (FIFO)
|
||||
local function AddToSharedList(key, message, maxItems)
|
||||
local data = GetSharedData(key)
|
||||
local list = data and Smallfolk.loads(data) or {}
|
||||
|
||||
table.insert(list, {
|
||||
timestamp = os.time(),
|
||||
message = message
|
||||
})
|
||||
|
||||
-- Trim to max size
|
||||
while #list > maxItems do
|
||||
table.remove(list, 1)
|
||||
end
|
||||
|
||||
SetSharedData(key, Smallfolk.dumps(list))
|
||||
end
|
||||
|
||||
-- AMS Handler: Client sends chat message for MCP to see
|
||||
local function HandleMCPChatMessage(player, data)
|
||||
local playerName = player:GetName()
|
||||
local message = data.message or ""
|
||||
local chatType = data.chatType or "CHAT"
|
||||
|
||||
local formatted = string.format("[%s] %s: %s", chatType, playerName, message)
|
||||
AddToSharedList("mcp_client_chat", formatted, MAX_CHAT_MESSAGES)
|
||||
|
||||
print("[MCP Bridge] Chat captured: " .. formatted)
|
||||
end
|
||||
|
||||
-- AMS Handler: Client sends log/error for MCP to see
|
||||
local function HandleMCPClientLog(player, data)
|
||||
local playerName = player:GetName()
|
||||
local logLevel = data.level or "INFO"
|
||||
local message = data.message or ""
|
||||
local source = data.source or "unknown"
|
||||
|
||||
local formatted = string.format("[%s] [%s] %s: %s", logLevel, source, playerName, message)
|
||||
AddToSharedList("mcp_client_logs", formatted, MAX_LOG_MESSAGES)
|
||||
|
||||
print("[MCP Bridge] Client log: " .. formatted)
|
||||
end
|
||||
|
||||
-- AMS Handler: Client requests any pending MCP messages
|
||||
local function HandleMCPGetMessages(player, data)
|
||||
local messagesData = GetSharedData("mcp_to_client")
|
||||
|
||||
-- MCP writes plain JSON, not Smallfolk format
|
||||
-- Just send the raw string - client can parse or display it
|
||||
local messages = {}
|
||||
|
||||
if messagesData and messagesData ~= "" and messagesData ~= "{}" and messagesData ~= "[]" then
|
||||
-- Try to extract simple messages from JSON-like format
|
||||
-- Format: [{"message":"text"},{"message":"text2"}]
|
||||
for msg in messagesData:gmatch('"message":"([^"]*)"') do
|
||||
table.insert(messages, {message = msg})
|
||||
end
|
||||
end
|
||||
|
||||
-- Clear after reading
|
||||
SetSharedData("mcp_to_client", "")
|
||||
|
||||
if #messages > 0 then
|
||||
AMS.Send(player, "MCP_MESSAGES_RESPONSE", {
|
||||
messages = messages
|
||||
})
|
||||
print("[MCP Bridge] Sent " .. #messages .. " messages to client")
|
||||
end
|
||||
end
|
||||
|
||||
-- AMS Handler: Client sends DB query result or other structured data
|
||||
local function HandleMCPClientData(player, data)
|
||||
local playerName = player:GetName()
|
||||
local dataType = data.dataType or "generic"
|
||||
local payload = data.payload or {}
|
||||
|
||||
-- Store in a type-specific key
|
||||
local key = "mcp_client_" .. dataType
|
||||
SetSharedData(key, Smallfolk.dumps({
|
||||
player = playerName,
|
||||
timestamp = os.time(),
|
||||
data = payload
|
||||
}))
|
||||
|
||||
print("[MCP Bridge] Client data received: " .. dataType)
|
||||
end
|
||||
|
||||
-- AMS Handler: Client sends UI state ("semantic screenshot")
|
||||
local function HandleMCPUIState(player, data)
|
||||
local playerName = player:GetName()
|
||||
|
||||
-- Store the UI state for MCP to read
|
||||
-- Convert to JSON-like string for MCP compatibility
|
||||
local uiState = {
|
||||
player = playerName,
|
||||
capturedAt = os.time(),
|
||||
target = data.target,
|
||||
playerInfo = data.player,
|
||||
mouseover = data.mouseover,
|
||||
tooltip = data.tooltip,
|
||||
openFrames = data.openFrames,
|
||||
mcpBridgeStatus = data.mcpBridgeStatus
|
||||
}
|
||||
|
||||
-- Store as JSON-ish format (simple key-value for MCP)
|
||||
local stateStr = string.format(
|
||||
'{"player":"%s","capturedAt":%d,"hasTarget":%s,"targetName":"%s","targetGuid":"%s","targetLevel":%s,"zone":"%s","openFrames":[%s]}',
|
||||
playerName,
|
||||
os.time(),
|
||||
data.target and "true" or "false",
|
||||
data.target and (data.target.name or "none") or "none",
|
||||
data.target and (data.target.guid or "") or "",
|
||||
data.target and tostring(data.target.level or 0) or "0",
|
||||
data.player and (data.player.zone or "unknown") or "unknown",
|
||||
data.openFrames and ('"' .. table.concat(data.openFrames, '","') .. '"') or ""
|
||||
)
|
||||
|
||||
SetSharedData("mcp_ui_state", stateStr)
|
||||
|
||||
-- Also store detailed target info separately if available
|
||||
if data.target then
|
||||
local targetStr = string.format(
|
||||
'{"name":"%s","guid":"%s","level":%d,"health":%d,"healthMax":%d,"creatureType":"%s","isDead":%s}',
|
||||
data.target.name or "unknown",
|
||||
data.target.guid or "",
|
||||
data.target.level or 0,
|
||||
data.target.health or 0,
|
||||
data.target.healthMax or 0,
|
||||
data.target.creatureType or "unknown",
|
||||
data.target.isDead and "true" or "false"
|
||||
)
|
||||
SetSharedData("mcp_current_target", targetStr)
|
||||
else
|
||||
SetSharedData("mcp_current_target", '{"hasTarget":false}')
|
||||
end
|
||||
|
||||
print("[MCP Bridge] UI state captured for " .. playerName)
|
||||
if data.target then
|
||||
print("[MCP Bridge] Target: " .. (data.target.name or "none") .. " (Level " .. (data.target.level or "?") .. ")")
|
||||
end
|
||||
end
|
||||
|
||||
-- Register AMS handlers
|
||||
local function RegisterAMSHandlers()
|
||||
-- Check if AMS is available
|
||||
if not AMS or not AMS.RegisterHandler then
|
||||
print("[MCP Bridge] Warning: AMS not available, skipping handler registration")
|
||||
return
|
||||
end
|
||||
|
||||
AMS.RegisterHandler("MCP_CHAT", HandleMCPChatMessage)
|
||||
AMS.RegisterHandler("MCP_CLIENT_LOG", HandleMCPClientLog)
|
||||
AMS.RegisterHandler("MCP_GET_MESSAGES", HandleMCPGetMessages)
|
||||
AMS.RegisterHandler("MCP_CLIENT_DATA", HandleMCPClientData)
|
||||
AMS.RegisterHandler("MCP_UI_STATE", HandleMCPUIState)
|
||||
|
||||
print("[MCP Bridge] AMS handlers registered (including UI state)")
|
||||
end
|
||||
|
||||
-- Initialize on load
|
||||
InitSharedData()
|
||||
RegisterAMSHandlers()
|
||||
|
||||
print("[MCP Bridge] Server-side bridge loaded")
|
||||
260
araxiaonline/lua_scripts/af_spawn_validator.lua
Normal file
260
araxiaonline/lua_scripts/af_spawn_validator.lua
Normal file
@@ -0,0 +1,260 @@
|
||||
--[[
|
||||
Spawn Validator - Headless creature spawn validation for MCP
|
||||
|
||||
Allows MCP to validate creature spawns without requiring a player in-game.
|
||||
Uses server-side APIs to check if creatures exist and are spawned correctly.
|
||||
|
||||
Shared Data Keys:
|
||||
- mcp_spawn_validation_request: Request from MCP to validate spawns
|
||||
- mcp_spawn_validation_result: Results of validation
|
||||
- mcp_spawn_query_result: Results of spawn queries
|
||||
]]
|
||||
|
||||
local Smallfolk = require("aa_Smallfolk")
|
||||
|
||||
-- Configuration
|
||||
local VALIDATION_INTERVAL = 1000 -- Check for requests every 1 second
|
||||
|
||||
-- Initialize shared data keys
|
||||
local function InitSharedData()
|
||||
if not HasSharedData("mcp_spawn_validation_request") then
|
||||
SetSharedData("mcp_spawn_validation_request", "")
|
||||
end
|
||||
if not HasSharedData("mcp_spawn_validation_result") then
|
||||
SetSharedData("mcp_spawn_validation_result", "")
|
||||
end
|
||||
if not HasSharedData("mcp_spawn_query_result") then
|
||||
SetSharedData("mcp_spawn_query_result", "")
|
||||
end
|
||||
if not HasSharedData("mcp_force_spawn_request") then
|
||||
SetSharedData("mcp_force_spawn_request", "")
|
||||
end
|
||||
end
|
||||
|
||||
-- Get all creatures in a specific area
|
||||
local function GetCreaturesInArea(mapId, centerX, centerY, centerZ, radius)
|
||||
local creatures = {}
|
||||
|
||||
-- Use GetCreaturesInWorld to find creatures
|
||||
local allCreatures = GetCreaturesInWorld(mapId)
|
||||
|
||||
if allCreatures then
|
||||
for _, creature in ipairs(allCreatures) do
|
||||
local cx, cy, cz = creature:GetLocation()
|
||||
local dx = cx - centerX
|
||||
local dy = cy - centerY
|
||||
local dz = cz - centerZ
|
||||
local dist = math.sqrt(dx*dx + dy*dy + dz*dz)
|
||||
|
||||
if dist <= radius then
|
||||
table.insert(creatures, {
|
||||
guid = creature:GetGUIDLow(),
|
||||
entry = creature:GetEntry(),
|
||||
name = creature:GetName(),
|
||||
x = cx,
|
||||
y = cy,
|
||||
z = cz,
|
||||
distance = dist,
|
||||
isAlive = creature:IsAlive(),
|
||||
level = creature:GetLevel()
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return creatures
|
||||
end
|
||||
|
||||
-- Get creature by entry ID
|
||||
local function GetCreaturesByEntry(mapId, entryId)
|
||||
local creatures = {}
|
||||
local allCreatures = GetCreaturesInWorld(mapId)
|
||||
|
||||
if allCreatures then
|
||||
for _, creature in ipairs(allCreatures) do
|
||||
if creature:GetEntry() == entryId then
|
||||
local x, y, z = creature:GetLocation()
|
||||
table.insert(creatures, {
|
||||
guid = creature:GetGUIDLow(),
|
||||
entry = creature:GetEntry(),
|
||||
name = creature:GetName(),
|
||||
x = x,
|
||||
y = y,
|
||||
z = z,
|
||||
isAlive = creature:IsAlive(),
|
||||
level = creature:GetLevel()
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return creatures
|
||||
end
|
||||
|
||||
-- Count all creatures on a map
|
||||
local function CountCreaturesOnMap(mapId)
|
||||
local allCreatures = GetCreaturesInWorld(mapId)
|
||||
return allCreatures and #allCreatures or 0
|
||||
end
|
||||
|
||||
-- Force spawn a creature at a location
|
||||
local function ForceSpawnCreature(mapId, entryId, x, y, z, orientation)
|
||||
-- SpawnCreature(entry, map, x, y, z, o, despawnType, despawnTime)
|
||||
-- despawnType: 0 = manual, 1 = timed
|
||||
local creature = PerformIngameSpawn(1, entryId, mapId, 0, x, y, z, orientation or 0, false, 0)
|
||||
|
||||
if creature then
|
||||
return {
|
||||
success = true,
|
||||
guid = creature:GetGUIDLow(),
|
||||
entry = creature:GetEntry(),
|
||||
name = creature:GetName(),
|
||||
x = x,
|
||||
y = y,
|
||||
z = z
|
||||
}
|
||||
else
|
||||
return {
|
||||
success = false,
|
||||
error = "Failed to spawn creature " .. entryId
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
-- Process validation requests from MCP
|
||||
local function ProcessValidationRequest()
|
||||
local requestData = GetSharedData("mcp_spawn_validation_request")
|
||||
|
||||
if not requestData or requestData == "" then
|
||||
return
|
||||
end
|
||||
|
||||
-- Clear the request
|
||||
SetSharedData("mcp_spawn_validation_request", "")
|
||||
|
||||
-- Parse request (simple JSON-like format)
|
||||
-- Format: {"action":"query_area","mapId":870,"x":1606,"y":-1733,"z":274,"radius":50}
|
||||
local action = requestData:match('"action":"([^"]*)"')
|
||||
local mapId = tonumber(requestData:match('"mapId":(%d+)'))
|
||||
|
||||
local result = {}
|
||||
|
||||
if action == "query_area" then
|
||||
local x = tonumber(requestData:match('"x":([%-%.%d]+)'))
|
||||
local y = tonumber(requestData:match('"y":([%-%.%d]+)'))
|
||||
local z = tonumber(requestData:match('"z":([%-%.%d]+)'))
|
||||
local radius = tonumber(requestData:match('"radius":(%d+)')) or 50
|
||||
|
||||
if mapId and x and y and z then
|
||||
local creatures = GetCreaturesInArea(mapId, x, y, z, radius)
|
||||
result = {
|
||||
success = true,
|
||||
action = "query_area",
|
||||
mapId = mapId,
|
||||
center = {x = x, y = y, z = z},
|
||||
radius = radius,
|
||||
count = #creatures,
|
||||
creatures = creatures
|
||||
}
|
||||
print("[Spawn Validator] Found " .. #creatures .. " creatures in area")
|
||||
else
|
||||
result = {success = false, error = "Missing coordinates"}
|
||||
end
|
||||
|
||||
elseif action == "query_entry" then
|
||||
local entryId = tonumber(requestData:match('"entryId":(%d+)'))
|
||||
|
||||
if mapId and entryId then
|
||||
local creatures = GetCreaturesByEntry(mapId, entryId)
|
||||
result = {
|
||||
success = true,
|
||||
action = "query_entry",
|
||||
mapId = mapId,
|
||||
entryId = entryId,
|
||||
count = #creatures,
|
||||
creatures = creatures
|
||||
}
|
||||
print("[Spawn Validator] Found " .. #creatures .. " creatures with entry " .. entryId)
|
||||
else
|
||||
result = {success = false, error = "Missing mapId or entryId"}
|
||||
end
|
||||
|
||||
elseif action == "count_map" then
|
||||
if mapId then
|
||||
local count = CountCreaturesOnMap(mapId)
|
||||
result = {
|
||||
success = true,
|
||||
action = "count_map",
|
||||
mapId = mapId,
|
||||
count = count
|
||||
}
|
||||
print("[Spawn Validator] Map " .. mapId .. " has " .. count .. " creatures")
|
||||
else
|
||||
result = {success = false, error = "Missing mapId"}
|
||||
end
|
||||
|
||||
elseif action == "force_spawn" then
|
||||
local entryId = tonumber(requestData:match('"entryId":(%d+)'))
|
||||
local x = tonumber(requestData:match('"x":([%-%.%d]+)'))
|
||||
local y = tonumber(requestData:match('"y":([%-%.%d]+)'))
|
||||
local z = tonumber(requestData:match('"z":([%-%.%d]+)'))
|
||||
local o = tonumber(requestData:match('"orientation":([%-%.%d]+)')) or 0
|
||||
|
||||
if mapId and entryId and x and y and z then
|
||||
result = ForceSpawnCreature(mapId, entryId, x, y, z, o)
|
||||
result.action = "force_spawn"
|
||||
print("[Spawn Validator] Force spawn result: " .. (result.success and "success" or "failed"))
|
||||
else
|
||||
result = {success = false, error = "Missing spawn parameters"}
|
||||
end
|
||||
|
||||
else
|
||||
result = {success = false, error = "Unknown action: " .. (action or "nil")}
|
||||
end
|
||||
|
||||
-- Store result as JSON
|
||||
local resultJson = string.format(
|
||||
'{"success":%s,"action":"%s","mapId":%d,"count":%d,"error":"%s","timestamp":%d}',
|
||||
result.success and "true" or "false",
|
||||
result.action or "unknown",
|
||||
result.mapId or 0,
|
||||
result.count or 0,
|
||||
result.error or "",
|
||||
os.time()
|
||||
)
|
||||
|
||||
-- For creature lists, append them
|
||||
if result.creatures and #result.creatures > 0 then
|
||||
local creatureStrs = {}
|
||||
for _, c in ipairs(result.creatures) do
|
||||
table.insert(creatureStrs, string.format(
|
||||
'{"guid":%d,"entry":%d,"name":"%s","x":%.2f,"y":%.2f,"z":%.2f,"level":%d,"alive":%s}',
|
||||
c.guid or 0,
|
||||
c.entry or 0,
|
||||
c.name or "unknown",
|
||||
c.x or 0,
|
||||
c.y or 0,
|
||||
c.z or 0,
|
||||
c.level or 0,
|
||||
c.isAlive and "true" or "false"
|
||||
))
|
||||
end
|
||||
resultJson = resultJson:gsub('}$', ',"creatures":[' .. table.concat(creatureStrs, ',') .. ']}')
|
||||
end
|
||||
|
||||
SetSharedData("mcp_spawn_validation_result", resultJson)
|
||||
end
|
||||
|
||||
-- Register a world update hook to check for requests
|
||||
local function OnWorldUpdate(event, diff)
|
||||
ProcessValidationRequest()
|
||||
end
|
||||
|
||||
-- Initialize
|
||||
InitSharedData()
|
||||
|
||||
-- Register world update event (fires every server tick)
|
||||
RegisterServerEvent(14, OnWorldUpdate) -- WORLD_EVENT_ON_UPDATE
|
||||
|
||||
print("[Spawn Validator] Loaded - MCP can now validate spawns without a player")
|
||||
print("[Spawn Validator] Actions: query_area, query_entry, count_map, force_spawn")
|
||||
19
araxiaonline/lua_scripts/ag_reload_helper.lua
Executable file
19
araxiaonline/lua_scripts/ag_reload_helper.lua
Executable file
@@ -0,0 +1,19 @@
|
||||
-- Eluna Reload Helper
|
||||
-- Provides an in-game command to reload Eluna scripts
|
||||
|
||||
local function ReloadElunaCommand(event, player, command)
|
||||
if command == "elunareload" then
|
||||
if player:GetGMRank() >= 3 then
|
||||
player:SendBroadcastMessage("Reloading Eluna scripts...")
|
||||
ReloadEluna()
|
||||
player:SendBroadcastMessage("Eluna scripts reloaded!")
|
||||
else
|
||||
player:SendBroadcastMessage("You don't have permission to reload Eluna.")
|
||||
end
|
||||
return false -- Prevent command from going to server
|
||||
end
|
||||
end
|
||||
|
||||
RegisterPlayerEvent(42, ReloadElunaCommand) -- PLAYER_EVENT_ON_COMMAND
|
||||
|
||||
print("[Eluna] Reload helper loaded. Use '.elunareload' in-game to reload scripts.")
|
||||
40
araxiaonline/lua_scripts/zz_init.lua
Executable file
40
araxiaonline/lua_scripts/zz_init.lua
Executable file
@@ -0,0 +1,40 @@
|
||||
-- Eluna Integration Tests Initialization
|
||||
-- This script is loaded at server startup and runs the integration test suite
|
||||
|
||||
-- Only run tests in the global/world Eluna instance (mapId will be max uint32 for global)
|
||||
-- Each map instance will also load this script, but we skip execution for those
|
||||
local mapId = GetStateMapId()
|
||||
-- Global/world state is -1 (or 4294967295 as unsigned). Skip for map instances.
|
||||
if mapId ~= -1 and mapId ~= 4294967295 then
|
||||
return
|
||||
end
|
||||
|
||||
print("\n" .. string.rep("=", 80))
|
||||
print("ELUNA INTEGRATION TEST SUITE - AUTO-RUNNING AT STARTUP")
|
||||
print(string.rep("=", 80) .. "\n")
|
||||
|
||||
-- Load the test runner
|
||||
local testRunner = require("integration_tests/test_runner")
|
||||
|
||||
-- Load all test modules (they will register with TestRunner)
|
||||
require("integration_tests/test_core_functionality")
|
||||
require("integration_tests/test_events")
|
||||
require("integration_tests/test_data_types")
|
||||
require("integration_tests/test_bindings")
|
||||
|
||||
-- Run all registered tests
|
||||
testRunner:RunAll()
|
||||
|
||||
print("\n" .. string.rep("=", 80))
|
||||
print("ELUNA INTEGRATION TEST SUITE - COMPLETE")
|
||||
print("Loading AMS Server...")
|
||||
require("ab_AMS_Server.AMS_Server")
|
||||
print("AMS Server loaded successfully")
|
||||
|
||||
-- Load AMS test handlers
|
||||
require("ac_ams_test_handlers")
|
||||
|
||||
-- Load AraxiaTrinityAdmin server handlers (use dofile to force re-execution after AMS is loaded)
|
||||
dofile("/opt/trinitycore/lua_scripts/ad_admin_handlers.lua")
|
||||
|
||||
print(string.rep("=", 80) .. "\n")
|
||||
Reference in New Issue
Block a user