#!/usr/bin/env python3 """Registry edits — add an asset's name to a consumer's hardcoded list. Most GODVERSE consumers do NOT scan a directory. They load from a literal array in source (djsim's PED_NAMES, thriftgod's HERO_FLOOR) or from a JSON manifest, so dropping a file in the right folder achieves nothing until its name appears in that list. This module makes that edit. It is deliberately paranoid, because the targets are live shipping games: · dry-run by default — you see the exact new line before anything is written · idempotent — an entry already present is a no-op, never a duplicate · backs up to .bak- before writing · refuses if the anchor pattern does not match EXACTLY once, rather than guessing which of several arrays was meant · works on local files and, over ssh, on remote ones (djsim lives on ultra) registry.py show registry.py add [--write] """ import json, os, re, shlex, subprocess, sys, time HOME = os.path.expanduser('~') # Every entry below was read out of the consumer's real loader, not its docs. `array` is the # literal declaration to append into; `both` flags a consumer with TWO live copies of the same # list, where updating one and not the other is a silent half-fix. TARGETS = { 'djsim-ped': { 'host': 'johnking@100.91.239.7', 'files': [ {'path': 'Documents/90sDJsim/web/world/crowd/rigs.js', 'anchor': r'(const\s+PED_NAMES\s*=\s*\{\s*normal:\s*\[)', 'note': 'the array that actually owns the street crowd'}, {'path': 'Documents/90sDJsim/web/world/index.html', 'anchor': r'(const\s+PED_NAMES\s*=\s*\{\s*normal:\s*\[)', 'note': 'second live copy — djsim keeps two'}, ], 'quote': "'", 'both': True, }, 'thriftgod-prop': { 'host': None, 'files': [ {'path': 'Documents/thriftgod/web/index.html', 'anchor': r"(const\s+HERO_FLOOR\s*=\s*\[)", 'note': 'floor props; HERO_COUNTER is the counter-top equivalent'}, ], 'quote': "'", 'both': False, }, 'procity-ped': { 'host': None, 'files': [ # citizens/, not crowd/ — procity and 90sDJsim diverged here despite sharing the # loadPedFleet shape, and the declaration is `export const` {'path': 'Documents/PROCITY/web/js/citizens/rigs.js', 'anchor': r'(export\s+const\s+PED_NAMES\s*=\s*\{\s*normal:\s*\[)', 'note': 'loadPedFleet fills fixed slots BY INDEX — appending is safe, reordering is not'}, ], 'quote': "'", 'both': False, }, 'not-tonight': { 'host': None, 'json': True, 'files': [{'path': 'Documents/not-tonight/public/props/manifest.json', 'note': 'Phaser preloads only what this manifest names'}], }, } def _close_bracket(src, open_idx): """Index of the ']' closing the '[' at open_idx, skipping brackets inside strings.""" depth, i, n = 0, open_idx, len(src) quote = None while i < n: c = src[i] if quote: if c == '\\': i += 2 continue if c == quote: quote = None elif c in '"\'': quote = c elif c == '[': depth += 1 elif c == ']': depth -= 1 if depth == 0: return i i += 1 return -1 def _run(host, argv): if host: return subprocess.run(['ssh', '-o', 'BatchMode=yes', '-o', 'ConnectTimeout=10', host, ' '.join(shlex.quote(a) for a in argv)], capture_output=True, text=True, timeout=120) return subprocess.run(argv, capture_output=True, text=True, timeout=120) def read(host, path): r = _run(host, ['cat', path if host else os.path.join(HOME, path)]) if r.returncode != 0: raise RuntimeError(f'cannot read {path}: {r.stderr.strip()[:120]}') return r.stdout def write(host, path, text): stamp = time.strftime('%Y%m%d-%H%M%S') full = path if host else os.path.join(HOME, path) b = _run(host, ['cp', full, f'{full}.bak-{stamp}']) if b.returncode != 0: raise RuntimeError(f'backup failed for {path}: {b.stderr.strip()[:120]}') if host: p = subprocess.run(['ssh', '-o', 'BatchMode=yes', host, f'cat > {shlex.quote(full)}'], input=text, capture_output=True, text=True, timeout=120) if p.returncode != 0: raise RuntimeError(p.stderr.strip()[:160]) else: open(full, 'w').write(text) return f'{full}.bak-{stamp}' def add(target, name, do_write=False): if target not in TARGETS: raise SystemExit(f'unknown target {target!r} (want {list(TARGETS)})') t = TARGETS[target] results = [] for f in t['files']: src = read(t['host'], f['path']) if t.get('json'): data = json.loads(src) items = data if isinstance(data, list) else data.get('props', data.get('items', [])) if name in items: results.append({'file': f['path'], 'status': 'already present'}) continue items.append(name) new = json.dumps(data, indent=2) else: q = t['quote'] if re.search(re.escape(q + name + q), src): results.append({'file': f['path'], 'status': 'already present'}) continue hits = list(re.finditer(f['anchor'], src)) if len(hits) != 1: # Guessing which array was meant is how you corrupt a shipping game. results.append({'file': f['path'], 'status': f'ANCHOR MATCHED {len(hits)}x — refusing'}) continue close = _close_bracket(src, hits[0].end() - 1) if close < 0: results.append({'file': f['path'], 'status': 'unbalanced array — refusing'}) continue # APPEND, never prepend. procity's loadPedFleet fills fixed slots by PED_NAMES index, # so inserting at the front silently reassigns every existing character. new = src[:close] + f', {q}{name}{q}' + src[close:] results.append({'file': f['path'], 'status': 'would add' if not do_write else 'added', 'note': f.get('note', ''), 'backup': write(t['host'], f['path'], new) if do_write else None}) return {'target': target, 'name': name, 'host': t['host'] or 'local', 'wrote': do_write, 'both_copies': t.get('both', False), 'results': results} if __name__ == '__main__': cmd = sys.argv[1] if len(sys.argv) > 1 else 'show' if cmd == 'show': print(json.dumps({k: {'host': v['host'] or 'local', 'files': [f['path'] for f in v['files']]} for k, v in TARGETS.items()}, indent=1)) elif cmd == 'add': print(json.dumps(add(sys.argv[2], sys.argv[3], '--write' in sys.argv), indent=1))