The Safe Change Workflow
Editing a data/*.json file is safe as long as you run the checks afterward. The engine swallows
data mistakes silently: a typo'd material grants nothing, an unknown recipe category renders
nowhere, a missing balance key falls back to a stale hardcoded number. This loop catches those
before they reach a player. Follow it every time.
The loop
- Edit the JSON. Change the number or field in the relevant
data/*.jsonfile. Save. - Run the data validator. Expect the last line
DATA OK. This catches broken references, unknown categories, BOMs, and missing balance keys. - Run the smoke test for any gameplay-affecting change. Expect
SMOKE PASS. This proves saves still round-trip and the offline simulation still holds its invariants over the real game code. - Feel-check in the editor. Open the game and actually play the part you changed. The two automated gates prove the data is valid and the game still runs — only a human can tell you the change feels right.
Skipping step 4 is the most common mistake. A validator cannot tell you the Maw now eats too fast.
Step 2 — the data validator
Run after any data/*.json edit. It loads every data file and checks the cross-references the
engine would otherwise ignore.
<godot console exe> --headless --path . --script res://tools/validate_data.gd
It passes when the final line is DATA OK (exit code 0). On failure it prints one VALIDATE: line
per problem and ends with DATA FAIL: N error(s). What it checks:
- Every JSON file parses and has no UTF-8 BOM (see the gotcha below).
- Recipe inputs and outputs point at real materials/tools/machines, amounts are above zero, and the
categoryis one the game actually renders. - Reward codes are keyed by a real SHA-256 hash and grant only things that exist.
- Building costs, discovery layers, and food-trade materials all reference real ids.
- Every required
balance.jsonkey is present (this is the anti-drift check — seeREQUIRED_BALANCE_KEYSbelow). - The apothecary brewing schema is internally sane.
Step 3 — the smoke test
Run for any change that affects gameplay (drain rates, the Maw, machines, offline behaviour, the economy). It boots the real autoload stack headless, mutates state across several managers, saves and reloads it, and runs a ten-minute offline simulation — asserting nothing broke.
printf '\n[autoload]\n\nZZSmokeTest="*res://tools/smoke_test.gd"\n' >> project.godot
<godot console exe> --headless --path . # expect SMOKE PASS
It appends itself as the last autoload, runs, and prints SMOKE PASS (exit 0) or
SMOKE FAIL: N assertion(s) failed. It uses the first free save slot and deletes it when done; if
all slots are occupied it prints SMOKE SKIP rather than risk a real save.
Important — restoring project.godot afterward. The published instructions end with
git checkout -- project.godot, but on this project's working copy that is unsafe: uncommitted
tweaks are routinely kept in project.godot, and a hard checkout would discard them. Instead, copy
the file aside before you append the autoload line and copy it back afterward:
cp project.godot project.godot.bak
printf '\n[autoload]\n\nZZSmokeTest="*res://tools/smoke_test.gd"\n' >> project.godot
<godot console exe> --headless --path . # expect SMOKE PASS
cp project.godot.bak project.godot && rm project.godot.bak
Optional — the parse-check
If your change touched anything beyond plain data (or you just want the syntax gate the way CI runs it), the parse-check compiles every autoload and greps for script errors:
powershell -ExecutionPolicy Bypass -File tools\parse_check.ps1
Exit 0 means clean (no SCRIPT ERROR or Parse Error lines). Pure JSON edits do not need this,
but it is harmless to run.
Finding the Godot console exe
The validator and smoke test need the Godot console executable — the plain GUI build detaches
from the terminal and its output (the DATA OK / SMOKE PASS line you are checking for) is lost.
The scripts default to the dev-machine location:
%USERPROFILE%\Downloads\Godot_v4.6.2-stable_win64.exe\Godot_v4.6.2-stable_win64_console.exe
Override it by setting the GODOT_CONSOLE environment variable to the full path of your
Godot_*_console.exe.
Gotchas that will bite you
UTF-8 BOM
Some editors save JSON with a hidden "byte-order mark" at the very start of the file. Godot tolerates it, but strict JSON parsers (and the validator) reject it. If the validator reports a file "starts with a UTF-8 BOM", re-save it as plain UTF-8 without the mark.
_comment and other underscore keys
Many data files carry a "_comment" note at the top explaining the format. That is fine — the
validator and the game skip any key starting with an underscore. The one exception is
materials.json: a meta key there inflates the game's completion-percentage denominator, so the
validator forbids it. Put explanatory notes for materials somewhere else, never as a top-level key
in materials.json.
REQUIRED_BALANCE_KEYS — the anti-drift list
The validator holds a list (REQUIRED_BALANCE_KEYS in tools/validate_data.gd) of every
balance.json key the game reads. If a key is missing from balance.json, the game silently runs
on a hardcoded fallback that can drift out of sync — exactly the bug this list exists to catch. Two
consequences for you:
- Do not delete a balance key to "turn something off". Removing it fails the validator and, if it somehow shipped, would hand control back to a stale constant. Set the value, do not remove the key.
- If you are adding a brand-new balance key that a script starts reading, add its name to
REQUIRED_BALANCE_KEYStoo, or the validator cannot protect it. (This is the one time a data change also touches atools/script.)
Recipe categories
A recipe's category must be one the game renders: tools, structures, machines,
components, processing, or village. Any other value and the recipe becomes invisible — it
exists but shows up nowhere. The validator flags this.
The golden rule still applies
Do not "fix" a value by editing the matching constant in a .gd script. The JSON overrides the
constant; editing the script leaves the JSON in charge and your change ignored. Tune the JSON, run
the checks, play it.