Harden base progression and cleanup; validate finite physical resupply
This commit is contained in:
+65
-12
@@ -19,6 +19,9 @@ from native_report import analyze
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
PROFILE_MARKER = '.erri-game-profile.json'
|
||||
PROFILE_ID = {'kind': 'erri-ordinary-game-disposable-profile', 'version': 1}
|
||||
CONTROL = 'vanilla-control'
|
||||
CONTROL_MARKER = '.erri-vanilla-control.json'
|
||||
CONTROL_ID = {'kind': 'erri-vanilla-only-startup-control', 'version': 1}
|
||||
GAME = re.compile(r'^armareforgersteam(?:diag)?(?:\.exe)?$', re.I)
|
||||
|
||||
|
||||
@@ -121,18 +124,56 @@ def validate_paths(root, steam, compat, profile):
|
||||
|
||||
|
||||
def command_for(root, steam, compat, profile, project):
|
||||
project_text = (root/'addons'/project/'addon.gproj').read_text()
|
||||
guid = re.search(r'\bGUID "([A-F0-9]{16})"', project_text)[1]
|
||||
return ['gamescope', '--backend', 'headless', '-W', '1280', '-H', '720',
|
||||
'-w', '1280', '-h', '720', '-r', '30', '--',
|
||||
str(steam/'steamapps/common/Proton - Experimental/proton'), 'run',
|
||||
str(steam/'steamapps/common/Arma Reforger/ArmaReforgerSteam.exe'),
|
||||
'-addonsDir', 'Z:'+str(root/'addons'), '-addons', guid,
|
||||
'-profile', 'Z:'+str(profile), '-errOrdinaryScenario', project,
|
||||
'-noBackend', '-noSound', '-noSplash', '-noThrow', '-disableCrashReporter', '-logconsole']
|
||||
if project not in {'core', 'chat', CONTROL}:
|
||||
refuse('Unsupported ordinary game project/control')
|
||||
command = ['gamescope', '--backend', 'headless', '-W', '1280', '-H', '720',
|
||||
'-w', '1280', '-h', '720', '-r', '30', '--',
|
||||
str(steam/'steamapps/common/Proton - Experimental/proton'), 'run',
|
||||
str(steam/'steamapps/common/Arma Reforger/ArmaReforgerSteam.exe')]
|
||||
if project != CONTROL:
|
||||
project_text = (root/'addons'/project/'addon.gproj').read_text()
|
||||
guid = re.search(r'\bGUID "([A-F0-9]{16})"', project_text)[1]
|
||||
command += ['-addonsDir', 'Z:'+str(root/'addons'), '-addons', guid]
|
||||
command += ['-profile', 'Z:'+str(profile)]
|
||||
if project != CONTROL:
|
||||
command += ['-errOrdinaryScenario', project, '-noSplash']
|
||||
return command + ['-noBackend', '-noSound', '-noThrow', '-disableCrashReporter', '-logconsole']
|
||||
|
||||
|
||||
def validate_control_profile(profile):
|
||||
"""Never reuse the successor or user's profile as a vanilla control."""
|
||||
if profile.exists():
|
||||
marker = profile/CONTROL_MARKER
|
||||
if marker.is_symlink() or not marker.is_file() or json.loads(marker.read_text()) != CONTROL_ID:
|
||||
refuse('Vanilla control requires a new or explicitly control-owned disposable profile')
|
||||
|
||||
|
||||
def vanilla_evidence(content):
|
||||
report = analyze(content, 'console.log')
|
||||
modules = re.findall(r'Module: (GameLib|Game);[^\n]*defines:\s*"([^"]*)"', content)
|
||||
ordinary = {name for name, flags in modules} == {'GameLib', 'Game'}
|
||||
bad_defines = any({'WORKBENCH', 'ENF_WB'}.intersection(flags.split(',')) for _, flags in modules)
|
||||
# Only the engine's Loaded-addons blocks count, not its available inventory.
|
||||
blocks = re.findall(r"Loaded addons:\r?\n((?:[^\n]*ENGINE[^\n]*gproj:[^\n]*\n)+)", content)
|
||||
guids = [guid.upper() for block in blocks for guid in re.findall(r"guid: '([0-9A-Fa-f]{16})'", block)]
|
||||
allowed = {'5614BBCCBB55ED1C', '58D0FB3206B6F859'}
|
||||
wrong_addons = bool(set(guids) - allowed)
|
||||
base_only = bool(blocks) and set(guids) == allowed and not wrong_addons
|
||||
successor = any(token in content for token in ('ERR ORDINARY:', 'ERR SMOKE ', 'Rereintroduced [mission]'))
|
||||
menu = "Entities load 'worlds/MainMenuWorld/MainMenuWorld.ent'" in content and 'Entered main menu.' in content
|
||||
failed = bool(report['engine_errors'] or report['script_or_runtime_failures']) or bad_defines or wrong_addons or successor
|
||||
return {**report, 'control_only': True, 'loaded_addon_guids': guids, 'vanilla_addons_only': base_only,
|
||||
'game_module_defines': modules, 'workbench_undefined': ordinary and not bad_defines,
|
||||
'main_menu_initialized': menu, 'unexpected_successor_activity': successor,
|
||||
'normal_mission_ready': False, 'local_participant_verified': False,
|
||||
'scenario_header_verified': False, 'failed': failed,
|
||||
'ready': ordinary and base_only and menu and not failed,
|
||||
'scope': 'vanilla-only ordinary main-menu startup control; no successor mission, input, multiplayer or release qualification'}
|
||||
|
||||
|
||||
def game_evidence(content, project=None):
|
||||
if project == CONTROL:
|
||||
return vanilla_evidence(content)
|
||||
report = analyze(content, 'console.log')
|
||||
defines = re.findall(r'Module: Game;[^\n]*defines:\s*"([^"]*)"', content)
|
||||
non_workbench = bool(defines) and all(not {'WORKBENCH', 'ENF_WB'}.intersection(value.split(',')) for value in defines)
|
||||
@@ -229,6 +270,12 @@ def run(a):
|
||||
if not 0 <= a.failure_grace <= 30:
|
||||
refuse('Use failure-grace 0..30 seconds')
|
||||
assert_no_game()
|
||||
if a.project == CONTROL:
|
||||
if a.timeout > 120 or a.settle > 30:
|
||||
refuse('Vanilla control is bounded to timeout 60..120 and settle 5..30 seconds')
|
||||
validate_control_profile(profile)
|
||||
elif (profile/CONTROL_MARKER).exists():
|
||||
refuse('Successor runs must not reuse the vanilla-control profile')
|
||||
validate_paths(ROOT, steam, compat, profile)
|
||||
# Use the SAME prefix lock as headless_workbench.py: never run them together.
|
||||
with (compat/'.erri-workbench.lock').open('a') as prefix_lock:
|
||||
@@ -254,6 +301,8 @@ def run_locked(a, steam, compat, profile):
|
||||
marker = profile/PROFILE_MARKER
|
||||
if not marker.exists():
|
||||
marker.write_text(json.dumps(PROFILE_ID)+'\n')
|
||||
if a.project == CONTROL:
|
||||
(profile/CONTROL_MARKER).write_text(json.dumps(CONTROL_ID)+'\n')
|
||||
addons = profile/'addons'
|
||||
addons.mkdir(exist_ok=True)
|
||||
vanilla = steam/'steamapps/common/Arma Reforger/addons/data'
|
||||
@@ -347,6 +396,8 @@ def run_locked(a, steam, compat, profile):
|
||||
result = 'game-failed'
|
||||
if sealed and not sealed['source_unchanged_after_run']:
|
||||
result = 'source-changed'
|
||||
if a.project == CONTROL and result == 'game-started':
|
||||
result = 'vanilla-control-started'
|
||||
summary = {**sealed, **bindings(captured), 'started_unix': started, 'result': result,
|
||||
'session': str(session) if session else None, 'project': a.project,
|
||||
'cleanup_confirmed': cleanup_ok, 'runner_error': runner_error,
|
||||
@@ -355,13 +406,15 @@ def run_locked(a, steam, compat, profile):
|
||||
'first_failure_elapsed_seconds': (monitor.first_failure_at - monitor.started) if monitor.first_failure_at is not None else None,
|
||||
'executable': str(executable), 'dedicated_server': False,
|
||||
'display': 'headless Gamescope', 'audio': '-noSound plus own silent sink',
|
||||
'scope': 'ordinary game compile/startup; no scripted fixture, full escape or multiplayer acceptance'}
|
||||
'control_only': a.project == CONTROL,
|
||||
'source_manifest_scope': 'successor addon snapshot retained for provenance bookkeeping only; none loaded by vanilla control' if a.project == CONTROL else 'successor addon sources',
|
||||
'scope': final.get('scope', 'ordinary game compile/startup; no scripted fixture, full escape or multiplayer acceptance')}
|
||||
if session:
|
||||
(session/'runner.json').write_text(json.dumps(summary, indent=2)+'\n')
|
||||
(session/'game-startup-report.json').write_text(json.dumps(final, indent=2)+'\n')
|
||||
shutil.copyfile(launch_log, session/'headless-launch.log')
|
||||
print(json.dumps(summary, indent=2), flush=True)
|
||||
return 0 if result == 'game-started' else 1
|
||||
return 0 if result in {'game-started', 'vanilla-control-started'} else 1
|
||||
|
||||
|
||||
def main():
|
||||
@@ -369,7 +422,7 @@ def main():
|
||||
parser.add_argument('--steam', type=Path, default=Path.home()/'.local/share/Steam')
|
||||
parser.add_argument('--compat', type=Path, required=True)
|
||||
parser.add_argument('--profile', type=Path, default=Path('/tmp/erri-game-profile'))
|
||||
parser.add_argument('--project', choices=['core', 'chat'], default='core')
|
||||
parser.add_argument('--project', choices=['core', 'chat', CONTROL], default='core')
|
||||
parser.add_argument('--inspect-processes', action='store_true', help='Read-only process preflight report; never launch, lock, write a profile or configure audio')
|
||||
parser.add_argument('--timeout', type=int, default=180)
|
||||
parser.add_argument('--settle', type=int, default=15)
|
||||
|
||||
Reference in New Issue
Block a user