Enforce hardcore retirement with level-one faction-balanced replacements
This commit is contained in:
@@ -85,6 +85,11 @@ local function emit(kind, payload)
|
||||
AuthDBExecute(sql)
|
||||
end
|
||||
|
||||
local function area_name(id)
|
||||
local ok,name=pcall(GetAreaName,id)
|
||||
return ok and name or tostring(id)
|
||||
end
|
||||
|
||||
local function player_payload(player)
|
||||
if not player then return {} end
|
||||
local isBot = is_playerbot(player)
|
||||
@@ -96,7 +101,7 @@ local function player_payload(player)
|
||||
mapId=player:GetMapId(),
|
||||
zoneId=player:GetZoneId(),
|
||||
areaId=player:GetAreaId(),
|
||||
zoneName=GetAreaName(player:GetZoneId()) or tostring(player:GetZoneId()),
|
||||
zoneName=area_name(player:GetZoneId()),
|
||||
race=player:GetRace(),
|
||||
class=player:GetClass(),
|
||||
raceName=player:GetRaceAsString() or tostring(player:GetRace()),
|
||||
@@ -136,6 +141,12 @@ function AzerothAIEmitPlayerSnapshot(name)
|
||||
return 1
|
||||
end
|
||||
|
||||
function AzerothAIEmitPermanentDeath(player)
|
||||
local payload=player_payload(player)
|
||||
payload.deathId="hardcore-"..tostring(player:GetGUIDLow())
|
||||
emit("player.permanent-death",payload)
|
||||
end
|
||||
|
||||
local function mark_consumed(id)
|
||||
AuthDBExecute("UPDATE azerothai_command_inbox SET consumed_at=NOW() WHERE id=" .. db_text(to_hex(id)))
|
||||
end
|
||||
@@ -254,7 +265,7 @@ RegisterPlayerEvent(27, function(event, player, newZone, newArea)
|
||||
local payload=player_payload(player)
|
||||
payload.zone=newZone
|
||||
payload.area=newArea
|
||||
payload.zoneName=GetAreaName(newZone) or tostring(newZone)
|
||||
payload.zoneName=area_name(newZone)
|
||||
emit("player.zone",payload)
|
||||
end)
|
||||
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
-- Persistent hardcore retirement. The native random-bot manager fills vacancies
|
||||
-- from its factory-created, unused level-1 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 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 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()==1 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
|
||||
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.guid<b.guid
|
||||
end)
|
||||
for _,item in ipairs(returning) do
|
||||
local team=faction(item.row)
|
||||
if team then
|
||||
if counts[team]<TARGET_PER_FACTION or item.row.protected or string.lower(item.row.name)=="zaku" then
|
||||
counts[team]=counts[team]+1
|
||||
online[item.guid]=true
|
||||
else
|
||||
-- Returning residents keep their identity and progress while held offline.
|
||||
mgr:Remove(item.player)
|
||||
mgr:SetValue(item.guid,"logout",1,"AzerothAI faction capacity")
|
||||
gates[item.guid]=false
|
||||
gateAt[item.guid]=os.time()
|
||||
end
|
||||
end
|
||||
end
|
||||
players=GetPlayersInWorld() or {}
|
||||
for _,player in pairs(players) do
|
||||
local row,guid=identity(player)
|
||||
if row and row.state~="dead" and row.state~="blocked" then
|
||||
if player:IsDead() then
|
||||
recordDeath(player)
|
||||
elseif row.state=="reserve" then
|
||||
if (arrivals[guid] or player:GetLevel())~=1 then
|
||||
row.state="blocked"
|
||||
AuthDBExecute("UPDATE azerothai_lifecycle SET state='blocked' WHERE guid="..guid.." AND state='reserve'")
|
||||
PrintError("[AzerothAI] Replacement did not start at level 1: "..row.name)
|
||||
elseif faction(row) and counts[faction(row)]<TARGET_PER_FACTION then
|
||||
counts[faction(row)]=counts[faction(row)]+1
|
||||
row.state="active"
|
||||
online[guid]=true
|
||||
AuthDBExecute("UPDATE azerothai_lifecycle SET state='active',entry_level=1,activated_at=COALESCE(activated_at,NOW()) WHERE guid="..guid.." AND state='reserve'")
|
||||
if AzerothAIEmitPlayerSnapshot then AzerothAIEmitPlayerSnapshot(row.name) end
|
||||
PrintInfo("[AzerothAI] New level-1 resident entered the world: "..row.name)
|
||||
else
|
||||
-- A login already in flight must not push its faction over the target.
|
||||
mgr:Remove(player)
|
||||
mgr:SetValue(guid,"logout",1,"AzerothAI faction capacity")
|
||||
gates[guid]=false
|
||||
gateAt[guid]=os.time()
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
-- Open only enough fresh identities to fill each faction's actual deficit.
|
||||
-- Persisted login vetoes keep the native random selector inside these slots.
|
||||
local slots={math.max(0,TARGET_PER_FACTION-counts[1]),math.max(0,TARGET_PER_FACTION-counts[2])}
|
||||
local candidates={}
|
||||
for guid,row in pairs(residents) do if (row.state=="reserve" or row.state=="active") and not online[guid] and not row.protected and string.lower(row.name)~="zaku" then candidates[#candidates+1]=guid end end
|
||||
table.sort(candidates,function(a,b)
|
||||
if residents[a].state~=residents[b].state then return residents[a].state=="active" end
|
||||
return a<b
|
||||
end)
|
||||
for _,guid in ipairs(candidates) do
|
||||
local team=faction(residents[guid])
|
||||
local allowed=team~=nil and slots[team]>0
|
||||
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")
|
||||
Reference in New Issue
Block a user