# 7 Days to Die — Cluster Canonical `.nim` ID Freeze > How the panel stops a 7DTD cluster's **shared player saves** from corrupting on > cross‑server transfer (items "morphing" into other items, characters resetting > to level 1 / naked). Read this before touching cluster `.nim` files, the 7dtd > `entrypoint.sh` freeze guard, the canonical, or regenerating a clustered world. > > Author: deployed live 2026‑06‑13 on cluster `cl_37e3a7f72cdc` (Refuge). > Companion code: `modules/7dtd/entrypoint.sh` (the guard), > `modules/7dtd/nim-freeze/rebuild.py` (regenerate the canonical), and > `modules/7dtd/nim-freeze/remap_7dt.py` + `REMAP_RUNBOOK.md` (new‑world deco remap). --- ## ⚠️ CORRECTION / SECOND HALF OF THE PROBLEM (2026‑06‑13, later same day) The freeze below fixes **inventory** (`.ttp`) transfers — correct and live. But it is **NOT sufficient for a freshly‑generated DIFFERENT world**, and that's why insane/pvp came up "online but unjoinable" with a `DecoManager.TryAddToOccupiedMap` NullReference. Root cause (proven): a world also bakes its **decorations** into `decoration.7dt` (and structures into `multiblocks.7dt`) **by numeric block id**, using the engine's **native runtime ids at generation time**. The cluster's current runtime (RefugeBot v1.2.39) assigns the tree‑deco band at ids **21895+**, while the season10‑anchored canonical assigns the same trees at **24141+** (season10's world was baked *before* a RefugeBot rebuild shifted the band). Stamping the canonical over a new world's `.nim` makes its native‑baked deco ids resolve to a **null Block** → NRE → world‑load coroutine dies → never `GameStartDone`. season10/creative load clean only because their deco was baked when runtime == canonical. **Fix for a new world = a one‑time per‑world deco remap** (`remap_7dt.py`): translate `decoration.7dt`/`multiblocks.7dt` block ids `native → name → canonical`, preserving the high/rotation bits, then stamp the canonical. The engine **preserves** loaded deco ids on save (only writes native at *generation*), so the remap is **stable across reboots**. `.7dt` format: `u8 ver=6, u32 count, count×17B recs; id = u16@+12 & 0x7FFF`. The cluster runtime native table is identical across all instances (one capture is authoritative). **Full step‑by‑step: `nim-freeze/REMAP_RUNBOOK.md`.** insane (Yixacove) + pvp (Tuxeno) were fixed this way and verified: `GameStartDone`, `NRE=0`, deco band 24141, on canon. Permanent removal of the per‑world remap step: pin block ids explicitly in a top‑priority modlet (so runtime native == canonical) or freeze the RefugeBot build season10 was baked under. Until then, every NEW clustered world needs this one remap. --- ## TL;DR * 7DTD stores a player's **inventory by numeric ID, not by name**. Each *world* bakes its own `name <-> id` lookup tables: `itemmappings.nim` and `blockmappings.nim` in that world's save dir. * Our cluster **shares one player save folder** across servers that each run a **different world**. The shared `.ttp` carries one world's numbers; every other world decodes them through a **different** table → glass block reads as car hood, etc. Severe mismatches fail to load → fresh level‑1 character, written back over the shared save = **permanent wipe**. * **Fix:** build ONE *complete, season10‑anchored* canonical id table and **freeze it onto every world in the cluster**. Because it preserves season10's exact ids and contains every block/item the mods can produce, every world decodes the shared `.ttp` identically and the engine never appends → it stays frozen. * A small **entrypoint guard** re‑stamps the canonical on every boot, so it's self‑healing and survives recreates/regens. --- ## The problem (root cause, proven by forensics) 1. A player's `.ttp` inventory region is a stream of **2‑byte numeric ids** — no item/block name strings (names only appear later, for name‑keyed progression like perks/recipes, which is why **level/skills survive** a transfer but **inventory/placed‑blocks do not**). 2. Those numbers mean something only relative to the **per‑world** `.nim` tables. The engine logs `INF Block IDs with mapping` / `INF ItemIDs from Mapping` on every world load — it resolves stored ids → names **through that world's `.nim`**. 3. The `.nim` files are **NOT shared** — only the `Player/` folder is symlinked to `/cluster/Player`. Each world baked its table independently at genesis, so the same id maps to a different name per world. Live proof on the cluster: * block id **9243** = `steelShapes:cube` on season10 but `awningShapes:cube` on the others. * item id **66/67** = the inverse swap `meleeHandPlayer <-> meleeHandZombie01`. 4. **Why identical mods don't save you:** ids for un‑pinned/modded content are **auto‑assigned at world genesis in encounter order and frozen into that world's `.nim`**. Two worlds generated at different times/states freeze *different* assignments even from byte‑identical XML. So mod‑syncing, mod symlinks, identical load order — all irrelevant. The divergence lives in the **saves** volume, not the mods. 5. The "naked / level 1" variant: the destination's table is missing ids the `.ttp` references (e.g. season10 had 1888 item ids; a fresh RWG world's table had ~133). Unresolvable ids are dropped; enough drops and the character loads empty/level 1. Because the save is shared and **written back**, the wipe is permanent (`.ttp` *and* `.ttp.bak` both overwritten). This is the unsupported corner of 7DTD: **The Fun Pimps disable cross‑world characters for exactly this reason.** Sharing one inventory across servers running *different worlds* only works if every world uses an identical, frozen id table. --- ## The fix: a complete, frozen canonical table on every world ### What "canonical" means here A single pair of `.nim` files that is: * **Anchored to season10** — every id season10 currently uses is preserved **byte‑for‑byte**. season10 authored the 87 shared inventories and has the richest/oldest table, so the existing saves already match it. This makes the freeze **non‑destructive to season10's world and all existing inventories**. * **Complete** — contains every block and item the mods can ever produce: * all named items + **all 237 `item_modifier` names** (these serialize into `itemmappings.nim` when installed — the missing‑21 of these was the live char‑wipe vector), * all named blocks + the **entire `base:shape` cross‑product** (10 shape‑helper bases × every shape in `shapes.xml`) + the engine's authoritative block dump. Because it's complete, the engine never meets a name not already in the table → the **append branch never fires** → the file is never rewritten → **frozen**. Verified live on creative/season10: the `.nim` sha256 is byte‑identical before and after boot + world‑load. ### Final numbers (cluster `cl_37e3a7f72cdc`) | file | count | sha256 (prefix) | |---|---|---| | `blockmappings.nim` | 38,626 blocks (ids 0..63004) | `0a7db915…` | | `itemmappings.nim` | 2,719 items (ids 1..3513) | `fa4af1df…` | u16 ceiling is 65,535 — headroom remains. --- ## Components & locations | What | Where | |---|---| | **Deployed canonical** (the source of truth the guard reads) | `figaro:/home/refuge/panel/data/7dtdcluster//canonical/{blockmappings.nim,itemmappings.nim}` — this is the host side of the shared `/cluster` bind mount, so it appears as `/cluster/canonical/` inside every member container. | | **The freeze guard** | `modules/7dtd/entrypoint.sh`, the `--- cluster canonical .nim freeze guard ---` block. Baked into `panel-7dtd:latest`. | | **Per‑world `.nim`** (what gets stamped) | `/.local/share/7DaysToDie/Saves///{block,item}mappings.nim` | | **Rebuild tool** | `modules/7dtd/nim-freeze/rebuild.py` (re‑anchor) | | **Build artifacts / name registries** (for a from‑scratch rebuild) | `figaro:/home/refuge/nimcanon/` — `tool/`, `registry/` (`blocks_shapes_complete.txt`, `item_modifiers_all.txt`, `blocks_named_currentconfig.txt`, …), `canonical_final/`, `src/` (anchor copies). Persisted out of `/tmp` so a figaro reboot doesn't lose them. | --- ## The entrypoint guard (the automation) On every boot, after the cluster Player‑symlink setup, the entrypoint runs: ```sh if [ -f /cluster/canonical/blockmappings.nim ] && [ -f /cluster/canonical/itemmappings.nim ] \ && [ -d "$SAVE_BASE" ]; then cp -f /cluster/canonical/itemmappings.nim "$SAVE_BASE/itemmappings.nim" cp -f /cluster/canonical/blockmappings.nim "$SAVE_BASE/blockmappings.nim" fi ``` * `SAVE_BASE` = `Saves/$CLUSTER_PLAYER_SAVE` (set by the Cluster‑tab "Shared player world" picker), or the literal `Saves/$GameWorld/$GameName` fallback. * Only fires for cluster members that have a `/cluster/canonical/` present → non‑cluster servers and other clusters are untouched. * **Idempotent + self‑healing**: a complete canonical never triggers an append, so re‑stamping the same bytes every boot is a no‑op; if a stray append ever happened, the next boot resets it. You'll see this line in the boot log when it works: ``` [panel-7dtd] cluster : stamped canonical id maps over ...// (frozen, divergence-proof) ``` --- ## Is it automated? (what's hands‑off vs. operator action) * **Hands‑off:** every existing frozen world stays frozen forever; every boot/recreate/`/rebuild` re‑applies the canonical automatically. * **One operator step for a NEW world:** when a clustered server **regenerates an RWG world** (new seed), the new world's folder name is **seed‑derived and unknown until after gen**, so the guard can't target it until you set the Cluster‑tab **Shared player world** (`CLUSTER_PLAYER_SAVE`). Set that (and the Region Medic `world`) to the new world and the guard freezes it on the next boot. This is the same one‑click step a normal cluster *join* already needs. * **Rebuild needed only if mods change** (see below). > Future hardening (not yet done): make the guard/symlink **auto‑detect** the > active world dir (newest `Saves/*//` with a `main.ttw`) so even RWG > regens need zero manual settings. Deliberately left as an explicit Cluster‑tab > setting for now. --- ## Operating ### Add a clustered server / regenerate a clustered world (RWG) Goal: fresh world, frozen on the canonical, sharing the player folder. (Fresh worlds are clean — worldgen places vanilla blocks whose ids already match the canonical, so **no morph**.) 1. **New map:** change the seed — `POST /api/instances//env-config` body `{"updates":{"world_seed":""}}`. The engine gens a new seed‑named world. 2. Find the new world dir (newest `Saves/*//main.ttw`). 3. **Point the cluster settings at it:** * `POST .../env-config` `{"updates":{"CLUSTER_PLAYER_SAVE":"/"}}` → recreate → guard stamps it + symlinks `Player -> /cluster/Player`. * Write `region-medic.json` `"world": "/"` into the saves volume (keep `enabled`/`keep`/`discord_channel`). 4. Confirm the boot log shows the "stamped canonical" line and the world `.nim` sha256 == the cluster canonical. ### After a mod change (new blocks/items added) — REBUILD the canonical The canonical is a static snapshot of the merged config's name universe. New mods add names it doesn't have, so those would append per‑world again. 1. Re‑harvest the complete name set (the two ultracode workflows did this; the essential outputs are in `~/nimcanon/registry/`): * blocks: `:` cross‑product from `shapes.xml` × the 10 shape‑helper bases, ∪ the engine's authoritative block dump (`exportcurrentconfigs` / runtime `Block` list), ∪ named blocks. * items: every `` ∪ every `` (`grep -rhoE '/canonical/` (chmod 644) and `/rebuild` each member. ### Verify a server is frozen ```sh # .nim on disk == the cluster canonical: docker run --rm -v panel--saves:/sv:ro debian:12-slim \ sha256sum "/sv/.local/share/7DaysToDie/Saves///blockmappings.nim" # vs: sha256sum /home/refuge/panel/data/7dtdcluster//canonical/blockmappings.nim # and the boot log: docker logs panel- 2>&1 | grep "stamped canonical" ``` The **real** test is in‑game: transfer a player between two members and confirm glass stays glass, inventory intact, no level reset. ### Troubleshooting / gotchas * **`/rebuild` left the server STOPPED.** If an instance was stopped *before* `/rebuild`, the recreate suppresses autostart and leaves it `Created`/stopped. Just **Start** it — the guard runs on boot. * **Guard didn't stamp / wrong dir.** `CLUSTER_PLAYER_SAVE` is empty or stale → for RWG the fallback `Saves/RWG/` doesn't exist, so the guard skips. Set the Cluster‑tab Shared‑player‑world to the real (seed‑derived) world. * **season10 grew between snapshot and stamp.** season10's `.nim` appends as players place new shapes (we saw 12259 → 12263 in a few hours). **Always re‑anchor (`rebuild.py`) against season10's CURRENT, STOPPED `.nim` immediately before stamping season10.** Once season10 is frozen on the complete canonical it stops growing. * **Stamp only while the target is STOPPED**, as the runtime uid (`1000:1000`), mode `0644`. An unreadable `.nim` makes the engine silently gen a fresh table = full remap. * Old orphaned worlds (e.g. `Tefasizi County`, `Kopaneke Territory`) are left in the saves volumes after an RWG regen — safe to delete to reclaim space. --- ## If we need to pivot (alternatives, ranked) If the freeze ever proves unworkable (e.g. a mod that registers ids at **runtime** — none currently do; all 7DTD content here is static XML): 1. **One shared world** across the whole cluster (same GameWorld+seed+GameName). One world → one `.nim` → impossible to diverge. Cost: every server is the SAME map (defeats distinct‑experience servers). Simplest and bulletproof. 2. **Don't carry numeric‑id state across worlds.** Share only name‑keyed progression (level/skills/perks/recipes — these already transfer cleanly) and **strip bag/belt/equipment** on join to a world that didn't author the inventory (an admin/RefugeBot hook). Players keep their character, not carried items. Preserves distinct worlds. 3. **Re‑encode on transfer** (true but fiddly): decode the `.ttp` inventory through the SOURCE world's `.nim` and re‑encode through the DESTINATION's, dropping ids the destination lacks. Must run while the player is offline. 4. The current approach (**freeze a complete canonical**) is strictly better than "sync the `.nim` across different worlds" — never try that bare, because the `.nim` is also the codebook for each world's OWN placed blocks: swapping it on a world with existing modded builds **morphs that world's terrain** (that's why insane/pvp got fresh worlds instead of an in‑place stamp). --- ## Key facts to remember * Inventory/placed blocks = **numeric id**; level/skills/perks/recipes = **name‑keyed** (the latter survive any transfer). * `.nim` is written **only on table growth**, not per save — a complete table is therefore frozen. * The canonical must be **anchored to season10** (the authoring world) or existing inventories morph. * New RWG world folder names are **seed‑derived**; you must set `CLUSTER_PLAYER_SAVE` + medic `world` to the real name after gen. * The deployed canonical lives in the **shared cluster dir** (`/cluster/canonical`) so one copy serves every member and the guard is purely local file ops.