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>
169 lines
7.0 KiB
Python
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))
|