#!/usr/bin/env python3 """Resume isolated Classic startup and native level-one roster creation. Never switches ports, touches Wrath databases, or starts bot login. Native characters remain offline until the connector and gameplay checks are ready. """ import argparse import json import os import pathlib import re import subprocess import time def main(root): credentials=json.loads((root/'database-credentials.json').read_text()) env=dict(os.environ,MYSQL_PWD=credentials['root']) def mysql(sql): result=subprocess.run(['docker','exec','-i','-e','MYSQL_PWD','azerothai-classic-db','mysql','-uroot','-N','-B','--raw','classiccharacters'],input=sql,text=True,capture_output=True,env=env) if result.returncode: raise RuntimeError('Isolated Classic database operation failed: '+result.stderr.replace(credentials['root'],'[redacted]').replace(credentials['password'],'[redacted]')) return result.stdout.strip() def status(stage,**extra): value={'at':time.time(),'stage':stage,'cutover':False,**extra} temp=root/'bootstrap-status.tmp';temp.write_text(json.dumps(value,indent=2));temp.replace(root/'bootstrap-status.json') print(stage,flush=True) def inspect(name): result=subprocess.run(['docker','inspect','--format','{{json .State}}',name],capture_output=True,text=True) return json.loads(result.stdout) if result.returncode==0 else None def configure(name,values): template=root/'run/etc'/f'{name}.conf.dist' text=template.read_text() for key,value in values.items(): line=key+' = '+str(value) pattern=r'^'+re.escape(key)+r'\s*=.*$' text=re.sub(pattern,lambda _:line,text,flags=re.M) if re.search(pattern,text,re.M) else text+'\n'+line+'\n' target=template.with_suffix('');target.write_text(text);target.chmod(0o600) try: status('waiting for verified extraction and native build') deadline=time.time()+6*3600 while time.time()/work/run/console; exec ./mangosd -c /work/run/etc/mangosd.conf <&3'],check=True,capture_output=True) elif not state['Running']:subprocess.run(['docker','start','azerothai-classic-world'],check=True,capture_output=True) status('starting isolated Classic world; bot login disabled') for _ in range(120): state=inspect('azerothai-classic-world') if not state or not state['Running']:raise RuntimeError('Classic world exited during startup; inspect container log') log=subprocess.run(['docker','logs','--tail','60','azerothai-classic-world'],capture_output=True,text=True).stdout if 'SERVER STARTUP TIME' in log or 'World initialized' in log:break time.sleep(5) else:raise RuntimeError('Classic startup not confirmed within ten minutes') status('creating 50 level-one native characters; bot login disabled') with fifo.open('w') as console: for bot in roster: name=bot['name'] if not mysql("SELECT guid FROM characters WHERE name='"+name+"'"): console.write(f"rndbot create name={name} race={bot['raceId']} class={bot['classId']} level=1 login=false gear=none\n");console.flush() for _ in range(20): if mysql("SELECT guid FROM characters WHERE name='"+name+"'"):break time.sleep(1) else:raise RuntimeError('Native creation not confirmed for '+name) bound=[] for bot in roster: row=mysql("SELECT guid,account,race,class,level,online FROM characters WHERE name='"+bot['name']+"'").split('\t') if len(row)!=6:raise RuntimeError('Missing character identity') guid,account,race,cls,level,online=map(int,row) if (race,cls,level,online)!=(bot['raceId'],bot['classId'],1,0):raise RuntimeError('Native identity or level validation failed: '+bot['name']) mysql(f"INSERT INTO azerothai_roster(guid,account_id,name,enabled,plot_armor) VALUES ({guid},{account},'{bot['name']}',0,{int(bot['name']=='Zaku')}) ON DUPLICATE KEY UPDATE account_id=VALUES(account_id),name=VALUES(name)") bound.append(dict(bot,characterGuid=guid,accountId=account)) if int(mysql('SELECT COUNT(*) FROM characters'))!=50:raise RuntimeError('Population differs from 50') (root/'classic-roster-bound.json').write_text(json.dumps(bound,indent=2)+'\n') status('Classic world ready; 50 level-one characters verified offline; connector and gameplay validation remain',characters=50,alliance=25,horde=25) except Exception as error: status('bootstrap blocked',error=str(error).replace(credentials['root'],'[redacted]').replace(credentials['password'],'[redacted]')) raise if __name__=='__main__': parser=argparse.ArgumentParser();parser.add_argument('--root',type=pathlib.Path,required=True) main(parser.parse_args().root.resolve())