-- Persistent hardcore retirement. The native random-bot manager fills vacancies -- from its factory-created, unused starting-level reserve; identities are never reset. -- Install only after deploy/playable/install-lifecycle.py has seeded the registry. local residents = {} local ready = false local vetoAt = {} local unmanaged = {} local TARGET_PER_FACTION = 25 local gates = {} local gateAt = {} local arrivals = {} local reconciledAt = 0 local alliance = {[1]=true,[3]=true,[4]=true,[7]=true,[11]=true} local horde = {[2]=true,[5]=true,[6]=true,[8]=true,[10]=true} local function faction(row) if alliance[row.race] then return 1 end if horde[row.race] then return 2 end return nil end local function sqltext(value) return "CONVERT(UNHEX('"..(string.gsub(value,".",function(c) return string.format("%02X",string.byte(c)) end)).."') USING utf8mb4)" end local function startingLevel(player) return player:GetClass()==6 and 55 or 1 end local function enroll(player) local guid=player:GetGUIDLow() if unmanaged[guid] then return nil end local account=player:GetAccountId() local name=player:GetName() if string.lower(name)=="zaku" then return nil end -- The native factory can create additional accounts during startup. Accept -- only its accounts and only genuinely unused characters as new residents. local owned=AuthDBQuery("SELECT id FROM account WHERE id="..account.." AND username REGEXP '^RNDBOT[0-9]+$'") if not owned then unmanaged[guid]=true;return nil end local character=CharDBQuery("SELECT totaltime FROM characters WHERE guid="..guid.." AND account="..account) if not character then return nil end local state=(player:GetLevel()==startingLevel(player) and character:GetUInt32(0)<=60) and "reserve" or "blocked" local row={name=name,account=account,protected=false,state=state,race=player:GetRace()} residents[guid]=row AuthDBExecute("INSERT IGNORE INTO azerothai_lifecycle(guid,name,account_id,protected,state,race) VALUES (".. guid..","..sqltext(name)..","..account..",0,"..sqltext(state)..","..row.race..")") return row end local function reload() local q=AuthDBQuery("SELECT guid,name,account_id,protected,state,race FROM azerothai_lifecycle") if not q then error("AzerothAI lifecycle registry is unavailable or empty") end local nextResidents={} repeat nextResidents[q:GetUInt32(0)]={name=q:GetString(1),account=q:GetUInt32(2), protected=q:GetUInt32(3)==1,state=q:GetString(4),race=q:GetUInt32(5)} until not q:NextRow() residents=nextResidents ready=true end local function identity(player) if not ready or not player then return nil end local guid=player:GetGUIDLow() local row=residents[guid] or enroll(player) if not row or row.account~=player:GetAccountId() or row.name~=player:GetName() then return nil end if row.protected or string.lower(row.name)=="zaku" then return nil end return row,guid end local function recordDeath(player) local row,guid=identity(player) if not row or row.state=="dead" or row.state=="blocked" then return end row.state="dead" AuthDBExecute("UPDATE azerothai_lifecycle SET state='dead',died_at=COALESCE(died_at,NOW()) WHERE guid=".. guid.." AND protected=0 AND LOWER(name)<>'zaku'") if AzerothAIEmitPermanentDeath then AzerothAIEmitPermanentDeath(player) end end local function enforce() if not ready then reload() end -- The dashboard may receive a verified death from an older transport backlog -- after this process seeded its registry. Import terminal states only; never -- overwrite a locally observed death with an older active database row. if os.time()-reconciledAt>=10 then local q=AuthDBQuery("SELECT guid,name,account_id FROM azerothai_lifecycle WHERE state='dead' AND protected=0 AND LOWER(name)<>'zaku'") if q then repeat local row=residents[q:GetUInt32(0)] if row and not row.protected and row.name==q:GetString(1) and row.account==q:GetUInt32(2) then row.state="dead" end until not q:NextRow() end reconciledAt=os.time() end local mgr=GetRandomPlayerbotMgr() -- Disallow every retired identity before the native manager fills vacancies. for guid,row in pairs(residents) do if (row.state=="dead" or row.state=="blocked") and not row.protected and string.lower(row.name)~="zaku" then local player=GetPlayerByName(row.name) local removed=false if player and player:GetGUIDLow()==guid and player:GetAccountId()==row.account then mgr:Remove(player) removed=true end -- Remove clears the manager's event cache, so set the login veto afterward. if removed or not vetoAt[guid] or os.time()-vetoAt[guid]>300 then mgr:SetValue(guid,"add",0) mgr:SetValue(guid,"logout",1,"AzerothAI permanent retirement") vetoAt[guid]=os.time() end end end local players=GetPlayersInWorld() or {} local counts={0,0} local online={} local returning={} for _,player in pairs(players) do local guid=player:GetGUIDLow() local row=residents[guid] if row and row.state=="active" and row.account==player:GetAccountId() and row.name==player:GetName() and not player:IsDead() then returning[#returning+1]={player=player,row=row,guid=guid} end end table.sort(returning,function(a,b) local ap=a.row.protected or string.lower(a.row.name)=="zaku" local bp=b.row.protected or string.lower(b.row.name)=="zaku" if ap~=bp then return ap end return a.guid0 if allowed then slots[team]=slots[team]-1 end if gates[guid]~=allowed or not gateAt[guid] or os.time()-gateAt[guid]>300 then mgr:SetValue(guid,"logout",allowed and 0 or 1,"AzerothAI faction capacity") gates[guid]=allowed gateAt[guid]=os.time() end if not allowed and mgr:GetValue(guid,"add")~=0 then mgr:SetValue(guid,"add",0) end end end -- Record death synchronously in memory; logout happens outside the death hook. RegisterPlayerEvent(8,function(event,killer,killed) recordDeath(killed) end) RegisterPlayerEvent(6,function(event,killer,killed) recordDeath(killed) end) RegisterPlayerEvent(3,function(event,player) local row,guid=identity(player) if row and row.state=="reserve" then arrivals[guid]=player:GetLevel() end end) CreateLuaEvent(function() local ok,err=pcall(enforce) if not ok then PrintError("[AzerothAI] Lifecycle enforcement failed: "..tostring(err)) end end,1000,0) reload() enforce() PrintInfo("[AzerothAI] Persistent hardcore retirement and reserve replacement enabled")