From 360f1649052f9a146b7819ad74dd55f1ad20741d Mon Sep 17 00:00:00 2001 From: Codex Date: Tue, 22 Sep 2026 16:20:47 -0400 Subject: [PATCH] Allow level-55 Death Knights within race-based faction limits --- ale/azerothai_lifecycle.lua | 16 ++++++++++------ deploy/playable/install-lifecycle.py | 6 +++--- docs/hardcore-replacement.md | 10 +++++----- tests/lifecycle-lua.test.py | 16 ++++++++++++++++ 4 files changed, 34 insertions(+), 14 deletions(-) diff --git a/ale/azerothai_lifecycle.lua b/ale/azerothai_lifecycle.lua index 6952d26..c6d3dfc 100644 --- a/ale/azerothai_lifecycle.lua +++ b/ale/azerothai_lifecycle.lua @@ -1,5 +1,5 @@ -- 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. local residents = {} 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)" 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 @@ -33,7 +37,7 @@ local function enroll(player) 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 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 (".. @@ -130,17 +134,17 @@ local function enforce() if player:IsDead() then recordDeath(player) 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" 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)]= 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) if not args.apply: 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') settings = {'AC_AI_PLAYERBOT_DISABLE_RANDOM_LEVELS': '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() for key, value in settings.items(): pattern = r'(?m)^(\s*)' + key + r':.*$' diff --git a/docs/hardcore-replacement.md b/docs/hardcore-replacement.md index e866b1a..24a3983 100644 --- a/docs/hardcore-replacement.md +++ b/docs/hardcore-replacement.md @@ -8,7 +8,7 @@ installation registry; human accounts are never included. The installer promotes unused manual-summoning reserve accounts into the native 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 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 when it is exhausted. The population will fall below target if no eligible characters remain. Check `azerothai_lifecycle` state counts before extending it. -Death Knights are excluded from new random logins because they do not start at -level 1. Normal gameplay can still level a replacement after its initial entry. +Death Knights are eligible and count toward their race’s faction, within the same +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 25 Alliance and 25 Horde, including protected residents. It holds excess-faction 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 -and only when fresh and level 1. +and only when fresh and at their class starting level. ## Installation diff --git a/tests/lifecycle-lua.test.py b/tests/lifecycle-lua.test.py index 31d016c..3c3af29 100644 --- a/tests/lifecycle-lua.test.py +++ b/tests/lifecycle-lua.test.py @@ -14,6 +14,7 @@ function player(guid,name,account,level,dead) function p:GetName() return self.name end function p:GetAccountId() return self.account 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:IsDead() return self.dead end 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(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')