panel public release
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user