The Map Model
You need this when you are walking islands or buildings, attributing something to a platform, or asking "where does this machine's state actually live?". Task-shaped versions: Find buildings and Read machine state.
Everything placed in the world hangs off IMapModel. The hierarchy is three deep:
IMapModel the world
└── IslandModel a platform / island (struct)
└── BuildingModel a machine on that island (struct)
└── SimulationStateContainer its persisted state
IslandModel and BuildingModel are structs — cheap handles that carry an id, a
reference back to the map, and an instance record. Passing them around costs nothing;
storing them long-term is a mistake, because a handle to a deleted entity does not
become null, it becomes stale. Store the IslandId / BuildingId instead and look
it up again.
IMapModel
public interface IMapModel :
IMapLayers, IIslandModelAccessor, IBuildingModelAccessor,
ISuperChunkAccessor, IBunchEditor
{
IMapLayout Layout { get; }
ISimulator Simulator { get; }
IGameResourcesAccessor ResourcesAccessor { get; }
BuildingModel CreateBuilding(IBuildingDefinition definition, in GlobalTileTransform transform, IBuildingConfiguration configuration);
void DeleteBuilding(in BuildingId buildingId);
IslandModel CreateIsland(IIslandDefinition definition, GlobalChunkTransform transform, IIslandConfiguration configuration);
void DeleteIsland(in IslandId islandId);
void GetIslandBuildings(IslandId islandId, ICollection<BuildingModel> buildings);
IEnumerable<BuildingId> GetChunkBuildings(GlobalChunkCoordinate chunk_GC);
int GetBuildingsCount(IslandId id);
}
Simulator is the bridge from "a thing exists here" to "a thing is doing something" —
see Simulations and Item Lanes.
Enumeration and lookup come from IIslandModelAccessor:
IEnumerable<IslandModel> Islands { get; }
int IslandCount { get; }
IEvent<IslandModel> OnIslandAdded { get; }
IEvent<IslandModel> OnBeforeIslandRemoved { get; }
bool TryGetIsland(IslandId islandId, out IslandModel island);
bool TryGetIsland(GlobalChunkCoordinate position, out IslandModel island);
bool TryGetIsland(GlobalTileCoordinate position, out IslandModel island);
bool HasIslandAt(GlobalChunkCoordinate position);
IslandModel GetIsland(IslandId islandId); // throws if absent
IBuildingModelAccessor mirrors this for buildings, including Buildings,
TryGetBuilding, OnBuildingAdded, and OnBeforeBuildingRemoved.
The four add/remove events are how the game's own caches stay current — MapDrawer
registers on all of them to invalidate combined meshes. If you cache anything per
island or per building, do the same.
IslandModel
public readonly struct IslandModel : IEquatable<IslandModel>
{
public IslandId Id { get; }
public IMapModel Map { get; }
public IIslandDefinition Definition { get; }
public IIslandConfiguration Configuration { get; }
public IslandDefinitionId DefinitionId { get; }
public GlobalChunkTransform Transform { get; }
public GlobalChunkCoordinate Position { get; }
public GridRotation Rotation { get; }
public GlobalChunkBounds Bounds { get; }
public IEnumerable<GlobalChunkCoordinate> Chunks { get; }
public int BuildingsCount { get; }
public IEnumerable<BuildingModel> Buildings { get; }
public void GetBuildings(List<BuildingModel> buildings);
public BuildingModel GetBuilding(in IslandTileCoordinate tile_I);
public bool TryGetBuilding(in IslandTileCoordinate tile_I, out BuildingModel building);
public SimulationStateContainer State { get; }
}
Islands are measured in chunks; buildings are measured in tiles
(Coordinates). Chunks walks every chunk the island occupies, which
is what you want for drawing anything that covers a whole platform.
Two allocation notes:
Buildingsis an iterator that internally rents a scoped list — fine toforeach, wasteful in a tight loop. When you are sweeping every island every frame, preferGetBuildings(List<BuildingModel>)with a list you own and clear.LayoutQueryis marked[Obsolete("This is very inconvenient. Check alternative usages (e.g. Chunks).")]and allocates a query object per call. UseChunksorBounds.
BuildingModel
public readonly struct BuildingModel : IEquatable<BuildingModel>
{
public readonly BuildingId Id;
public readonly IMapModel Map;
public bool IsValid { get; } // re-checks the map — see below
public IBuildingDefinition Definition { get; }
public IBuildingConfiguration Configuration { get; }
public IslandModel Island { get; }
public GlobalTileTransform Transform { get; }
public IslandTileTransform Transform_I { get; }
public GlobalTileCoordinate Tile_G { get; }
public IslandTileCoordinate Tile_I { get; }
public GlobalTileBounds Bounds_G { get; }
public IEnumerable<GlobalTileCoordinate> Tiles_G { get; }
public GridRotation Rotation_G { get; }
public int NumConnectors { get; }
public BuildingConnector GetConnector(int connectionIndex);
public BuildingConnection GetConnection(int connectionIndex);
public SimulationStateContainer State { get; }
}
IsValid is not a cheap flag — it does a map lookup every time you read it. Call it
once when you are unsure whether a stored handle is still live, not in a loop.
Tiles_G yields every tile a multi-tile building covers, already rotated into world
orientation. Use it whenever you want to cover a building's footprint rather than just
its origin tile.
Definitions vs. configurations vs. state
Three different things, easily confused:
| Scope | Example | |
|---|---|---|
Definition (IBuildingDefinition) |
one per building type | the cutter's connector layout, its meshes, its base processing duration |
Configuration (IBuildingConfiguration) |
per placed instance, chosen at placement | which shape a miner filters for |
State (SimulationStateContainer) |
per placed instance, changes every tick | what item is on the input lane, how far along it is |
Custom data on a definition
Definitions carry a bag of attached data objects, read through ICustomDataReader:
public interface ICustomDataReader
{
IReadOnlyList<object> All { get; }
TData Get<TData>(); // throws if absent
bool Has<TData>();
bool TryGet<TData>(out TData typedData);
TData GetOrDefault<TData>(TData defaultData);
}
This is how rendering data, sound definitions, connector data, and efficiency data are all attached to a building type without bloating one class. Lookup matches by assignable type and caches the result, distinguishing "no match", "one match", and "several matches" — so asking for an interface works, but asking for one that several attached objects implement will not resolve to a single value.
if (building.Definition.CustomData.TryGet<IBuildingEfficiencyData>(out var efficiency))
{
float baseDuration = efficiency.OriginalProcessingDuration;
int lanes = efficiency.ProcessingLaneCount;
}
Note
BuildingEfficiencyData is attached to every building definition by
BuildingDefinitionFactory, including belts. Its presence does not mean "this is a
processing machine" — check the value, not the existence.
Your own Flow-built buildings get theirs from .WithEfficiencyData(...).
Reading state
SimulationStateContainer is a typed one-slot box:
public bool Is<T>() where T : class, ISimulationState;
public bool Is<T>(out T state) where T : class, ISimulationState;
public T As<T>() where T : class, ISimulationState; // null if wrong type
public T Cast<T>() where T : class, ISimulationState; // throws if wrong type
public T New<T>() where T : class, ISimulationState, new();
if (building.State.Is<HalfCutterSimulationState>(out var cutterState))
{
// the persisted lane states live here
}
State is what gets serialized into the save. For reading what a machine is doing right now, the live simulation object is usually the better source — see Simulations and Item Lanes.
Walking the world
IMapModel map = GameHelper.Core?.LocalPlayer?.CurrentMap;
if (map == null) return;
List<BuildingModel> buildings = new List<BuildingModel>();
foreach (IslandModel island in map.Islands)
{
buildings.Clear();
island.GetBuildings(buildings);
foreach (BuildingModel building in buildings)
{
// building.Definition.Id, building.Tile_G, building.State …
}
}
On a large save this loop is thousands of buildings. Anything you do inside it happens thousands of times — see Mod Lifecycle for pacing it.