Harden base progression and cleanup; validate finite physical resupply

This commit is contained in:
2026-09-19 10:39:39 +02:00
parent 40a474934c
commit 7c53b4bb81
377 changed files with 51882 additions and 92 deletions
+10 -2
View File
@@ -32,12 +32,20 @@ if launched:
if (source/'game-startup-report.json').is_file():
# Recompute from the sealed console rather than trusting the saved summary.
# Ordinary startup has no scripted completion and cannot qualify as an escape.
from headless_game import game_evidence
from headless_game import game_evidence, CONTROL
runner=json.loads((source/'runner.json').read_text())
report=game_evidence((dest/'console.log.txt').read_text(errors='replace'),runner.get('project'))
report['log']=str((dest/'console.log.txt').relative_to(ROOT))
control=runner.get('project')==CONTROL
expected_result='vanilla-control-started' if control else 'game-started'
if control:
consistent=runner.get('control_only') is True and report.get('control_only') is True
report['qualification']='vanilla startup control only; no successor mission or release qualification'
else:
consistent=runner.get('control_only',False) is False and report.get('control_only',False) is False
report['control_metadata_consistent']=consistent
(dest/'report.json').write_bytes(encoded(report))
status=0 if report['ready'] and runner.get('result')=='game-started' and runner.get('cleanup_confirmed') is True else 1
status=0 if report['ready'] is True and consistent and runner.get('result')==expected_result and runner.get('cleanup_confirmed') is True else 1
else:
with (dest/'report.json').open('w') as report:
result=subprocess.run([sys.executable,str(ROOT/'tools/native_report.py'),str((dest/'console.log.txt').relative_to(ROOT))],cwd=ROOT,stdout=report)
+1 -1
View File
@@ -59,7 +59,7 @@ def check():
assert set(mapped)==set(inventory['script_paths']), 'Original inventory has untracked or unknown entries'
print(f'PASS: {len(mapped)} original script entries tracked across {len(feature_ids)} unfinished responsibilities')
print(f'PASS: {len(ids)} isolated addons, acyclic dependencies, {len(resources)} unique resources, exact vanilla references')
for name in ['test_powered_flight_policy.py','test_native_local_admission_probe.py','test_mortar_report_ledger.py','test_dedicated_native.py','test_native_mortar_transfer.py','test_mortar_aim_renewal.py','test_dedicated_preflight.py','test_mortar_transactions.py','test_native_mortar_probe.py','test_diagnostic_no_splash.py','test_casualty_incapacitation.py','test_casualty_remove_probe.py','test_extraction_placement_policy.py','test_guard_cancellation_policy.py','test_casualty_ground_policy.py','test_casualty_reservation_death.py','test_casualty_loading_policy.py','test_initial_guard_policy.py','test_headless_game_runner.py','test_site_discovery_policy.py','test_faction_catalog_scope.py','test_mission_rules.py','test_admin_setting_policy.py', 'test_player_admission_policy.py','test_local_admission.py','test_prison_placement_policy.py', 'test_recovery_policy.py','test_time_policy.py','test_environment_policy.py','test_environment_restart_runner.py', 'test_inventory_enumeration_policy.py','test_radio_defence_policy.py','test_radio_feedback_policy.py','test_performance_comparison.py','test_release_check.py','test_settings_migration.py','hq_network_regression.py','helicopter_availability_regression.py','mortar_salvo_regression.py']:
for name in ['test_item_station_teardown.py', 'test_source_bundle.py', 'test_native_flight_footprint.py', 'test_native_flight_input_diagnostics.py', 'test_mortar_logistics.py', 'test_mortar_courier_storage.py','test_powered_flight_pad.py','test_prison_escape_policy.py','test_powered_flight_policy.py','test_native_local_admission_probe.py','test_mortar_report_ledger.py','test_dedicated_native.py','test_native_mortar_transfer.py','test_mortar_aim_renewal.py','test_dedicated_preflight.py','test_mortar_transactions.py','test_native_mortar_probe.py','test_diagnostic_no_splash.py','test_casualty_incapacitation.py','test_casualty_remove_probe.py','test_extraction_placement_policy.py','test_guard_cancellation_policy.py','test_casualty_ground_policy.py','test_casualty_reservation_death.py','test_casualty_loading_policy.py','test_initial_guard_policy.py','test_headless_game_runner.py','test_vanilla_game_control.py','test_archive_game_control.py','test_site_discovery_policy.py','test_faction_catalog_scope.py','test_mission_rules.py','test_admin_setting_policy.py', 'test_player_admission_policy.py','test_progression_identity.py','test_discovery_map_position.py','test_local_admission.py','test_prison_placement_policy.py', 'test_recovery_policy.py','test_time_policy.py','test_environment_policy.py','test_environment_restart_runner.py', 'test_inventory_enumeration_policy.py','test_radio_defence_policy.py','test_radio_feedback_policy.py','test_performance_comparison.py','test_release_check.py','test_settings_migration.py','hq_network_regression.py','helicopter_availability_regression.py','mortar_salvo_regression.py']:
subprocess.run([sys.executable,str(ROOT/'tests'/name)],check=True)
print('All repository checks passed. Native compilation and multiplayer tests are separate requirements.')
if __name__=='__main__':check()
+65 -12
View File
@@ -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)
+8 -3
View File
@@ -11,8 +11,9 @@ p = argparse.ArgumentParser(description=__doc__)
p.add_argument('--steam', type=Path, default=Path.home()/'.local/share/Steam')
p.add_argument('--compat', type=Path, required=True, help='Disposable cloned Proton compatdata directory outside Steam')
p.add_argument('--profile', type=Path, required=True, help='Separate disposable successor Workbench profile')
p.add_argument('--smoke', choices=['powered-flight', 'local-admission', 'mortar-expiry', 'mortar-transfer', 'mortar-fire', 'patrol-hq', 'victory', 'lost-helicopter', 'player-death', 'radio-network', 'hq-response', 'mortar-inventory', 'startup-failure', 'site-survey', 'cache-loss', 'extraction-capacity', 'early-helicopter-loss', 'extraction-startup-failure', 'extraction-boundary-failure', 'transport-loss', 'transport-survey', 'lifecycle', 'objective-ui', 'recovery', 'prisoner-kit', 'medical', 'casualty-load', 'casualty-direct', 'casualty-bench', 'casualty-remove', 'environment', 'environment-restart', 'patrols', 'radio-document-loss', 'radio-document-loss-partial', 'guard-cancel', 'casualty-service'], default='victory')
p.add_argument('--smoke', choices=['mortar-courier', 'station-teardown', 'prison-breach', 'powered-flight', 'local-admission', 'mortar-expiry', 'mortar-transfer', 'mortar-fire', 'patrol-hq', 'victory', 'lost-helicopter', 'player-death', 'radio-network', 'hq-response', 'mortar-inventory', 'startup-failure', 'site-survey', 'cache-loss', 'extraction-capacity', 'early-helicopter-loss', 'extraction-startup-failure', 'extraction-boundary-failure', 'transport-loss', 'transport-survey', 'lifecycle', 'objective-ui', 'recovery', 'prisoner-kit', 'medical', 'casualty-load', 'casualty-direct', 'casualty-bench', 'casualty-remove', 'environment', 'environment-restart', 'patrols', 'radio-document-loss', 'radio-document-loss-partial', 'guard-cancel', 'casualty-service'], default='victory')
p.add_argument('--station-teardown-diagnostics', action='store_true', help='Opt-in bounded native deletion diagnostics; preserves all validation errors')
p.add_argument('--flight-input-diagnostics', action='store_true', help='Opt-in read-only powered-flight input/application and native rotor signal observations')
p.add_argument('--rounds', type=int, default=2)
p.add_argument('--timeout', type=int, default=360)
p.add_argument('--project', choices=['core', 'chat'], default='chat', help='Core-only base or full companion integration')
@@ -20,9 +21,12 @@ a = p.parse_args()
steam, compat, profile = a.steam.resolve(), a.compat.resolve(), a.profile.resolve()
if not 1 <= a.rounds <= 10 or not 60 <= a.timeout <= 1800:
p.error('Use 1..10 rounds and a 60..1800 second timeout')
if a.smoke == 'mortar-courier' and (a.project != 'chat' or a.rounds != 1 or a.timeout < 500):
p.error('mortar-courier requires --project chat --rounds 1 --timeout at least 500')
if a.smoke in {'mortar-expiry', 'mortar-transfer', 'mortar-fire'} and (a.project != 'chat' or a.rounds != 1 or a.timeout < 400):
p.error('mortar-expiry/mortar-transfer/mortar-fire requires --project chat --rounds 1 --timeout at least 400')
if a.smoke == 'powered-flight' and (a.project != 'core' or a.rounds != 1 or a.timeout < 420): p.error('powered-flight requires Core, one round, and at least 420 seconds')
if a.smoke == 'station-teardown' and (a.project != 'core' or a.rounds != 1): p.error('station-teardown requires --project core --rounds 1')
if a.smoke == 'environment-restart':
if a.project != 'core' or a.rounds != 2: p.error('environment-restart requires --project core --rounds 2')
fixture_names = ['erri-environment-restart.json', 'erri-environment-restart.json.bak', 'erri-environment-restart.json.tmp', 'erri-environment-restart-round.json']
@@ -58,11 +62,12 @@ for proc in Path('/proc').iterdir():
except (FileNotFoundError, PermissionError, ProcessLookupError): pass
command = ['gamescope', '--backend', 'headless', '-W', '1280', '-H', '720', '-w', '1280', '-h', '720', '-r', '30', '--', str(proton), 'run', str(wb/'ArmaReforgerWorkbenchSteamDiag.exe'), '-gproj', 'Z:'+str(ROOT/'addons'/a.project/'addon.gproj'), '-addonsDir', 'Z:'+str(ROOT/'addons'), '-profile', profile_arg, '-logconsole', '-wbModule=WorldEditor', '-load', 'Worlds/ERR_Everon.ent', '-plugin=ERR_ValidateWorldPlugin', '-errPlay', '1', '-errSmoke', '1', '-errSmokeOutcome', a.smoke, '-noThrow', '-disableCrashReporter']
if a.station_teardown_diagnostics: command += ['-errStationTeardownDiag', '1']
if a.flight_input_diagnostics: command += ['-errFlightInputDiag', '1']
if a.project == 'core':
if a.smoke not in {'powered-flight', 'local-admission', 'victory', 'startup-failure', 'site-survey', 'cache-loss', 'extraction-capacity', 'early-helicopter-loss', 'extraction-startup-failure', 'extraction-boundary-failure', 'transport-loss', 'transport-survey', 'lifecycle', 'objective-ui', 'recovery', 'prisoner-kit', 'medical', 'casualty-load', 'casualty-direct', 'casualty-bench', 'casualty-remove', 'environment', 'environment-restart', 'patrols', 'radio-document-loss', 'radio-document-loss-partial', 'guard-cancel', 'casualty-service'}: p.error('Unsupported Core-only fixture')
if a.smoke not in {'station-teardown', 'prison-breach', 'powered-flight', 'local-admission', 'victory', 'startup-failure', 'site-survey', 'cache-loss', 'extraction-capacity', 'early-helicopter-loss', 'extraction-startup-failure', 'extraction-boundary-failure', 'transport-loss', 'transport-survey', 'lifecycle', 'objective-ui', 'recovery', 'prisoner-kit', 'medical', 'casualty-load', 'casualty-direct', 'casualty-bench', 'casualty-remove', 'environment', 'environment-restart', 'patrols', 'radio-document-loss', 'radio-document-loss-partial', 'guard-cancel', 'casualty-service'}: p.error('Unsupported Core-only fixture')
command[command.index('-plugin=ERR_ValidateWorldPlugin')] = '-plugin=ERR_ValidateBaseWorldPlugin'
command[command.index('-errSmoke')] = '-errBaseSmoke'
if a.smoke in {'powered-flight', 'local-admission', 'cache-loss', 'extraction-capacity', 'early-helicopter-loss', 'extraction-startup-failure', 'extraction-boundary-failure', 'transport-loss', 'transport-survey', 'lifecycle', 'objective-ui', 'recovery', 'prisoner-kit', 'medical', 'casualty-load', 'casualty-direct', 'casualty-bench', 'casualty-remove', 'environment', 'environment-restart', 'patrols', 'radio-document-loss', 'radio-document-loss-partial', 'guard-cancel', 'casualty-service'} and a.project != 'core': p.error('This fixture requires --project core')
if a.smoke in {'station-teardown', 'prison-breach', 'powered-flight', 'local-admission', 'cache-loss', 'extraction-capacity', 'early-helicopter-loss', 'extraction-startup-failure', 'extraction-boundary-failure', 'transport-loss', 'transport-survey', 'lifecycle', 'objective-ui', 'recovery', 'prisoner-kit', 'medical', 'casualty-load', 'casualty-direct', 'casualty-bench', 'casualty-remove', 'environment', 'environment-restart', 'patrols', 'radio-document-loss', 'radio-document-loss-partial', 'guard-cancel', 'casualty-service'} and a.project != 'core': p.error('This fixture requires --project core')
if a.smoke in {'startup-failure', 'site-survey', 'transport-survey', 'extraction-startup-failure', 'extraction-boundary-failure'} and (a.project != 'core' or a.rounds != 1): p.error('This diagnostic requires --project core --rounds 1')
env = os.environ.copy()
env.update(STEAM_COMPAT_DATA_PATH=str(compat), STEAM_COMPAT_CLIENT_INSTALL_PATH=str(steam), WINEPREFIX=str(compat/'pfx'))
+246
View File
@@ -0,0 +1,246 @@
#!/usr/bin/env python3
"""Preview or write a deterministic DEVELOPMENT SOURCE BUNDLE; never build/publish."""
import argparse
import hashlib
import io
import json
import os
from pathlib import Path, PurePosixPath
import re
import stat
import tempfile
import zipfile
import sys
sys.dont_write_bytecode = True
from source_manifest import addon_manifest
from release_check import audit, read_json, evidence_path
ROOT = Path(__file__).resolve().parents[1]
BASE = '58D0FB3206B6F859'
PROJECTS = {
'core': ('ERR_Core', '2528466DE220D91C', ()),
'ai': ('ERR_Ai', '5192138CB9D5B911', ('core',)),
'intel': ('ERR_Intel', 'D31039C7C53E348A', ('core',)),
'map': ('ERR_Map', '9AAEE9041D79809E', ('core',)),
'chat': ('ERR_Chat', 'FA804F411BDDF2C6', ('core', 'ai', 'intel', 'map')),
}
DIRECTORIES = {'Scripts', 'Prefabs', 'Configs', 'Missions', 'Worlds', 'UI', 'Materials'}
SUFFIXES = {'.c', '.et', '.conf', '.ent', '.layer', '.layout', '.emat', '.meta', '.gproj'}
EXCLUDED = {'.git', '.build', '.local', '__pycache__', 'profiles', 'profile', 'cache',
'runtime', 'evidence', 'vendor', 'vanilla', 'original', 'node_modules', '.agents', '.codex'}
README = '''# DEVELOPMENT SOURCE BUNDLE — Escapists Rereintroduced
This ZIP contains authored source and editable resources for all five successor
addon projects. It is NOT a native .pak, a qualified release, or a published mod.
No original Escapists or installed vanilla assets are redistributed. Vanilla asset
references require a separately installed, compatible Arma Reforger and Workbench.
Extract into a NEW development folder, never over an existing mission/project.
Configure Workbench's addon search directory to the extracted addons directory.
Open addons/core/addon.gproj for the independent Core project, or
addons/chat/addon.gproj for the complete companion dependency set. Every project
also declares vanilla Arma Reforger (58D0FB3206B6F859): core; ai -> core;
intel -> core; map -> core; chat -> core, ai, intel, map.
Core scenario: {AD62322D5213C440}Missions/ERR_Everon_Base.conf
Companion scenario: {C1E20667DCE324ED}Missions/ERR_Everon.conf
Use the installed engine's normal project/scenario workflow. This archive has not
been compiled by its bundler; no native caches or binaries are included.
Target: vanilla Everon, up to 12 players. This remains development source.
Manual prison/scavenging/radio/extraction play, remote joins/reconnects and
ownership, twelve connected players, aircraft support, repeated companion cleanup,
matched-workload FPS improvement and release qualification require separate
source-bound validation. Existing scripted fixtures are not those acceptances.
WORKBENCH-only probes are source, not proof of a test run or normal gameplay.
BUNDLE-MANIFEST.json lists each payload file's SHA-256 and project dependencies.
Verify extracted bytes against it before editing. The manifest excludes its own
hash to avoid self-reference. These hashes identify bytes, not authenticity,
license grants or provenance proof. Source review remains required before sharing.
Evidence, acceptance records, runtime profiles and test tools are intentionally
absent; consult the maintained repository for current status and retained logs.
Even --mode release only adds the existing repository qualification audit before
this same development-source handoff; it does not build/publish a game release.
'''
def canonical(name):
p = PurePosixPath(name)
if (not name or p.is_absolute() or p.as_posix() != name or '\\' in name
or ':' in name or any(ord(c) < 32 for c in name)
or any(part in {'.', '..'} or part.endswith((' ', '.')) for part in p.parts)):
raise ValueError(f'unsafe bundle path: {name!r}')
return p
def inventory(root):
"""Reuse the evidence inventory; impose a narrower reviewed handoff schema."""
# Preflight directories without following links, including otherwise excluded
# entries themselves. Excluded trees are pruned from this preflight and
# excluded from the payload; the shared source manifest may still hash them.
addons = root / 'addons'
if addons.is_symlink() or not addons.is_dir():
raise ValueError('addons must be a real directory')
for base, dirs, files in os.walk(addons, followlinks=False):
for name in list(dirs) + files:
path = Path(base) / name
if path.is_symlink():
raise ValueError(f'symlink refused: {path.relative_to(root)}')
if not path.is_dir() and not stat.S_ISREG(path.lstat().st_mode):
raise ValueError(f'nonregular addon entry: {path.relative_to(root)}')
dirs[:] = [d for d in dirs if d.lower() not in EXCLUDED]
original = addon_manifest(root)
result = {}
folded = set()
for name, digest in original.items():
parts = canonical(name).parts
if any(part.lower() in EXCLUDED for part in parts):
continue
if any(part.startswith('.') for part in parts):
raise ValueError(f'unreviewed hidden addon content: {name}')
if len(parts) < 3 or parts[1] not in PROJECTS:
raise ValueError(f'unknown addon project: {name}')
path = root / name
if not stat.S_ISREG(path.lstat().st_mode):
raise ValueError(f'nonregular addon file: {name}')
if name.casefold() in folded:
raise ValueError(f'case-insensitive path collision: {name}')
folded.add(name.casefold())
if parts[2] == 'addon.gproj' and len(parts) == 3:
pass
elif len(parts) < 4 or parts[2] not in DIRECTORIES or path.suffix not in SUFFIXES - {'.gproj'}:
raise ValueError(f'unreviewed addon source/resource format: {name}')
result[name] = digest
return result
def validate(payload, root):
projects = {}
own_ids = read_json(evidence_path(root, 'docs/addons.json'))
if own_ids != {name: item[1] for name, item in PROJECTS.items()}:
raise ValueError('successor identity catalog differs from the five owned projects')
for name, (identity, guid, deps) in PROJECTS.items():
text = payload.get(f'addons/{name}/addon.gproj', b'').decode('utf-8')
match = re.fullmatch(r'\s*GameProject\s*\{\s*ID "([^"\n]+)"\s*GUID "([A-F0-9]{16})"\s*TITLE "([^"\n]+)"\s*Dependencies\s*\{([\s"A-F0-9]*)\}\s*\}\s*', text)
expected = {BASE, *(PROJECTS[d][1] for d in deps)}
found = re.findall(r'"([A-F0-9]{16})"', match[4]) if match else []
if not match or match[1] != identity or match[2] != guid or set(found) != expected or len(found) != len(expected):
raise ValueError(f'identity/dependency closure mismatch: {name}')
projects[name] = {'id': identity, 'guid': guid, 'dependencies': sorted(found)}
resources = {}
for name, data in payload.items():
if not name.endswith('.meta'):
continue
m = re.search(r'Name "\{([A-F0-9]{16})\}([^"\n]+)"', data.decode('utf-8'))
owner = name.split('/')[1]
if not m or f'addons/{owner}/{m[2]}.meta' != name or name[:-5] not in payload:
raise ValueError(f'mismatched/missing resource metadata: {name}')
if m[1] in resources or m[1] in own_ids.values() or m[1] == BASE:
raise ValueError(f'duplicate resource identity: {name}')
resources[m[1]] = (m[2], owner)
vanilla = read_json(evidence_path(root, 'docs/vanilla-resources.json'))
if set(resources) & set(vanilla.values()):
raise ValueError('successor resource reuses a vanilla identity')
for name, data in payload.items():
suffix = PurePosixPath(name).suffix
if suffix in {'.meta', '.gproj'}:
continue
text = data.decode('utf-8') # Current reviewed handoff contains text assets only.
if re.search(r'\b(?:ESCT\w*|SCRTC_\w+|SCRT_\w+|EAI_\w+)\b', text):
raise ValueError(f'original ecosystem dependency: {name}')
if suffix not in {'.c', '.layer'} and name + '.meta' not in payload:
raise ValueError(f'resource lacks successor metadata: {name}')
owner = name.split('/')[1]
visible = {owner, *PROJECTS[owner][2]}
for guid, resource in re.findall(r'\{([A-F0-9]{16})\}([^"\n]+)', text):
if guid in resources:
path, target = resources[guid]
if path != resource or target not in visible:
raise ValueError(f'resource outside project dependency closure: {name}')
elif vanilla.get(resource) != guid:
raise ValueError(f'unresolved or nonvanilla resource: {name}')
return projects
def build(root=ROOT, mode='development'):
root = Path(root).resolve()
if mode not in {'development', 'release'}:
raise ValueError('unknown bundle mode')
if mode == 'release':
blockers = audit(root)
if blockers:
raise ValueError('Not release-qualified:\n - ' + '\n - '.join(blockers))
before = inventory(root)
payload = {name: (root / name).read_bytes() for name in before}
if {name: hashlib.sha256(data).hexdigest() for name, data in payload.items()} != before:
raise ValueError('addon bytes changed during collection')
projects = validate(payload, root)
if mode == 'release':
blockers = audit(root)
if blockers:
raise ValueError('Not release-qualified after collection:\n - ' + '\n - '.join(blockers))
if inventory(root) != before:
raise ValueError('addon tree changed during collection')
payload['BUNDLE-README.md'] = README.encode('utf-8')
manifest = {'schema_version': 1, 'kind': 'DEVELOPMENT SOURCE BUNDLE',
'native_build': False, 'release_package': False, 'projects': projects,
'files': {name: hashlib.sha256(data).hexdigest() for name, data in sorted(payload.items())}}
payload['BUNDLE-MANIFEST.json'] = (json.dumps(manifest, indent=2, sort_keys=True) + '\n').encode()
stream = io.BytesIO()
with zipfile.ZipFile(stream, 'w', compression=zipfile.ZIP_STORED) as archive:
for name, data in sorted(payload.items()):
entry = zipfile.ZipInfo(name, date_time=(1980, 1, 1, 0, 0, 0))
entry.create_system = 3
entry.external_attr = (stat.S_IFREG | 0o644) << 16
archive.writestr(entry, data)
return stream.getvalue(), manifest
def write_new(output, content, root=ROOT):
path = Path(output).absolute()
canonical(path.as_posix().lstrip('/'))
if path.suffix != '.zip' or path.resolve().is_relative_to(Path(root).resolve()):
raise ValueError('output must be a new .zip outside the source checkout')
for part in (path, *path.parents):
if part.is_symlink():
raise ValueError('output symlinks are refused')
if path.exists() or not path.parent.is_dir():
raise ValueError('output exists or parent directory is missing')
temporary = None
try:
with tempfile.NamedTemporaryFile(prefix='.erri-bundle-', dir=path.parent, delete=False) as file:
temporary = Path(file.name)
file.write(content)
file.flush()
os.fsync(file.fileno())
# Same-filesystem atomic publication; link refuses an existing destination.
os.link(temporary, path, follow_symlinks=False)
finally:
if temporary is not None:
temporary.unlink()
def main(argv=None):
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--mode', choices=('development', 'release'), default='development')
parser.add_argument('--output', type=Path, help='explicit new .zip outside checkout; omitted means read-only preview')
args = parser.parse_args(argv)
try:
content, manifest = build(mode=args.mode)
if args.output:
write_new(args.output, content)
print(json.dumps({'kind': manifest['kind'], 'preview': args.output is None,
'payload_files': len(manifest['files']), 'archive_bytes': len(content),
'archive_sha256': hashlib.sha256(content).hexdigest(),
'projects': manifest['projects']}, indent=2, sort_keys=True))
return 0
except (OSError, ValueError, UnicodeError) as error:
print(f'Bundle refused: {error}')
return 1
if __name__ == '__main__':
raise SystemExit(main())