Modding
Mods are folders of JSON files. You need no SDK, no compiler and no editor. Drop a folder into Mods/, start the game,
and the mod loads. Bad files never crash the game: every problem becomes a readable line in the log, and the broken
part is skipped.
| Tier | What | Files | Status |
|---|---|---|---|
| 1 | Change numbers and tables: weapons, attachments, prices, economy, loot, enemies, rounds, classes | patches/*.json |
works |
| 2 | New gun, attachment and enemy-part models, built from voxel shapes | recipes/*.json |
guns bake and show in game; attachment models show in the shop preview |
| 3 | GameFeature plugins (new assets, code) from paks | plugin/ |
off in this build |
Quick start: the example mod “hardcore”
Section titled “Quick start: the example mod “hardcore””The game ships one example mod in Mods/hardcore/. It makes every gun hit 25% harder, carry a third less reserve ammo
and cost half as much again. The bolt rifle reloads slower. It also adds a new gun, the RIVET CARBINE (a semi-auto copy
of the SMG with its own stats and its own voxel model, sold off the wall).
The example ships with "enabled": false, so nobody plays it by accident. -mods=hardcore on the command line turns it
on (naming a mod in -mods= overrides its enabled flag).
Packaged game
- The mod lives in
<install folder>/MOTH_ZOMBIES/Mods/hardcore/(theModsfolder sits next toContent). Copy any other mod to the same place. - Start the game with
-mods=hardcore(a mod of your own with"enabled": trueloads without it). - Open the console (
~) and typeZombies.Mods.List. In the log you should seemanifest m<16 hex digits>,applied yes, a rowhardcore 1.0 ... loaded prio 0, 2 patch / 1 recipe / 0 plugin file(s), the targets with their row counts (weapon(N)counts the core guns plus the carbine) and the recipehardcore.gun.rivet_carbine. - Host or play solo, then type
Zombies.Mods.Give hardcore.wpn.rivet_carbine. The carbine is added to your loadout and selected. Give works only on the host, and only for testing.
Turning mods off
-nomodson the command line loads none.-mods=a,bloads only those mods (and turns them on even if theirmod.jsonsays"enabled": false)."enabled": falsein a mod’smod.jsonskips that mod unless-mods=names it.- Deleting the folder removes the mod.
-modsdir=<path>reads mods from another folder.
Folder layout
Section titled “Folder layout”Mods/ mymod/ folder name = mod name mod.json required patches/*.json tier 1, applied in file-name order recipes/*.json tier 2 plugin/ tier 3 (ignored in this build)mod.json:
{ "name": "mymod", "version": "1.0", "title": "My Mod", "description": "What it does.", "author": "You", "priority": 0, "enabled": true}namemust equal the folder name. It takes 2-24 characters froma-z,0-9and_, and starts with a letter. Core words are reserved (ds,ww,base,core,mod, every class name such astank, …).- The name is also your namespace. Every id your mod adds starts with
mymod.(mymod.wpn.zapper). versionis shown to players when versions differ. It may use letters, digits and. _ + -, up to 16 characters.prioritysets load order: lower loads first, and ties go by name. A later mod’s write wins.- JSON has no comments and no trailing commas. Use a
"comment"key instead, which is allowed almost everywhere.
Patches (tier 1)
Section titled “Patches (tier 1)”A patch file holds { "patches": [ ... ] }, a bare list [ ... ], or one patch object. One patch:
{ "comment": "anything", "target": "weapon", "id": "ds.wpn.nightjar", "where": { "Family": "rifle" }, "set": { "Damage": 60, "DisplayName": "KESSLER MK2" }, "add": { "MagSize": 2 }, "mul": { "Price": 1.5 }, "append": { "Tags": [ "heavy" ] }, "remove": [ "Tags[old]" ]}| Key | Meaning |
|---|---|
target |
Which table (see Targets). |
id |
Rows by id: exact ("ds.wpn.nightjar"), prefix ("ds.wpn.*"), or a list. Leave it out to hit every row. |
where |
Extra filter, field: value (a list value matches any of them). |
new + clone |
Add a row. new must start with your mod name. clone copies an existing row first. |
set |
Write values. Objects merge into structs; lists replace lists. |
add, mul |
Add to or multiply numbers. Whole-number fields round to the nearest. |
append |
Add elements to the end of a list. |
remove |
Remove list elements by path. |
Inside one patch the ops run in the order remove, append, set, add, mul. Patches run in file order, files in name order, and mods in priority-then-name order. Every launch starts from the unmodded values, so the same mods always give the same result.
Paths. Use A.B for a field inside a struct and List[2] for an index. List[-1] is the last element,
List[Id=x] picks by field, and List[x] picks the element whose Id, Key, Kind or Name is x. Field names ignore case,
and a bool’s b prefix is optional ("Automatic" = "bAutomatic").
Values. Numbers, true/false, text (names and display text), enum names ("Blunt"), objects for structs
({ "X": 1 }), lists for lists.
Targets
Section titled “Targets”| Target | Rows |
|---|---|
weapon |
every gun (ds.wpn.nightjar, ds.wpn.razor, ds.wpn.stahl, ds.wpn.mudlark, …) plus mod guns. Takes new rows. |
attachment |
every attachment, by Id. Takes new rows. |
economy, rounds, enemies, loot, classes, … |
one target per rules table, one row: the whole table. Leave out id. |
<table>.<list> |
each keyed list inside a rules table, by its rows’ Id / Key / Kind / Name: enemies.rows, loot.find, loot.crate, loot.market, … |
Zombies.Mods.List -v prints every target with its row type, its id field and whether it takes new rows. A list
keyed by an enum (one row per kind) can’t take new rows.
Every rules table is checked after the patches run (one enemy row per kind, min below max, …). A table that fails is put back whole and every patch to it is dropped, with the reason in the log.
Mistakes are errors: an unknown target, field, row id or list key skips that patch and says so in the log. A patch
whose id filter or where matches no rows only warns.
A gun with the tag wallbuy in Tags is sold on the wall. Mod guns cloned from a core wall gun keep the tag.
Weapon fields
Section titled “Weapon fields”| Field | Meaning |
|---|---|
DisplayName |
Name shown in game. |
Family |
pistol / smg / rifle / shotgun / sniper / lmg / special. |
DamageClass |
Pierce / Blunt / Slash / Impale / Blast / … |
Damage |
Damage per pellet. |
Pellets |
Pellets per shot (1-32). |
Rpm |
Rounds per minute. |
bAutomatic |
Full auto. |
MagSize, ReserveMax |
Rounds in the magazine and in reserve. |
ReloadSeconds |
Reload time. |
SpreadRad, AdsSpreadScale |
Hip spread in radians, prototype width (each axis jitters +-half; a sprint doubles it); the scale applied when aiming. |
RecoilRad |
Pitch kick per shot in radians. |
RangeM, FalloffStartM, FalloffMinScale |
Range in metres. Full damage up to FalloffStartM, then down to FalloffMinScale at RangeM. |
CalibreDmm, MuzzleEnergyJ, ProfileId |
Wound model inputs (calibre in 0.1 mm). |
Price |
Shop / wall-buy price. |
RecipeId |
The voxel model: a core gun id (ds.wpn.<key>) or your recipe id. |
FireModes |
Selector positions (once the gun handling update is in). Empty means “from bAutomatic”. |
Id can’t be changed with set (that would rename the row). Use new + clone instead.
Operation rewards
Section titled “Operation rewards”ops.tiers holds the threat levels by Id (skirmish, contested, … deadsector). Each row’s Rewards has two
cosmetic fields:
| Field | Meaning |
|---|---|
Rewards.ClearTitle |
Title a player earns the first time they clear that threat level. Up to 32 characters. "" gives none. |
Rewards.ClearFinish |
Gun finish earned the same way: the Id of a finish row. "None" gives none. |
Finish rows live in claw.cosmetics, by Id. It takes new rows.
| Field | Meaning |
|---|---|
Metal, Furniture |
Tint for the gun’s metal, and for its wood and grips: { "R": 0.02, "G": 0.025, "B": 0.035, "A": 1 }. |
MetalAmount, FurnitureAmount |
How much of the tint shows. 0 leaves the gun as built, 1 is all tint. |
A finish Id with no row leaves the gun as built.
Recipes (tier 2)
Section titled “Recipes (tier 2)”A recipe builds a voxel model out of shapes. Units are dmm (0.1 mm). X points forward (to the muzzle), Y left, and Z up.
{ "id": "mymod.gun.zapper", "kind": "gun", "name": "ZAPPER", "base": "ds.wpn.razor", "action": "bolt", "travel_dmm": 450, "cell_dmm": 90, "ops": [ { "op": "target", "t": 0 }, { "op": "box", "c": [0, 0, 300], "s": [2400, 480, 520], "paint": { "layer": "Armour", "mat": "GunBlue" } }, { "op": "socket", "name": "muzzle", "pos": [1300, 0, 300], "dir": [127, 0, 0] }, { "op": "socket", "name": "grip", "pos": [-500, 0, -650] }, { "op": "target", "t": 1 }, { "op": "box", "c": [-500, 300, 450], "s": [800, 100, 140], "paint": { "layer": "Armour", "mat": "GunEdge" } } ]}| Key | Meaning |
|---|---|
id |
Must start with your mod name. Point a weapon’s RecipeId at it. |
kind |
gun, attachment or part. |
base |
Gun only: the core gun whose pose, smoke and casings it borrows. |
action |
none, bolt, slide or pump. Defaults to the base gun’s. |
travel_dmm |
How far the moving part travels on each shot. |
cell_dmm |
Voxel size, 20-400. Defaults to the core gun cell. Set it, so your model doesn’t change if the core cell does. |
Targets ({ "op": "target", "t": N }): 0 is the frame (everything fixed) and 1 is the moving bolt / slide / pump.
Targets 2+ are baked but not shown yet. A gun needs a muzzle and a grip socket. mag, optic and stock are
optional.
Ops
| Op | Keys |
|---|---|
box |
c centre, s size [x, y, z], optional bevel |
ellipsoid |
c centre, r radii [x, y, z] |
cylinder |
c centre, axis "x"/"y"/"z", len, r, optional hole (bore radius), vents true |
profile |
pts outline [[x, z], ...] (3-512 points) pushed out w wide, optional cy (centre Y) |
rod |
a, b end points, r radius |
cell |
i one cell [x, y, z] |
socket |
name, pos, optional dir (unit x 127, default forward) |
Every shape op takes "paint": { "layer": ..., "mat": ..., "tint": 0-255, "flags": 0-255 }.
- Layers:
Empty,Armour,Cloth,Skin,Flesh,Bone,Organ. Guns useArmour. - Gun materials:
GunBlue,GunEdge,GunDark,GunOlive,Walnut,WalnutLight,Bakelite,Brass,Glass,Wrap,Leather,Rivet. Any other palette name works too (PlateSteel,PlateRust,Webbing, …). - Later shapes overwrite earlier ones. Paint with
"layer": "Empty"to carve holes.
Limits. Files up to 4 MB, 512 cells per axis, 120000 solid cells. Coordinates must stay within 1024 cells of the
origin. Ops per recipe, grid cells and op cells are capped at 1.25x the largest core gun (at least 4000 ops);
the log prints them (recipe caps: ...) the first time a recipe is checked. A recipe over a limit is refused with the reason.
Start from a core gun. Zombies.Mods.DumpRecipe ds.wpn.razor mymod.gun.my_smg writes the SMG as a recipe file
to Saved/Mods/recipes/. Copy it into your mod’s recipes/ folder and edit it.
Multiplayer
Section titled “Multiplayer”Everyone in a game must run the same mods. The session carries a manifest tag: a hash of every loaded mod’s name, version, priority and file contents, in load order. Line endings don’t matter. A player whose mods differ is refused in the lobby with the reason, for example:
- “This game needs mods: hardcore 1.0. You have: none. Install the same mods to join.”
- “This game runs without mods. You have: hardcore 1.0. Remove them from the Mods folder (or start with -nomods) to join.”
- “This game needs hardcore 1.1. You have: hardcore 1.0. Install the same versions to join.”
- “Your copy of hardcore differs from the host’s. Reinstall the same files to join.”
Join from the server list or the direct-connect box (or Zombies.Net.Join <ip>). A console open <ip> sends no
manifest and is refused with “Missing mod manifest: …”.
Mod guns have the same name on every machine, so they replicate like core guns.
Saves store ids as they are, mod ids included (hardcore.wpn.rivet_carbine). When a profile loads, ids belonging to
a mod that is no longer loaded are dropped (installed but off, or loaded on this PC before and now gone). Core ids and
ids from namespaces no mod ever used are always kept. Saved/Mods/seen.txt remembers which mods this PC has loaded.
Plugins (tier 3)
Section titled “Plugins (tier 3)”A mod may carry a GameFeature plugin in plugin/ (one .uplugin plus its .pak files). This build doesn’t load them:
the mod still loads its patches and recipes, and the log says the plugin was skipped. Plugins can run code, so a build
that supports them also needs zombies.mods.GameFeatures 1. Only enable that for mods you trust.
Console commands
Section titled “Console commands”| Command | What it does |
|---|---|
Zombies.Mods.List |
Mods found, load state, manifest tag, targets with row counts, recipes, problem counts. |
Zombies.Mods.Reload |
Puts everything back, rescans Mods/ and applies again. Use it after editing files. |
Zombies.Mods.Hash |
Prints the manifest tag and mod list (compare between two PCs that can’t join). |
Zombies.Mods.Check [name] |
Dry-runs one mod (or all of them) and prints every problem without changing the game. Run it with mods not applied (with -nomods). |
Zombies.Mods.DumpRecipe <gun> [new id] |
Writes a core gun as a recipe file to Saved/Mods/recipes/. |
Zombies.Mods.Give <weapon id> |
Host / solo only: adds a weapon to your loadout and selects it. |
Messages and troubleshooting
Section titled “Messages and troubleshooting”Messages go to the LogZMods log category (Saved/Logs/MOTH_ZOMBIES.log)
with [mods] in front. Each message names the file, the patch number and the field. Examples:
| Message | Fix |
|---|---|
bad JSON: ... Line: 12 Ch: 5 (JSON has no comments or trailing commas; use a "comment" key) |
Fix the JSON at that line. The whole file is skipped until you do. |
unknown field 'Damge' on ZWeaponDef (did you mean 'Damage'?); skipped |
Typo in a field name. |
'MagSize' takes a whole number, got text; skipped |
Wrong value type. |
unknown target 'weapons' (did you mean 'weapon'?); patch skipped |
Typo in target. |
target 'enemies.rows' can't take new rows; patch skipped |
The list is keyed by kind; patch an existing row instead. |
enemies: every patch to it dropped (after the check that failed) |
The patched table broke a check. Fix the values it names. |
new id 'zap.gun' must start with 'mymod.' (the mod's name) |
Prefix new ids with your mod name. |
no 'weapon' row 'ds.wpn.nightjr' (did you mean 'ds.wpn.nightjar'?) |
Typo in id. |
guns need a "muzzle" socket (op socket); recipe skipped |
Add the socket op. |
Known limits
Section titled “Known limits”- Mod guns don’t get the core guns’ edge-wear pass or a moving fire selector. Target 2+ shapes are baked but not drawn.
- Mod attachment models show in the shop preview only; fitted to a gun they aren’t drawn yet.
- Mod-added upgrade nodes and enemy parts come in a later version.
- No GameFeature plugins (tier 3) in this build.
- Mods load at game start. After editing files, use
Zombies.Mods.Reload(or restart).