"""Query the public Workshop catalogue using a disposable, silent game session.""" import fcntl import os from pathlib import Path import re import subprocess import tempfile import time from headless_game import assert_prefix_idle, cleanup_owned_run SCRIPT = '''class ERR_VersionQuery : PageParams { override void OnPack() { StoreString("search", "Co10 escape Reintroduced"); } } modded class SCR_AddonManager { protected ref ERR_VersionQuery m_Query; protected ref BackendCallback m_Callback; override void EOnInit(IEntity owner) { super.EOnInit(owner);GetGame().GetCallqueue().CallLater(QueryVersion,10000,false); } void QueryVersion() { m_Query=new ERR_VersionQuery();m_Query.limit=100;m_Query.offset=0; m_Callback=new BackendCallback();m_Callback.SetOnSuccess(VersionReady); GetGame().GetBackendApi().GetWorkshop().RequestPage(m_Callback,m_Query,true); } void VersionReady(BackendCallback callback) { array items={};GetGame().GetBackendApi().GetWorkshop().GetPageItems(items); foreach(WorkshopItem item:items) { if(item.Id()!="2528466DE220D91C")continue; Revision revision=item.GetLatestRevision(); if(revision)PrintFormat("ERR_WORKSHOP_PUBLISHED %1 %2",item.Id(),revision.GetVersion()); } } } ''' def published_from_log(text, guid): versions = set(re.findall(r'\bERR_WORKSHOP_PUBLISHED ' + re.escape(guid) + r' (\d+\.\d+\.\d+)\s*$', text, re.MULTILINE)) if len(versions) > 1: raise ValueError('Workshop returned conflicting published versions.') return next(iter(versions), None) def native_version(root, compat, guid): if guid != '2528466DE220D91C': raise ValueError('Native query is restricted to the Escape addon.') compat = Path(compat).resolve() steam = Path.home() / '.local/share/Steam' if not compat.is_relative_to(root / '.local') or not (compat / 'pfx').is_dir(): raise ValueError('Use an existing disposable compat prefix under this checkout/.local.') proton = steam / 'steamapps/common/Proton - Experimental/proton' game = steam / 'steamapps/common/Arma Reforger/ArmaReforgerSteam.exe' if not proton.is_file() or not game.is_file(): raise ValueError('Installed Reforger and Proton Experimental are required for the native query.') with (compat / '.erri-workbench.lock').open('a') as lock: fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB) assert_prefix_idle(compat) scratch = root / '.local/workshop-version' scratch.mkdir(parents=True, exist_ok=True) with tempfile.TemporaryDirectory(prefix='query-', dir=scratch) as temp: work = Path(temp) addon = work / 'addons/query' (addon / 'Scripts/Game').mkdir(parents=True) (addon / 'addon.gproj').write_text('GameProject { ID "ERR_VersionQuery" GUID "8F729DB72D635EF1" Dependencies { "58D0FB3206B6F859" } }\n') (addon / 'Scripts/Game/VersionQuery.c').write_text(SCRIPT) profile = work / 'profile' profile.mkdir() env = os.environ.copy() env.update(STEAM_COMPAT_DATA_PATH=str(compat), STEAM_COMPAT_CLIENT_INSTALL_PATH=str(steam), WINEPREFIX=str(compat / 'pfx'), SteamAppId='1874880', SteamGameId='1874880') command = ['gamescope', '--backend', 'headless', '-W', '1280', '-H', '720', '-r', '30', '--', str(proton), 'run', str(game), '-profile', 'Z:' + str(profile), '-addonDownloadDir', 'Z:' + str(profile), '-addonsDir', 'Z:' + str(work / 'addons'), '-addons', '8F729DB72D635EF1', '-noSound', '-noSplash', '-noThrow', '-disableCrashReporter', '-logconsole'] version = None with (work / 'launch.log').open('w') as output: child = subprocess.Popen(command, cwd=game.parent, env=env, stdout=output, stderr=subprocess.STDOUT, start_new_session=True) try: deadline = time.monotonic() + 60 while child.poll() is None and time.monotonic() < deadline: for log in (profile / 'logs').glob('*/console.log'): version = published_from_log(log.read_text(errors='replace'), guid) if version: break if version: break time.sleep(.5) finally: cleanup = cleanup_owned_run(child, proton, env, compat) if not cleanup.get('cleanup_confirmed'): raise ValueError('Workshop query process cleanup could not be confirmed.') if not version: raise ValueError('Live Workshop query did not return a published version within 60 seconds.') return version