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.
164 lines
9.1 KiB
Python
164 lines
9.1 KiB
Python
#!/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' / 'core'
|
|
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': 'Reintroduced 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())
|