106 lines
6.0 KiB
Lua
106 lines
6.0 KiB
Lua
-- Native bounded invitation retries and verified-death membership cleanup.
|
|
local registry,registryAt,pending={},0,{}
|
|
local function text(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_party_audit (
|
|
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
kind VARCHAR(32),resident VARCHAR(64),peer VARCHAR(64),detail VARCHAR(255))]])
|
|
local function audit(kind,a,b,detail)
|
|
AuthDBExecute('INSERT INTO azerothai_party_audit(kind,resident,peer,detail) VALUES ('..text(kind)..','..text(a)..','..text(b)..','..text(detail)..')')
|
|
end
|
|
local function refresh()
|
|
if os.time()-registryAt<5 then return end
|
|
local q=AuthDBQuery('SELECT guid,name,account_id,state FROM azerothai_lifecycle');local rows={}
|
|
if q then repeat rows[q:GetUInt32(0)]={name=q:GetString(1),account=q:GetUInt32(2),state=q:GetString(3)} until not q:NextRow() end
|
|
registry=rows;registryAt=os.time()
|
|
end
|
|
local function resident(p)
|
|
local r=p and registry[p:GetGUIDLow()]
|
|
return p and p:IsPlayerbot() and r and r.state=='active' and r.name==p:GetName() and r.account==p:GetAccountId() and not p:IsDead() and p:GetBotAI() and not p:GetBotAI():HasGameClientMaster()
|
|
end
|
|
local function available(p)return resident(p) and not p:IsInCombat() and not p:IsCasting() and not p:IsTaxi() end
|
|
local function cleanGroup(p)
|
|
local g=p:GetGroup();if not g or g:IsRaidGroup() then return end
|
|
local members=g:GetMembers();for _,m in pairs(members) do
|
|
if not m:IsPlayerbot() or not m:GetBotAI() or m:GetBotAI():HasGameClientMaster() or m:IsInCombat() then return end
|
|
end
|
|
-- Validate the complete persisted roster, including offline members, before changing it.
|
|
local q=CharDBQuery('SELECT c.guid,c.name,c.account FROM group_member gm JOIN characters c ON c.guid=gm.memberGuid WHERE gm.guid=(SELECT guid FROM group_member WHERE memberGuid='..p:GetGUIDLow()..')')
|
|
if not q then return end
|
|
local count,dead=0,{}
|
|
repeat
|
|
local id,name,account=q:GetUInt32(0),q:GetString(1),q:GetUInt32(2);local r=registry[id]
|
|
if not r or r.name~=name or r.account~=account or (r.state~='active' and r.state~='dead') then return end
|
|
if r.state=='dead' then dead[#dead+1]={id=id,name=name}
|
|
else
|
|
local live=GetPlayerByName(name)
|
|
if not resident(live) or live:GetGUIDLow()~=id or live:GetGroup()~=g then return end
|
|
end
|
|
count=count+1
|
|
until not q:NextRow()
|
|
if count~=g:GetMembersCount() then return end
|
|
-- Removing from a two-member group can destroy the Group object: stop after one mutation.
|
|
local d=dead[1]
|
|
if d then
|
|
local guid=g:GetMemberGUID(d.name)
|
|
if GetGUIDLow(guid)~=d.id then return end
|
|
if g:RemoveMember(guid,0) then
|
|
audit('retired-member-removed',p:GetName(),d.name,'Removed verified permanent-death membership; living members preserved')
|
|
if AzerothAIEmitSnapshots then AzerothAIEmitSnapshots() end
|
|
end
|
|
end
|
|
end
|
|
local function attempt(r)
|
|
local a,b=GetPlayerByName(r.a.name),GetPlayerByName(r.b.name)
|
|
if not a or not b then return false,'Resident offline' end
|
|
if a:GetGUIDLow()~=r.a.guid or a:GetAccountId()~=r.a.account or b:GetGUIDLow()~=r.b.guid or b:GetAccountId()~=r.b.account then return nil,'Identity changed' end
|
|
if not resident(a) or not resident(b) then return nil,'Resident retired or human-controlled' end
|
|
if a:GetTeam()~=b:GetTeam() or math.abs(a:GetLevel()-b:GetLevel())>5 then return nil,'Faction or level mismatch' end
|
|
if not available(a) or not available(b) then return false,'Waiting for combat, cast, or taxi to finish' end
|
|
if a:GetMapId()~=b:GetMapId() or a:GetPhaseMask()~=b:GetPhaseMask() or a:GetDistance(b)>60 then return false,'Waiting for a nearby meeting within 60 yards' end
|
|
local g=a:GetGroup()
|
|
if g and b:GetGroup()==g then return true,'Already grouped' end
|
|
if b:GetGroup() then return nil,'Companion joined another party' end
|
|
if g then
|
|
if g:IsRaidGroup() or g:GetMembersCount()>=5 then return nil,'Party full or raid' end
|
|
if #g:GetMembers()~=g:GetMembersCount() then return false,'Waiting for incomplete party cleanup' end
|
|
for _,m in pairs(g:GetMembers()) do if not resident(m) then return nil,'Human or unmanaged party is protected' end end
|
|
if not g:AddMember(b) then return nil,'Native party join rejected' end
|
|
else g=a:GroupCreate(b) end
|
|
if not g or b:GetGroup()~=g then return nil,'Party membership was not applied' end
|
|
return true,'Confirmed party'
|
|
end
|
|
function AzerothAIQueueParty(id,a,b,expires)
|
|
if pending[id] then return {azerothaiDeferred=true} end
|
|
if type(id)~='string' or type(expires)~='number' or expires<=os.time() or expires>os.time()+60 then error('Invitation expired before delivery') end
|
|
for _,r in pairs(pending) do if r.a.guid==a.guid or r.b.guid==a.guid or r.a.guid==b.guid or r.b.guid==b.guid then error('Resident already has a pending invitation') end end
|
|
pending[id]={a=a,b=b,expires=expires}
|
|
return {azerothaiDeferred=true}
|
|
end
|
|
local function pulse()
|
|
refresh()
|
|
for id,r in pairs(pending) do
|
|
local ran,ok,reason
|
|
if os.time()>=r.expires then ran=true;ok=false;reason=r.reason or 'No safe nearby meeting' else ran,ok,reason=pcall(attempt,r) end
|
|
r.reason=reason
|
|
if not ran then reason=tostring(ok);ok=nil end
|
|
if ok or ok==nil or os.time()>=r.expires then
|
|
pending[id]=nil
|
|
local result=ok and reason or (ok==nil and reason or 'Invitation expired: '..reason)
|
|
audit(ok and 'invitation-completed' or 'invitation-failed',r.a.name,r.b.name,result)
|
|
AzerothAIComplete(id,ok==true,result)
|
|
if AzerothAIEmitSnapshots then AzerothAIEmitSnapshots() end
|
|
end
|
|
end
|
|
end
|
|
CreateLuaEvent(function()local ok,e=pcall(pulse);if not ok then PrintError('[AzerothAI parties] '..tostring(e)) end end,2000,0)
|
|
CreateLuaEvent(function()
|
|
refresh();local seen={}
|
|
for _,p in pairs(GetPlayersInWorld() or {}) do
|
|
if available(p) and p:GetGroup() then
|
|
local id=tostring(p:GetGroup():GetGUID())
|
|
if not seen[id] then seen[id]=true;local ok,e=pcall(cleanGroup,p);if not ok then PrintError('[AzerothAI parties] '..tostring(e)) end end
|
|
end
|
|
end
|
|
end,10000,0)
|
|
PrintInfo('[AzerothAI] Bounded native invitations and verified-death party cleanup enabled')
|