wardrobegod/tools/registry.py
type-two 7c5fc53224 Registry writer: make the four non-file-drop consumers one-click, and keep 3GOD as a depot
Most GODVERSE consumers do not scan a directory — they load from a literal array in source
(djsim/procity PED_NAMES, thriftgod HERO_FLOOR) or a JSON manifest, so a dropped file does
nothing until its NAME is in that list. tools/registry.py makes that edit, and the export hub
now offers it instead of only reporting the file:line.

Paranoid by construction, because the targets are live shipping games:
· dry-run by default; write:true is required to touch source
· idempotent — an existing entry is a no-op, verified against a real djsim ped
· backs up to <file>.bak-<stamp> before every write
· refuses if the anchor matches anything other than exactly once, rather than guessing which
  of several arrays was meant
· handles djsim keeping TWO live copies of PED_NAMES (rigs.js owns the street, index.html has
  a second) — updating one and not the other is a silent half-fix
· works locally and over ssh, since djsim lives on ultra

Bug I introduced and caught while testing: the first implementation inserted after the opening
bracket, i.e. PREPENDED. procity's loadPedFleet fills fixed slots BY PED_NAMES INDEX, so that
silently reassigns every existing character — and my own note on that target said "appending is
safe, reordering is not" three lines above the code that did it. Now scans to the matching
close bracket (string-aware) and appends. Verified: leading entries byte-identical, new name at
the tail, then reverted from backup with git clean.

Also corrected procity's path — its rigs.js is under web/js/citizens/, not crowd/, and declares
`export const`; the two games diverged despite sharing the loadPedFleet shape.

On 3GOD: NOT retired. It looked like a spent planning experiment, but thriftgod fetches its
props from it at runtime in production (index.html:1176 DEPOT = 'https://digalot.fyi/3god') and
procity does the same — 773 live assets. It is two things bolted together: a dumb asset CDN
(/a, /dl, /t, /api/list) which stays, and an authoring UI (upload/tag/thumb/fetch/meta) which
wardrobegod now supersedes. Absorb the bench, keep the depot, publish into it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 21:55:04 +10:00

169 lines
7.0 KiB
Python

#!/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 <file>.bak-<stamp> 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 <target>
registry.py add <target> <name> [--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))