-- Conservative recovery for managed residents with fresh quest/explore intent. -- Native position/progress is sampled independently of dashboard summaries. local intents, observations = {}, {} local LIMIT = 600 local COOLDOWN = 1800 local function sqltext(s) return "CONVERT(UNHEX('"..string.gsub(s,".",function(c)return string.format('%02X',string.byte(c))end).."') USING utf8mb4)" end AuthDBExecute([[CREATE TABLE IF NOT EXISTS azerothai_recovery_log ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,guid INT UNSIGNED NOT NULL, name VARCHAR(64) NOT NULL,at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, detail VARCHAR(255) NOT NULL,INDEX(guid,at))]]) function AzerothAISetRecoveryIntents(rows) local nextIntents={} for _,r in ipairs(rows) do if type(r.guid)=='number' and type(r.account)=='number' and type(r.name)=='string' and (r.action=='quest' or r.action=='explore') then nextIntents[r.guid]=r;r.at=os.time() end end intents=nextIntents return 'Recovery intentions refreshed' end local function safe(p,a) if p:IsDead() or p:IsInCombat() or p:IsCasting() or p:IsTaxi() or a:HasGameClientMaster() or p:IsRooted() or a:IsMovementImpaired() then return false end local g=p:GetGroup() if g then if #g:GetMembers()~=g:GetMembersCount() then return false end for _,m in pairs(g:GetMembers()) do if not m:IsPlayerbot() then return false end end end return true end local function sample() local now=os.time() for guid,r in pairs(intents) do local p=GetPlayerByName(r.name) if now-r.at>120 or not p or not p:IsPlayerbot() or p:GetGUIDLow()~=guid or p:GetAccountId()~=r.account then observations[guid]=nil else local a=p:GetBotAI() if not a or not safe(p,a) then observations[guid]=nil else local x,y,z,map=p:GetX(),p:GetY(),p:GetZ(),p:GetMapId() local level,hp=p:GetLevel(),p:GetHealth() local old=observations[guid] if not old or old.map~=map or old.level~=level or old.hp~=hp or (x-old.x)^2+(y-old.y)^2+(z-old.z)^2>25 then observations[guid]={x=x,y=y,z=z,map=map,level=level,hp=hp,since=now} elseif now-old.since>=LIMIT then -- Persistent cooldown survives ALE reloads; exact identity must still be active. local q=AuthDBQuery('SELECT guid FROM azerothai_lifecycle WHERE guid='..guid..' AND account_id='..r.account..' AND BINARY name='..sqltext(r.name).." AND state='active'") local recent=AuthDBQuery('SELECT id FROM azerothai_recovery_log WHERE guid='..guid..' AND at>NOW()-INTERVAL '..COOLDOWN..' SECOND LIMIT 1') if q and not recent then local staleFlight=p:GetMovementType()==7 and not p:IsTaxi() if staleFlight then p:MoveExpire();p:MoveClear() end a:Reset(true) local moved=a:DoSpecificAction('move random',true) local detail=(staleFlight and 'Cleared stale flight; ' or '')..'AI replanned; walking request='..tostring(moved) AuthDBExecute('INSERT INTO azerothai_recovery_log(guid,name,detail) VALUES ('..guid..','..sqltext(r.name)..','..sqltext(detail)..')') PrintInfo('[AzerothAI recovery] '..r.name..': '..detail) end old.since=now end end end end -- Forget residents whose intent changed to resting/socializing or who went offline. for guid in pairs(observations) do if not intents[guid] then observations[guid]=nil end end end CreateLuaEvent(function()local ok,err=pcall(sample);if not ok then PrintError('[AzerothAI recovery] '..tostring(err)) end end,30000,0) PrintInfo('[AzerothAI] Conservative stuck recovery enabled: 10 minutes idle, 30 minute cooldown')