#!/usr/bin/env python3 """Remap 7DTD decoration.7dt / multiblocks.7dt block ids from a world's NATIVE table to the cluster CANONICAL table, BY NAME. Preserves all non-id bits. VENDORED, SELF-CONTAINED copy of nim-freeze/remap_7dt.py with nimtool.read_nim inlined and the host sys.path dependency removed, so it runs inside the panel-7dtd container (python3-minimal, no extra packages). Keep behaviour byte-identical to remap_7dt.py — the entrypoint's auto-provision relies on apply()'s exit code (0 = remapped + self-verified safe, 1 = refuse to start). 7dt format (little-endian, no trailing bytes): [0] u8 version (=6) [1:5] u32 count then count * 17-byte records; block id = u16 @ record offset +12, low 15 bits (& 0x7FFF) = id, high bit + bytes +14/+16 = rotation/placement. Rewrite rule: new = (orig & ~0x7FFF) | (canon_id & 0x7FFF). """ import sys, struct REC = 17; HDR = 5; IDOFF = 12 # --- inlined from nimtool.py (read_nim / parse_nim_bytes), byte-faithful --- def read_nim(path): with open(path, "rb") as f: data = f.read() return parse_nim_bytes(data) def parse_nim_bytes(data): if len(data) < 8: raise ValueError("file too short for header (%d bytes)" % len(data)) version, count = struct.unpack_from(" len(data): raise ValueError("record %d: truncated header at offset %d (len=%d)" % (i, off, len(data))) rid, flag = struct.unpack_from(" len(data): raise ValueError("record %d (id=%d): name truncated at offset %d (len=%d)" % (i, rid, off, len(data))) name = data[off:off + namelen].decode("utf-8") off += namelen records.append((rid, flag, name)) if off != len(data): raise ValueError("trailing bytes: consumed %d of %d" % (off, len(data))) return records, version # --- end inlined --- def load_id2name(path): if path.endswith('.nim'): recs, _ = read_nim(path) return {i: n for i, _, n in recs} d = {} for line in open(path, encoding='utf-8', errors='replace'): line = line.rstrip('\n') if not line: continue p = line.split('\t') if len(p) >= 2 and p[0].isdigit(): d[int(p[0])] = p[1] return d def load_name2id(path): recs, _ = read_nim(path) return {n: i for i, _, n in recs} def canon_idset(path): recs, _ = read_nim(path) return {i for i, _, n in recs} def decode_ids(b): ver = b[0]; count = struct.unpack_from('>> FULLY REMAPPABLE: {ok}") return ok def decodecheck(table, infile): b = open(infile, 'rb').read(); ids = canon_idset(table) if table.endswith('.nim') else set(load_id2name(table)) ver, count, recs = decode_ids(b) from collections import Counter dist = Counter(r[2] for r in recs) miss = sum(dist[i] for i in dist if i not in ids) print(f" {infile.split('/')[-1]} vs {table.split('/')[-1]}: {count} instances, {len(dist)} distinct ids, MISSING-in-table {miss} ({100*miss/count:.1f}%) [clean world should be ~0%]") def apply(native, canon, infile, outfile): src = open(infile, 'rb').read(); b = bytearray(src) n2 = load_id2name(native); c2 = load_name2id(canon); cset = canon_idset(canon) ver, count, recs = decode_ids(b) changed = 0; bad = [] for (pos, orig, bid, high) in recs: name = n2.get(bid) if name is None: bad.append(('noid', bid)); continue cid = c2.get(name) if cid is None: bad.append(('noname', name)); continue new = high | (cid & 0x7FFF) if new != orig: struct.pack_into('