Persist fatal encounters and audit resident readiness with party outcome metrics
Core tests / core (push) Successful in 42s
Core tests / core (push) Successful in 42s
This commit is contained in:
@@ -147,12 +147,19 @@ local function world_payload(player)
|
||||
p.humanControlled=ai and ai:HasGameClientMaster() or false
|
||||
p.quests={};if ai then for slot=0,24 do local q=player:GetQuestSlotQuestId(slot);if q and q>0 then p.quests[#p.quests+1]=q end end end
|
||||
if ai then p.combatRole=player:IsHealer() and 'healer' or player:IsTank() and 'tank' or 'damage' end
|
||||
if AzerothAISurvivalState then p.dangerAt,p.survivalAction,p.survivalAt=AzerothAISurvivalState(player) end
|
||||
if AzerothAISurvivalState then p.dangerAt,p.survivalAction,p.survivalAt,p.survivalSince=AzerothAISurvivalState(player) end
|
||||
local g=player:GetGroup()
|
||||
p.groupId=g and tostring(g:GetGUID()) or ""
|
||||
local members={}
|
||||
if g then for _,m in pairs(g:GetMembers()) do members[#members+1]=m:GetName() end end
|
||||
p.groupNames=table.concat(members,",")
|
||||
p.isDead=player:IsDead();p.isGhost=player:HasAura(8326)
|
||||
p.manaPct=player:GetPowerType()==0 and player:GetPowerPct(0) or nil
|
||||
if p.manaPct and (p.manaPct~=p.manaPct or p.manaPct==math.huge) then p.manaPct=nil end
|
||||
local leader=ai and ai:GetGroupLeader()
|
||||
p.leaderName=leader and leader:GetName() or ''
|
||||
if AzerothAIExperienceSnapshot then for k,v in pairs(AzerothAIExperienceSnapshot(player)) do p[k]=v end end
|
||||
if AzerothAIReadinessSnapshot then for k,v in pairs(AzerothAIReadinessSnapshot(player)) do p[k]=v end end
|
||||
p.nearbyBots=nearby_bot_names(player)
|
||||
return p
|
||||
end
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
-- Bounded durable encounter memory. No changes to health, resurrection or stats.
|
||||
local cache={}
|
||||
local function quote(s)return "UNHEX('"..string.gsub(tostring(s),'.',function(c)return string.format('%02X',string.byte(c))end).."')" end
|
||||
-- Synchronous DDL: the next snapshot may query immediately after ALE reload.
|
||||
AuthDBQuery([[CREATE TABLE IF NOT EXISTS azerothai_death_experience (
|
||||
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,guid INT UNSIGNED,account_id INT UNSIGNED,
|
||||
name VARCHAR(64),at BIGINT UNSIGNED,killer VARCHAR(100),entry INT UNSIGNED,
|
||||
map INT UNSIGNED,x FLOAT,y FLOAT,level INT UNSIGNED,INDEX resident(guid,at))]])
|
||||
local function identity(p)
|
||||
if not p or not p:IsPlayerbot() or not p:GetBotAI() or p:GetBotAI():HasGameClientMaster() then return false end
|
||||
local q=AuthDBQuery("SELECT name,account_id FROM azerothai_lifecycle WHERE state='active' AND guid="..p:GetGUIDLow())
|
||||
return q and q:GetString(0)==p:GetName() and q:GetUInt32(1)==p:GetAccountId()
|
||||
end
|
||||
local function memories(p)
|
||||
local guid,now=p:GetGUIDLow(),os.time();local c=cache[guid]
|
||||
if c and c.account==p:GetAccountId() and c.name==p:GetName() and now-c.loaded<60 then return c.rows end
|
||||
local rows={}
|
||||
local q=AuthDBQuery('SELECT id,at,killer,entry,map,x,y,level FROM azerothai_death_experience WHERE guid='..guid..' AND account_id='..p:GetAccountId()..' AND name='..quote(p:GetName())..' AND at>'..(now-30*86400)..' ORDER BY id DESC LIMIT 32')
|
||||
if q then repeat rows[#rows+1]={id=q:GetUInt32(0),at=q:GetUInt32(1),killer=q:GetString(2),entry=q:GetUInt32(3),map=q:GetUInt32(4),x=q:GetFloat(5),y=q:GetFloat(6),level=q:GetUInt32(7)} until not q:NextRow() end
|
||||
cache[guid]={loaded=now,account=p:GetAccountId(),name=p:GetName(),rows=rows};return rows
|
||||
end
|
||||
function AzerothAILearnedDanger(p,target)
|
||||
local creature=target and target:ToCreature();if not creature then return false end
|
||||
for _,m in ipairs(memories(p)) do
|
||||
-- Reassess after three levels or three days. Avoid this creature type near the fatal encounter.
|
||||
if m.entry==creature:GetEntry() and m.map==p:GetMapId() and os.time()-m.at<3*86400 and p:GetLevel()<m.level+3 and (target:GetX()-m.x)^2+(target:GetY()-m.y)^2<=10000 then return true end
|
||||
end
|
||||
return false
|
||||
end
|
||||
function AzerothAIExperienceSnapshot(p)
|
||||
local m=memories(p)[1]
|
||||
if m then return {deathMemoryId=m.id,deathMemoryAt=m.at,deathMemoryKiller=m.killer,deathMemoryMap=m.map,deathMemoryX=m.x,deathMemoryY=m.y,deathMemoryLevel=m.level} end
|
||||
return {}
|
||||
end
|
||||
local function died(_,killer,p)
|
||||
if not identity(p) then return end
|
||||
local now=os.time();local c=cache[p:GetGUIDLow()]
|
||||
if c and c.recorded==now then return end
|
||||
local creature=killer and killer:ToCreature()
|
||||
AuthDBExecute('INSERT INTO azerothai_death_experience(guid,account_id,name,at,killer,entry,map,x,y,level) VALUES ('..p:GetGUIDLow()..','..p:GetAccountId()..','..quote(p:GetName())..','..now..','..quote(killer and killer:GetName() or 'Unknown')..','..(creature and creature:GetEntry() or 0)..','..p:GetMapId()..','..p:GetX()..','..p:GetY()..','..p:GetLevel()..')')
|
||||
cache[p:GetGUIDLow()]={loaded=0,recorded=now,rows={}}
|
||||
end
|
||||
RegisterPlayerEvent(8,died)
|
||||
RegisterPlayerEvent(6,died)
|
||||
CreateLuaEvent(function()
|
||||
AuthDBExecute('DELETE FROM azerothai_death_experience WHERE at<'..(os.time()-30*86400)..' LIMIT 2000')
|
||||
for guid,c in pairs(cache) do if os.time()-c.loaded>600 then cache[guid]=nil end end
|
||||
end,3600000,0)
|
||||
@@ -0,0 +1,69 @@
|
||||
-- Inspect actual inventory and equipment once per minute; normal NPC interactions only.
|
||||
local snapshots,registry={},{}
|
||||
-- WotLK 3.3.5 UpdateFields.h: OBJECT_END (6) + 0x36 / 0x37.
|
||||
-- Read only; verified against this AzerothCore build's item update fields.
|
||||
local ITEM_DURABILITY,ITEM_MAX_DURABILITY=0x003C,0x003D
|
||||
local registryAt=0
|
||||
local consumables={}
|
||||
-- Match playerbots' own food/drink categories, without an incomplete item-ID list.
|
||||
local q=WorldDBQuery('SELECT entry,spellcategory_1 FROM item_template WHERE class=0 AND subclass IN (0,5) AND spellcategory_1 IN (11,59)')
|
||||
if q then repeat consumables[q:GetUInt32(0)]=q:GetUInt32(1) until not q:NextRow() end
|
||||
local function managed(p)
|
||||
local r=p and registry[p:GetGUIDLow()]
|
||||
return r and p:IsPlayerbot() and r.name==p:GetName() and r.account==p:GetAccountId() 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:IsTaxi() then return false end
|
||||
local g=p:GetGroup()
|
||||
if g then
|
||||
if g:IsRaidGroup() or #g:GetMembers()~=g:GetMembersCount() then return false end
|
||||
for _,m in pairs(g:GetMembers()) do if not managed(m) then return false end end
|
||||
end
|
||||
return not (AzerothAISurvivalOverride and AzerothAISurvivalOverride(p))
|
||||
end
|
||||
function AzerothAIReadinessSnapshot(p)
|
||||
local r=snapshots[p:GetGUIDLow()]
|
||||
return r and r.name==p:GetName() and r.account==p:GetAccountId() and r.values or {}
|
||||
end
|
||||
local function inspect(p)
|
||||
local ai=p:GetBotAI();local v={readinessAt=os.time(),brokenItems=0,freeTalents=p:GetFreeTalentPoints(),foodCount=0,drinkCount=0}
|
||||
for slot=0,18 do
|
||||
local item=p:GetItemByPos(255,slot)
|
||||
if item then
|
||||
if item:IsBroken() then v.brokenItems=v.brokenItems+1 end
|
||||
local maximum=item:GetUInt32Value(ITEM_MAX_DURABILITY)
|
||||
if maximum>0 then local pct=100*item:GetUInt32Value(ITEM_DURABILITY)/maximum;v.minDurabilityPct=math.min(v.minDurabilityPct or 100,pct) end
|
||||
end
|
||||
end
|
||||
for _,item in pairs(ai:GetInventoryItems()) do
|
||||
local category=consumables[item:GetEntry()]
|
||||
if category and p:CanUseItem(item) then
|
||||
if category==11 then v.foodCount=v.foodCount+item:GetCount() else v.drinkCount=v.drinkCount+item:GetCount() end
|
||||
end
|
||||
end
|
||||
snapshots[p:GetGUIDLow()]={name=p:GetName(),account=p:GetAccountId(),values=v}
|
||||
if not safe(p) then return end
|
||||
if v.freeTalents>0 then ai:DoSpecificAction('auto talents',true) end
|
||||
local supplies=v.foodCount<5 or p:GetPowerType()==0 and v.drinkCount<5
|
||||
if (v.minDurabilityPct or 100)<30 or v.brokenItems>0 or supplies then
|
||||
-- RPG handles finding an appropriate vendor and walking there. No spawned supplies.
|
||||
ai:ChangeStrategy('+rpg',1)
|
||||
if not ai:HasCheat(2) then
|
||||
if (v.minDurabilityPct or 100)<30 or v.brokenItems>0 then ai:DoSpecificAction('repair',true) end
|
||||
if supplies then ai:DoSpecificAction('rpg buy',true) end
|
||||
end
|
||||
end
|
||||
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 old=snapshots[p:GetGUIDLow()]
|
||||
if not old or now-old.values.readinessAt>=60 then local ok,err=pcall(inspect,p);if not ok then PrintError('[AzerothAI readiness] '..tostring(err)) end end
|
||||
end end
|
||||
for guid,r in pairs(snapshots) do if now-r.values.readinessAt>600 then snapshots[guid]=nil end end
|
||||
end,10000,0)
|
||||
@@ -55,7 +55,7 @@ local function dangerousSelection(p,r,now)
|
||||
local target=p:GetSelection()
|
||||
if not target then return false end
|
||||
local key=tostring(target:GetGUID())
|
||||
if not dangerous(p,target) and not (r.avoidTargets and (r.avoidTargets[key] or 0)>now) then return false end
|
||||
if not dangerous(p,target) and not (AzerothAILearnedDanger and AzerothAILearnedDanger(p,target)) and not (r.avoidTargets and (r.avoidTargets[key] or 0)>now) then return false end
|
||||
for _,enemy in pairs(p:GetUnfriendlyUnitsInRange(40)) do
|
||||
if enemy:GetGUID()==target:GetGUID() then return true end
|
||||
end
|
||||
@@ -91,7 +91,7 @@ end
|
||||
function AzerothAISurvivalState(p)
|
||||
local r=observed[p:GetGUIDLow()]
|
||||
if not r then return 0,'',0 end
|
||||
return r.dangerAt or 0,r.action or '',r.actionAt or 0
|
||||
return r.dangerAt or 0,r.action or '',r.actionAt or 0,r.actionSince or r.actionAt or 0
|
||||
end
|
||||
-- Prevent slower model intentions from undoing an immediate safety action.
|
||||
function AzerothAISurvivalOverride(p)
|
||||
@@ -105,6 +105,7 @@ local function sample()
|
||||
if previous then outcome(p,previous,now) end
|
||||
if managed(p) then
|
||||
local id=p:GetGUIDLow();local ai=p:GetBotAI();local r=observed[id] or {};observed[id]=r
|
||||
local previousAction,previousAt=r.action,r.actionAt or 0
|
||||
local hp=p:GetHealthPct()
|
||||
local falling=r.hp and r.hp-hp>=20
|
||||
local mana=p:GetPowerType()==0 and p:GetPowerPct(0) or 100
|
||||
@@ -125,7 +126,7 @@ local function sample()
|
||||
for _,m in pairs(peers) do if m~=p and m:GetMapId()==p:GetMapId() and m:GetPhaseMask()==p:GetPhaseMask() and p:GetDistance(m)<=35 and m:GetHealthPct()>=70 then support=support+1 end end
|
||||
local attackers=0;for _ in pairs(p:GetAttackers()) do attackers=attackers+1 end
|
||||
local enemy=p:GetVictim()
|
||||
local overmatched=dangerous(p,enemy)
|
||||
local overmatched=dangerous(p,enemy) or (AzerothAILearnedDanger and AzerothAILearnedDanger(p,enemy))
|
||||
r.avoidTargets=r.avoidTargets or {}
|
||||
for key,expires in pairs(r.avoidTargets) do if expires<=now then r.avoidTargets[key]=nil end end
|
||||
if enemy and (overmatched or attackers>support+1 or falling and hp<80) then r.avoidTargets[tostring(enemy:GetGUID())]=now+600 end
|
||||
@@ -195,6 +196,7 @@ local function sample()
|
||||
end
|
||||
end
|
||||
end
|
||||
if r.actionAt and (r.action~=previousAction or now-previousAt>30) then r.actionSince=now end
|
||||
end
|
||||
end
|
||||
for id,r in pairs(observed) do if now-(r.seen or 0)>600 then observed[id]=nil;ownedMasters[id]=nil end end
|
||||
|
||||
Reference in New Issue
Block a user