Smooth convoy startup and record Workshop 0.0.153
continuous-integration/drone/push Build is passing

This commit is contained in:
2026-11-10 15:34:24 +01:00
parent d8352136e8
commit 2390f77554
72 changed files with 15088 additions and 161 deletions
@@ -0,0 +1,53 @@
from pathlib import Path
import sys,os,subprocess,json,fcntl,time,hashlib,re
ROOT=Path('/home/jakub/devel/escapistsrereintroduced')
sys.path.insert(0,str(ROOT/'tools'))
from headless_game import assert_no_game,assert_prefix_idle,cleanup_owned_run
from external_addons import policy,prepare_profile,verify_profile
base=Path('/tmp/erri-ai-19'); target=base/sys.argv[1]; run=Path(sys.argv[2]); run.mkdir(exist_ok=False)
compat=base/'compat'; steam=Path.home()/'.local/share/Steam'; proton=steam/'steamapps/common/Proton - Experimental/proton'; wb=steam/'steamapps/common/Arma Reforger Tools/Workbench'
lock=(compat/'.erri-workbench.lock').open('a'); fcntl.flock(lock,fcntl.LOCK_EX|fcntl.LOCK_NB); assert_no_game(); assert_prefix_idle(compat)
profile=run/'profile'; addons=profile/'addons'; addons.mkdir(parents=True); (addons/'ArmaReforger').symlink_to(steam/'steamapps/common/Arma Reforger/addons/data',target_is_directory=True)
settings={'configVersion':1,'factions':{'escapists':'fia','enemy':'ussr','independent':'us'}}
(profile/'profile').mkdir(); (profile/'profile/Escapists.json').write_text(json.dumps(settings,indent=2)+'\n'); (run/'input-settings.json').write_text(json.dumps(settings,indent=2)+'\n')
source=steam/'steamapps/compatdata/1874880/pfx/drive_c/users/steamuser/Documents/My Games/ArmaReforger/addons'; rules=policy(ROOT); cache=run/'external-cache.json'; records=prepare_profile(addons,source,rules,cache)
source_hashes={str(p.relative_to(target)):hashlib.sha256(p.read_bytes()).hexdigest() for p in target.rglob('*') if p.is_file() and p.suffix in {'.c','.gproj','.conf','.ent','.meta','.json'}}
(run/'source-hashes.json').write_text(json.dumps(source_hashes,indent=2)+'\n')
projects=run/'projects'; projects.mkdir(); (projects/'core').symlink_to(target,target_is_directory=True)
cmd=['gamescope','--backend','headless','-W','1280','-H','720','-w','1280','-h','720','-r','30','--',str(proton),'run',str(wb/'ArmaReforgerWorkbenchSteamDiag.exe'),'-gproj','Z:'+str(target/'addon.gproj'),'-addonsDir','Z:'+str(projects),'-profile','Z:'+str(profile),'-logconsole','-wbModule=WorldEditor','-load','Worlds/ERR_Everon.ent','-plugin=ERR_ValidateWorldPlugin','-errPlay','1','-errSmoke','1','-errSmokeOutcome','supply-convoys','-noThrow','-noSound','-disableCrashReporter']
(run/'command.json').write_text(json.dumps(cmd,indent=2)+'\n'); env=os.environ.copy(); env.update(STEAM_COMPAT_DATA_PATH=str(compat),STEAM_COMPAT_CLIENT_INSTALL_PATH=str(steam),WINEPREFIX=str(compat/'pfx'))
marker='ERR SUPPLY PASS: production auto-started three five-vehicle convoys, exact ledger admission, no troop transport, finite native cargo'
result={'scope':'startup phase only, not full supply-convoys smoke','deadline_seconds':900,'reason':'timeout','source':str(target),'ready_elapsed':None,'ready_perf_windows':None,'cleanup':None}
started=time.monotonic(); previous=-1; ready_windows=None
with (run/'launch.log').open('w') as log:
child=subprocess.Popen(cmd,stdout=log,stderr=subprocess.STDOUT,env=env,cwd=wb,start_new_session=True)
result['wrapper_pid']=child.pid
try:
while time.monotonic()-started < 900:
logs=sorted(profile.glob('logs/logs_*/console.log')); content=''; perf=''
if logs:
content=logs[-1].read_text(errors='replace'); result['console']=str(logs[-1]); pp=logs[-1].parent/'rereintroduced-performance.log'
if pp.exists(): perf=pp.read_text(errors='replace')
windows=set(re.findall(r'ERR PERF run=\S+ window=(\d+) ',perf))
if ready_windows is None and marker in content:
ready_windows=windows; result['ready_elapsed']=time.monotonic()-started; result['ready_perf_windows']=sorted(windows); print('STARTUP_READY',result['ready_elapsed'],flush=True)
if re.search(r'ERR (?:SMOKE|SUPPLY) FAIL:|scenario preparation failed|Virtual Machine Exception|Can.t compile|Cannot initialize game project settings',content):
result['reason']='native-failure'; break
if ready_windows is not None and windows-ready_windows:
result['reason']='startup-ready-plus-flush'; result['final_perf_windows']=sorted(windows); break
if child.poll() is not None: result['reason']='unexpected-exit'; break
elapsed=int(time.monotonic()-started)
if elapsed//30 != previous:
previous=elapsed//30; print(json.dumps({'elapsed':elapsed,'log_bytes':len(content),'perf_windows':sorted(windows),'ready':ready_windows is not None,'tail':content.splitlines()[-1:] }),flush=True)
time.sleep(1)
finally:
result['elapsed_seconds']=time.monotonic()-started; result['cleanup']=cleanup_owned_run(child,proton,env,compat)
# Preserve the initial verdict; permit only natural process teardown, no new signals.
for retry in range(10):
try: assert_prefix_idle(compat); result['cleanup_followup_idle']=True; break
except (OSError,ValueError,subprocess.SubprocessError): time.sleep(0.5)
result['external_sources_unchanged']=verify_profile(records,source,rules,cache,addons)
result['source_unchanged']=all((target/name).is_file() and hashlib.sha256((target/name).read_bytes()).hexdigest()==value for name,value in source_hashes.items())
(run/'result.json').write_text(json.dumps(result,indent=2)+'\n')
print(json.dumps(result,indent=2),flush=True)
sys.exit(0 if result['reason']=='startup-ready-plus-flush' and result.get('cleanup_followup_idle') and result['source_unchanged'] and result['external_sources_unchanged'] else 1)