Advance standalone admission, finite artillery and native validation

This commit is contained in:
2026-09-19 09:02:31 +02:00
parent af5146a317
commit 40a474934c
532 changed files with 56456 additions and 199 deletions
+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_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_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_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']:
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()
+139
View File
@@ -0,0 +1,139 @@
#!/usr/bin/env python3
"""Bounded Linux server diagnostic: isolated network, frozen sources, no clients."""
import argparse
import fcntl
import hashlib
import json
import os
from pathlib import Path
import shutil
import signal
import subprocess
import time
PROBE = '''import json,os,socket,sys
state={"netns":os.readlink("/proc/self/ns/net"),"interfaces":socket.if_nameindex(),"routes":open("/proc/net/route").read(),"ipv6Routes":open("/proc/net/ipv6_route").read()}
print("ERR ISOLATION "+json.dumps(state),flush=True)
assert state["netns"] != sys.argv[1], "network namespace was not isolated"
assert [name for _,name in state["interfaces"]] == ["lo"], "non-loopback interface present"
assert len(state["routes"].splitlines()) == 1, "IPv4 route present"
assert all(line.split()[-1] == "lo" for line in state["ipv6Routes"].splitlines()), "non-loopback IPv6 route present"
if len(sys.argv)>2: os.execv(sys.argv[2],sys.argv[2:])
'''
def manifest(source):
result = {}
for path in sorted(source.rglob('*')):
if path.is_symlink():
raise ValueError('Linked source entry is not allowed: ' + str(path))
if path.is_file():
result[str(path.relative_to(source))] = hashlib.sha256(path.read_bytes()).hexdigest()
return result
def sandbox(runtime, source, run):
return ['bwrap', '--unshare-net', '--unshare-pid', '--unshare-ipc', '--unshare-uts',
'--die-with-parent', '--cap-drop', 'ALL', '--clearenv',
'--ro-bind', '/', '/', '--tmpfs', '/home', '--tmpfs', '/tmp', '--tmpfs', '/run',
'--proc', '/proc', '--dev', '/dev', '--ro-bind', str(runtime), '/tmp/runtime',
'--bind', str(source), '/tmp/source', '--bind', str(run), '/tmp/work',
'--setenv', 'HOME', '/tmp/work/home', '--setenv', 'TMPDIR', '/tmp/work/tmp',
'--setenv', 'XDG_CONFIG_HOME', '/tmp/work/home/.config',
'--setenv', 'XDG_DATA_HOME', '/tmp/work/home/.local/share',
'--setenv', 'XDG_CACHE_HOME', '/tmp/work/home/.cache',
'--setenv', 'PATH', '/usr/bin:/bin', '--setenv', 'LANG', 'C.UTF-8',
'--chdir', '/tmp/runtime']
def engine_args(route):
args = ['/tmp/runtime/ArmaReforgerServer' + ('Diag' if route == 'diag-world' else ''),
'-profile', '/tmp/work/profile', '-noBackend', '-noSound', '-noSplash', '-noThrow',
'-bindIP', '127.0.0.1', '-bindPort', '2001', '-maxFPS', '30', '-logStats', '5000']
if route == 'vanilla-everon':
return args + ['-server', 'Missions/21_GM_Eden.conf']
args += ['-addonsDir', '/tmp/source/addons', '-addons', '2528466DE220D91C']
if route == 'header':
return args + ['-server', '{AD62322D5213C440}Missions/ERR_Everon_Base.conf']
return args + ['-server', '{B6C516F86302C679}Worlds/ERR_Everon.ent',
'-worldSystemsConfig', '{C50579A2EC48E8EF}Configs/Systems/MissionSystems.conf']
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--source', type=Path, required=True)
parser.add_argument('--runtime', type=Path, default=Path('/tmp/erri-dedicated-runtime/server'))
parser.add_argument('--archive', type=Path, required=True)
parser.add_argument('--route', choices=['header', 'diag-world', 'vanilla-everon'], default='header')
parser.add_argument('--seconds', type=int, default=90)
args = parser.parse_args()
if not 1 <= args.seconds <= 110:
parser.error('Runtime bound must be 1..110 seconds (plus at most 10 seconds owned cleanup)')
source, runtime, archive = args.source.resolve(), args.runtime.resolve(), args.archive.resolve()
if not source.is_relative_to('/tmp') or not runtime.is_relative_to('/tmp') or not archive.is_relative_to('/tmp'):
parser.error('Sources, runtime and archive must be isolated /tmp paths')
if archive == source or source in archive.parents or archive in source.parents or archive == runtime or runtime in archive.parents or archive in runtime.parents:
parser.error('Source, archive and runtime must not overlap')
if archive.exists() or archive.is_symlink():
parser.error('Archive must be new')
origin = runtime.parent / 'runtime-origin.json'
if not origin.is_file() or json.loads(origin.read_text()).get('app_id') != 1874900:
parser.error('Stable private runtime origin missing')
binary = runtime / Path(engine_args(args.route)[0]).name
with binary.open('rb') as stream:
elf = stream.read(20)
if binary.is_symlink() or len(elf) != 20 or elf[:6] != b'\x7fELF\x02\x01' or int.from_bytes(elf[18:20], 'little') != 62:
parser.error('Expected real native x86-64 ELF server')
archive.mkdir(mode=0o700, parents=True)
(archive / 'runner.py').write_bytes(Path(__file__).read_bytes())
for child in ['home', 'profile', 'tmp']:
(archive / child).mkdir()
# Reject links before copytree follows them into the isolated source snapshot.
manifest(source)
frozen = archive / 'source'
shutil.copytree(source, frozen, ignore=shutil.ignore_patterns('.git', '__pycache__', 'evidence', 'generated'))
initial = manifest(frozen)
(archive / 'source-manifest.json').write_text(json.dumps(initial, indent=2)+'\n')
base = sandbox(runtime, frozen, archive)
netns = os.readlink('/proc/self/ns/net')
check = base + ['/usr/bin/python3', '-c', PROBE, netns]
preflight = subprocess.run(check, capture_output=True, text=True, timeout=15)
(archive / 'isolation-preflight.json').write_text(json.dumps({'command':check,'exit':preflight.returncode,'stdout':preflight.stdout,'stderr':preflight.stderr}, indent=2)+'\n')
if preflight.returncode:
print('Isolation refused; no server launched:', archive)
return 2
command = base + ['/usr/bin/python3', '-c', PROBE, netns] + engine_args(args.route)
record = {'command': command, 'engine': engine_args(args.route), 'seconds':args.seconds,
'binarySHA256':hashlib.sha256(binary.read_bytes()).hexdigest(),
'serverLaunched':False,'realPlayers':0,'multiplayerProven':False,
'scope':'vanilla Everon control; no successor loaded' if args.route == 'vanilla-everon' else 'successor empty-server startup'}
(archive / 'command.json').write_text(json.dumps(record, indent=2)+'\n')
lock_fd = os.open('/tmp/erri-dedicated-native-diagnostic.lock', os.O_WRONLY | os.O_CREAT | os.O_NOFOLLOW, 0o600)
with os.fdopen(lock_fd, 'a') as lock:
fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
with (archive / 'stdout.log').open('wb') as log:
process = subprocess.Popen(command, stdout=log, stderr=subprocess.STDOUT, start_new_session=True)
record['serverLaunched'] = True
record['supervisorPid'] = process.pid
record['startedUTC'] = time.strftime('%Y-%m-%dT%H:%M:%SZ',time.gmtime())
try:
record['exit'] = process.wait(timeout=args.seconds)
record['timeout'] = False
except subprocess.TimeoutExpired:
record['timeout'] = True
finally:
if process.poll() is None:
os.killpg(process.pid, signal.SIGTERM)
try: process.wait(timeout=5)
except subprocess.TimeoutExpired:
os.killpg(process.pid, signal.SIGKILL)
process.wait(timeout=5)
record['exit'] = process.returncode
record['finishedUTC'] = time.strftime('%Y-%m-%dT%H:%M:%SZ',time.gmtime())
(archive / 'result.json').write_text(json.dumps(record, indent=2)+'\n')
(archive / 'source-manifest-after.json').write_text(json.dumps(manifest(frozen), indent=2)+'\n')
print('Diagnostic archived; inspect actual compilation/header/systems/errors:',archive)
return 0
if __name__ == '__main__':
raise SystemExit(main())
+163
View File
@@ -0,0 +1,163 @@
#!/usr/bin/env python3
"""Prepare reviewable private dedicated-server inputs; never launch or download."""
import argparse
import hashlib
import json
import os
from pathlib import Path
import re
import shutil
from source_manifest import addon_manifest
APP_ID = '1874900'
HEADERS = {'core': 'Missions/ERR_Everon_Base.conf', 'chat': 'Missions/ERR_Everon.conf'}
SYSTEMS = '{C50579A2EC48E8EF}Configs/Systems/MissionSystems.conf'
WORLD = '{B6C516F86302C679}Worlds/ERR_Everon.ent'
def digest(path):
value = hashlib.sha256()
with path.open('rb') as stream:
for chunk in iter(lambda: stream.read(1024 * 1024), b''):
value.update(chunk)
return value.hexdigest()
def selected_scenario(source, project):
addon = source / 'addons' / project
text = (addon / 'addon.gproj').read_text()
guid = re.search(r'\bGUID "([A-F0-9]{16})"', text)
if not guid:
raise ValueError('Selected addon has no exact GUID')
relative = HEADERS[project]
header = addon / relative
meta = header.with_suffix('.conf.meta').read_text()
resource = re.search(r'\bName "(\{[A-F0-9]{16}\}[^"\n]+)"', meta)
if not resource or resource[1][18:] != relative:
raise ValueError('Selected header resource identity/path mismatch')
data = header.read_text()
for key, expected in [('World', WORLD), ('SystemsConfig', SYSTEMS)]:
match = re.search(r'\b' + key + r' "([^"\n]+)"', data)
if not match or match[1] != expected:
raise ValueError('Selected header has unexpected ' + key)
return {'project': project, 'addon_guid': guid[1], 'header': resource[1],
'world': WORLD, 'systems_config': SYSTEMS,
'addon_directory': str(source / 'addons')}
def inspect_runtime(runtime):
binary = runtime / 'server' / 'ArmaReforgerServer'
manifest = runtime / 'server' / 'steamapps' / ('appmanifest_' + APP_ID + '.acf')
result = {'runtime_directory': str(runtime), 'binary': str(binary),
'app_id': APP_ID, 'platform': 'linux', 'branch': 'public',
'binary_present': binary.is_file(), 'binary_elf_x86_64': False,
'binary_executable': False, 'steam_build_id': None,
'install_complete': False, 'engine_version': None,
'native_runtime_executed': False}
if manifest.is_file():
values = dict(re.findall(r'"(appid|buildid|StateFlags)"\s+"([^"]*)"', manifest.read_text()))
if values.get('appid') != APP_ID:
raise ValueError('Runtime manifest is not the stable dedicated-server app')
result.update(steam_build_id=values.get('buildid'),
install_complete=values.get('StateFlags') == '4',
manifest_sha256=digest(manifest))
if binary.is_file():
if binary.is_symlink():
raise ValueError('Private runtime binary must not be a symlink to another installation')
with binary.open('rb') as stream:
header = stream.read(20)
result['binary_elf_x86_64'] = len(header) == 20 and header[:6] == b'\x7fELF\x02\x01' and int.from_bytes(header[18:20], 'little') == 62
result['binary_executable'] = os.access(binary, os.X_OK)
result['binary_sha256'] = digest(binary)
return result
def build_plan(source, runtime, project='core', port=2001):
source, runtime = source.resolve(), runtime.resolve()
if not 1024 <= port <= 65535:
raise ValueError('Use an unprivileged port in 1024..65535')
if source == runtime or source in runtime.parents or runtime in source.parents:
raise ValueError('Runtime must be separate from the authored source tree')
manifest = addon_manifest(source)
scenario = selected_scenario(source, project)
installation = inspect_runtime(runtime)
config = {
'bindAddress': '127.0.0.1', 'bindPort': port,
'publicAddress': '127.0.0.1', 'publicPort': port,
'game': {'name': 'Rereintroduced private validation', 'scenarioId': scenario['header'],
'maxPlayers': 2, 'visible': False,
'gameProperties': {'fastValidation': True, 'battlEye': True}, 'mods': []},
'operating': {'disableCrashReporter': True},
}
blockers = []
if not (installation['install_complete'] and installation['binary_elf_x86_64'] and installation['binary_executable']):
blockers.append('Stable native Linux dedicated-server installation is missing or incomplete.')
blockers.extend([
'Server binary dependencies and exact engine-version compatibility have not been validated by a native run.',
'Unpublished local-addon loading with authenticated server configuration/header selection needs native verification; do not combine -config and -addons.',
'Loopback server configuration must be reviewed before any listener is started.',
'Two genuine accepted native client identities/controllers are not available as evidence; copied profiles and synthetic actors do not qualify.',
'Existing ordinary runner refuses concurrent game instances; it is not a multi-client supervisor.',
])
result = {'schema': 1, 'scope': 'no-launch dedicated preparation only', 'launch_allowed': False,
'listener_started': False, 'genuine_clients_verified': 0, 'multiplayer_passed': False,
'source_directory': str(source), 'scenario': scenario, 'runtime': installation,
'network': {'bind': '127.0.0.1', 'port': port, 'public_listing': False,
'rcon_configured': False, 'a2s_configured': False},
'source_manifest_sha256': hashlib.sha256((json.dumps(manifest, indent=2, sort_keys=True) + '\n').encode()).hexdigest(),
'disk_free_bytes': shutil.disk_usage(runtime if runtime.exists() else runtime.parent).free,
'blockers': blockers,
'evidence_required': [
'Server and both clients: exact version, role, source/addon manifest and complete sealed raw logs.',
'Server: native dedicated mode, selected header and actual MissionSystems config, two distinct accepted connections/current controlled prisoners.',
'Client1 and Client2: separate native ownership/local controller and actual replicated state; record no account credentials.',
'Both clients: JIP, same-account reconnect/reservation expiry, death/unconsciousness, spectator and extraction lifecycle.',
'No synthetic-client substitution and no multiplayer/FPS claim from headless startup alone.',
]}
return result, config, manifest
def write_plan(destination, result, config, manifest):
destination = destination.absolute()
runtime = Path(result['runtime']['runtime_directory'])
if destination.is_symlink() or destination.exists():
raise ValueError('Plan directory must be new; existing evidence is never overwritten')
if not destination.parent.is_dir() or destination.parent.resolve() != runtime:
raise ValueError('Plan directory must be a new direct child of the private runtime')
marker = runtime / 'runtime-origin.json'
if not marker.is_file() or marker.is_symlink():
raise ValueError('Private runtime origin marker is required before writing plans')
origin = json.loads(marker.read_text())
if origin.get('app_id') != int(APP_ID) or origin.get('purpose') != 'private anonymous Reforger dedicated server preparation; no server launched':
raise ValueError('Runtime origin marker is not the authorized private preparation')
destination.mkdir(mode=0o700)
for filename, value in [('preflight.json', result), ('server-config.review.json', config), ('source-manifest.json', manifest)]:
(destination / filename).write_text(json.dumps(value, indent=2, sort_keys=True) + '\n')
# A human-readable plan is deliberately not an executable launch script.
(destination / 'REVIEW.txt').write_text('NO SERVER STARTED. DO NOT LAUNCH BEFORE CONFIGURATION REVIEW.\n'
'server-config.review.json binds loopback and preserves native fast validation/BattlEye.\n'
'Local unpublished-addon/config compatibility, engine dependencies/version and two genuine clients remain unverified.\n'
'Do not add -addons to a -config command or treat this plan as runtime/MP evidence.\n')
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--source', type=Path, required=True)
parser.add_argument('--runtime', type=Path, default=Path('/tmp/erri-dedicated-runtime'))
parser.add_argument('--project', choices=HEADERS, default='core')
parser.add_argument('--port', type=int, default=2001)
parser.add_argument('--write-plan', type=Path, help='Write review files into a NEW direct child of the private runtime; never launch')
args = parser.parse_args()
try:
result, config, manifest = build_plan(args.source, args.runtime, args.project, args.port)
if args.write_plan:
write_plan(args.write_plan, result, config, manifest)
print(json.dumps(result, indent=2, sort_keys=True))
return 0 # success means inspection completed, never launch/MP qualification
except (OSError, ValueError) as error:
parser.error(str(error))
if __name__ == '__main__':
raise SystemExit(main())
+4 -2
View File
@@ -138,13 +138,15 @@ def game_evidence(content, project=None):
non_workbench = bool(defines) and all(not {'WORKBENCH', 'ENF_WB'}.intersection(value.split(',')) for value in defines)
headers = re.findall(r'ERR ORDINARY: header verified project=(core|chat) systems=MissionSystems', content)
header_verified = bool(headers) and (project is None or project in headers)
ready = header_verified and 'Rereintroduced [mission]: phase 0 -> 1' in content
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
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 'Can\'t compile "Game"' in content or unexpected_fixture
or any({'WORKBENCH', 'ENF_WB'}.intersection(value.split(',')) for value in defines))
return {**report, 'scenario_header_verified': header_verified, 'verified_header_projects': headers, 'game_module_defines': defines, 'workbench_undefined': non_workbench,
return {**report, 'local_participant_verified': participant_verified, 'scenario_header_verified': header_verified, 'verified_header_projects': headers, 'game_module_defines': defines, 'workbench_undefined': non_workbench,
'normal_mission_ready': ready, 'unexpected_smoke_fixture': unexpected_fixture,
'failed': failed, 'ready': non_workbench and ready and not failed,
'scope': 'ordinary local game startup only; no player input, full escape, multiplayer or FPS proof'}
+6 -3
View File
@@ -11,7 +11,7 @@ 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=['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=['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('--rounds', type=int, default=2)
p.add_argument('--timeout', type=int, default=360)
@@ -20,6 +20,9 @@ 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 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 == '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']
@@ -56,10 +59,10 @@ for proc in Path('/proc').iterdir():
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.project == 'core':
if a.smoke not in {'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 {'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 {'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 {'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'))