Integrate physical radio calls and finite vehicle maintenance

This commit is contained in:
2026-09-20 06:37:17 +02:00
parent 963cab50c0
commit 915476f709
152 changed files with 26426 additions and 28 deletions
+191
View File
@@ -0,0 +1,191 @@
"""Explicit v3 DEVELOPMENT packed Chat consumption; v1/v2 remain unchanged.
Only the audited installed external closure is linked, never copied or published.
"""
import hashlib
import json
from pathlib import Path
import re
import shutil
import time
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')),
}
FILES = {'addon.gproj','data.pak','resourceDatabase.rdb'}
MANIFEST = 'packed-input-provenance.json'
def require(condition, message):
if not condition: raise ValueError(message)
def digest(path):
require(path.is_file() and not path.is_symlink(),'Expected regular file: '+str(path))
before=path.stat(); h=hashlib.sha256()
with path.open('rb') as stream:
for block in iter(lambda:stream.read(1024*1024),b''): h.update(block)
after=path.stat()
require((before.st_dev,before.st_ino,before.st_size,before.st_mtime_ns,before.st_ctime_ns)==(after.st_dev,after.st_ino,after.st_size,after.st_mtime_ns,after.st_ctime_ns),'Input changed while hashing: '+str(path))
return h.hexdigest()
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')
records={}
for key,(name,guid,deps) in PROJECTS.items():
addon=folder/key
require(addon.is_dir() and not addon.is_symlink(),'Packed addon must be a real directory')
require({p.name for p in addon.iterdir()}==FILES,'Packed addon requires exactly three runtime files')
for filename in sorted(FILES):
path=addon/filename;sha=digest(path)
require(path.stat().st_size>0,'Empty packed input: '+str(path))
records[key+'/'+filename]={'bytes':path.stat().st_size,'sha256':sha}
descriptor=addon/'addon.gproj';text=descriptor.read_bytes().decode()
require(hashlib.sha256(text.encode()).hexdigest()==records[key+'/addon.gproj']['sha256'],'Project changed during parsing')
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)
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')
session=getattr(args,'packed_session',None)
require(session is not None,'Packed v3 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')
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')
cache={};source=Path(args.external_addons).resolve();records=external.installed(source,rules,cache)
names={}
for guid,record in records.items():
descriptor=Path(record['path'])/'addon.gproj';data=descriptor.read_bytes()
require(hashlib.sha256(data).hexdigest()==record['files']['addon.gproj']['sha256'],'External project changed while parsing its mount name')
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}
def verify_external(packed,profile=None):
require(digest(packed['policy_path'])==packed['policy_sha256'],'External policy changed')
cache=packed['external_cache']
if 'cache_path' in packed:
require(digest(packed['cache_path'])==packed['cache_sha256'],'External fingerprint cache changed')
cache=json.loads(packed['cache_path'].read_text())
require(external.installed(packed['external_source'],packed['policy'],cache)==packed['external_records'],'External packed inputs changed')
if profile is not None:
require(profile.is_dir() and not profile.is_symlink() and profile==profile.resolve(),'Private runtime profile redirected')
addons=profile/'addons'
require(addons.is_dir() and not addons.is_symlink(),'Private dependency directory changed')
expected={'ArmaReforger',*('ERRI_'+g for g in packed['external_records'])}
require({p.name for p in addons.iterdir()}-{'saves'}==expected,'Missing/unrelated private addon link')
require(not (addons/'saves').is_symlink(),'Redirected private saves directory')
if (addons/'saves').exists():
require((addons/'saves').is_dir() and not any((addons/'saves').rglob('*.gproj')),'Unexpected addon in private saves')
vanilla=addons/'ArmaReforger'
require(vanilla.is_symlink() and vanilla.resolve()==packed['vanilla'].resolve(),'Private vanilla link changed')
require(external.verify_profile(packed['external_records'],packed['external_source'],packed['policy'],packed['cache_path'],addons),'External private profile changed')
return True
def prepare(packed):
verify_external(packed)
require(inventory(packed['candidate'])==packed['manifest'],'Packed candidate changed before copy')
packed['session'].mkdir(exist_ok=False);packed['input'].mkdir(parents=True,exist_ok=False)
for name in packed['manifest']:
target=packed['input']/name;target.parent.mkdir(parents=True,exist_ok=True)
with (packed['candidate']/name).open('rb') as src,target.open('xb') as dst:shutil.copyfileobj(src,dst)
require(inventory(packed['input'])==packed['manifest'] and inventory(packed['candidate'])==packed['manifest'],'Packed input changed while copying')
packed['cache_path']=packed['session']/'external-source-cache.json'
packed['cache_path'].write_text(json.dumps(packed['external_cache'],sort_keys=True))
packed['cache_sha256']=digest(packed['cache_path'])
(packed['session']/'external-addons-policy.json').write_text(json.dumps(packed['policy'],sort_keys=True))
(packed['session']/'external-addons-inputs.json').write_text(json.dumps(packed['external_records'],sort_keys=True))
def prepare_profile(packed,steam,profile):
verify_external(packed)
packed['vanilla']=steam/'steamapps/common/Arma Reforger/addons/data'
records=external.prepare_profile(profile/'addons',packed['external_source'],packed['policy'],packed['cache_path'])
require(records==packed['external_records'],'External inputs changed while preparing private links')
packed['cache_sha256']=digest(packed['cache_path'])
packed['profile']=profile
verify_external(packed,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'}
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['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')
require((packed['session']/MANIFEST).read_bytes()==raw,'Session packed provenance changed')
if session:require((session/MANIFEST).read_bytes()==raw,'Native session packed provenance changed')
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}
def evidence(content,packed,steam,profile,legacy):
report=legacy.game_evidence(content,'chat');game=steam/'steamapps/common/Arma Reforger'
def normalized(path):return legacy.native_path(path,game)
# Permit only each exact private alias and its captured canonical target, never
# an arbitrary workshop path or generic symlink/path normalization.
paths={};mounts={};counts={};databases={}
def add(guid,name,folders,project_file,pak_count=None):
paths[guid]={str(p/project_file) for p in folders}
mounts[name]={str(p) for p in folders};databases[name]={str(p/'resourceDatabase.rdb') for p in folders};counts[name]=pak_count
add('5614BBCCBB55ED1C','core',[game/'addons/core'],'core.gproj')
add('58D0FB3206B6F859','ArmaReforger',[game/'addons/data',profile/'addons/ArmaReforger'],'ArmaReforger.gproj')
for key,(name,guid,_) in PROJECTS.items():add(guid,name,[packed['input']/key],'addon.gproj',1)
for guid,record in packed['external_records'].items():
add(guid,packed['external_names'][guid],[profile/'addons'/('ERRI_'+guid),Path(record['path'])],'addon.gproj',sum(name.endswith('.pak') for name in record['files']))
violations=[];complete=False
for block in re.findall(r"Loaded addons:\r?\n((?:[^\n]*ENGINE[^\n]*gproj:[^\n]*\n)+)",content):
entries=re.findall(r"gproj: '([^']+)' guid: '([0-9A-Fa-f]{16})'",block);seen=set()
if len(entries)!=len(block.splitlines()):violations.append('Malformed loaded-addon entry')
for path,guid in entries:
guid=guid.upper()
if guid in seen or guid not in paths or normalized(path) not in paths.get(guid,set()):violations.append('Unexpected/redirected/duplicate loaded addon: '+path+' '+guid)
seen.add(guid)
complete=seen==set(paths)
mounted=set()
for path,count,name in re.findall(r"FileSystem: Adding package '([^']+)' \(pak count: (\d+)\) to filesystem under name ([^\s]+)",content):
if name not in mounts or normalized(path) not in mounts.get(name,set()) or int(count)<1 or (counts.get(name) is not None and int(count)!=counts[name]):violations.append('Unexpected packed mount/count: '+name+' '+path)
else:mounted.add(name)
for path,name in re.findall(r"FileSystem: Adding relative directory '([^']+)' to filesystem under name ([^\s]+)",content):
resolved=normalized(path)
if name=='profile' and resolved==str(profile/'profile'):continue
if name=='logs' and re.fullmatch(re.escape(str(profile/'logs'))+r'/logs_[0-9_-]+',resolved):continue
violations.append('Unexpected loose directory mount: '+name+' '+path)
allowed_dirs={str(packed['input']),str(game/'addons'),str(profile/'addons')}
dirs=re.findall(r"ENGINE\s*:\s*dir: '([^']+)'",content)
for path in dirs:
resolved=str(profile/'addons') if path=='$profile:../addons/' else normalized(path)
if resolved not in allowed_dirs:violations.append('Unexpected addon search directory: '+path)
loaded_databases=set()
for name,path in re.findall(r'ResourceDB: loading cache \(id=\d+ name=([^\s]+) path=([^\r\n]+)\)',content):
resolved=normalized(path)
if name=='profile' and resolved==str(profile/'profile/resourceDatabase.rdb'):continue
if name=='logs' and re.fullmatch(re.escape(str(profile/'logs'))+r'/logs_[0-9_-]+/resourceDatabase\.rdb',resolved):continue
if name not in databases or resolved not in databases.get(name,set()):violations.append('Unexpected resource database: '+name+' '+path)
else:loaded_databases.add(name)
modules=re.findall(r'Module: (GameLib|Game);[^\n]*defines:\s*"([^"]*)"',content)
normal={name for name,_ in modules}=={'GameLib','Game'} and all(not {'WORKBENCH','ENF_WB'}.intersection(flags.split(',')) for _,flags in modules)
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'}