"""Retain complete bounded ComicDessert/FiveDim repository captures offline. This does not unlock activities, fabricate initial progress or modify game saves. An incomplete capture fails rather than generating a misleading catalog. """ import argparse import hashlib import json import re from pathlib import Path def main(): parser = argparse.ArgumentParser(description=__doc__) inputs = parser.add_mutually_exclusive_group(required=True) inputs.add_argument('--log', type=Path) inputs.add_argument('--evidence', type=Path, help='Complete exact flat-getter Frida evidence') parser.add_argument('--assembly-sha256', required=True) parser.add_argument('--output', type=Path, required=True) args = parser.parse_args() if not re.fullmatch(r'[0-9a-f]{64}', args.assembly_sha256): raise SystemExit('An exact verified assembly SHA256 is required') raw = (args.evidence or args.log).read_bytes() if args.evidence: evidence = json.loads(raw) observed = evidence['normalized_result'] if observed.get('messages_truncated'): raise SystemExit('Truncated evidence cannot produce a catalog') tables, counts, complete, scripts = {}, {}, set(), set() ended = False for message in observed['messages']: value = message.get('payload', {}) if value.get('kind') == 'ATTACHED' and value.get('scope', '').startswith('two exact lazy flat getters'): scripts.add(message['script_id']) if message.get('script_id') not in scripts: continue kind, name = value.get('kind'), value.get('table') if kind == 'ERROR': raise SystemExit('Flat capture failed: ' + value.get('error', 'unknown')) if kind == 'BEGIN': if name in tables or name not in ('cycle', 'level') or not 0 < value['count'] <= 256: raise SystemExit('Unexpected or empty table') tables[name], counts[name] = [], value['count'] elif kind == 'ROWS': if name not in tables or name in complete: raise SystemExit('Rows outside capture boundaries') tables[name].extend(json.loads(row) for row in value['rows']) elif kind == 'TABLE_END': if name not in tables or len(tables[name]) != counts[name] or value['count'] != counts[name]: raise SystemExit('Table count mismatch') complete.add(name) elif kind == 'END': ended = True if len(scripts) != 1 or not ended or complete != {'cycle', 'level'}: raise SystemExit('Complete single flat capture required') level_ids = {row['ID'] for row in tables['level']} if len(level_ids) != len(tables['level']) or len({r['ID'] for r in tables['cycle']}) != len(tables['cycle']): raise SystemExit('Duplicate configuration IDs') if any(level not in level_ids for row in tables['cycle'] for level in row['LevelIDList']): raise SystemExit('Cycle references missing levels') result = { 'format_version': 1, 'client_assembly_sha256': args.assembly_sha256, 'evidence_id': evidence['evidence_id'], 'source': 'Current named ComicDessert CycleRow/LevelRow FromBinary -> dictionary Add -> lazy flat GetAll chains; no gameplay or scene factories', 'capture_sha256': hashlib.sha256(raw).hexdigest(), 'cycles': tables['cycle'], 'levels': tables['level'], } args.output.parent.mkdir(parents=True, exist_ok=True) args.output.write_text(json.dumps(result, ensure_ascii=False, indent=2) + '\n', encoding='utf-8') print(json.dumps({'cycles': len(tables['cycle']), 'levels': len(tables['level']), 'output': str(args.output)})) return marker = '[GAC_COMIC_STELLA_CONFIG] ' repositories, expected, completed, configs = {}, {}, set(), {} ended = False for line in raw.decode('utf-8', errors='replace').splitlines(): if marker not in line: continue value = line.split(marker, 1)[1] if value.startswith('ERROR '): raise SystemExit('Capture stopped: ' + value) if value.startswith('BEGIN '): match = re.fullmatch(r'BEGIN (\w+) count=(\d+)', value) if not match or match[1] in repositories: raise SystemExit('Repeated/incompatible capture: use a single run') name, count = match[1], int(match[2]) if count > 256: raise SystemExit('Repository outside capture bound') repositories[name], expected[name] = [], count elif value.startswith('ROW '): _, name, body = value.split(' ', 2) if name not in repositories or name in completed: raise SystemExit('Repository row outside its bounded run') repositories[name].append(json.loads(body)) elif value.startswith('END_REPOSITORY '): match = re.fullmatch(r'END_REPOSITORY (\w+) rows=(\d+)', value) if not match or match[1] not in repositories: raise SystemExit('Unexpected repository completion') name = match[1] if len(repositories[name]) != int(match[2]) or len(repositories[name]) != expected[name]: raise SystemExit('Repository row count mismatch: ' + name) completed.add(name) elif value.startswith('FIVEDIM_CONFIG '): _, name, body = value.split(' ', 2) if name in configs: raise SystemExit('Duplicate FiveDim config capture: ' + name) configs[name] = json.loads(body) elif value == 'END': ended = True required = {'CycleConfigRepository', 'LevelConfigRepository'} if not ended or completed != required or not all(repositories.get(name) for name in required): raise SystemExit('Complete nonempty cycle and level repositories are required') result = { 'format_version': 1, 'client_assembly_sha256': args.assembly_sha256, 'source': 'Read-only existing ComicDessert repositories and selected FiveDim config; no gameplay method invocation', 'capture_sha256': hashlib.sha256(raw).hexdigest(), 'repositories': repositories, 'five_dim_config': configs, } args.output.parent.mkdir(parents=True, exist_ok=True) args.output.write_text(json.dumps(result, ensure_ascii=False, indent=2) + '\n', encoding='utf-8') print(json.dumps({'output': str(args.output), 'rows': {k: len(v) for k, v in repositories.items()}, 'five_dim_config': sorted(configs)}, ensure_ascii=False)) if __name__ == '__main__': main()