Combine Workshop addon and fix prison startup

Merge owned systems into one addon while preserving resource identities. Gate player admission on the replicated prison scene, restore the original framed door, and hold enemy AI until escape. Correct scenario branding and author metadata, include publishing assets, and retain regression and native verification evidence.
This commit is contained in:
2026-09-20 16:42:52 +02:00
parent 7d94953b53
commit cab998fb64
513 changed files with 18940 additions and 862 deletions
+7 -2
View File
@@ -3,15 +3,20 @@
from pathlib import Path
import json,re,subprocess,sys
from external_addons import policy, ALLOWED, direct_for, resource_allowed
from prefab_identity import check_prefabs
ROOT=Path(__file__).resolve().parents[1]
BASE='58D0FB3206B6F859'
def check():
print(f'PASS: {check_prefabs((ROOT/"addons").rglob("*.et"))} prefabs have unique saved root entity IDs')
subprocess.run([sys.executable,str(ROOT/'tests/test_prefab_identity.py')],check=True)
subprocess.run([sys.executable,str(ROOT/'tests/test_spawn_scene.py')],check=True)
subprocess.run([sys.executable,str(ROOT/'tests/test_prison_ai.py')],check=True)
ids={};graph={};resources={};paths={};owners={}
for p in sorted((ROOT/'addons').glob('*/addon.gproj')):
text=p.read_text();g=re.search(r'GUID "([A-F0-9]{16})"',text)[1]
assert g not in ids,f'Duplicate addon GUID: {p}'
ids[g]=p.parent.name;graph[g]=re.findall(r'"([A-F0-9]{16})"',re.search(r'\bDependencies\s*\{([^{}]*)\}',text)[1])
assert len(ids)==5,'Expected five addon projects'
assert ids == {'2528466DE220D91C': 'core'}, 'Expected one combined addon project'
external=policy(ROOT)
for guid,name in ids.items():
assert (set(graph[guid]) & set(ALLOWED)) == direct_for(name), f'Unreviewed direct external dependency scope: {name}'
@@ -37,7 +42,7 @@ def check():
if dep!=BASE:result.update(visible(dep))
return result
visibility={ids[g]:visible(g) for g in ids}
assert not (visibility['core'] & set(ALLOWED)), 'Core must remain vanilla-only'
assert set(ALLOWED) <= visibility['core'], 'Combined addon requires the authorized external closure'
vanilla=json.loads((ROOT/'docs/vanilla-resources.json').read_text())
for p in (ROOT/'addons').rglob('*'):
if not p.is_file() or p.suffix not in ['.c','.et','.conf','.ent','.layer','.layout','.emat']:continue
+4 -9
View File
@@ -50,15 +50,10 @@ def sandbox(runtime, source, run):
PROJECTS = {
'core': ('2528466DE220D91C', '{AD62322D5213C440}Missions/ERR_Everon_Base.conf'),
'chat': ('FA804F411BDDF2C6', '{C1E20667DCE324ED}Missions/ERR_Everon.conf'),
}
ADDON_GUIDS = {
'core': PROJECTS['core'][0],
'ai': '5192138CB9D5B911',
'intel': 'D31039C7C53E348A',
'map': '9AAEE9041D79809E',
'chat': PROJECTS['chat'][0],
'chat': ('2528466DE220D91C', '{C1E20667DCE324ED}Missions/ERR_Everon.conf'),
}
ADDON_GUIDS = {'core': PROJECTS['core'][0]}
WORLD = '{B6C516F86302C679}Worlds/ERR_Everon.ent'
SYSTEMS = '{C50579A2EC48E8EF}Configs/Systems/MissionSystems.conf'
@@ -119,7 +114,7 @@ def main():
parser.add_argument('--archive', type=Path, required=True)
parser.add_argument('--route', choices=['header', 'diag-world', 'vanilla-everon'], default='header')
parser.add_argument('--project', choices=['core', 'chat'], default='core',
help='Core base (default) or Chat with all five declared successor addons')
help='Diagnostic scenario header; both select the combined addon')
parser.add_argument('--seconds', type=int, default=90)
args = parser.parse_args()
if args.route == 'vanilla-everon' and args.project != 'core':
+2 -2
View File
@@ -25,7 +25,7 @@ def digest(path):
def selected_scenario(source, project):
addon = source / 'addons' / project
addon = source / 'addons' / 'core'
text = (addon / 'addon.gproj').read_text()
guid = re.search(r'\bGUID "([A-F0-9]{16})"', text)
if not guid:
@@ -85,7 +85,7 @@ def build_plan(source, runtime, project='core', port=2001):
config = {
'bindAddress': '127.0.0.1', 'bindPort': port,
'publicAddress': '127.0.0.1', 'publicPort': port,
'game': {'name': 'Rereintroduced private validation', 'scenarioId': scenario['header'],
'game': {'name': 'Reintroduced private validation', 'scenarioId': scenario['header'],
'maxPlayers': 2, 'visible': False,
'gameProperties': {'fastValidation': True, 'battlEye': True}, 'mods': []},
'operating': {'disableCrashReporter': True},
+1 -1
View File
@@ -35,6 +35,6 @@ workspace.windowAdded.connect(function(w) {
var windows = workspace.windowList();
// Existing user editors are deliberately left untouched.
for (var i = 0; i < windows.length; i++) {
if (String(windows[i].caption).indexOf("Rereintroduced") >= 0) place(windows[i]);
if (String(windows[i].caption).indexOf("Reintroduced") >= 0) place(windows[i]);
}
print("ERRI_DISPLAY guard active for " + targetName);
+1 -1
View File
@@ -34,7 +34,7 @@ def policy(root):
require(entry['addon'] in ALLOWED and re.fullmatch(r'[A-F0-9]{16}',entry['guid']) is not None,'Unknown resource owner/identity')
return value
def direct_for(project):return set(DIRECT) if project=='ai' else set()
def direct_for(project):return set(DIRECT) if project=='core' else set()
def resource_allowed(value,visible,guid,path):
entry=value['resources'].get(path)
+13 -12
View File
@@ -129,7 +129,7 @@ def packed_request(a):
def prepare_packed(packed):
"""Called only while holding the ordinary runner's existing prefix lock."""
if packed.get('schema_version') == 3:
if packed.get('schema_version') == 4:
from packed_external import prepare
return prepare(packed)
packed['session'].mkdir(exist_ok=False)
@@ -148,7 +148,7 @@ def prepare_packed(packed):
def packed_snapshot(packed, command, profile):
if packed.get('schema_version') == 3:
if packed.get('schema_version') == 4:
from packed_external import snapshot
return snapshot(packed, command, profile)
project = packed.get('project', 'core')
@@ -169,7 +169,7 @@ def packed_snapshot(packed, command, profile):
def packed_seal(packed, raw, session):
"""Fail closed on changed/deleted inputs, retaining a useful failure record."""
if packed.get('schema_version') == 3:
if packed.get('schema_version') == 4:
from packed_external import seal
return seal(packed, raw, session)
metadata = json.loads(raw)
@@ -201,7 +201,7 @@ def native_path(value, game_root):
def packed_evidence(content, packed, steam, profile):
if packed.get('schema_version') == 3:
if packed.get('schema_version') == 4:
from packed_external import evidence
return evidence(content, packed, steam, profile, sys.modules[__name__])
project = packed.get('project', 'core')
@@ -379,9 +379,9 @@ def command_for(root, steam, compat, profile, project, packed=None, capture_pris
if project != packed.get('project', 'core') or project not in {'core', 'chat'}:
refuse('Packed input project does not match requested ordinary scenario')
addon_root = packed['input'] if project == 'chat' else packed['input'].parent
command += ['-addonsDir', 'Z:'+str(addon_root), '-addons', PACKED_PROJECTS[project][1]]
command += ['-addonsDir', 'Z:'+str(addon_root), '-addons', ('2528466DE220D91C' if packed.get('schema_version') == 4 else PACKED_PROJECTS[project][1])]
elif project != CONTROL:
project_text = (root/'addons'/project/'addon.gproj').read_text()
project_text = (root/'addons/core/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)]
@@ -411,7 +411,7 @@ def vanilla_evidence(content):
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]'))
successor = any(token in content for token in ('ERR ORDINARY:', 'ERR SMOKE ')) or bool(re.search(r'Re(?:re)?introduced \[mission\]', content))
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,
@@ -433,10 +433,11 @@ def game_evidence(content, project=None):
header_verified = bool(headers) and (project is None or project in headers)
participants = re.findall(r'ERR ORDINARY: local participant verified project=(core|chat) phase=prison', content)
participant_verified = bool(participants) and (project is None or project in participants)
ready = header_verified and participant_verified and 'Rereintroduced [mission]: phase 0 -> 1' in content
# Accept the historical spelling when reviewing immutable older evidence.
ready = header_verified and participant_verified and bool(re.search(r'Re(?:re)?introduced \[mission\]: phase 0 -> 1', content))
unexpected_fixture = 'ERR SMOKE ' in content
failed = (bool(report['engine_errors'] or report['script_or_runtime_failures'])
or 'Rereintroduced: scenario preparation failed' in content
or bool(re.search(r'Re(?:re)?introduced: scenario preparation failed', content))
or 'Can\'t compile "Game"' in content or unexpected_fixture
or any({'WORKBENCH', 'ENF_WB'}.intersection(value.split(',')) for value in defines))
return {**report, 'local_participant_verified': participant_verified, 'scenario_header_verified': header_verified, 'verified_header_projects': headers, 'game_module_defines': defines, 'workbench_undefined': non_workbench,
@@ -1297,7 +1298,7 @@ def run_locked(a, steam, compat, profile, packed=None):
link = addons/'ArmaReforger'
if not link.exists() and not link.is_symlink():
link.symlink_to(vanilla, target_is_directory=True)
if packed and packed.get('schema_version') == 3:
if packed and packed.get('schema_version') == 4:
from packed_external import prepare_profile
prepare_profile(packed, steam, profile)
elif link.resolve() != vanilla.resolve() or any(x.name not in {'ArmaReforger', 'saves'} for x in addons.iterdir()) or any((addons/'saves').rglob('*.gproj')):
@@ -1469,8 +1470,8 @@ def main():
parser.add_argument('--project', choices=['core', 'chat', CONTROL], default='core')
packed_options = parser.add_mutually_exclusive_group()
packed_options.add_argument('--packed-core', type=Path, help='Unqualified Core candidate directory containing exactly addon.gproj, data.pak and resourceDatabase.rdb')
packed_options.add_argument('--packed-addons', type=Path, help='Unqualified complete core/ai/intel/map/chat packed directories; requires --project chat')
parser.add_argument('--external-addons', type=Path, help='Explicit DEVELOPMENT packed Chat v3 only: audited installed RHS/WCS/BMP root; six private links, no broad workshop mount')
packed_options.add_argument('--packed-addons', type=Path, help='Packed addon root; with --external-addons expects the single combined core folder (v4), otherwise historical five-addon v2; requires --project chat')
parser.add_argument('--external-addons', type=Path, help='Explicit DEVELOPMENT combined packed mod v4 only: audited installed RHS/WCS/BMP root; six private links, no broad workshop mount')
parser.add_argument('--packed-session', type=Path, help='New /tmp/erri-packed-core-<name> or erri-packed-chat-<name> session; owns a fresh input copy and profile')
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('--inspect-headless-display', action='store_true', help='Read-only ownership inspection within the hidden display; no input sent, loose Core/Chat only')
+4 -5
View File
@@ -25,7 +25,7 @@ p.add_argument('--flight-action-inputs', action='store_true', help='Experimental
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')
p.add_argument('--project', choices=['core', 'chat'], default='chat', help='Diagnostic fixture family (both load the combined mod)')
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:
@@ -61,9 +61,8 @@ if a.smoke == 'environment-restart':
if any((profile/name).exists() or (profile/name).is_symlink() for name in fixture_names): p.error('environment-restart fixture paths must not preexist; preserve or move prior evidence before retrying')
if compat.is_relative_to(steam) or not (compat/'pfx/system.reg').is_file():
p.error('--compat must be a disposable cloned prefix outside the Steam installation')
if a.external_addons and a.project != 'chat': p.error('External AI dependencies are not permitted in Core-only tests')
if a.external_addons and profile == Path('/tmp/erri-workbench-profile'): p.error('Use a separate external-dependency profile; preserve the vanilla test profile')
if a.project == 'chat' and not a.external_addons: p.error('Full Chat now requires --external-addons pointing to installed authorized RHS/WCS/BMP dependencies')
if not a.external_addons: p.error('The combined mod requires --external-addons pointing to installed authorized RHS/WCS/BMP dependencies')
prefix_lock = (compat/'.erri-workbench.lock').open('a')
try: fcntl.flock(prefix_lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
except BlockingIOError: p.error('Another test runner owns this disposable Wine prefix')
@@ -103,7 +102,7 @@ for proc in Path('/proc').iterdir():
if profile_arg.encode() in args and any(x.endswith(b'ArmaReforgerWorkbenchSteamDiag.exe') for x in args):
p.error('Another Workbench already uses this test profile')
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']
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/core/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.convoy_cover: command += ['-errConvoyCover', '1']
if a.field_service_cancel: command += ['-errSmokeFieldServiceCancel', '1']
if a.field_terrain_audit: command += ['-errSmokeFieldTerrainAudit', '1']
@@ -179,7 +178,7 @@ with (profile/'headless-launch.log').open('w') as output:
recorded_sessions.add(session)
console = session/'console.log'
content = console.read_text(errors='replace') if console.exists() else ''
if ('Rereintroduced: scenario preparation failed' in content and a.smoke not in {'startup-failure', 'extraction-startup-failure', 'extraction-boundary-failure'}) or 'ERR SMOKE FAIL:' in content or 'Virtual Machine Exception' in content or 'Can\'t compile "Game"' in content:
if ('Reintroduced: scenario preparation failed' in content and a.smoke not in {'startup-failure', 'extraction-startup-failure', 'extraction-boundary-failure'}) or 'ERR SMOKE FAIL:' in content or 'Virtual Machine Exception' in content or 'Can\'t compile "Game"' in content:
result = 'test-failed'; break
if content.count('ERR SMOKE COMPLETE:') >= a.rounds:
if complete_at is None: complete_at = time.time()
+2 -2
View File
@@ -18,7 +18,7 @@ if a.execute and a.mode=='workbench' and not a.visible_output:
p.error('Workbench opens windows. Select/configure a spare display and supply --visible-output first.')
command=[str(a.exe),'-addonsDir',str(ROOT/'addons'),'-profile',str(a.profile),'-logconsole']
if a.mode=='workbench':
command+=['-gproj',str(ROOT/'addons'/a.project/'addon.gproj'),'-wbModule=WorldEditor','-load','Worlds/ERR_Everon.ent']
command+=['-gproj',str(ROOT/'addons/core/addon.gproj'),'-wbModule=WorldEditor','-load','Worlds/ERR_Everon.ent']
if a.smoke:command+=['-plugin','ERR_ValidateWorldPlugin','-errPlay','1','-errSmoke','1','-errSmokeOutcome',a.smoke]
if a.project=='core' and a.smoke:
if a.smoke not in {'victory', 'cache-loss', 'startup-failure', 'site-survey', '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', 'environment', 'patrols'}:p.error('Unsupported Core-only fixture')
@@ -26,7 +26,7 @@ if a.mode=='workbench':
command[command.index('-errSmoke')]='-errBaseSmoke'
else:
if 'server' not in a.exe.name.lower():p.error('Use an actual dedicated server binary for background tests.')
command+=['-addons',{'core':'ERR_Core','chat':'ERR_Chat'}[a.project],'-server','Worlds/ERR_Everon.ent','-maxFPS','60','-logStats','1000']
command+=['-addons','2528466DE220D91C','-server','Worlds/ERR_Everon.ent','-maxFPS','60','-logStats','1000']
print(json.dumps(command,indent=2))
if a.execute:
if not a.exe.is_file():p.error('Executable not found')
+10 -10
View File
@@ -59,11 +59,11 @@ def project_order(project):
def external_request(project, source, root):
needs_external = project in {'ai', 'chat'}
needs_external = project in {'core'}
if needs_external and source is None:
raise ValueError('AI and Chat builds require --external-addons with the audited installed RHS/WCS/BMP closure')
raise ValueError('Combined mod builds require --external-addons with the audited installed RHS/WCS/BMP closure')
if source is not None and not needs_external:
raise ValueError('Core, Intel and Map builds must use vanilla-only profiles; --external-addons is not permitted')
raise ValueError('Unknown project; only the combined core addon is supported')
if source is None:
return None
source = Path(source).resolve()
@@ -127,9 +127,9 @@ def verify_package_profile(profile, steam, records):
def expose_external(project, profile, steam, context):
records = {}
if project in {'ai', 'chat'}:
if project in {'core'}:
if context is None:
raise ValueError('AI and Chat native packaging require audited external inputs')
raise ValueError('Combined mod native packaging require audited external inputs')
verify_external(context)
records = prepare_profile(profile/'addons', context['source'], context['policy'], context['cache_path'])
if records != context['records']:
@@ -300,8 +300,8 @@ def quiet_streams(application_id, sink, compat):
def run_project(steam, compat, session, project, timeout, authored, silent_workbench=False, exit_after_init=False, *, external=None):
if (project in {'ai', 'chat'}) != (external is not None):
raise ValueError('Only AI/Chat package profiles require and permit audited external inputs')
if (project in {'core'}) != (external is not None):
raise ValueError('The combined addon requires audited external inputs')
command = command_for(steam, session, project, silent_workbench, exit_after_init)
proton = Path(command[command.index('--')+1])
executable = Path(command[command.index('--')+3])
@@ -506,7 +506,7 @@ def run(a, root=ROOT):
continue_on_failure = getattr(a, 'continue_on_failure', False)
stopped_reason = None
for project in order:
selected_external = external if project in {'ai', 'chat'} else None
selected_external = external if project in {'core'} else None
report = run_project(steam, compat, session, project, a.timeout, authored, silent_workbench, exit_after_init, external=selected_external)
reports.append(report)
# Diagnostic continuation never grants authority to reuse an unsafe
@@ -588,12 +588,12 @@ def run(a, root=ROOT):
def main(argv=None):
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--project', choices=PROJECTS, default='core', help='Build selected addon and its successor dependencies in order')
parser.add_argument('--project', choices=PROJECTS, default='core', help='Build the single combined addon')
parser.add_argument('--session', type=Path, required=True, help='New direct child of /tmp; never overwritten')
parser.add_argument('--compat', type=Path, required=True, help='Idle disposable cloned Tools prefix under /tmp')
parser.add_argument('--steam', type=Path, default=Path.home()/'.local/share/Steam')
parser.add_argument('--timeout', type=int, default=300, help='30..1800 seconds per addon')
parser.add_argument('--external-addons', type=Path, help='Required for AI/Chat: installed audited RHS/WCS/BMP root; only allowlisted links enter private profiles')
parser.add_argument('--external-addons', type=Path, help='Required for the combined mod: installed audited RHS/WCS/BMP root; only allowlisted links enter private profiles')
lifecycle = parser.add_mutually_exclusive_group()
lifecycle.add_argument('--silent-workbench', action='store_true', help='Opt-in documented -wbSilent experiment; packing compatibility still requires native evidence')
lifecycle.add_argument('--exit-after-init', action='store_true', help='Opt-in documented -exitAfterInit experiment; natural exit and all native error gates remain mandatory')
+15 -20
View File
@@ -1,4 +1,4 @@
"""Explicit v3 DEVELOPMENT packed Chat consumption; v1/v2 remain unchanged.
"""Explicit v4 DEVELOPMENT combined-mod consumption; v1/v2 remain unchanged.
Only the audited installed external closure is linked, never copied or published.
"""
import hashlib
@@ -11,13 +11,8 @@ import uuid
import external_addons as external
from project_schema import parse_project
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')),
}
from source_bundle import PROJECTS
FILES = {'addon.gproj','data.pak','resourceDatabase.rdb'}
MANIFEST = 'packed-input-provenance.json'
@@ -35,8 +30,8 @@ def digest(path):
def inventory(folder):
folder=Path(folder)
require(folder.is_dir() and not folder.is_symlink() and folder==folder.resolve(),'Packed v3 closure must be a canonical real directory')
require({p.name for p in folder.iterdir()}==set(PROJECTS),'Packed v3 requires exactly five successor addon folders')
require(folder.is_dir() and not folder.is_symlink() and folder==folder.resolve(),'Packed v4 closure must be a canonical real directory')
require({p.name for p in folder.iterdir()}==set(PROJECTS),'Packed v4 requires exactly the combined core addon folder')
records={}
for key,(name,guid,deps) in PROJECTS.items():
addon=folder/key
@@ -51,16 +46,16 @@ def inventory(folder):
parsed=parse_project(text,allow_core_menu=key=='core')
actual=re.findall(r'"([A-F0-9]{16})"',parsed[4]) if parsed else []
expected={'58D0FB3206B6F859',*(PROJECTS[d][1] for d in deps),*external.direct_for(key)}
require(parsed and parsed[1]==name and parsed[2]==guid and set(actual)==expected and len(actual)==len(expected),'Packed v3 exact project identity/dependency mismatch: '+key)
require(parsed and parsed[1]==name and parsed[2]==guid and set(actual)==expected and len(actual)==len(expected),'Packed v4 exact project identity/dependency mismatch: '+key)
return records
def request(args,root):
require(args.project=='chat' and getattr(args,'packed_addons',None) is not None and getattr(args,'packed_core',None) is None,'--external-addons requires --project chat --packed-addons (explicit packed v3)')
require(args.profile==Path('/tmp/erri-game-profile') and args.settle==15,'Packed v3 owns a new private profile and requires 15-second settling')
require(args.project=='chat' and getattr(args,'packed_addons',None) is not None and getattr(args,'packed_core',None) is None,'--external-addons requires --project chat --packed-addons (explicit packed v4)')
require(args.profile==Path('/tmp/erri-game-profile') and args.settle==15,'Packed v4 owns a new private profile and requires 15-second settling')
session=getattr(args,'packed_session',None)
require(session is not None,'Packed v3 requires --packed-session')
require(session is not None,'Packed v4 requires --packed-session')
session=Path(session)
require(session.is_absolute() and session.parent==Path('/tmp') and session==session.resolve() and re.fullmatch(r'erri-packed-chat-[A-Za-z0-9_-]+',session.name) and not session.exists() and not session.is_symlink(),'Packed v3 requires a new canonical /tmp/erri-packed-chat-<name> session')
require(session.is_absolute() and session.parent==Path('/tmp') and session==session.resolve() and re.fullmatch(r'erri-packed-chat-[A-Za-z0-9_-]+',session.name) and not session.exists() and not session.is_symlink(),'Packed v4 requires a new canonical /tmp/erri-packed-chat-<name> session')
candidate=Path(args.packed_addons);artifacts=inventory(candidate)
policy_path=Path(root)/'docs/external-addons.json';policy_sha=digest(policy_path);rules=external.policy(root)
require(digest(policy_path)==policy_sha,'External policy changed during parsing')
@@ -72,7 +67,7 @@ def request(args,root):
match=re.search(r'\bID\s+"([A-Za-z0-9_-]+)"',data.decode())
require(match and match[1] not in {'core','ArmaReforger',*(p[0] for p in PROJECTS.values()),*names.values()},'Invalid/duplicate external project mount name')
names[guid]=match[1]
return {'schema_version':3,'project':'chat','candidate':candidate,'session':session,'input':session/'input/addons','manifest':artifacts,'external_source':source,'policy_path':policy_path,'policy_sha256':policy_sha,'policy':rules,'external_records':records,'external_names':names,'external_cache':cache}
return {'schema_version':4,'project':'chat','candidate':candidate,'session':session,'input':session/'input/addons','manifest':artifacts,'external_source':source,'policy_path':policy_path,'policy_sha256':policy_sha,'policy':rules,'external_records':records,'external_names':names,'external_cache':cache}
def verify_external(packed,profile=None):
require(digest(packed['policy_path'])==packed['policy_sha256'],'External policy changed')
@@ -121,14 +116,14 @@ def prepare_profile(packed,steam,profile):
def snapshot(packed,command,profile):
verify_external(packed,profile)
require(inventory(packed['input'])==packed['manifest'] and inventory(packed['candidate'])==packed['manifest'],'Packed inputs changed before launch')
value={'schema_version':3,'kind':'packed-chat-external-runtime-input','project':'chat','addon_order':list(PROJECTS),'launch_id':str(uuid.uuid4()),'captured_unix':time.time(),'candidate_directory':str(packed['candidate']),'runtime_directory':str(packed['input']),'profile':str(profile),'command':list(command),'artifacts':packed['manifest'],'external_policy_sha256':packed['policy_sha256'],'external_policy':packed['policy'],'external_addons':packed['external_records'],'external_project_names':packed['external_names'],'build_qualification':'DEVELOPMENT unqualified candidate; original packaging verdict unchanged'}
value={'schema_version':4,'kind':'packed-chat-external-runtime-input','project':'chat','addon_order':list(PROJECTS),'launch_id':str(uuid.uuid4()),'captured_unix':time.time(),'candidate_directory':str(packed['candidate']),'runtime_directory':str(packed['input']),'profile':str(profile),'command':list(command),'artifacts':packed['manifest'],'external_policy_sha256':packed['policy_sha256'],'external_policy':packed['policy'],'external_addons':packed['external_records'],'external_project_names':packed['external_names'],'build_qualification':'DEVELOPMENT unqualified candidate; original packaging verdict unchanged'}
raw=(json.dumps(value,indent=2)+'\n').encode();(packed['session']/MANIFEST).write_bytes(raw)
return raw
def seal(packed,raw,session):
value=json.loads(raw);unchanged=False;error=None
try:
require(value['schema_version']==3 and value['kind']=='packed-chat-external-runtime-input','Wrong packed v3 provenance schema')
require(value['schema_version']==4 and value['kind']=='packed-chat-external-runtime-input','Wrong packed v4 provenance schema')
require(value['external_addons']==packed['external_records'] and value['external_policy_sha256']==packed['policy_sha256'] and value['external_policy']==packed['policy'] and value['external_project_names']==packed['external_names'],'External provenance mismatch')
verify_external(packed,packed['profile'])
require(inventory(packed['input'])==value['artifacts'] and inventory(packed['candidate'])==value['artifacts'],'Packed inputs changed after launch')
@@ -137,7 +132,7 @@ def seal(packed,raw,session):
unchanged=True
except (OSError,ValueError,KeyError) as caught:error=str(caught)
console=session/'console.log' if session else None
return {'packed_only':True,'packed_schema_version':3,'packed_input_provenance':MANIFEST,'packed_input_provenance_sha256':hashlib.sha256(raw).hexdigest(),'packed_launch_id':value['launch_id'],'stopped_unix':time.time(),'packed_inputs_unchanged_after_run':unchanged,'packed_external_inputs_unchanged_after_run':unchanged,'packed_input_error':error,'external_addon_count':len(packed['external_records']),'console_sha256':digest(console) if console and console.is_file() else None,'build_qualified':False,'release_qualified':False}
return {'packed_only':True,'packed_schema_version':4,'packed_input_provenance':MANIFEST,'packed_input_provenance_sha256':hashlib.sha256(raw).hexdigest(),'packed_launch_id':value['launch_id'],'stopped_unix':time.time(),'packed_inputs_unchanged_after_run':unchanged,'packed_external_inputs_unchanged_after_run':unchanged,'packed_input_error':error,'external_addon_count':len(packed['external_records']),'console_sha256':digest(console) if console and console.is_file() else None,'build_qualified':False,'release_qualified':False}
def evidence(content,packed,steam,profile,legacy):
report=legacy.game_evidence(content,'chat');game=steam/'steamapps/common/Arma Reforger'
@@ -188,4 +183,4 @@ def evidence(content,packed,steam,profile,legacy):
if any({'WORKBENCH','ENF_WB'}.intersection(flags.split(',')) for _,flags in modules):violations.append('Workbench defines in packed ordinary runtime')
path_proof=complete and mounted==set(mounts) and loaded_databases==set(databases) and bool(dirs)
failed=report['failed'] or bool(violations)
return {**report,'packed_only':True,'packed_schema_version':3,'external_addon_count':len(packed['external_records']),'packed_loaded_paths_verified':path_proof,'packed_input_violations':violations,'packed_normal_modules':normal,'failed':failed,'ready':report['ready'] and path_proof and normal and not failed,'build_qualified':False,'release_qualified':False,'scope':'DEVELOPMENT packed Chat plus exact audited external closure; ordinary local startup only, no clean-build, human escape, multiplayer, FPS or release qualification'}
return {**report,'packed_only':True,'packed_schema_version':4,'external_addon_count':len(packed['external_records']),'packed_loaded_paths_verified':path_proof,'packed_input_violations':violations,'packed_normal_modules':normal,'failed':failed,'ready':report['ready'] and path_proof and normal and not failed,'build_qualified':False,'release_qualified':False,'scope':'DEVELOPMENT packed combined mod plus exact audited external closure; ordinary local startup only, no clean-build, human escape, multiplayer, FPS or release qualification'}
+36
View File
@@ -0,0 +1,36 @@
"""Check serialized root entity IDs required for reproducible prefab replication."""
import re
def root_id(text):
# Braces in resource strings and comments must not change object depth.
tokens = re.findall(r'//[^\n]*|/\*[\s\S]*?\*/|"(?:\\.|[^"\\])*"|[{}]|\b\w+\b', text)
tokens = [token for token in tokens if not token.startswith(('//', '/*'))]
depth = 0
identities = []
for index, token in enumerate(tokens):
if token == '{':
depth += 1
elif token == '}':
depth -= 1
elif token == 'ID' and depth == 1:
identities.append(tokens[index + 1] if index + 1 < len(tokens) else '')
if len(identities) != 1 or not re.fullmatch(r'"[A-F0-9]{16}"', identities[0]):
raise ValueError('Prefab needs exactly one saved root entity ID')
identity = identities[0][1:-1]
if identity == '0000000000000000':
raise ValueError('Prefab root entity ID must not be zero')
return identity
def check_prefabs(paths):
seen = {}
for path in sorted(paths):
try:
identity = root_id(path.read_text())
except ValueError as error:
raise ValueError(f'{path}: {error}') from error
if identity in seen:
raise ValueError(f'Duplicate prefab root ID {identity}: {seen[identity]} and {path}')
seen[identity] = path
return len(seen)
+16 -24
View File
@@ -21,37 +21,29 @@ 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')),
}
PROJECTS = {'core': ('ERR_Core', '2528466DE220D91C', ())}
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
README = '''# DEVELOPMENT SOURCE BUNDLE — Escapists Reintroduced
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, installed vanilla or third-party assets are redistributed. Vanilla asset
references require a separately installed, compatible Arma Reforger and Workbench.
Author: Jakub Jim Žáček.
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.
This ZIP contains the single Escapists Reintroduced addon, with Core, AI,
Intel, Map and Chat script folders. It is NOT a native .pak or a published mod.
Third-party assets are not redistributed.
Core scenario: {AD62322D5213C440}Missions/ERR_Everon_Base.conf
Companion scenario: {C1E20667DCE324ED}Missions/ERR_Everon.conf
Extract into a NEW development folder and register addons/core/addon.gproj in
Workbench. Install and register the RHS/WCS dependencies in EXTERNAL-ADDONS.json.
Open addons/core/addon.gproj and use Missions/ERR_Everon.conf.
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: Everon, up to 12 players; Core is vanilla-only and full AI requires the listed external addons. This remains development source.
Target: Everon, up to 12 players. The mod requires the listed external addons. 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
@@ -124,7 +116,7 @@ def validate(payload, root):
external = external_policy(root)
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')
raise ValueError('successor identity catalog differs from the single owned project')
for name, (identity, guid, deps) in PROJECTS.items():
text = payload.get(f'addons/{name}/addon.gproj', b'').decode('utf-8')
match = parse_project(text, allow_core_menu=name == 'core')
@@ -158,7 +150,7 @@ def validate(payload, root):
raise ValueError(f'resource lacks successor metadata: {name}')
owner = name.split('/')[1]
visible = {owner, *PROJECTS[owner][2]}
if 'ai' in visible: visible.update(ALLOWED)
if 'core' in visible: visible.update(ALLOWED)
for guid, resource in re.findall(r'\{([A-F0-9]{16})\}([^"\n]+)', text):
if guid in resources:
path, target = resources[guid]
@@ -188,7 +180,7 @@ def build(root=ROOT, mode='development'):
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 + '\nThe AI/full scenario requires the separately installed authorized addons listed in EXTERNAL-ADDONS.json. Their assets are not bundled. Core remains vanilla-only.\n').encode('utf-8')
payload['BUNDLE-README.md'] = (README + '\nThe combined scenario requires the separately installed authorized addons listed in EXTERNAL-ADDONS.json. Their assets are not bundled.\n').encode('utf-8')
payload['EXTERNAL-ADDONS.json'] = json.dumps(external_policy(root),indent=2,sort_keys=True).encode('utf-8')
manifest = {'schema_version': 1, 'kind': 'DEVELOPMENT SOURCE BUNDLE',
'native_build': False, 'release_package': False, 'projects': projects,