PZ MP: Missing dictionary script after a Workshop mod dies Build 42

How to fix a multiplayer save that refuses to load because Steam deleted (or you removed) a Workshop mod, without regenerating WorldDictionary.bin and scrambling item IDs. Written from a real Planet Express / Buildable Roofs case.

Do not delete WorldDictionary.bin Forcing a regen reassigns registry IDs. Inventories and placed items can deserialize against the wrong types. Patch the load flags instead.

Symptom

Clients fail while joining. Server logs (or the client console) show:

WorldDictionaryException: [SpriteConfigs] Missing dictionary script on client: Base.SomeModThing
WorldDictionary: Cannot load world due to WorldDictionary error.

The save still marks obsolete SpriteConfig scripts as loaded. The server may tolerate stale entries; clients do not.


Fix overview

StepAction
1 Stop the server. Back up the whole world folder (at least WorldDictionary.bin + WorldDictionaryReadable.lua).
2 In the SCRIPTS section of WorldDictionaryReadable.lua, identify obsolete entries with isLoaded = true.
3 In WorldDictionary.bin, flip each matching script load flag from 0x03 to 0x00 (same length, same IDs).
4 Remove the dead Workshop ID from WorkshopItems=. Clients cannot download a delisted item.
5 Drop that Mods= id, or point WorkshopItems at a Steam-live successor if you have one. Start and join.

What you are looking for

Under the SCRIPTS section header in WorldDictionaryReadable.lua, entries look like:

{
    registryID = 247,
    name = "Base.CFBEAutomaticBrownRoofBottom10WidthFirst",
    version = "4035058024937495098",
    isLoaded = true,
},

Those isLoaded = true scripts are what every client must still provide. For a dead mod, unload them in the binary (optionally mirror isLoaded = false in the readable dump).

Binary layout (B42 script load list)

Names are stored without the Base. prefix when the fulltype starts with Base.. Immediately before the name:

FieldSizeNotes
flag1 byte0x03 = loaded, 0x00 = unloaded
registryID2 bytesbig-endian; matches readable registryID
version8 bytesbig-endian signed (>q); matches readable version
name length2 bytesbig-endian
nameUTF-8e.g. CFBEAutomaticBrownRoofBottom10WidthFirst

Patch only the flag byte. Do not rewrite names or versions unless you intentionally remap to another existing script of the same string length.

Example Python

Stop the server first. Change PREFIX to your dead mod’s script prefix.

from pathlib import Path
import re
import struct

world = Path("path/to/Saves/Multiplayer/YourWorld")
bin_path = world / "WorldDictionary.bin"
readable = world / "WorldDictionaryReadable.lua"

# Scripts that must no longer be required on clients:
PREFIX = "Base.CFBE"  # change to your dead mod's script prefix

data = bytearray(bin_path.read_bytes())
text = readable.read_text(encoding="utf-8", errors="replace")
marker = "--[[ ---- SCRIPTS ---- --]]"
scripts = text[text.find(marker) :]

pattern = (
    r'\{\s*registryID\s*=\s*(\d+)\s*,\s*name\s*=\s*"([^"]+)"\s*,'
    r'\s*version\s*=\s*"(-?\d+)"\s*,\s*isLoaded\s*=\s*(true|false)\s*,\s*\}'
)

entries = []
for match in re.finditer(pattern, scripts):
    name = match.group(2)
    if not name.startswith(PREFIX):
        continue
    entries.append(
        {
            "id": int(match.group(1)),
            "name": name,
            "version": int(match.group(3)),
            "loaded": match.group(4) == "true",
        }
    )

patched = 0
for entry in entries:
    short = entry["name"][5:] if entry["name"].startswith("Base.") else entry["name"]
    utf = short.encode("utf-8")
    pat = struct.pack(">H", len(utf)) + utf
    offset = 0
    while True:
        found = data.find(pat, offset)
        if found < 0:
            raise SystemExit(f"not found in bin: {entry['name']}")
        version = struct.unpack(">q", data[found - 8 : found])[0]
        if version != entry["version"]:
            offset = found + 1
            continue
        flag_at = found - 8 - 3
        registry_id = struct.unpack(">H", data[found - 8 - 2 : found - 8])[0]
        assert registry_id == entry["id"], (entry, registry_id)
        if data[flag_at] == 0x03:
            data[flag_at] = 0x00
            patched += 1
        break

bin_path.write_bytes(data)
print(f"unloaded {patched} scripts")

Aftercare

Objects from the dead mod may be invisible, broken, or inert. This patch restores loadability, not a perfect migration.

Keep the backup until you have played a session and checked loot and vehicles.

A Workshop rewrite of the same mod is a new item — do not expect old constructions to map cleanly.

Worked case: Buildable Roofs

Legacy 3802011026 was taken down. Saves still required hundreds of Base.CFBE… SpriteConfig scripts.

Unloading those flags, then pointing WorkshopItems at the author’s V2 item 3805628478 (same Mod ID Buildable_Roofs_Garage_Doors), let Steam clients download again without a full dictionary regen.

Post tip

Lead with the error string so search finds the guide. Call out the do-not-delete-the-bin rule early — that is where most people brick the save.