"""Shared paths, ownership checks, and typed arguments. No console transport.""" import datetime import hashlib import json import shutil import subprocess import sys from pathlib import Path ROOT=Path(__file__).resolve().parent.parent from . import game_paths as manage # Compatibility name for existing Python clients. STATE_ROOT=manage.STATE_ROOT ARTIFACTS=STATE_ROOT/'artifacts' COMMANDS={'hello','snapshot','pause','resume','timescale','teleport','spawn','cleanup','events','saves','load','exit'} def run(*args, timeout=10): p = subprocess.run(args, capture_output=True, text=True, timeout=timeout) if p.returncode: raise RuntimeError(f'{args[0]} failed: {p.stderr.strip()[:1000]}') return p.stdout.strip() def now(): return datetime.datetime.now(datetime.timezone.utc).isoformat() def atomic_json(path, value): temp = path.with_suffix('.tmp') temp.write_text(json.dumps(value, indent=2) + '\n') temp.chmod(0o600) temp.replace(path) def process_identity(pid): p = Path('/proc') / str(pid) try: argv = (p / 'cmdline').read_bytes().split(b'\0')[0].decode(errors='replace') if argv.replace('\\', '/').rsplit('/', 1)[-1].lower() != 'witcher3.exe': return None start = (p / 'stat').read_text().rsplit(')', 1)[1].split()[19] return {'pid': int(pid), 'start': start, 'executable': argv} except (OSError, IndexError): return None def process(): found = [identity for p in Path('/proc').iterdir() if p.name.isdigit() if (identity := process_identity(p.name))] if len(found) != 1: raise RuntimeError(f'Expected one Witcher process, found {len(found)}') return found[0] def window(identity, compiler=False): if process_identity(identity['pid']) != identity: raise RuntimeError('Game process identity changed') result = subprocess.run(['xdotool', 'search', '--pid', str(identity['pid'])], capture_output=True, text=True) found = [] for wid in result.stdout.split(): title = run('xdotool', 'getwindowname', wid) wanted = 'Script Compilation Errors' in title if compiler else title == 'The Witcher 3' if wanted: found.append(wid) if len(found) != 1: raise RuntimeError(f'Expected one matching game window, found {len(found)}') return found[0] def validate(command, args): # Compatibility entry point; the CLI and bridge now share one constraint table. from .arguments import validate as validate_arguments if command not in COMMANDS: raise ValueError('Unsupported bridge command') validate_arguments(command, args) def record(state, kind, data): folder = ARTIFACTS / state['session'] folder.mkdir(parents=True, exist_ok=True) with (folder / 'actions.jsonl').open('a') as f: f.write(json.dumps({'at': now(), 'kind': kind, 'data': data}) + '\n') def bridge_source(root=None): root = root or ROOT return '\n'.join((root/'scripts'/name).read_text() for name in ( 'bridge.ws.in','observations.ws.in','encounters.ws.in','request_algorithm.ws.in','crossbow.ws.in','overlay.ws.in')) def deploy(): manage.ensure_game_closed() receipt_path = STATE_ROOT / 'deployment.json' receipt = json.loads(receipt_path.read_text()) target = Path(receipt['mod_path']) if manage.digest(target) != receipt['mod_sha256']: raise RuntimeError('Installed bridge has changed; refusing overwrite') source = bridge_source() build = hashlib.sha256(source.encode()).hexdigest()[:16] previous = ARTIFACTS / ('bridge-' + receipt['build'] + '.ws') if not previous.exists(): shutil.copy2(target, previous) target.write_text(source.replace('@@BUILD@@', build)) receipt.update(build=build, mod_sha256=manage.digest(target)) atomic_json(receipt_path, receipt) return {'ok': True, 'build': build, 'compilation': 'unverified'}