Import verified AzerothAI snapshot from GitHub
Core tests / core (push) Successful in 9m57s
Full verification / application (push) Failing after 5m16s
Full verification / local-brain (push) Failing after 6m15s
Realm simulation / simulated-live-world (push) Failing after 5m32s
WoW integration lab / real-worldserver (push) Canceled after 17m23s

Source branch: feature/ai-brain-router
Source commit: c2a09e0460465201a58aaab56c71424e9a96405e
This commit is contained in:
Richard Clark
2026-09-22 13:12:54 -04:00
commit 5f0fc86cf9
175 changed files with 25807 additions and 0 deletions
+345
View File
@@ -0,0 +1,345 @@
-- AzerothAI <-> AzerothCore ALE bridge
-- Install this file into AzerothCore/lua_scripts after building mod-ale.
--
-- The live command/event transport is intentionally database-backed:
-- AzerothAI API <-> host relay <-> acore_auth spool <-> ALE
-- This avoids mod-ale's outbound HTTP worker entirely for the control plane.
--
-- Optional worldserver.conf additions:
-- AzerothAI.PollMs = "750"
-- AzerothAI.ProtectedCharacter = "Zaku"
local POLL_MS = tonumber(GetConfigValue("AzerothAI.PollMs")) or 750
local PROTECTED = GetConfigValue("AzerothAI.ProtectedCharacter")
if not PROTECTED or PROTECTED == "" then PROTECTED = "Zaku" end
local polling = false
-- The deployment helper fills this with authoritative RNDBOT account IDs.
-- This makes bot detection work even on an older mod-ale build that does not
-- yet expose IsPlayerbot()/IsRandomBot() to Lua.
local KNOWN_BOT_ACCOUNTS = {}
local function is_playerbot(player)
if not player then return false end
local ok,value = pcall(function() return player:IsPlayerbot() end)
if ok and value then return true end
ok,value = pcall(function() return player:IsRandomBot() end)
if ok and value then return true end
ok,value = pcall(function() return player:IsBot() end)
if ok and value then return true end
local okAccount,accountId = pcall(function() return player:GetAccountId() end)
if okAccount and KNOWN_BOT_ACCOUNTS[tonumber(accountId) or -1] then return true end
return false
end
local function escape_json(value)
value = tostring(value or "")
value = string.gsub(value, "\\", "\\\\")
value = string.gsub(value, '"', '\\"')
value = string.gsub(value, "\r", "\\r")
value = string.gsub(value, "\n", "\\n")
value = string.gsub(value, "\t", "\\t")
return value
end
local function json_value(value)
local t = type(value)
if value == nil then return "null" end
if t == "boolean" then return value and "true" or "false" end
if t == "number" then return tostring(value) end
return '"' .. escape_json(value) .. '"'
end
local function encode_object(tbl)
local parts = {}
for k,v in pairs(tbl or {}) do
table.insert(parts, '"' .. escape_json(k) .. '":' .. json_value(v))
end
return "{" .. table.concat(parts, ",") .. "}"
end
local function to_hex(value)
value=tostring(value or "")
local out={}
for i=1,#value do
out[#out+1]=string.format("%02X",string.byte(value,i))
end
return table.concat(out)
end
local function db_text(hex)
return "CONVERT(UNHEX('" .. hex .. "') USING utf8mb4)"
end
function AzerothAIComplete(id, ok, result)
local sql="INSERT INTO azerothai_result_outbox(command_id,ok,result) VALUES (" ..
db_text(to_hex(id)) .. "," .. (ok and "1" or "0") .. "," .. db_text(to_hex(result)) ..
") ON DUPLICATE KEY UPDATE ok=VALUES(ok),result=VALUES(result),delivered_at=NULL"
AuthDBExecute(sql)
end
local function emit(kind, payload)
local body=encode_object(payload)
local sql="INSERT INTO azerothai_event_outbox(kind,payload) VALUES (" ..
db_text(to_hex(kind)) .. "," .. db_text(to_hex(body)) .. ")"
AuthDBExecute(sql)
end
local function player_payload(player)
if not player then return {} end
local isBot = is_playerbot(player)
return {
name=player:GetName(),
characterGuid=player:GetGUIDLow(),
accountId=player:GetAccountId(),
level=player:GetLevel(),
mapId=player:GetMapId(),
zoneId=player:GetZoneId(),
areaId=player:GetAreaId(),
zoneName=GetAreaName(player:GetZoneId()) or tostring(player:GetZoneId()),
race=player:GetRace(),
class=player:GetClass(),
raceName=player:GetRaceAsString() or tostring(player:GetRace()),
className=player:GetClassAsString() or tostring(player:GetClass()),
healthPct=player:GetHealthPct(),
isBot=isBot
}
end
local function selected_bot_name(player)
local ok,target=pcall(function() return player:GetSelection() end)
if not ok or not target or not is_playerbot(target) then return "" end
local okName,name=pcall(function() return target:GetName() end)
if okName and name then return tostring(name) end
return ""
end
local function nearby_bot_names(player)
local ok,players=pcall(function() return player:GetPlayersInRange(35) end)
if not ok or type(players)~="table" then return "" end
local names={}
for _,other in pairs(players) do
if other and is_playerbot(other) then
local okName,name=pcall(function() return other:GetName() end)
if okName and name and name~="" then table.insert(names,tostring(name)) end
end
end
table.sort(names)
return table.concat(names,",")
end
function AzerothAIEmitPlayerSnapshot(name)
if not name or name == "" then return 0 end
local ok,player=pcall(function() return GetPlayerByName(name) end)
if not ok or not player or not is_playerbot(player) then return 0 end
emit("player.snapshot",player_payload(player))
return 1
end
local function mark_consumed(id)
AuthDBExecute("UPDATE azerothai_command_inbox SET consumed_at=NOW() WHERE id=" .. db_text(to_hex(id)))
end
local function poll()
if polling then return end
polling=true
local ok,err=pcall(function()
local q=AuthDBQuery("SELECT id,source FROM azerothai_command_inbox WHERE consumed_at IS NULL ORDER BY created_at,id LIMIT 1")
if not q then return end
local id=q:GetString(0)
local source=q:GetString(1)
local loader=load or loadstring
local chunk,compileErr=loader(source)
if not chunk then
AzerothAIComplete(id,false,"command compile failed: "..tostring(compileErr))
mark_consumed(id)
return
end
local ran,runErr=pcall(chunk)
if not ran then
AzerothAIComplete(id,false,"command crashed: "..tostring(runErr))
end
mark_consumed(id)
end)
polling=false
if not ok then PrintError("[AzerothAI] DB command poll crashed: "..tostring(err)) end
end
-- Durable command lane: the host relay places wrapped Lua commands into the
-- auth database, so ALE never depends on its fragile outbound HTTP client.
CreateLuaEvent(function() poll() end, POLL_MS, 0)
-- Keep AzerothAI grounded in real online Playerbots even when no human is logged in.
-- Expose a safe global refresh hook too, so the autonomy worker can explicitly
-- request a fresh authoritative snapshot if the periodic timer ever stalls.
function AzerothAIEmitSnapshots()
local ok,players=pcall(function() return GetPlayersInWorld() end)
if not ok or type(players)~="table" then return 0 end
local emitted=0
for _,player in pairs(players) do
if is_playerbot(player) then
emit("player.snapshot",player_payload(player))
emitted=emitted+1
end
end
return emitted
end
CreateLuaEvent(function() AzerothAIEmitSnapshots() end, 30000, 0)
-- High-value authoritative world events.
RegisterServerEvent(14, function(event)
emit("server.startup",{players=GetPlayerCount()})
end)
RegisterServerEvent(15, function(event)
emit("server.shutdown",{players=GetPlayerCount()})
end)
RegisterServerEvent(25, function(event, zoneId, state, grade)
emit("world.weather",{zoneId=zoneId,state=state,grade=grade})
end)
RegisterServerEvent(34, function(event, gameEventId)
emit("world.game-event.start",{gameEventId=gameEventId})
end)
RegisterServerEvent(35, function(event, gameEventId)
emit("world.game-event.stop",{gameEventId=gameEventId})
end)
RegisterPlayerEvent(3, function(event, player)
emit("player.login",player_payload(player))
end)
RegisterPlayerEvent(4, function(event, player)
emit("player.logout",player_payload(player))
end)
RegisterPlayerEvent(8, function(event, killer, killed)
local payload=player_payload(killed)
payload.killer = killer and killer:GetName() or "creature"
emit("player.killed-by-creature",payload)
end)
RegisterPlayerEvent(13, function(event, player, oldLevel)
local payload=player_payload(player)
payload.oldLevel=oldLevel
emit("player.level",payload)
end)
RegisterPlayerEvent(18, function(event, player, msg, chatType, lang)
local payload=player_payload(player)
payload.message=msg
payload.chatType=chatType
payload.language=lang
if not payload.isBot then
payload.selectedPlayer=selected_bot_name(player)
payload.nearbyBots=nearby_bot_names(player)
end
emit("player.chat",payload)
end)
RegisterPlayerEvent(19, function(event, player, msg, chatType, lang, receiver)
local payload=player_payload(player)
payload.message=msg
payload.chatType=chatType
payload.language=lang
payload.receiver=receiver and receiver:GetName() or ""
payload.receiverIsBot=receiver and is_playerbot(receiver) or false
emit("player.whisper",payload)
end)
RegisterPlayerEvent(27, function(event, player, newZone, newArea)
local payload=player_payload(player)
payload.zone=newZone
payload.area=newArea
payload.zoneName=GetAreaName(newZone) or tostring(newZone)
emit("player.zone",payload)
end)
RegisterPlayerEvent(28, function(event, player)
emit("player.map",player_payload(player))
end)
RegisterPlayerEvent(30, function(event, player)
emit("player.first-login",player_payload(player))
end)
local COMBAT_EVENT_INTERVAL = 10
local lastCombatEvent = {}
local function emit_combat(kind, player, payload)
local guid=player and player:GetGUIDLow() or 0
local key=tostring(guid)
local now=os.time()
local last=lastCombatEvent[key] or 0
if now-last < COMBAT_EVENT_INTERVAL then return end
lastCombatEvent[key]=now
emit(kind,payload)
end
RegisterPlayerEvent(33, function(event, player, enemy)
local payload=player_payload(player)
payload.enemy=enemy and enemy:GetName() or ""
emit_combat("player.combat.enter",player,payload)
end)
RegisterPlayerEvent(34, function(event, player)
emit_combat("player.combat.leave",player,player_payload(player))
end)
RegisterPlayerEvent(35, function(event, player)
emit("player.repop",player_payload(player))
end)
RegisterPlayerEvent(36, function(event, player)
emit("player.resurrect",player_payload(player))
end)
RegisterPlayerEvent(45, function(event, player, achievement)
local payload=player_payload(player)
payload.achievement=tostring(achievement)
emit("player.achievement",payload)
end)
RegisterPlayerEvent(63, function(event, player, quest)
local payload=player_payload(player)
payload.quest=tostring(quest)
emit("player.quest.accept",payload)
end)
-- Zaku plot armor: clamp common player/creature-originated damage paths to leave 1 HP.
-- This is runtime-only. Never send this rule into Zaku's AI prompt or memories.
local function clamp_target_damage(target, damage)
if not target or not target.GetName or target:GetName() ~= PROTECTED then return damage end
local health=target:GetHealth()
if health <= 1 then return 0 end
if damage >= health then
local payload=player_payload(target)
payload.healthBefore=health
payload.incomingDamage=damage
emit("player.close-call",payload)
return health - 1
end
return damage
end
local function clamp_player_damage(event, player, target, damage, ...)
return clamp_target_damage(target, damage)
end
for _,eventId in ipairs({66,68,69,70,72}) do
RegisterPlayerEvent(eventId, clamp_player_damage)
end
local function clamp_creature_damage(event, creature, target, damage, ...)
return clamp_target_damage(target, damage)
end
for _,eventId in ipairs({7,9,10,11,13}) do
RegisterAllCreatureEvent(eventId, clamp_creature_damage)
end
emit("bridge.ready",{pollMs=POLL_MS,protectedCharacter=PROTECTED})
PrintInfo("[AzerothAI] ALE bridge loaded; durable DB command polling every " .. tostring(POLL_MS) .. " ms")