#!/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//canonical/ chmod 644 /home/refuge/panel/data/7dtdcluster//canonical/*.nim 5. Rebuild each cluster member (POST /api/instances//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, 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("= 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()