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
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:
@@ -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")
|
||||
Reference in New Issue
Block a user