Read machine state
Problem. A BuildingModel tells you a machine exists. You want to know what it is
doing — running, waiting for input, or backed up.
Solution. Go from the building to its simulation, then read its lanes.
Building → simulation
if (!map.Simulator.TryFindTileSimulation(building.Tile_G, out ILocalizedTileSimulation localized))
{
return; // no simulation here (decoration, foundation, …)
}
if (localized.Simulation is IItemSimulation itemSimulation)
{
// most machines and belts land here
}
IItemSimulation is the generic interface: it exposes inputs, outputs, and internal
lanes without you knowing the concrete type. That is what lets one piece of code cover
cutters, painters, stackers, and anything a different mod added.
Running, starved, or blocked
public enum MachineStatus { Unknown, Starved, Blocked, Running }
public static MachineStatus Classify(IItemSimulation simulation)
{
// Output holding an item the next lane refuses → the problem is downstream.
for (int i = 0; i < simulation.NumItemProviders; i++)
{
if (simulation.GetItemProvider(i) is not IItemLane output) continue;
if (!output.HasItem) continue;
IBeltItem item = output.GetItem(0);
if (output.NextLane != null && !output.NextLane.CanAcceptItem(item))
{
return MachineStatus.Blocked;
}
}
// Nothing on any input → the problem is upstream.
for (int i = 0; i < simulation.NumItemReceivers; i++)
{
if (simulation.GetItemReceiver(i) is IItemLane input && input.HasItem)
{
return MachineStatus.Running;
}
}
return MachineStatus.Starved;
}
Blocked vs. Starved is the distinction players act on: blocked means fix what is
downstream, starved means feed it more. A single percentage number cannot tell them
which.
This costs two property reads per lane and needs no warm-up, so it is cheap enough to run across thousands of buildings — but a single reading is a snapshot. Sample a few times a second and keep a rolling ratio before you show anything.
Note
This classification is derived from the lane contracts, not a vanilla mechanism. The game itself measures throughput instead — see below.
Looking at every internal lane
Inputs and outputs are not the whole machine; a cutter also has a processing lane.
TraverseLanes visits all of them:
public struct OccupancyCounter : IItemLaneTraverser
{
public int Total;
public int Occupied;
public void Traverse(IItemLane lane)
{
Total++;
if (lane.HasItem) Occupied++;
}
}
OccupancyCounter counter = new OccupancyCounter();
itemSimulation.TraverseLanes(counter);
Use a struct traverser — the method is generic specifically so the call inlines and
allocates nothing.
Measuring actual throughput
This is what the game's own side panel does
(HUDSidePanelModuleBuildingEfficiency): hook the output lane, timestamp every item
that arrives, keep a 60-second window, and compare the average interval against the
building's theoretical processing duration.
AcceptHookDelegate saved = outputLane.AcceptHook;
outputLane.AcceptHook = delegate(IItemReceiver receiver, ref IBeltItem item, ref Ticks remaining_T)
{
Ticks now = map.Simulator.GetSimulationTimeFor(localized)
+ map.Simulator.GetSimulationUpdateDeltaTimeFor(localized);
Timestamps.Enqueue(now - remaining_T);
saved?.Invoke(receiver, ref item, ref remaining_T); // ALWAYS chain
};
The theoretical rate comes from the definition:
if (building.Definition.CustomData.TryGet<IBuildingEfficiencyData>(out var data))
{
float baseDuration = data.OriginalProcessingDuration;
int laneCount = data.ProcessingLaneCount;
}
Accurate, but it costs a delegate call per item and needs ~60 s of history per machine. Reasonable for one selected building; expensive for ten thousand.
Gotchas
- Always chain
AcceptHook. Overwriting it stops the machine doing its own work — the cutter that no longer cuts. Save the old delegate, invoke it, and restore it on cleanup. BuildingEfficiencyDatais attached to every building definition, belts included. Its presence does not mean "processing machine" — check the value.building.Stateholds the serialized state (…SimulationState), not the live simulation. For "what is happening now", go through the simulator.- Remove lane hooks when the map unloads, or they pin a dead map alive.