From 8e96c7ff479ebdc53f1c2955dcf0cb042a2f70a9 Mon Sep 17 00:00:00 2001 From: Codex Date: Tue, 22 Sep 2026 18:15:35 -0400 Subject: [PATCH] Reconcile delayed hardcore deaths with native population registry --- ale/azerothai_lifecycle.lua | 12 ++++++++++++ docs/community-and-operations.md | 8 ++++++++ scripts/operations-agent.py | 21 +++++++++++++++++++++ 3 files changed, 41 insertions(+) diff --git a/ale/azerothai_lifecycle.lua b/ale/azerothai_lifecycle.lua index c6d3dfc..e4d303e 100644 --- a/ale/azerothai_lifecycle.lua +++ b/ale/azerothai_lifecycle.lua @@ -9,6 +9,7 @@ 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) @@ -77,6 +78,17 @@ 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 diff --git a/docs/community-and-operations.md b/docs/community-and-operations.md index 1bc5b3a..099134d 100644 --- a/docs/community-and-operations.md +++ b/docs/community-and-operations.md @@ -41,6 +41,14 @@ already does. Archived snapshots and terminal application commands use the chose state and memorials are preserved. Current application log files rotate at 10 MB with three copies. Database files may retain freed SQLite pages for reuse. +The minute operations check also reconciles verified AzerothCore death records +with the native hardcore registry using character GUID, account and exact name. +Protected residents are excluded. Each correction keeps the old registry row +and supporting death in `lifecycle-reconciliation.jsonl`. The Lua guard imports +terminal registry states every ten seconds, removes the retired identity, and +allows its faction's replacement slot to fill under the existing 25/25 cap. +This handles delayed death events received after the registry was first seeded. + Install `deploy/playable/install-user-recovery.py` as the realm owner. It registers `azerothai-runtime.service` with Restart=always and a minute operations timer in the user's systemd manager. Enable user lingering for reboot startup. Telemetry diff --git a/scripts/operations-agent.py b/scripts/operations-agent.py index 21bfb3a..0c5d923 100644 --- a/scripts/operations-agent.py +++ b/scripts/operations-agent.py @@ -44,6 +44,27 @@ try: state=json.load(urllib.request.urlopen(base+'/api/state',timeout=10));days=max(3,min(30,int(state.get('opsRetentionDays',7)))) env=os.environ.copy();env['MYSQL_PWD']=cfg['DOCKER_DB_ROOT_PASSWORD'] def sql(q):return subprocess.check_output(['docker','exec','-e','MYSQL_PWD','ac-database','mysql','-uroot','-N','-B','-e',q],env=env,text=True) + # Reconcile delayed verified deaths by the full native identity. This never + # revives an identity, changes a protected resident, or counts a human as a bot. + if state.get('settings',{}).get('hardcore',{}).get('enabled'): + retired=0 + registry={int(line.split('\t')[0]):line for line in sql("SELECT guid,name,account_id,state,died_at FROM acore_auth.azerothai_lifecycle WHERE state<>'dead' AND protected=0 AND LOWER(name)<>'zaku'").splitlines() if line} + for bot in state.get('bots',[]): + identity=bot.get('autonomy') or {};death=bot.get('death') or {} + if bot.get('status')!='dead' or not death.get('verified') or death.get('source')!='azerothcore' or not identity.get('managed'):continue + if not bot.get('mortal') or bot.get('plotArmorHpFloor',0)>0 or bot.get('name','').lower()=='zaku':continue + guid=identity.get('characterGuid');account=identity.get('accountId') + if type(guid) is not int or type(account) is not int or guid<=0 or account<=0:continue + before=registry.get(guid,'');fields=before.split('\t') + if len(fields)!=5 or fields[1]!=bot['name'] or int(fields[2])!=account:continue + name=bot['name'].encode().hex() + where=f"guid={guid} AND account_id={account} AND BINARY name=UNHEX('{name}') AND protected=0 AND LOWER(name)<>'zaku' AND state<>'dead'" + # Keep an audit of the old registry row and supporting death before changing it. + audit=lab/'lifecycle-reconciliation.jsonl' + with audit.open('a') as f:f.write(json.dumps({'at':time.time(),'before':before.strip(),'death':death})+'\n') + changed=sql("UPDATE acore_auth.azerothai_lifecycle SET state='dead',died_at=COALESCE(died_at,NOW()) WHERE "+where+'; SELECT ROW_COUNT()') + retired+=int(changed.strip()) + maintenance.append(f'Hardcore registry checked; {retired} delayed deaths reconciled') for table,column in [('azerothai_event_outbox','delivered_at'),('azerothai_result_outbox','delivered_at')]: sql(f'DELETE FROM acore_auth.{table} WHERE {column} IS NOT NULL AND {column}