Add a simulation system
Problem. You want behaviour that is not tied to one building instance — something that runs every tick across many entities, or maintains its own graph.
Solution. A simulation system. Buildings have simulations; systems own and update them.
The distinction
| Simulation | System | |
|---|---|---|
| Scope | one placed entity | all entities of a kind |
| Created by | a system, on placement | the session, once |
| Example | HalfCutterSimulation |
FluidNetworkSimulationSystem, TrainSystem |
If you are adding a machine, you do not need to write a system by hand — Flow's
.WithSimulation(factoryBuilder, logger) registers one for you
(add a building). Write a system directly when your behaviour spans
entities: a network, a global tracker, a cross-platform mechanic.
The contract
public interface ISimulationSystem
{
IEvent<IConnectableSimulation> OnSimulationCreated { get; }
IEvent<IConnectableSimulation> OnBeforeSimulationDestroyed { get; }
IEnumerable<IConnectableSimulation> ConnectableSimulations { get; }
}
That is the minimum — enumerate your simulations and announce lifecycle. Systems that
tick also implement IUpdateableSimulationSystem (Update(Ticks startTicks, Ticks deltaTicks)); ones keyed by chunk implement IChunkSimulationSystem
(TryGetChunkSimulation); ones that attach to islands implement
IIslandTenantSimulationSystem (IslandIsOffered / IslandOfferIsTerminated).
TrainSystem implements all four and is the best worked example in the game of a
non-trivial system.
Registering one
using ShapezShifter.Hijack;
public class MySystemsRewirer : ISimulationSystemsRewirer
{
public void ModifySimulationSystems(
ICollection<ISimulationSystem> simulationSystems,
SimulationSystemsDependencies dependencies)
{
simulationSystems.Add(new MySystem(dependencies.ShapeRegistry, dependencies.Logger));
}
}
// in your IMod constructor:
RewirerHandle handle = GameRewirers.AddRewirer(new MySystemsRewirer());
You receive the live collection, so you can add, and in principle remove or replace, an existing system. Removing a vanilla system is a blunt instrument — it will break every building that depends on it — but it is the mechanism if you genuinely need to replace one wholesale.
What you get handed
SimulationSystemsDependencies is everything a system might need, which doubles as a
map of what the simulation layer is built from:
public readonly Ticks InitialSimulationTime;
public readonly GameMode Mode;
public readonly IShapeRegistry ShapeRegistry;
public readonly IShapeIdManager ShapeIdManager;
public readonly IGameResourcesMap ResourcesMap;
public readonly IFluidRegistry FluidRegistry;
public readonly FluidPackageItemSolver FluidPackagesItem;
public readonly ISignalChannelRegistry SignalChannelRegistry;
public readonly IResearchUnlockManager ResearchUnlockManager;
public readonly ILogger Logger;
InitialSimulationTime matters for anything time-based: a system created when loading a
save must not assume time starts at zero.
Finding systems at runtime
ISimulator simulator = map.Simulator;
MySystem system = simulator.GetSystem<MySystem>();
if (simulator.TryGetSystem<MySystem>(out MySystem s)) { … }
foreach (IUpdateableSimulationSystem u in simulator.GetSystems<IUpdateableSimulationSystem>()) { … }
This also works for vanilla systems, which is often all you need: reading
FluidNetworkSimulationSystem or TrainSystem requires no system of your own.
Determinism
Simulation systems run inside the game's deterministic tick. Two rules:
- No
Randomwithout a seeded, serialised generator, and no wall-clock time.Updategives youTicks; use them. - No frame-rate dependence.
deltaTicksis simulation time, not frame time.
Breaking either produces the worst class of bug in this game: a save that replays differently than it ran.
Gotchas
- Ticking over every entity every tick is the most expensive thing a mod can do. Keep
per-tick work proportional to what actually changed, and lean on
OnSimulationCreated/OnBeforeSimulationDestroyedto maintain incremental state. - Anything your system holds that must survive a reload belongs in a serialised
ISimulationStateon an entity, or in mod save data — not in system fields. - Dispose the
RewirerHandleinIMod.Dispose(), and make your systemIDisposableif it holds anything.
Note
No sample mod adds a bare simulation system — the samples all go through Flow. This
page is derived from the interfaces and from TrainSystem; verify the lifecycle
callbacks you rely on against your game version.