Fix progression stalls and add normal class trainer visits
Core tests / core (push) Successful in 29s

This commit is contained in:
Codex
2026-09-24 09:24:57 +12:00
parent d57d793a84
commit 132bf75f9d
24 changed files with 343 additions and 31 deletions
+131
View File
@@ -0,0 +1,131 @@
-- Class training uses the core's trainer eligibility and purchase transaction.
-- Travel uses ordinary pathfinding. No spell grants, money grants or teleportation.
local registry,records,trainers={}, {}, {}
local registryAt=0
local strategies={'new rpg','rpg','quest','grind','gather','loot','follow','travel'}
local function quote(s)return "UNHEX('"..string.gsub(tostring(s),'.',function(c)return string.format('%02X',string.byte(c))end).."')" end
AuthDBQuery([[CREATE TABLE IF NOT EXISTS azerothai_training (
guid INT UNSIGNED PRIMARY KEY,name VARCHAR(64),account_id INT UNSIGNED,checked_level INT,
pending INT DEFAULT 0,trainer VARCHAR(100),status VARCHAR(100),at BIGINT UNSIGNED,
learned INT DEFAULT 0,last_learned BIGINT UNSIGNED DEFAULT 0,
flags VARCHAR(200) DEFAULT '',lease_expires BIGINT UNSIGNED DEFAULT 0)]])
local q=WorldDBQuery([[SELECT t.Requirement,c.id,c.map,c.position_x,c.position_y,c.position_z,ct.name,f.FactionGroup
FROM creature c JOIN creature_default_trainer d ON d.CreatureId=c.id JOIN trainer t ON t.Id=d.TrainerId
JOIN creature_template ct ON ct.entry=c.id JOIN factiontemplate_dbc f ON f.ID=ct.faction
WHERE t.Type=0 AND t.Requirement>0 AND c.map IN (0,1,530,571)]])
if q then repeat trainers[#trainers+1]={class=q:GetUInt32(0),entry=q:GetUInt32(1),map=q:GetUInt32(2),x=q:GetFloat(3),y=q:GetFloat(4),z=q:GetFloat(5),name=q:GetString(6),team=q:GetUInt32(7)} until not q:NextRow() end
local function managed(p)
local row=p and registry[p:GetGUIDLow()]
return row and row.name==p:GetName() and row.account==p:GetAccountId() and p:IsPlayerbot() and p:GetBotAI() and not p:GetBotAI():HasGameClientMaster()
end
local function safe(p)
if not managed(p) or p:IsDead() or p:IsInCombat() or p:IsCasting() or p:HasUnitState(0x00000100) then return false end
local group=p:GetGroup()
if group then
if group:IsRaidGroup() or #group:GetMembers()~=group:GetMembersCount() then return false end
for _,m in pairs(group:GetMembers()) do if not managed(m) then return false end end
end
return not (AzerothAISurvivalOverride and AzerothAISurvivalOverride(p))
end
local function save(p,r)
AuthDBExecute('INSERT INTO azerothai_training(guid,name,account_id,checked_level,pending,trainer,status,at,learned,last_learned) VALUES ('..p:GetGUIDLow()..','..quote(p:GetName())..','..p:GetAccountId()..','..p:GetLevel()..','..(r.pending or 0)..','..quote(r.target and r.target.name or '')..','..quote(r.status or '')..','..os.time()..','..(r.learned or 0)..','..(r.lastLearned or 0)..') ON DUPLICATE KEY UPDATE name=VALUES(name),account_id=VALUES(account_id),checked_level=VALUES(checked_level),pending=VALUES(pending),trainer=VALUES(trainer),status=VALUES(status),at=VALUES(at),learned=VALUES(learned),last_learned=VALUES(last_learned)')
end
local function release(p,r)
local row=AuthDBQuery('SELECT flags FROM azerothai_training WHERE guid='..p:GetGUIDLow()..' AND account_id='..p:GetAccountId()..' AND name='..quote(p:GetName()))
if row then
local flags=row:GetString(0)
for _,strategy in ipairs(strategies) do if string.find(','..flags..',',','..strategy..',',1,true) then p:GetBotAI():ChangeStrategy('+'..strategy,1) end end
AuthDBQuery("UPDATE azerothai_training SET flags='',lease_expires=0 WHERE guid="..p:GetGUIDLow())
end
r.holding=false;r.visitSince=nil
end
local function hold(p,r,now)
local ai=p:GetBotAI()
if not r.holding then
local flags={};for _,s in ipairs(strategies) do if ai:HasStrategy(s,1) then flags[#flags+1]=s end end
-- Save synchronously before suppressing work, so a reload cannot strand these flags.
AuthDBQuery('INSERT INTO azerothai_training(guid,name,account_id,checked_level,flags,lease_expires) VALUES ('..p:GetGUIDLow()..','..quote(p:GetName())..','..p:GetAccountId()..','..p:GetLevel()..','..quote(table.concat(flags,','))..','..(now+120)..') ON DUPLICATE KEY UPDATE flags=VALUES(flags),lease_expires=VALUES(lease_expires)')
r.holding=true;r.leaseSaved=now
elseif now-(r.leaseSaved or 0)>=30 then
AuthDBExecute('UPDATE azerothai_training SET lease_expires='..(now+120)..' WHERE guid='..p:GetGUIDLow());r.leaseSaved=now
end
for _,s in ipairs(strategies) do ai:ChangeStrategy('-'..s,1) end
ai:ChangeStrategy('-stay',1)
end
function AzerothAITrainingOverride(p)
local r=records[p:GetGUIDLow()];return r and r.holding and r.status=='traveling' or false
end
function AzerothAITrainingSnapshot(p)
local r=records[p:GetGUIDLow()];if not r then return {} end
return {trainingPending=r.pending or 0,trainingLevel=r.level or 0,trainingTrainer=r.target and r.target.name or '',trainingStatus=r.status or '',trainingAt=r.at or 0,trainingLearned=r.learned or 0,lastTrainingAt=r.lastLearned or 0}
end
local function audit(p,r,now)
local ai=p:GetBotAI();r.level=p:GetLevel();r.checked=now;r.target=nil;r.pending=0
if not ai.GetClassTrainerStatus then r.status='native trainer API unavailable';return end
local best,seen,found=math.huge,{},false
local team=p:GetTeam()==0 and 2 or 4
for _,t in ipairs(trainers) do
if t.class==p:GetClass() and t.map==p:GetMapId() and math.floor(t.team/team)%2==1 then
found=true
if not seen[t.entry] then local n,cost=ai:GetClassTrainerStatus(t.entry);seen[t.entry]={n=n,cost=cost} end
local need=seen[t.entry];local dist=(p:GetX()-t.x)^2+(p:GetY()-t.y)^2
if need.n>0 and dist<best then best=dist;r.target=t;r.pending=need.n;r.cost=need.cost end
end
end
if not found then r.status='no class trainer on this map'
elseif not r.target then r.status='no new trainable spells'
elseif best>6000^2 then r.status='class trainer too far for a local visit'
elseif p:GetCoinage()<r.cost then r.status='saving for training'
else r.status='training due' end
end
local function step(p,r,now)
if r.holding and now-(r.at or now)>120 then release(p,r) end
r.at=now
if p:GetLevel()~=r.level then r.retryAt=0 end
if not r.checked or p:GetLevel()~=r.level or now-r.checked>=60 then audit(p,r,now) end
if not safe(p) then if r.holding then release(p,r) end;save(p,r);return end
if r.status~='training due' and r.status~='traveling' then if r.holding then release(p,r) end;save(p,r);return end
if now<(r.retryAt or 0) then r.status='visit retry cooldown';save(p,r);return end
local t=r.target
if not t then return end
if not r.visitSince then r.visitSince=now;r.progressAt=now;r.distance=math.huge end
local distance=math.sqrt((p:GetX()-t.x)^2+(p:GetY()-t.y)^2)
if distance<r.distance-3 then r.progressAt=now;r.distance=distance end
if now-r.visitSince>300 or now-r.progressAt>45 then
release(p,r);r.status='trainer path needs retry';r.retryAt=now+300;save(p,r);return
end
hold(p,r,now);r.status='traveling'
local npc=p:GetNearestCreature(10,t.entry)
if npc and p:GetDistance(npc)<=5 then
p:MoveStop()
local learned=p:GetBotAI():TrainAtClassTrainer(npc)
if learned>0 then r.learned=(r.learned or 0)+learned;r.lastLearned=now end
release(p,r);audit(p,r,now);r.retryAt=now+30
else
p:MoveTo(7821,t.x,t.y,t.z,true)
end
save(p,r)
end
CreateLuaEvent(function()
local now=os.time()
if now-registryAt>=60 then
local rows={};local result=AuthDBQuery("SELECT guid,name,account_id FROM azerothai_lifecycle WHERE state='active'")
if result then repeat rows[result:GetUInt32(0)]={name=result:GetString(1),account=result:GetUInt32(2)} until not result:NextRow() end
registry=rows;registryAt=now
end
for _,p in pairs(GetPlayersInWorld() or {}) do if managed(p) then
local id=p:GetGUIDLow();local r=records[id]
if not r then
r={};records[id]=r
local row=AuthDBQuery('SELECT learned,last_learned FROM azerothai_training WHERE guid='..id..' AND account_id='..p:GetAccountId()..' AND name='..quote(p:GetName()))
if row then r.learned=row:GetUInt32(0);r.lastLearned=row:GetUInt32(1) end
release(p,r)
end
local ok,err=pcall(step,p,r,now);if not ok then PrintError('[AzerothAI training] '..tostring(err));release(p,r) end
else
local r=records[p:GetGUIDLow()]
-- Return our paused strategies when ownership changes; do not continue the visit.
if r and r.holding and p:IsPlayerbot() and p:GetBotAI() then release(p,r) end
end end
for id,r in pairs(records) do if now-(r.at or now)>600 then records[id]=nil end end
end,10000,0)