Imported from andretosin/space-age-easy-mode (
AGENTS.md). Install upstream withnpx skills add andretosin/space-age-easy-mode. Copyright stays with the author.
AGENTS.md — Maintainer guide for Space Age Easy Mode
This file is a handoff guide for any AI assistant (or human) maintaining this mod. It captures the domain knowledge, prototype gotchas, and workflow conventions used to build the mod so far. Read it before making changes.
1. What this mod is
Space Age Easy Mode is a Factorio mod that reduces grind and speeds up progression by buffing existing entities. Key facts:
- Data-only. All logic lives in
data-updates.lua. There is nocontrol.lua, no runtime scripting, no events, and no custom prototypes with their own names. - It modifies existing vanilla / Space Age prototypes in
data.raw, generically (iterating by prototype type, never hardcoding entity names). - Target: Factorio 2.0 with optional Space Age (
? space-agedependency). - Because it only tweaks
data.raw, the only user-facing strings are the mod name and description (see Localization).
Repo layout
| Path | Purpose |
|---|---|
info.json |
Mod manifest: name, version, title, description, factorio_version, dependencies. |
data-updates.lua |
All prototype modifications. Runs after every other mod's data stage. |
changelog.txt |
Factorio-format changelog (strict format — see §7). |
locale/<code>/info.cfg |
Localized mod name/description per language. |
scripts/package.sh |
Builds the release zip (<mod>_<version>.zip). |
scripts/package-dev.sh |
Builds a dev zip and stages build/<mod>_<version>/ for CI. |
scripts/publish-factorio.sh |
Uploads the zip to the Factorio Mod Portal. |
.github/workflows/release.yml |
On push to main: tag, GitHub Release, Mod Portal publish. |
.github/workflows/develop.yml |
On push to develop: build a downloadable artifact. |
README.md |
Human docs + a "Version History" list mirroring the changelog. |
2. How data-updates.lua is organized
It runs after all mods' data.lua/data-updates.lua, so every entity already
exists and can be patched. The file is split into labelled feature blocks:
- Helpers — small local functions (see below).
- FR-1: Global Machine Buff — base speed/productivity/consumption on productive machines.
- FR-2: Module Rebalance — doubles module effects.
- FR-3: Drone Buff — 2× robot speed and cargo.
- FR-4: Power Generation Buff — 2× power for generators/solar/fusion/lightning; lightning-rod range; accumulator capacity/throughput.
- FR-5: Roboport Upgrades — 2× radius, ×4 charging slots + energy.
Helpers (reuse these; don't reinvent)
multiply_energy(s, m)— multiplies the numeric part of a Factorio Energy string (e.g."300kW"→"600kW"). Use for anybuffer_capacity,*_flow_limit,production,charging_energy, etc.add(a, b)—(a or 0) + b.get_bonus(v)— reads an effect value that may be a number or a{bonus=n}table.has_flag(entity, flag)— true ifentity.flagscontainsflag(e.g."player-creation").double_effectivity(prototype)— doublesprototype.effectivityif present.
The general method to add a buff
- Find the real prototype in the installed game files (don't guess field names) —
see §9 for paths.
grepfortype = "<prototype-type>". - Confirm field semantics at https://lua-api.factorio.com/latest/ (prototype pages).
- Add a block:
if data.raw["<type>"] then for _, e in pairs(...) do ... end end. - Reuse
multiply_energyfor Energy strings; multiply plain numbers directly. - Gate with
has_flag(e, "player-creation")when a change should only affect player-buildable entities (see the lightning-rod gotcha). - Iterate generically — never hardcode entity names.
3. Buffs currently implemented (and the fields they touch)
| Feature | Prototype type | Field(s) changed |
|---|---|---|
| Global machine buff | assembling-machine, furnace, mining-drill, lab, rocket-silo, recycling-machine, agricultural-tower |
effect_receiver.base_effect.{speed,productivity,consumption} |
| Module rebalance | module |
effect.{speed,productivity,quality} ×2 (consumption/pollution left alone) |
| Drone buff | logistic-robot, construction-robot |
speed ×2, max_payload_size ×2 |
| Power — generators | generator, fusion-generator |
effectivity ×2 |
| Power — solar | solar-panel |
production ×2 |
| Power — lightning | lightning-attractor |
efficiency/effectivity ×2, energy_source.{input_flow_limit,output_flow_limit,buffer_capacity} ×2 |
| Lightning range | lightning-attractor (player-creation only) |
range_elongation ×2 |
| Accumulators | accumulator |
energy_source.{buffer_capacity,input_flow_limit,output_flow_limit} ×2 |
| Roboport | roboport |
logistics_radius/construction_radius/logistics_connection_distance ×2; charging_offsets regenerated (×4 slots); charging_energy ×4 |
4. Factorio prototype knowledge & gotchas (hard-won)
4.1 Roboport charging — the big one
charging_offsetsis an array of{x, y}positions where robots park to charge. It sets both the slot count and the positions — but only whencharging_station_count == 0.- If you set
charging_station_countto a non-zero value, the engine ignorescharging_offsetsand sends every robot tostationing_offset(the port centre). Result: all robots charge stacked on one pixel. This is a trap — do not raisecharging_station_count. - Vanilla roboport:
charging_offsets = {{-1.5,-1},{1.5,-1},{1.5,1},{-1.5,1}},stationing_offset = {0,0}, nocharging_station_count. - To add more charging slots: generate more
charging_offsetsand leavecharging_station_countat0/nil. Spread them evenly on a ring (not tight clusters) so charging robots don't visually overlap. The current code puts the ×4 slots on a ring whose radius auto-scales to keep ~0.85 tiles between robots. - Robots waiting to charge hover at their current position within
charge_approach_distance— you can't neatly arrange the queue, only the chargers.
4.2 Lightning rods = lightning-attractor
- Two player rods:
lightning-rod(efficiency 0.2,range_elongation 15) andlightning-collector/ advanced (efficiency 0.4,range_elongation 25). range_elongationis the lightning collection range. Double it for 2× range.- Gate on
player-creation. There is also afulgoran-ruin-attractor(a decorative world entity,efficiency 0) — do NOT change its range/gameplay. The power buff (efficiency/energy) is applied to all attractors but is a no-op on the ruin (efficiency 0); only the range buff needs the flag gate.
4.3 Accumulators
- Type
accumulator, everything lives inenergy_source(typeelectric):buffer_capacity(storage),input_flow_limit(charge rate),output_flow_limit(discharge rate). Vanilla:5MJ,300kW,300kW.
4.4 Energy strings & effects
- Energy values are strings with units (
"500kW","100MJ"). Never do math on them directly — usemultiply_energy. - Machine base bonuses go in
effect_receiver.base_effect(plain numbers, not{bonus=n}tables). Accumulate onto existing values so other mods are respected. - Module
effectfields can be{bonus=n}tables or plain numbers — useget_bonus.
5. Localization
- Data-only mods only need
locale/<code>/info.cfgwith[mod-name]and[mod-description]keyed by the internal mod name (space-age-easy-mode). - Files must be UTF-8 without BOM. Verify after writing (a BOM can break the
first
[category]header). - Use Factorio's locale folder codes:
en,de,ru,fr,es-ES,zh-CN,ja,pl,cs,pt-BR,uk(note the hyphens:es-ES,zh-CN,pt-BR). - Keep the expansion name "Space Age"; translate the rest of the title/description.
- The Mod Portal listing shows the
info.json(English) description; the locale files only change what's shown in-game once the mod is installed and the game language matches.
6. Verify against the real game, don't guess
Before touching a prototype, read its actual definition in the installed game:
- Base:
C:\Program Files (x86)\Steam\steamapps\common\Factorio\data\base\prototypes\ - Space Age:
...\Factorio\data\space-age\prototypes\ - Roboport/accumulator/lightning defs are in
.../entity/entities.lua. - Cross-check field meaning at https://lua-api.factorio.com/latest/.
There is no Lua interpreter in this environment, so you can't run the mod. Keep changes small and idiomatic, and ask the author to test in-game before releasing.
7. Changelog & version bump
changelog.txt uses Factorio's strict format:
---------------------------------------------------------------------------------------------------
Version: 1.0.9
Date: 14. 06. 2026
Features:
- Some player-facing feature.
Bugfixes:
- Some fix.
Info:
- Infra / tooling note.
---------------------------------------------------------------------------------------------------
Rules:
- Separator lines are exactly 99 dashes. New versions go at the top.
- Category lines indent 2 spaces (
Features:); entries indent 4 spaces (- ...). Common categories:Features,Bugfixes,Info. - To bump a version, edit three files together:
info.json(version),changelog.txt(new block at top),README.md(new "Version History" line). - Prepend the changelog block programmatically (reuse the existing separator and the file's line endings) to avoid miscounting dashes and to preserve encoding.
- Player-facing buffs →
Features; bug fixes →Bugfixes; CI/publishing →Info.
8. Release pipeline & Mod Portal publishing
Branch/PR flow
- Work on a branch off
develop; open a PR intodevelop. - Release by merging
develop→main, which triggersrelease.yml. - Don't merge to
mainuntil the author has tested in-game.
release.yml (push to main)
package.shbuilds<mod>_<version>.zip.- Creates tag
v<version>(tolerates re-runs if the tag already points at the current commit). softprops/action-gh-releaseattaches the zip to a GitHub Release.publish-factorio.shuploads to the Mod Portal.
Mod Portal upload API (learned the hard way)
- Endpoint:
POST https://mods.factorio.com/api/v2/mods/releases/init_upload. - Auth =
Authorization: Bearer <API_KEY>header +mod=<name>as multipart form-data. Do NOT put the key/mod in the URL query string. - The response gives an
upload_url; POST the file there with-F file=@<zip>(that pre-signed URL needs no auth header). - The mod page must already exist on the portal —
init_uploadcannot create the first release. Publish the first version manually via the website. - The API key comes from https://factorio.com/profile and must have the
"Mod Portal: Upload Mods" usage/permission. A wrong-type or under-scoped key
returns
{"error":"InvalidApiKey"}. - Trim whitespace from the secret before building the header (a trailing newline
in a pasted GitHub secret corrupts the Bearer header). In Actions, also
echo "::add-mask::$TOKEN"(guarded by$GITHUB_ACTIONS) to mask the trimmed value. - Never print the token; keep
curl -sS(no-v), noset -x.
develop.yml artifact gotcha
actions/upload-artifactalways re-zips its input. Uploading a pre-built.zipgives a zip-inside-a-zip on download. Instead, stage the mod folder underbuild/<mod>_<version>/and uploadpath: build(the parent) — the artifact then contains the mod folder directly (one usable zip)..gitignorecoversbuild/and*.zip; both are excluded from the packaged mod.
9. Environment notes (this repo's setup)
- OS: Windows; shell: PowerShell (a POSIX
bashis also available for scripts). ghCLI is installed atC:\Program Files\GitHub CLI\gh.exe(may not be on PATH).- PowerShell here-strings mangle commit/PR bodies that contain parentheses or
quotes. Write commit messages and PR bodies to a temp file and use
git commit -F <file>/gh pr create --body-file <file>. bash -n script.shfor a quick shell syntax check;pythonis available for encoding/format validation.rsync/zipare NOT available in the local bash (they only run in CI on Linux).- Commit convention: Conventional Commits (
feat:,fix:,chore:,docs:), and end messages with aCo-Authored-By:trailer.
10. Quick checklist for a new buff
- Read the real prototype in the game install; confirm fields.
- Add a generic
data.raw["<type>"]loop in the right FR block ofdata-updates.lua. - Reuse helpers; gate with
player-creationif it should only hit buildable entities. - If shipping now: add a
Featuresline to the current unreleased changelog version (or bump to a new version), and updateREADME.md. - If it adds user-facing strings (it usually won't): update every
locale/*/info.cfg. - Branch off
develop→ PR intodevelop. Let the author test, thendevelop → main.