Imported from jodli/CreativeMod (
.pi/skills/factorio-mod-dev/SKILL.md). Install upstream withnpx skills add jodli/CreativeMod --skill factorio-mod-dev. Copyright stays with the author.
Factorio Mod Dev
Repo layout
creative-mod/
├── info.json # mod metadata (name, version, factorio_version, deps)
├── settings.lua # startup/runtime settings (data stage)
├── data.lua # data stage entry — loads prototypes/
├── data-final-fixes.lua # data stage post-processing (runs after all mods)
├── control.lua # runtime entry — requires scripts/, registers events
├── defines.lua # all name/prefix constants → creative_mode_defines
├── scripts/ # runtime modules (one feature per file)
├── prototypes/ # entity, item, recipe, technology definitions
├── migrations/ # version migration scripts
└── locale/ # translations
Verification loop
verify.py is the canonical way to check the mod. It loads creative-mod in the
local Factorio install, runs assertions, and exits 0/non-zero with a stable,
greppable RESULT: line, so you can edit → verify → read result → iterate.
Run it via uv:
uv run verify.py doctor # preflight: factorio binary + version, uv, jq
uv run verify.py static # luacheck . + stylua --check .
uv run verify.py load # data + control load gate (incl. silent-crash guard)
uv run verify.py behavior # headless server + RCON assertion batch
uv run verify.py all # static → load → behavior, aggregated
uv run verify.py --help
The layered model is static → load → behavior (cheapest to deepest); all
runs the three in sequence. Read the result by grepping ^RESULT: and/or
checking $?:
RESULT: load=PASS # exit 0
RESULT: load=FAIL (control stage incomplete) # exit non-zero, reason names the failure
For investigation, use the bounded tooling modes (successors to the removed standalone shell wrappers):
uv run verify.py shell '/c rcon.print(game.tick)' # one-shot RCON; omit arg for a stdin REPL
uv run verify.py debug --command '/c ...' # bounded headless session
uv run verify.py debug --gui # manual-only graphical escape hatch
uv run verify.py load --clean # recreate the debug save from scratch
RCON gotcha: the /c console runs in its own environment — the mod's
runtime globals (super_boiler, global_util, storage.creative_mode, etc.)
are not reachable from it, so you can't call mod functions directly. (util
appears to work but resolves to core lualib's util, not the mod's.) To test mod
logic via RCON, inline a replica of the code against a live entity, or add a
behavior assertion that places an entity with raise_built=true (which registers
it into the real per-tick loops) and checks the effect after the server ticks.
Output channels for the values you inspect:
| Goal | Use | Where |
|---|---|---|
| Inspect a value | rcon.print(v) |
echoed back to terminal |
| Trace code | log("msg") |
factorio-current.log |
| Dump large table | helpers.write_file("f", d) |
.debug/script-output/f |
→ See VERIFY.md (this skill folder) for the full subcommand reference, the
RESULT:/exit-code contract, and the replicable local install setup.
→ See DEBUG.md for the output-channel reference.
→ See DEBUGGING.md (this skill folder) for debugging methodology and porting guide.
→ See RELEASE.md (this skill folder) for release checklist and GitHub Actions workflow reference.
Changelog & versioning
Every PR that changes user-facing behavior (a feature, bugfix, or change) must
include a changelog.txt entry in the same PR — it is part of the feature, not
a separate step.
- Prepend the entry at the top of
changelog.txtusing the documented format, under the right section (Features:/Bugfixes:/Changes:). Set theVersion:header to the next release version (pick the bump from the version table inRELEASE.md: new user-facing feature → minor; bugfix → patch). - Do not bump
info.jsonin the feature PR. Theinfo.jsonversion bump and the git tag happen together as a dedicated release step onmaster— seeRELEASE.md. This keeps feature branches free of version churn/conflicts.
Base game as a reference
The full Factorio base mod ships with the install and is readable on disk:
/mnt/quickstuff/git/factorio_linux/data/
├── changelog.txt # the full engine + API change log (every version) — VERY complete
└── base/
└── prototypes/ # all vanilla entity, item, recipe, technology definitions
When something breaks or behaves unexpectedly, read the base game source first:
- API errors (
doesn't contain key X,nil value, renamed/removed methods)?grepdata/changelog.txtfor the symbol — the API change log is extremely thorough and almost always names the exact rename/removal and its replacement (this is how theLuaEntity::fluidbox→get_fluid/set_fluid/fluids_countremoval was diagnosed). Confirm the new signature by probing a live entity via RCON before rewriting. - Correct fields for a prototype type? Find a vanilla example in
data/base/prototypes/. - How the Factorio devs actually use an API?
grepthe base mod's scripts and prototypes — prefer their real usage patterns over guessing or external docs. - What a
defines.*value is? Defined in the engine, but usages are visible throughout the base mod.
This is the ground truth for the running version — always prefer it over external docs.
Key internals
creative_mode_defines— single source of truth for all names/prefixes (defines.lua)storage.creative_mode— mod runtime state, initialised inscripts/global-util.luaevents— all event callbacks (scripts/events.lua), registered incontrol.luaremote_interface— public API for other mods (scripts/remote-interface.lua)- Prototype names: always use
creative_mode_defines.names.*, never hardcode strings - Data stage only:
data.raw,data:extend(), prototype tables - Runtime only:
game,storage,script,defines,helpers