Allow level-55 Death Knights within race-based faction limits

This commit is contained in:
Codex
2026-09-22 16:20:47 -04:00
parent 77f6b56c2f
commit 360f164905
4 changed files with 34 additions and 14 deletions
+10 -6
View File
@@ -1,5 +1,5 @@
-- Persistent hardcore retirement. The native random-bot manager fills vacancies -- Persistent hardcore retirement. The native random-bot manager fills vacancies
-- from its factory-created, unused level-1 reserve; identities are never reset. -- from its factory-created, unused starting-level reserve; identities are never reset.
-- Install only after deploy/playable/install-lifecycle.py has seeded the registry. -- Install only after deploy/playable/install-lifecycle.py has seeded the registry.
local residents = {} local residents = {}
local ready = false local ready = false
@@ -21,6 +21,10 @@ local function sqltext(value)
return "CONVERT(UNHEX('"..(string.gsub(value,".",function(c) return string.format("%02X",string.byte(c)) end)).."') USING utf8mb4)" return "CONVERT(UNHEX('"..(string.gsub(value,".",function(c) return string.format("%02X",string.byte(c)) end)).."') USING utf8mb4)"
end end
local function startingLevel(player)
return player:GetClass()==6 and 55 or 1
end
local function enroll(player) local function enroll(player)
local guid=player:GetGUIDLow() local guid=player:GetGUIDLow()
if unmanaged[guid] then return nil end if unmanaged[guid] then return nil end
@@ -33,7 +37,7 @@ local function enroll(player)
if not owned then unmanaged[guid]=true;return nil end if not owned then unmanaged[guid]=true;return nil end
local character=CharDBQuery("SELECT totaltime FROM characters WHERE guid="..guid.." AND account="..account) local character=CharDBQuery("SELECT totaltime FROM characters WHERE guid="..guid.." AND account="..account)
if not character then return nil end if not character then return nil end
local state=(player:GetLevel()==1 and character:GetUInt32(0)<=60) and "reserve" or "blocked" 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()} local row={name=name,account=account,protected=false,state=state,race=player:GetRace()}
residents[guid]=row residents[guid]=row
AuthDBExecute("INSERT IGNORE INTO azerothai_lifecycle(guid,name,account_id,protected,state,race) VALUES (".. AuthDBExecute("INSERT IGNORE INTO azerothai_lifecycle(guid,name,account_id,protected,state,race) VALUES ("..
@@ -130,17 +134,17 @@ local function enforce()
if player:IsDead() then if player:IsDead() then
recordDeath(player) recordDeath(player)
elseif row.state=="reserve" then elseif row.state=="reserve" then
if (arrivals[guid] or player:GetLevel())~=1 then if (arrivals[guid] or player:GetLevel())~=startingLevel(player) then
row.state="blocked" row.state="blocked"
AuthDBExecute("UPDATE azerothai_lifecycle SET state='blocked' WHERE guid="..guid.." AND state='reserve'") AuthDBExecute("UPDATE azerothai_lifecycle SET state='blocked' WHERE guid="..guid.." AND state='reserve'")
PrintError("[AzerothAI] Replacement did not start at level 1: "..row.name) PrintError("[AzerothAI] Replacement did not start at its class starting level: "..row.name)
elseif faction(row) and counts[faction(row)]<TARGET_PER_FACTION then elseif faction(row) and counts[faction(row)]<TARGET_PER_FACTION then
counts[faction(row)]=counts[faction(row)]+1 counts[faction(row)]=counts[faction(row)]+1
row.state="active" row.state="active"
online[guid]=true 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'") AuthDBExecute("UPDATE azerothai_lifecycle SET state='active',entry_level="..startingLevel(player)..",activated_at=COALESCE(activated_at,NOW()) WHERE guid="..guid.." AND state='reserve'")
if AzerothAIEmitPlayerSnapshot then AzerothAIEmitPlayerSnapshot(row.name) end if AzerothAIEmitPlayerSnapshot then AzerothAIEmitPlayerSnapshot(row.name) end
PrintInfo("[AzerothAI] New level-1 resident entered the world: "..row.name) PrintInfo("[AzerothAI] New resident entered at class starting level: "..row.name)
else else
-- A login already in flight must not push its faction over the target. -- A login already in flight must not push its faction over the target.
mgr:Remove(player) mgr:Remove(player)
+3 -3
View File
@@ -55,14 +55,14 @@ for guid, name, account, level, played, cls in characters:
status = 'dead' status = 'dead'
elif bot and bot['status'] != 'retired': elif bot and bot['status'] != 'retired':
status = 'active' status = 'active'
elif int(level) == 1 and int(played) == 0 and int(cls) != 6: elif int(level) == (55 if int(cls) == 6 else 1) and int(played) == 0:
status = 'reserve' status = 'reserve'
else: else:
status = 'blocked' status = 'blocked'
counts[status] = counts.get(status, 0) + 1 counts[status] = counts.get(status, 0) + 1
rows.append((guid, name, account, int(protected), status)) rows.append((guid, name, account, int(protected), status))
assert any(name.lower() == 'zaku' and protected for _, name, _, protected, _ in rows) assert any(name.lower() == 'zaku' and protected for _, name, _, protected, _ in rows)
assert counts.get('reserve', 0) >= counts.get('dead', 0), 'Insufficient unused level-1 reserve' assert counts.get('reserve', 0) >= counts.get('dead', 0), 'Insufficient unused starting-level reserve'
print('Lifecycle plan:', json.dumps(counts), flush=True) print('Lifecycle plan:', json.dumps(counts), flush=True)
if not args.apply: if not args.apply:
raise SystemExit(0) raise SystemExit(0)
@@ -119,7 +119,7 @@ for key in ['POLL_MS', 'PROTECTED', 'KNOWN_BOT_ACCOUNTS']:
shutil.copy2(repo / 'ale/azerothai_lifecycle.lua', core / 'azerothai-lua/azerothai_lifecycle.lua') shutil.copy2(repo / 'ale/azerothai_lifecycle.lua', core / 'azerothai-lua/azerothai_lifecycle.lua')
settings = {'AC_AI_PLAYERBOT_DISABLE_RANDOM_LEVELS': '1', settings = {'AC_AI_PLAYERBOT_DISABLE_RANDOM_LEVELS': '1',
'AC_AI_PLAYERBOT_RANDOMBOT_STARTING_LEVEL': '1', 'AC_AI_PLAYERBOT_RANDOMBOT_STARTING_LEVEL': '1',
'AC_AI_PLAYERBOT_DISABLE_DEATH_KNIGHT_LOGIN': '1'} 'AC_AI_PLAYERBOT_DISABLE_DEATH_KNIGHT_LOGIN': '0'}
content = override.read_text() content = override.read_text()
for key, value in settings.items(): for key, value in settings.items():
pattern = r'(?m)^(\s*)' + key + r':.*$' pattern = r'(?m)^(\s*)' + key + r':.*$'
+5 -5
View File
@@ -8,7 +8,7 @@ installation registry; human accounts are never included.
The installer promotes unused manual-summoning reserve accounts into the native The installer promotes unused manual-summoning reserve accounts into the native
autonomous pool. The native random-bot manager maintains its configured population target using autonomous pool. The native random-bot manager maintains its configured population target using
unused factory-created characters. Replacements start at level 1, with their unused factory-created characters. Replacements start at level 1 (Death Knights at level 55), with their
own names and AzerothAI recipes/memories assigned when observed. This does not own names and AzerothAI recipes/memories assigned when observed. This does not
reset or resurrect a dead character. Duplicate death events are harmless. reset or resurrect a dead character. Duplicate death events are harmless.
@@ -16,15 +16,15 @@ The reserve is finite: this deployment began with 450 unused level-1 characters.
This release does not create additional accounts or extend the factory reserve This release does not create additional accounts or extend the factory reserve
when it is exhausted. The population will fall below target if no eligible when it is exhausted. The population will fall below target if no eligible
characters remain. Check `azerothai_lifecycle` state counts before extending it. characters remain. Check `azerothai_lifecycle` state counts before extending it.
Death Knights are excluded from new random logins because they do not start at Death Knights are eligible and count toward their race’s faction, within the same
level 1. Normal gameplay can still level a replacement after its initial entry. 25-per-faction cap. Their approved starting level is 55. Normal gameplay can still level a replacement after its initial entry.
The guard allocates new arrivals against actual living faction counts, targeting The guard allocates new arrivals against actual living faction counts, targeting
25 Alliance and 25 Horde, including protected residents. It holds excess-faction 25 Alliance and 25 Horde, including protected residents. It holds excess-faction
reserves and excess returning residents offline without retiring them. Returning reserves and excess returning residents offline without retiring them. Returning
residents retain their progress and get priority over unused reserves. New activations record `entry_level=1`; subsequent leveling is allowed. residents retain their progress and get priority over unused reserves. New activations record `entry_level=1` or `entry_level=55` for Death Knights; subsequent leveling is allowed.
Native factory-created newcomers are enrolled only from verified RNDBOT accounts Native factory-created newcomers are enrolled only from verified RNDBOT accounts
and only when fresh and level 1. and only when fresh and at their class starting level.
## Installation ## Installation
+16
View File
@@ -14,6 +14,7 @@ function player(guid,name,account,level,dead)
function p:GetName() return self.name end function p:GetName() return self.name end
function p:GetAccountId() return self.account end function p:GetAccountId() return self.account end
function p:GetLevel() return self.level end function p:GetLevel() return self.level end
function p:GetClass() return self.class or 1 end
function p:GetRace() return self.race or 2 end function p:GetRace() return self.race or 2 end
function p:IsDead() return self.dead end function p:IsDead() return self.dead end
players[name]=p; return p players[name]=p; return p
@@ -114,4 +115,19 @@ assert(n==25 and players.Zaku,'Returning population must obey faction cap and pr
assert(deaths==0,'Holding returning residents offline must not retire them') assert(deaths==0,'Holding returning residents offline must not retire them')
assert(values['125logout']==1 and values['126logout']==1) assert(values['125logout']==1 and values['126logout']==1)
""") """)
dk=LuaRuntime()
dk.execute(setup)
dk.execute("""
players={};rows={{201,'AllianceDK',2,0,'reserve',1},{202,'HordeDK',2,0,'reserve',2}}
local a=player(201,'AllianceDK',2,55,false);a.class=6;a.race=1
local h=player(202,'HordeDK',2,55,false);h.class=6
""")
dk.execute((Path(__file__).parents[1] / 'ale/azerothai_lifecycle.lua').read_text())
dk.execute("""
assert(players.AllianceDK and players.HordeDK)
local entries=0
for _,q in pairs(writes) do if string.find(q,'entry_level=55',1,true) then entries=entries+1 end end
assert(entries==2,'Death Knights must be audited at level 55')
events[8](8,nil,players.HordeDK);tick();assert(not players.HordeDK and deaths==1)
""")
print('PASS: retirement, protection, level-1 entry, replay safety, and 25/25 faction balancing') print('PASS: retirement, protection, level-1 entry, replay safety, and 25/25 faction balancing')