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>
This commit is contained in:
parent
cc84911a9d
commit
7c5fc53224
26
server.py
26
server.py
@ -110,6 +110,11 @@ try: # the 2D paper-doll tier (PIL + numpy); 3D still
|
||||
except Exception as _e:
|
||||
DOLL = None
|
||||
print(f'2D doll tier off ({_e.__class__.__name__}: {_e}) — pip install pillow numpy to enable')
|
||||
try: # registry edits into consumers that load from hardcoded lists
|
||||
import registry as REG
|
||||
except Exception as _e:
|
||||
REG = None
|
||||
print(f'registry edits off ({_e.__class__.__name__}: {_e})')
|
||||
|
||||
JOBS = {} # id → {status, note, out, log}
|
||||
MODEL_EXT = ('.glb', '.gltf', '.fbx', '.obj')
|
||||
@ -806,11 +811,22 @@ class H(BaseHTTPRequestHandler):
|
||||
t = EXPORT_TARGETS[tgt]
|
||||
if not t['ready']:
|
||||
# Verified against each consumer's real loader: these ingest by a HARDCODED array
|
||||
# or a manifest, so dropping a file does nothing at all. Saying so beats a
|
||||
# green tick over a no-op.
|
||||
return self.j({'ok': False, 'ready': False, 'target': tgt,
|
||||
'why': t['why'], 'registry': t.get('registry'),
|
||||
'note': 'not a file drop — the consumer needs a registry edit first'}, 409)
|
||||
# or a manifest, so dropping a file does nothing until its NAME is in that list.
|
||||
# We can make that edit — but it is source surgery on a live game, so it dry-runs
|
||||
# unless explicitly told to write.
|
||||
if not REG:
|
||||
return self.j({'ok': False, 'ready': False, 'target': tgt, 'why': t['why'],
|
||||
'registry': t.get('registry')}, 409)
|
||||
nm = body.get('name') or os.path.basename(p).rsplit('.', 1)[0]
|
||||
try:
|
||||
res = REG.add(tgt, nm, do_write=bool(body.get('write')))
|
||||
except Exception as e:
|
||||
return self.j({'error': str(e)[:300]}, 500)
|
||||
return self.j({'ok': True, 'ready': False, 'registry_edit': res,
|
||||
'why': t['why'],
|
||||
'note': ('written — back up files are alongside each source'
|
||||
if body.get('write') else
|
||||
'DRY RUN — re-send with write:true to actually edit the source')})
|
||||
if not allowed(p) or not os.path.isfile(p):
|
||||
return self.j({'error': 'path outside library'}, 403)
|
||||
if is_nsfw(p):
|
||||
|
||||
168
tools/registry.py
Normal file
168
tools/registry.py
Normal file
@ -0,0 +1,168 @@
|
||||
#!/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))
|
||||
Loading…
Reference in New Issue
Block a user