658bda1d24
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
128 lines
5.4 KiB
Python
128 lines
5.4 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Rebuild the 7DTD cluster canonical .nim (re-anchor to season10's CURRENT state).
|
|
|
|
WHAT THIS DOES
|
|
Produces a complete, season10-anchored canonical block/item id map. Every id
|
|
the anchor (season10) currently uses is preserved BYTE-FOR-BYTE; every other
|
|
name from the "universe" (a prior complete canonical) is appended at a fresh,
|
|
non-colliding id above the anchor's max. The result is a superset that decodes
|
|
season10's world + all shared inventories identically, and is COMPLETE so the
|
|
engine never appends => the table stays frozen forever.
|
|
|
|
WHEN TO RUN
|
|
* season10's live .nim grew (players placed new shapes) and you need to
|
|
re-stamp it — re-anchor so the canonical preserves the new ids.
|
|
* You just want to regenerate the deployed canonical from the anchor.
|
|
NOTE: if MODS CHANGED (new blocks/items added), the universe won't contain the
|
|
new names. Re-harvest first — see CANONICAL_NIM_FREEZE.md "Rebuilding after a
|
|
mod change". This script only RE-ANCHORS an existing complete name set.
|
|
|
|
USAGE
|
|
1. STOP season10 (panel Stop) so its .nim is final.
|
|
2. Copy season10's CURRENT blockmappings.nim + itemmappings.nim out (the ANCHOR):
|
|
docker run --rm -v panel-rg-season-10-saves:/sv:ro -v /tmp/x:/o debian:12-slim \
|
|
sh -c 'cp "/sv/.local/share/7DaysToDie/Saves/West Apeeni Mountains/MyGame/blockmappings.nim" /o/anchor_block.nim;
|
|
cp "/sv/.local/share/7DaysToDie/Saves/West Apeeni Mountains/MyGame/itemmappings.nim" /o/anchor_item.nim'
|
|
3. python3 rebuild.py anchor_block.nim anchor_item.nim \
|
|
UNIVERSE_block.nim UNIVERSE_item.nim OUT_DIR
|
|
(UNIVERSE = a prior complete canonical, e.g. ~/nimcanon/canonical_final/*.nim)
|
|
4. Confirm "GATE PASS: True". Then copy OUT_DIR/*.nim into the cluster canonical
|
|
dir and chmod 644:
|
|
cp OUT_DIR/*.nim /home/refuge/panel/data/7dtdcluster/<cluster_id>/canonical/
|
|
chmod 644 /home/refuge/panel/data/7dtdcluster/<cluster_id>/canonical/*.nim
|
|
5. Rebuild each cluster member (POST /api/instances/<id>/rebuild) — the entrypoint
|
|
guard re-stamps the new canonical on boot. (If a member was STOPPED, /rebuild
|
|
leaves it stopped — Start it after.)
|
|
|
|
.nim BINARY FORMAT (little-endian, no trailing bytes):
|
|
u32 version(=1), u32 count, then count records of:
|
|
u16 id, u16 flag (items=1, blocks=0), u8 namelen, <namelen> UTF-8 name bytes
|
|
Records are stored sorted by ascending id. Ids are SPARSE.
|
|
"""
|
|
import struct, sys, hashlib, os
|
|
|
|
|
|
def read_nim(p):
|
|
b = open(p, "rb").read()
|
|
ver, cnt = struct.unpack_from("<II", b, 0)
|
|
off = 8
|
|
recs = []
|
|
for _ in range(cnt):
|
|
i, fl, nl = struct.unpack_from("<HHB", b, off)
|
|
off += 5
|
|
nm = b[off:off + nl].decode("utf-8")
|
|
off += nl
|
|
recs.append((i, fl, nm))
|
|
assert off == len(b), f"trailing bytes parsing {p} ({off} != {len(b)})"
|
|
return recs
|
|
|
|
|
|
def write_nim(recs, p):
|
|
out = struct.pack("<II", 1, len(recs))
|
|
for i, fl, nm in recs:
|
|
nb = nm.encode("utf-8")
|
|
out += struct.pack("<HHB", i, fl, len(nb)) + nb
|
|
open(p, "wb").write(out)
|
|
return out
|
|
|
|
|
|
def build(anchor, universe, flag):
|
|
"""anchor ids are SACRED (verbatim). append every universe name not present."""
|
|
recs = list(anchor)
|
|
present = {nm for _, _, nm in anchor}
|
|
used = {i for i, _, _ in anchor}
|
|
nid = max(used) + 1
|
|
for nm in sorted({nm for _, _, nm in universe} - present):
|
|
while nid in used:
|
|
nid += 1
|
|
if nid >= 65536:
|
|
raise SystemExit("FATAL: ran out of u16 id space (>65535)")
|
|
recs.append((nid, flag, nm))
|
|
used.add(nid)
|
|
nid += 1
|
|
recs.sort(key=lambda r: r[0])
|
|
return recs
|
|
|
|
|
|
def main():
|
|
if len(sys.argv) != 6:
|
|
print(__doc__)
|
|
sys.exit(2)
|
|
ab, ai, ub, ui, outdir = sys.argv[1:6]
|
|
os.makedirs(outdir, exist_ok=True)
|
|
anchor_b, anchor_i = read_nim(ab), read_nim(ai)
|
|
univ_b, univ_i = read_nim(ub), read_nim(ui)
|
|
fb, fi = build(anchor_b, univ_b, 0), build(anchor_i, univ_i, 1)
|
|
db = write_nim(fb, os.path.join(outdir, "blockmappings.nim"))
|
|
di = write_nim(fi, os.path.join(outdir, "itemmappings.nim"))
|
|
|
|
fbm = {i: nm for i, _, nm in fb}
|
|
fim = {i: nm for i, _, nm in fi}
|
|
alt_b = sum(1 for i, _, nm in anchor_b if fbm.get(i) != nm)
|
|
alt_i = sum(1 for i, _, nm in anchor_i if fim.get(i) != nm)
|
|
bids = [i for i, _, _ in fb]
|
|
bnm = [nm for _, _, nm in fb]
|
|
iids = [i for i, _, _ in fi]
|
|
inm = [nm for _, _, nm in fi]
|
|
compl_b = {nm for _, _, nm in univ_b} <= set(bnm)
|
|
compl_i = {nm for _, _, nm in univ_i} <= set(inm)
|
|
|
|
print(f"blocks={len(fb)} maxid={max(bids)} | items={len(fi)} maxid={max(iids)}")
|
|
print(f"block sha256 = {hashlib.sha256(db).hexdigest()}")
|
|
print(f"item sha256 = {hashlib.sha256(di).hexdigest()}")
|
|
print(f"ANCHOR altered (MUST be 0): block={alt_b} item={alt_i}")
|
|
print(f"dup ids: block={len(bids)-len(set(bids))} item={len(iids)-len(set(iids))}"
|
|
f" | dup names: block={len(bnm)-len(set(bnm))} item={len(inm)-len(set(inm))}")
|
|
print(f"complete (universe names all present): block={compl_b} item={compl_i}")
|
|
ok = (alt_b == 0 and alt_i == 0
|
|
and len(bids) == len(set(bids)) and len(iids) == len(set(iids))
|
|
and len(bnm) == len(set(bnm)) and len(inm) == len(set(inm))
|
|
and compl_b and compl_i and max(bids) < 65536 and max(iids) < 65536)
|
|
print("GATE PASS:", ok)
|
|
sys.exit(0 if ok else 1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|