Files
panel/modules/7dtd/nim-freeze/rebuild.py
T
2026-07-14 19:22:07 -07:00

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()