Factory Logistics in Mass: Architecture Plan
The Mass shape of the factory floor: what is an entity, which traits and fragments it carries, which processors touch them, and when and on which thread each one runs.
- Status
- Proposed. No code has been written, and all names are proposals.
- Source model
factory-logistics-simulator/floor.jsin the game-design-docs repository, as of commitaf448a7, 2 October 2026 ("Global Config, key values, and machine flags").- Data
- The factory database,
dropforge-database/data/recipes.jsonin the same repository. Changelists 2101 to 2103 imported it into the engine.
📦 1. Entities
Anything a player places or sees on the floor is a Mass entity. Each pipe network and each rail network is also an entity. The game works these out from what has been built.
| Entity | Type asset | Spawned by |
|---|---|---|
| Machine | UMachineData (existing) |
UMachineSpawner (existing), through Server_Build |
| Belt, rail or pipe section, up to 32 tiles | UMachineData. This is the existing tiled machine |
The same |
| Splitter | UMachineData |
The same |
| Pile on the ground, placed or just dropped | UFloorPileEntityTypeAsset |
UFloorPileSpawner (deferred). Whatever puts a component on the ground spawns the pile. That is a lifecycle observer, the topology processor or the bot hand-off processor |
| Bot | ULogisticsBotEntityTypeAsset (existing) |
ULogisticsSpawner (existing) |
| Pipe network. This is a set of connected pipes and the fluid ports they join | UPipeNetworkEntityTypeAsset |
ULogisticsTopologyProcessor (deferred) |
| Rail network. This is a set of connected rails and the ports they serve | URailNetworkEntityTypeAsset |
The same |
| The floor (supply, rules). There is one per surface | UFactoryFloorEntityTypeAsset |
AFactoryFloorManager, for its own surface |
1.1 Type assets and traits
A type asset holds no config. Its class only names the type, so that code can find the type by class. Everything a designer sets is a field on a trait. The trait turns its fields into a const shared fragment, or into the starting value of a fragment in the template, on every net mode that reads it. UMachineData breaks this rule today (section 8).
Each trait covers one behaviour, and every type with that behaviour has the same trait. So a fragment that several types have is added by one trait, never by a trait for each type. When two traits on one type add the same fragment, Mass keeps one copy and says nothing, unless mass.template.DuplicateElementsAsWarnings is turned on. A trait that reads another trait's fragments declares them with RequireFragment. When a type lacks one, Mass logs an error that names the trait and the fragment. It still makes the template.
Nothing in this design is a role. People call a machine a dock, a storage, a delivery line or an airlock. No trait, tag, field or enum says which of those a machine is, and no code asks. A machine is what its traits add and what its recipes say (4.3 and 4.4).
The two networks show the pattern. Each has its own type asset, because the topology processor spawns each by class. Both have UCarrierNetworkTrait for what every network has. Each then has one more trait for what only it does.
A parent config holds the traits that several types share. DA_MachineBase does this today for every UMachineData type. Two more parents hold the traits of a machine that holds components. DA_HoldingMachineBase has UFloorIndexTrait and UMachineBufferTrait. DA_RunningMachineBase is its child, and adds UMachineRecipeExecutionTrait. Mass builds a parent's traits before the type's own. So a trait on a parent cannot read anything the type's own traits hold. It can only add fragments that start empty. A trait that needs the type's own data is on the machine asset. Such a trait can test whether a parent's trait has already added a fragment.
| Trait | What it is for | On |
|---|---|---|
UMachineTrait (existing) |
Makes the entity a machine | Every UMachineData type, through DA_MachineBase |
UHorizonReplicationTrait (existing) |
Replicates the entity | Every type except the two networks, which exist only on the server |
UFloorIndexTrait |
Gives the entity a floor index, so that links can refer to it | Every machine that holds components, every section and the splitter |
UMachinePortsTrait (existing, changed) |
Gives the machine its ports. Belts, rails, pipes and other machines connect to some of them. Bots deliver and collect at the others | Any machine with ports, and the splitter |
UMachineBufferTrait |
Lets the machine hold components, and gives it a priority | Every machine that holds components |
UMachineRecipeAssignmentTrait (today UMachineProcessTrait) |
Gives the machine its recipes, which say what it takes and makes. The player turns them on and puts them in order. Where a recipe uses the wildcard, the trait also gives the machine a stock (4.3 and 4.4) | Every machine that holds components |
UMachineRecipeExecutionTrait |
Runs the machine's recipes, one at a time | Every machine that makes or consumes components |
UMachineFluidTrait |
Lets the machine hold fluid | Any machine type with a pipe port. It is on the machine asset |
UMachineStockCapacityTrait |
Sets the room of the machine's stock (4.4) | A type whose stock does not use the floor rule Space. The storage is one |
UMachineStartPriorityTrait |
Sets the priority a new machine of the type starts with | A type that does not start at the usual priority. The two debug sinks and the airlock are examples |
UFloorSupplyRefusalTrait |
Stops the machine taking from a machine that the floor's supply fills, except by belt or pipe (4.4) | The airlock |
UGroundSweepTrait |
Lets a player set the machine to take what lies on the ground (4.4) | The airlock |
UTiledMachineTrait (existing) |
Makes the machine a section of tiles | Belt, rail and pipe sections |
UCarrierFlowTrait |
Gives each tile a direction of travel and a build serial | Belt, rail and pipe sections |
UCarrierItemsVisualTrait |
Draws the stacks the entity carries | Belt sections, rail sections and the splitter |
UCarrierStacksTrait |
Carries stacks along a run | Belt sections and rail sections |
UBeltTrait |
Marks the section as a belt | Belt sections |
URailTrait |
Marks the section as a rail. Gives each stack a destination, and lets rails branch and take turns | Rail sections |
UPipeTrait |
Joins the section to a pipe network | Pipe sections |
UBeltSplitterTrait |
Shares stacks out between the belts at its sides | The splitter |
UFloorPileTrait |
Makes the entity a pile | The pile |
ULogisticsBotTrait (existing, changed) |
Makes the entity a bot | The bot |
UCarrierNetworkTrait |
Identifies a network by its oldest piece, and sets the order networks are processed in | The pipe network and the rail network |
UPipeNetworkTrait |
Holds the network's junctions, pipe runs and ports, and the fluid it carries | The pipe network |
URailNetworkTrait |
Holds the network's routes | The rail network |
UFactoryFloorRulesTrait |
Sets the floor's rules | The floor |
UFactoryFloorTrait |
Takes in the floor's supply | The floor |
Of these traits, only the ones below hold config. Every other trait adds state and tags, and has no fields.
| Trait | What a designer sets on it |
|---|---|
UMachinePortsTrait |
Each port, and the port a kind uses where the designer has chosen one (4.2). The factory database import writes both |
UMachineRecipeAssignmentTrait |
The processes the type can run, each with a speed (4.3). The factory database import writes them |
UMachineStockCapacityTrait |
The database key value that gives the room. On the storage it is capacity |
UMachineStartPriorityTrait |
The priority |
UTiledMachineTrait |
The section's shapes, as today |
ULogisticsBotTrait |
The bot's speed and meshes, as today. It gains the time a pick-up takes |
🔢 2. Not entities
| Thing | Representation |
|---|---|
| Component in a machine | Buffer count |
| Fluid in a machine | Buffer amount |
| Fluid in a pipe | Nothing held |
| Component on a belt or rail | Stack on section |
| Component crossing a splitter | Stack on splitter |
| Component on a bot | Stack on bot |
| Belt merge | Where runs join |
| Belt run, start to end | Links between sections |
| What a port or tile connects to | Links on that entity |
| Rail routes, pipe junctions | On the network |
| Tiles closed to bots | Occupancy grid |
A component is never an entity. Inside a machine it is a count. Anywhere else it is an FComponentStack (kind, quantity, temperature), stored in a fragment on the entity that is carrying it. A run is a whole belt from its start to its end, as in the simulator. An entity is one section of a run.
⏱ 3. What runs each frame
3.1 The server chain
These processors run on the server every frame. The table lists them in the simulator's order. That is not the order they run in. Each processor declares the processors it has to follow, and Mass schedules them. Worker means the processor may run off the game thread. Demand order ranks competing machines by priority, then longest waiting, then oldest.
Almost every processor runs on a worker. A processor is on the game thread only where it calls something that is safe nowhere else. The chain has two such things. One is marking a proxy dirty for Iris. The rep sync processors for pipes, piles and the floor do that, and so does UFactoryProxySendProcessor. The other is reading the navigation grid, which ULogisticsBotRailPriorityProcessor and ULogisticsBotDispatchProcessor do. The grid is a component of the floor manager, so Mass cannot schedule around it. ULogisticsBotPathRefillProcessor also talks to the navigation grid and stays on the game thread. It is not part of this chain.
A processor is kept safe by what it declares, not by its thread or by its place in a list.
- A processor that reads or writes another entity's fragments declares them with
AddIndirectFragmentRequirement. Every processor that follows a link does this. An indirect requirement reaches a regular fragment only, never a tag or a shared fragment. A processor sometimes needs a fact about the machine at the other end of a link. The topology processor copies that fact into a regular fragment on that machine (4.4). - A processor that reads the occupancy grid declares
UFactoryFloorSubsystemas a subsystem requirement (5.2).ULogisticsTopologyProcessorandUFloorPileProcessorread it. - A processor declares each processor it has to follow with
ExecuteAfter. The second table below lists them. - A processor turns off query-based pruning. Mass otherwise leaves out a processor whose queries match nothing, and a dependency that passes through a processor that was left out is lost.
- A processor defers its commands through its execution context.
| Processor | Thread | What it does |
|---|---|---|
UMachineSettingsProcessor |
Worker | Applies the settings changes players have asked for, on machines and splitters. Applies a requested change to the floor's supply. When it has applied a change, it adds FFloorTopologyDirtyTag to the floor entity |
ULogisticsTopologyProcessor |
Worker only while the floor entity has FFloorTopologyDirtyTag |
Gives each new machine, section and splitter a floor index. Reverses any run a player flipped. For the island that changed, it works out what every port and tile connects to and writes that into each entity's link fragment. It creates, updates or destroys the island's pipe and rail networks. It recalculates buffer shares and each stock's room (4.4). It recounts every reservation from the rail stacks and bot jobs that exist. It puts down as a pile, deferred, a rail stack that it cannot re-address. Then it removes the tag |
UFloorSupplyProcessor |
Worker | Adds each kind the floor supplies to a machine that the floor's supply fills (4.4), at the floor's rate for that kind. It picks the machine with the most room left for that kind, and the oldest on a tie. A solid arrives as whole components. While no machine has room for a solid kind, the kind's credit stops at one component. A fluid arrives as a small amount every frame |
UMachineRecipeExecutionProcessor |
Worker in parallel across chunks | Starts, advances and finishes recipe runs. A machine starts the first enabled recipe in its order that it holds the inputs for and has room for the outputs of. This applies to solids and fluids alike. The inputs leave its buffer when the run starts, and the outputs are added when it finishes. It never runs a recipe that takes nothing, or a recipe that makes exactly what it takes (4.3). It also records when demand for each kind began. It does that for every machine with a ledger, whether or not the machine runs recipes |
UDirectConnectionProcessor |
Worker | Handles an output port that directly faces an input port of the same type (belt or pipe). It moves across everything the receiving machine has room for. Solid components go oldest first. Fluid goes as an amount |
UPipeFlowProcessor |
Worker | Runs for each pipe network entity that carries a fluid, oldest network first. It moves fluid from the machines holding it to the machines with room for it. Receiving machines are served in demand order, and fluid is taken first from the machine holding the most. A pipe run carries at most the pipe rate multiplied by the frame time |
URailAdvanceProcessor |
Worker | Runs for each rail network entity, oldest first. It moves each stack along its rail with the same movement code as belts, at rail speed. At a branch, a stack takes the way to its destination. Where rails join, they take turns. It delivers a stack that reaches its machine. It rotates a full closed loop as one |
ULogisticsBotRailPriorityProcessor |
Game | Sends a bot to collect a component before a rail can load it. This applies only when the only source of that kind is a machine a rail loads from, and the machine demanding it has higher priority (ranks higher in demand order) than every machine the rail would deliver it to. If no idle bot is available, the rail loads the component as usual |
URailLoadProcessor |
Worker | Runs for each rail network entity, oldest first. It loads components that a downstream machine has demanded, and reserves room for each one at that machine |
UBeltTransportProcessor |
Worker | Moves stacks along each run at belt speed. It keeps them apart by the stack radius. A full loop of runs and splitters rotates as one. Then it does five things in order. First, a machine takes a stack from a belt passing its input port. It does so when it demands the kind, or when it holds a stock that accepts the kind (4.4). Second, a stack at the end of a run enters the machine there. Third, each splitter sends its front stack to an output. Fourth, anything joining a run adds a stack where there is a gap. That can be a port, a splitter or another run's end. Fifth, each splitter takes a stack from an input |
UFloorPileProcessor |
Worker | Places each unplaced pile (FFloorPileUnplacedTag) on the nearest free tile, or merges it into a pile of the same kind already there. It destroys a pile that it has merged into another, deferred. A tile holds one kind. It reads the occupancy grid for free tiles, and finds the piles already on a tile with a query |
ULogisticsBotHandoffProcessor |
Worker | Completes a pick-up or a delivery when a bot arrives. It destroys a pile, deferred, when a bot takes the last of it. Cancels a job whose source or destination has been removed. When the bot of a cancelled job is carrying a stack, it puts the stack down as a pile, deferred |
ULogisticsBotDispatchProcessor |
Game | Assigns idle bots to machine demand. It finds idle bots with a query (a bot without FLogisticsBotOrderedTag), not from a registry. For each demand, in demand order, it picks a source and the nearest idle bot. It reserves the component at the source and the room at the destination in the same pass. It tries a placed pile first. Next it tries a machine with no built connection delivering that kind from it. Last it tries a machine whose built connection does deliver that kind from it. When it has gone through every demand, it gives sweep jobs to each machine that is set to sweep (4.4). There is a limit on the number of jobs it creates per frame |
UMachineProductionRepSyncProcessor |
Worker | For each machine tagged FHorizonRepDirtyTag, copies its buffer, run, settings, recipe assignment and stock into its replicated fragments. It leaves the tag for UFactoryProxySendProcessor. Each processor that changes one of those things on a machine adds the tag to that machine, deferred |
UCarrierRepSyncProcessor |
Worker | Samples belt sections, rail sections and splitters into their replicated fragments. It does this no more often than the sample interval. It tags each entity it samples with FHorizonRepDirtyTag, deferred, so UFactoryProxySendProcessor sends the sample on the next frame |
UPipeRepSyncProcessor, UFloorPileRepSyncProcessor, UFactoryFloorRepSyncProcessor |
Game | Each sends its replicated state when that state has changed. Each one copies a replicated fragment into its proxy and marks the proxy dirty. UPipeRepSyncProcessor first copies the network's fluid on to the section (4.5) |
UFactoryProxySendProcessor |
Game | For each machine, section and splitter tagged FHorizonRepDirtyTag, copies its replicated fragments into its proxy and marks the proxy dirty. Then it removes the tag. This is the game-thread half of UMachineProductionRepSyncProcessor and UCarrierRepSyncProcessor |
A dirty tag is added deferred, so the topology processor runs on the frame after the change. For that one frame the chain uses the new settings with the old shares and links.
Eight existing processors are named in section 4 and are not part of this chain.
UTiledMachineSplitProcessorcuts and joins sections (4.6), on the game thread.UMachineFootprintRegistrationProcessoradds a machine's cells to the occupancy grid.UMachineUtilizationProcessorandUArmWorkProcessorwrite utilisation.ULogisticsBotStateProcessormoves a bot through its job.ULogisticsBotPathRefillProcessorgives a bot the next part of its path.ULogisticsBotRepSyncProcessorsends a bot's cargo.UMachineRepSyncProcessorsendsFMachineRepStateFragment.
What each processor follows. Mass keeps two processors apart while they touch the same fragment, but it does not choose which goes first. Where the order changes the result, the later processor declares the earlier one. Where it does not, nothing is declared. Mass may then run the two in either order, or at the same time when they share nothing.
Most of these dependencies come from one rule of the simulator. Direct connections, rails, belts and bots compete for the same components and the same room, and they take them in that order.
| Processor | Follows | Why |
|---|---|---|
ULogisticsTopologyProcessor |
UMachineSettingsProcessor, and the existing UMachineFootprintRegistrationProcessor |
It works out shares from the settings and recipes that the settings processor applies. It reads the occupancy grid to work out links, and footprint registration fills that grid |
UFloorSupplyProcessor |
ULogisticsTopologyProcessor |
It needs the shares |
UMachineRecipeExecutionProcessor |
ULogisticsTopologyProcessor |
It needs the shares, and the rows and room of a stock |
UDirectConnectionProcessor |
UFloorSupplyProcessor, UMachineRecipeExecutionProcessor |
It moves what the floor supplied and what a run made in this frame |
UPipeFlowProcessor |
UDirectConnectionProcessor |
A direct connection takes fluid before a pipe does |
URailAdvanceProcessor |
UDirectConnectionProcessor |
A direct connection fills room before a rail delivers into it |
ULogisticsBotRailPriorityProcessor |
URailAdvanceProcessor |
It acts between a rail advancing and a rail loading |
URailLoadProcessor |
ULogisticsBotRailPriorityProcessor |
It loads only what that processor did not send a bot for |
UBeltTransportProcessor |
URailLoadProcessor |
A rail loads a component before a belt takes it |
UFloorPileProcessor |
ULogisticsTopologyProcessor |
Topology recounts what is reserved on a pile |
ULogisticsBotHandoffProcessor |
UBeltTransportProcessor, UFloorPileProcessor |
Bots come after every built connection. A pile has to be merged before a bot can empty it |
ULogisticsBotDispatchProcessor |
ULogisticsBotHandoffProcessor |
A bot that has just delivered can be given a new job |
UMachineProductionRepSyncProcessor |
UPipeFlowProcessor, ULogisticsBotDispatchProcessor |
It copies what every processor has left in the machine |
UCarrierRepSyncProcessor |
UBeltTransportProcessor |
It samples the stacks after they have moved |
UPipeRepSyncProcessor |
UPipeFlowProcessor |
It copies a fragment after the last processor that writes it |
UFloorPileRepSyncProcessor |
ULogisticsBotHandoffProcessor |
The same |
UFactoryFloorRepSyncProcessor |
UMachineSettingsProcessor |
The same. The settings processor writes the supply rates |
UFactoryProxySendProcessor |
UMachineProductionRepSyncProcessor, UCarrierRepSyncProcessor |
It sends what they wrote |
Processors with no dependency between them are free of each other. UFloorSupplyProcessor and UMachineRecipeExecutionProcessor change different things. A machine that the floor's supply fills has no demand and runs no recipe. UPipeFlowProcessor moves fluid, and the rail, belt and bot processors move solids. UFloorPileProcessor does not touch a machine. The rep sync processors copy different entities.
3.2 Observers
An observer runs when Mass applies the removal, not at a fixed point in the chain. The existing server observers run on the game thread today. The new pile observer runs there too.
| Observer | On | Does |
|---|---|---|
UMachineLifecycleProcessor (existing) |
FActiveMachineTag removed |
Unregisters the footprint. For a section that takes its tiles out of the occupancy grid at their levels. Adds FFloorTopologyDirtyTag to the floor entity (deferred). Spawns a pile (deferred) for each solid kind the machine held. It also spawns one for each solid kind that a run in progress had taken. For a run of r-store that is the kind in RunKind (4.4). For a section or a splitter it spawns a pile for each stack. Any fluid the machine held, or a run had taken, is lost |
ULogisticsBotLifecycleProcessor (existing) |
FLogisticsBotActiveTag removed |
Spawns a pile (deferred) for the stack the bot was carrying. Adds FFloorTopologyDirtyTag to the floor entity (deferred), so the recount removes the reservations for its job |
UFloorPileLifecycleProcessor |
FActiveFloorPileTag removed |
Unregisters the pile from Horizon |
UClientMachineFootprintDeregistrar (existing, client only) |
FMachineRepStateFragment removed |
Does what it does today. For a section that takes its tiles out of the occupancy grid at their levels |
A section's tiles are in the occupancy grid at their own levels (5.2), so they need no new observer. On the server, UMachineFootprintRegistrationProcessor adds a section's tiles and UMachineLifecycleProcessor removes them. On a client, UClientMachineFootprintProcessor adds them and UClientMachineFootprintDeregistrar removes them. These four classes already maintain the occupancy grid in the same way.
A removal puts nothing on the ground when a whole floor is cleared. Clearing a floor destroys what the floor holds. When a section is cut or joined, the split processor has already moved the stacks on the tiles that stay to the new sections (4.6). So the observer puts down only a stack over a tile that the cut removed.
3.3 The client side
These processors run on clients and in standalone, and a listen host runs them too. They are in the mesh-instance writer group (DFMeshInstances::WriterGroup). They read only replicated state, never the server's stacks, and that includes on the host. Each new one follows the rule below for its thread.
Today's code follows one rule for these threads. A writer that only touches its own entity's fragments is not pinned to the game thread. UTiledMachineVisualizationProcessor and UMachineStatusVizProcessor are examples. A writer that loads meshes or submits effects is pinned. UMeshInstanceApplyProcessor is pinned and runs after the group.
| Processor | Reads | Draws |
|---|---|---|
UCarrierItemsVisualizationProcessor |
The stack samples of a belt section, rail section or splitter. Also the section's flow, tiles and shapes, and the render clock | One movable instance per stack. It slides each stack between samples |
UPipeVisualizationProcessor |
FPipeFlowRepStateFragment |
Sets a custom float on the section's tiles for the fluid, the flow level and whether it is at its maximum |
UFloorPileVisualizationProcessor |
FFloorPileRepStateFragment |
The kind's mesh, stacked according to the count |
UMachinePortVisualizationProcessor |
FMachinePortsSharedFragment, the occupancy grid and a splitter's priority sides |
Port markers, a scoop where a belt passes a port, and a bar along a splitter's priority side |
UTiledMachineVisualizationProcessor (existing) |
FCarrierFlowRepStateFragment, where it used to read tile order |
The direction float |
UMachineStatusVizProcessor (existing) |
FMachineProductionRepStateFragment, where it used to read slot states |
The status float. It works out run progress from the run's start time and length |
ULogisticsBotVisualizationProcessor (existing) |
The carried kind | The cargo mesh |
UClientMachineFootprintProcessor (existing, client only) |
Tiles, flow | Draws nothing. It adds a section's tiles to the occupancy grid at their levels |
A component has no mesh yet. Until it has one, a stack, a pile and a bot's cargo each draw one placeholder mesh for it.
🧩 4. Traits and fragments, by theme
The tables name the processors of section 3. Server means != NM_Client, everywhere means every net mode, and rendering means != NM_DedicatedServer. FKindId and FRecipeId are each a uint16. The factory database import writes a kind's id on the kind's asset, and the id never changes. The catalogue gives each recipe an id at start-up (5.2). Two kind ids are reserved. One means no kind. The other is the wildcard (4.3). The import never gives either to a component. The Trait column names the trait that adds the fragment or tag. One existing fragment has two (4.3). In these tables the machine rep sync processor is UMachineProductionRepSyncProcessor. Section 1.1 lists the traits and the types that have each.
4.1 Caps
| Cap | Limit | Array it sizes |
|---|---|---|
| Recipes assigned to a machine | 16 | Recipes, Order and RowStart in FMachineRecipeAssignmentFragment. A machine type can have no more recipes than this |
| Kinds a recipe takes, and kinds it makes (each) | 4 | A recipe's buffer rows |
| Buffer rows on a machine, solid and fluid | 48 | Held and Share in FMachineBufferFragment, and Amount in FMachineFluidFragment |
| Kinds a machine handles | 24 | InboundEnroute, OutboundReserved and DemandSince in FMachineLedgerFragment, and Kind in FMachineStockFragment. A stock (4.4) has rows for no more than 24 kinds at once. A kind that is named or supplied uses a row even while the stock holds none of it |
| Kinds a player can limit a stock to | 16 | FMachineKindFilterFragment::Kinds |
| Kinds a built connection brings to a stock, and kinds one takes from it (each) | 24 | The two lists of kinds in FMachineLinksFragment |
| Kinds a machine counts as consumed | 16 | FMachineConsumedFragment::Kind. Any later kinds are counted together |
| Ports on a machine | 16 | FMachinePortsSharedFragment |
| Solid components in a machine's queue | 256 | FMachineLedgerFragment::Queue. The queue is a machine's solid output, or every solid component in a stock. So a stock's room can be no more than 256 |
| Kinds supplied to a floor | 16 | FFactoryFloorSupplyFragment::Kind |
| Tiles in a section | 32 | This is the existing tiled machine's limit |
| Stacks on a belt or rail section | 64 | FCarrierStacksFragment::Items. A section of 32 tiles is 3,200 cm long, so this allows a stack radius as small as 25 cm |
| Stacks crossing a splitter | 2 | FBeltSplitterFragment::Items. A splitter is 100 cm across, so this allows the same smallest radius |
Three fragments hold arrays that grow, and have no cap. They are the two network fragments (4.5 and 4.8) and the bot's path fragment (4.10).
The factory database is well inside these today. Its largest machine type has 7 recipes, 16 buffer rows, 12 kinds and 12 ports, of which 2 are bot ports. No recipe has more than 3 kinds on a side.
4.2 Every machine that holds components
A machine that holds components has UFloorIndexTrait, UMachineBufferTrait and UMachineRecipeAssignmentTrait (4.3). Sections and splitters have UFloorIndexTrait too.
A machine with ports also has UMachinePortsTrait. The factory database import writes its ports. The splitter is the exception. The database gives it no ports, so they are set in the engine. Each port has a type from the factory config, which is belt, pipe or bot. A kind's form is solid or fluid. A belt port carries solids and a pipe port carries fluid. A component that the database calls loose counts as a solid. A belt or pipe port is an input or an output, and it is on one edge of one cell. A splitter's four ports are neither. The topology processor decides each one from the belt that joins it (4.7). A bot port marks a cell where bots deliver and collect.
The trait builds two shared fragments from that one list. FMachinePortsSharedFragment holds the ports that belts, rails, pipes and other machines connect to. FMachineBotPortsSharedFragment holds the bot cells.
The port a kind uses. The database gives a port for some kinds. The import writes those ports into two maps on the ports trait. One map is for what a machine takes and one is for what it makes. A kind can be in both, because a machine can take a kind and also make it. For a kind in neither map, the port is worked out by the rule the database's own tool uses. The kind takes the first port that carries its form and has the right direction. It passes over a port that another kind in one of its recipes already has. UMachineRecipeAssignmentTrait works this out when the type's template is built, and keeps the answer in FMachineRecipeSetSharedFragment (4.3). It reads the ports trait on its own asset to do so, which a trait may do while a template is built. A machine with a stock gives no kind a port. Each of its ports takes anything of its form (4.4).
| Fragment | Trait | Where | Fields | Written by | Read by |
|---|---|---|---|---|---|
FFloorIndexFragment |
UFloorIndexTrait |
Server | uint16 Index; uint16 Island; uint32 BuildSerial. Index is the number other entities' links use to refer to this one. Island is the group of connected machines it belongs to. BuildSerial is the order the entity was built in (5.1). For a section it is the lowest serial of its tiles |
The topology processor writes Index and Island. The spawn writes BuildSerial |
Every processor that follows a link or ranks demand |
FMachineLinksFragment |
UMachineBufferTrait |
Server | For each port (up to 16), what it is connected to. That is another machine, a belt or rail tile, a splitter or a pipe network. It also holds two bit sets per port. One is the kinds a built connection already delivers to the port. The other is the kinds one already delivers from it. Bots leave those kinds alone. A bit stands for one of the machine's kinds, in the order of its ledger. On a machine with a stock the kinds change between topology runs. So there the fragment holds two lists of kind ids, of up to 24 each. One is the kinds that built connections bring. The other is the kinds they take. Each list has a flag that means any kind. The fragment also holds uint8 TradeFlags. These are three facts about the machine itself. A processor at the other end of a link reads them (4.4) |
The topology processor | The direct connection processor, the two rail processors, the bot rail priority processor and the bot dispatch processor |
FMachineSettingsFragment |
UMachineBufferTrait |
Server | uint8 Priority; bool bBotLogistics. Priority starts at 11, or at the value UMachineStartPriorityTrait gives the type. bBotLogistics is whether bots deliver to the machine and collect from it. It starts true |
UMachineStartPriorityTrait sets the starting Priority in the template. The settings processor writes both fields after that |
Every processor that ranks demand, and the machine rep sync processor |
FMachineBufferFragment |
UMachineBufferTrait |
Server | uint16 Held[48]; uint16 Share[48]. A row is one kind, on one side (input or output), of one recipe the machine is assigned (4.3). On a machine with a stock, a row is one kind (4.4). Held is the number of components in a solid row. Share is the most the row may hold, whether solid or fluid. A row's room is its share, less what it holds and what is coming |
The topology processor writes Share. Every processor in 3.1 from the floor supply processor to the bot hand-off processor writes Held, except the pipe flow, bot rail priority and pile processors |
The same processors, the pipe flow processor, the bot rail priority processor, the bot dispatch processor, the machine rep sync processor and the machine observer |
FMachineLedgerFragment |
UMachineBufferTrait |
Server | uint16 InboundEnroute[24]; uint16 OutboundReserved[24]; double DemandSince[24]; uint8 Queue[256]; uint16 QueueCount. An entry is one kind. On a machine with no stock, the kinds are in the order they first appear in the type's recipes, and FMachineRecipeSetSharedFragment holds that list. A processor that reaches the machine through a link cannot read a shared fragment (3.1). It works the order out from the machine's assigned recipes and the catalogue, as it does for a kind's rows (4.3). InboundEnroute counts components on their way to the machine by rail or bot. Room is held for them. OutboundReserved counts output held for a bot that is on its way to collect it. DemandSince is when the machine began to wait for the kind. It is set when the kind first has room, and cleared when it has none. It starts again each time a rail or a bot reserves room for the kind, and each time a pipe delivers it. Queue is the machine's solid output in the order it was made. Each entry is a buffer row. On a machine with a stock, the queue is every solid component in the stock, oldest first |
The topology processor, when it recounts reservations. Every processor in 3.1 from the floor supply processor to the bot dispatch processor, except the pile processor | The same processors, and the machine rep sync processor |
FMachineProductionRepStateFragment |
UMachineBufferTrait |
Everywhere | uint8 Priority; uint8 Flags; uint8 RunRecipe; FKindId RunKind; float RunStartServerTime; float RunSeconds; uint8 Blocker; FKindId BlockerKind; uint8 Fill[48]. Flags say whether bots serve the machine and whether it is set to sweep. RunRecipe is the index of the running recipe in the machine's assigned recipes (4.3). RunKind is the kind that a run of r-store took (4.4) |
The machine rep sync processor | Status visualisation and the inspector |
FMachinePortsSharedFragment |
UMachinePortsTrait |
Const shared, everywhere | The ports that belts, rails, pipes and other machines connect to, in the machine's own frame. It also holds the two maps of chosen ports | The trait | The topology processor, port visualisation and placement preview |
FMachineBotPortsSharedFragment |
UMachinePortsTrait |
Const shared, everywhere | The bot cells, in the machine's own frame | The trait | The bot rail priority and bot dispatch processors, the navigation grid and placement preview |
FMachineSettingsRequestFragment |
None. A Server_ RPC adds it |
Server, transient | What a player has asked to change | Server_ RPCs |
The settings processor, which then removes it |
When a recipe is turned on or off, the machine's shares are worked out again from scratch. On a machine with no stock, the room on each side is Headroom runs of every recipe the type can run (4.11). The topology processor shares that room among the recipes that are turned on, in proportion to what one run of each takes or makes. Whole components left over go one each to the largest fractions, and to the earlier recipe on a tie. A row of a recipe that is turned off has no share. This is the simulator's rule.
4.3 Recipes and runs
Every machine in 4.2 has UMachineRecipeAssignmentTrait. It gives the machine its recipes. A recipe says what the machine takes and what it makes. The trait is today's UMachineProcessTrait, and it is on the machine asset.
A machine with UMachineRecipeExecutionTrait also runs its recipes. Every machine that makes or consumes components has it. UMachineRecipeExecutionTrait declares FMachineRecipeAssignmentFragment with RequireFragment.
What a type can run. The trait lists the processes the machine type can run, each with a speed. The factory database import writes that list. A machine can run every recipe of those processes. That is the one rule, and it is the same for every machine.
What a machine starts with. The catalogue is UFactoryCatalogueSubsystem (5.2). When the type's template is built, the trait asks the catalogue for every recipe of the type's processes. It writes them into FMachineRecipeAssignmentFragment in the template. So every machine of the type is spawned with its recipes, all turned on, in the catalogue's order. Nothing adds a recipe to a machine after that.
The trait adds that fragment and its replicated copy only for a type that has FMachineBufferFragment. A parent config's trait adds the buffer fragment, and Mass builds a parent's traits first (1.1). On any other type the trait adds FMachineRecipeSetSharedFragment and nothing else.
How long a run takes. A run takes the recipe's time, divided by the process's base speed and by the speed the type gives that process. The type's speed comes from the factory database. The base speed is set on DA_FactoryConfig in the engine, and the import keeps it. A recipe with no time takes 1 second.
The wildcard. The database has one kind that is not a component. It is written *, and it stands for any kind. Three recipes use it, and each is the only recipe of its process.
| Process | Recipe | Takes | Makes |
|---|---|---|---|
supply |
r-supply |
Nothing | One of the wildcard |
store |
r-store |
One of the wildcard | Nothing |
pass |
r-pass |
One of the wildcard | One of the wildcard |
A type gets these recipes as it gets any other, by listing the process. The catalogue keeps each as one recipe. There is no recipe for each kind. A type lists only these three processes, or it lists none of them. Validation reports a type that lists one of them beside any other process. Section 4.4 says what a machine with these recipes holds.
Recipes that never run. The recipe execution processor never runs a recipe that takes nothing, so nothing is made from nothing. It never runs a recipe that makes exactly what it takes, because a run would change nothing. Of the three recipes above, only r-store can run.
What a machine is assigned. FMachineRecipeAssignmentFragment holds up to 16 recipes. A recipe keeps its index in Recipes for as long as the machine exists, because other state refers to the recipe by that index. The running recipe is stored as an index, and a recipe's buffer rows start at the RowStart of its index. The player turns recipes on and off and puts them in order. The order is stored apart, in Order, so changing it moves nothing.
A recipe's rows. A recipe that does not use the wildcard has one buffer row for each kind it takes and one for each kind it makes. They are together in FMachineBufferFragment, with the kinds it takes first. A processor finds a kind's rows the same way on every machine that has no stock. It goes through the machine's assigned recipes, reads each recipe's kinds from the catalogue and counts on from RowStart. A recipe that uses the wildcard has no rows of its own (4.4).
Where an arrival goes. On a machine with no stock, a kind's room is the room left in its input rows of the recipes that are turned on, less what is coming. A component that arrives goes into the row of the first such recipe, in the machine's order, that has room. Fluid fills those rows in the same order.
| Fragment | Trait | Where | Fields | Written by | Read by |
|---|---|---|---|---|---|
FMachineRecipeAssignmentFragment |
UMachineRecipeAssignmentTrait, on a type that has FMachineBufferFragment |
Server | FRecipeId Recipes[16]; uint8 Order[16]; uint16 EnabledMask; uint8 RowStart[16]; uint8 Count. Recipes is the recipe at each index. Order lists the indexes, highest priority first. EnabledMask has one bit for each index. RowStart is the first buffer row of each index. Count is the number of recipes |
The trait sets what a machine starts with. The settings processor writes Order and EnabledMask after that |
The topology processor, the recipe execution processor, every processor that reads or writes a buffer row, and the machine rep sync processor |
FMachineRecipeSetSharedFragment |
UMachineRecipeAssignmentTrait |
Const shared, everywhere | It holds the processes the type can run, with the speed of each. It holds the type's recipe ids, in the order a new machine is assigned them. It holds the type's kinds, in the order of the ledger (4.2). It holds the port each kind uses, as a kind the type takes and as a kind the type makes (4.2). It is today's FMachineProcessSharedFragment |
The trait | The settings processor, the topology processor, the recipe execution processor and the windows |
FMachineRecipeAssignmentRepStateFragment |
UMachineRecipeAssignmentTrait, on a type that has FMachineBufferFragment |
Everywhere | The same fields as FMachineRecipeAssignmentFragment |
The machine rep sync processor | The inspector |
FMachineRecipeExecutionFragment |
UMachineRecipeExecutionTrait |
Server | uint8 Recipe; FKindId RunKind; double SecondsLeft; float Seconds; uint32 Made; uint8 Blocker; FKindId BlockerKind. Recipe is the index of the running recipe in the machine's assigned recipes. RunKind is the kind that a run of r-store took (4.4) |
The recipe execution processor | The machine rep sync processor, utilisation and the machine observer |
FMachineModifiersFragment (existing) |
UMachineRecipeExecutionTrait |
Server | Unchanged. It is a rate applied to the run | Nothing writes it today | The recipe execution processor |
FMachineUtilizationFragment (existing) |
UMachineRecipeExecutionTrait. UArmMachineTrait adds it to arms |
Server | Averages by blocker. It used to hold averages by slot state | The utilisation processor, and the arm processor for an arm | The monitor |
4.4 Machines that hold a stock
A machine whose recipes use the wildcard holds a stock. A stock is a store of components of any kind. Every recipe on the machine that uses the wildcard acts on that one stock.
- A recipe that takes the wildcard lets the stock take components in.
r-storeandr-passdo. - A recipe that makes the wildcard lets the stock give components out.
r-supplyandr-passdo. - A run of
r-storetakes one component out of the stock and makes nothing, so the component is consumed. The run takes the oldest solid. With no solid, it takes one unit of the fluid the stock holds most of. Only a machine withUMachineRecipeExecutionTraitruns anything.
A stock takes while a recipe that takes the wildcard is turned on. It gives while a recipe that makes the wildcard is turned on. So a debug sink with r-store turned off takes nothing. The storage stops taking only when r-store and r-pass are both off. It stops giving only when r-supply and r-pass are both off.
Six machine types in the factory database have a stock today.
| Type | Lists | Runs recipes | So its stock |
|---|---|---|---|
| The debug component source and the debug fluid source | supply |
No | Gives, and can take nothing. Nothing can be delivered to it. The floor's supply fills it |
| The debug component sink and the debug fluid sink | store |
Yes | Takes, and gives nothing. The machine consumes what it holds, one component for each run |
| The airlock | store |
Yes | The same as the two debug sinks |
| The storage | supply, store and pass |
No | Takes and gives, and consumes nothing. While r-store and r-supply are both on, r-pass adds nothing |
None of these is a role (1.1). Each difference between these machines is one of three things. It is a recipe that the type lists, a trait that the type has, or a value on a trait.
The setting that limits a stock. A player can limit a stock to a list of kinds. This section calls that list the stock's setting. FMachineKindFilterFragment holds it. It names up to 16 kinds, in the player's order. Server_SetMachineKinds sets it (section 6). A stock with no setting accepts any kind. A stock whose setting names no kinds takes nothing. The setting limits what comes in. It never stops a stock giving what it holds. A player can take a kind out of the setting while the stock holds some. Those components stay in the stock, and the stock still gives them.
Which forms a stock holds. A stock holds solids only where the type has a belt port, and fluid only where it has a pipe port. So the storage holds no fluid, and no check names the storage. The settings processor leaves out of a setting any kind whose form the stock does not hold. Validation reports a type that has a pipe port and no UMachineFluidTrait.
Where a stock is kept. A stock uses the fragments every machine in 4.2 has. On a machine with a stock, a buffer row is a kind. FMachineStockFragment::Kind says which kind each of the first 24 rows is for. Held is how many solid components of that kind the stock holds. FMachineFluidFragment::Amount is how much the stock holds of a fluid kind. FMachineLedgerFragment has three entries for the same row. InboundEnroute is how many are coming. OutboundReserved is how many are reserved for a bot. DemandSince is when demand began. The ledger's queue lists every solid component in the stock, oldest first.
Who gives a kind its row. A processor that brings a kind to a stock, or reserves room for it, gives the kind a row the first time. The topology processor gives a row to each kind the setting names. On a machine that the floor's supply fills, it also gives a row to each kind in the stock's list (see below). It also gives a row to the fluid of a pipe network on an input port (see Demand). Nothing clears a row when it empties. A processor that needs a row for a new kind takes the first free row and writes the new kind over the old one. It also clears that row's DemandSince. A row is free when five things are true.
- The row holds nothing.
- No component of its kind is coming or reserved.
- The setting does not name its kind.
- Its kind is not in the list of a stock that the floor's supply fills.
- Its kind is not the fluid of a pipe network on one of the machine's input ports.
A kind with no row, and no free row to take, has no room. When the setting names that kind, or the floor supplies it, the kind has no room and no share until the topology is next rebuilt.
How much a stock holds. A stock's room is the floor rule Space (4.11), which is 12 today. A type with UMachineStockCapacityTrait has a room of its own. The storage has that trait, and its room is the database key value capacity, which is 30. The topology processor writes the room into FMachineStockFragment::Room.
How kinds share the room. There are three cases. A stock's case comes from the recipes its type lists. Turning a recipe off does not change it.
- A stock whose type can take and can give is one pool. A kind's room is the whole room, less everything the stock holds and everything that is coming. The storage is such a stock.
- A stock with a list of kinds gives each kind a share. A stock that the floor's supply fills has a list. With a setting, the list is the kinds the setting names. With no setting, it is the kinds the floor supplies that the stock can hold, in the order of
FFactoryFloorSupplyFragment::Kind. A stock that can only take, and has a setting, also has a list. It is the kinds the setting names. Each kind in a list has an equal share of the room. Any remainder goes one each to the earliest kinds. When the list has more kinds than the room, the later kinds have no share. A kind's room is its own share, less what the stock holds of it and what is coming. So a kind that no machine collects from the stock cannot fill another kind's share. The topology processor writes each share intoShare. - A stock that can only take, and has no setting, has one total for solids and one for fluid. A solid's room is the whole room, less the solids the stock holds and the solids that are coming. A fluid's room is the whole room, less all the fluid the stock holds. So fluid from a pipe cannot keep solids out.
Only a stock with a list of kinds uses Share. On the other two, no processor reads it.
A stock can hold more than its room for a time. That happens when it still holds a kind that has left its list. The queue is the hard limit. When the queue is full, no solid has room.
A processor works out a kind's room again before each reservation, because kinds can share a stock's room.
What brings a kind to a stock. A stock that does not take has no room for anything in this table, whatever its setting names. Its shares are only for the floor's supply.
| What brings it | With no setting | With a setting |
|---|---|---|
| A direct connection | Any kind of the port's form that the stock has room for | The named kinds only. A component of another kind stays in the machine it is in. It does not block the named kinds |
| A belt | Any solid that passes the port or ends at it | The named kinds only. A stack of another kind goes past a port at the side of a belt. At a port at the end of a run, it waits |
| A pipe | Whatever fluid the network carries. The stock does not affect which fluid that is (4.5) | The named fluids. The topology processor counts them when it works out the network's fluid |
| A rail or a bot | Nothing | The named kinds, in demand order |
A component that was reserved before a setting or a recipe changed still arrives, and goes into its kind's row. A component that was reserved for a bot is still collected.
Demand. A stock that takes has demand for the kinds its setting names. With no setting it has none, because it would otherwise demand every kind on the floor. There is one more case. A stock that takes fluid and has no setting has demand for the fluid of a pipe network on one of its input ports. The topology processor keeps a row for that fluid. A stock that does not take has no demand.
The floor's supply. UFloorSupplyProcessor fills a machine that has FFloorSuppliedTag, while its stock gives. UMachineRecipeAssignmentTrait adds that tag when the type's template is built. The trait adds the tag to a type whose stock can give and can never take, because nothing else can fill such a stock. No designer sets the tag. With a setting, the floor's supply adds the named kinds only. With no setting, it adds every kind the floor supplies that the stock can hold.
Which machine may give to which. There are two rules.
- A stock that takes and gives passes a component to another such stock only when the other has the higher priority. So two storages never pass a component back and forth. The rule looks at the recipes that are turned on.
- A machine that the floor's supply fills gives nothing to a machine with
FRefusesFloorSupplyTag.UFloorSupplyRefusalTraitadds that tag, and the airlock has it.
The direct connection processor, the two rail processors, the bot rail priority processor and the bot dispatch processor apply both rules. The belt transport processor and the pipe flow processor do not, as in the simulator. So a belt or a pipe still carries the floor's supply into an airlock. A processor cannot read a tag on the machine at the other end of a link (3.1). So the topology processor copies three facts into FMachineLinksFragment::TradeFlags on each machine. The first is whether its stock takes and gives now. The second is whether the floor's supply fills it. The third is whether it refuses the floor's supply.
Sweeping. A player can set a machine with UGroundSweepTrait to sweep, and the airlock has that trait. The machine sweeps only while bots serve it. Bots then bring it components that lie on the ground. They bring a kind only when no machine demands it and the machine's own setting does not name it. The bot dispatch processor gives sweep jobs lower priority than every other job. A sweep job reserves the component at its pile and nothing at the machine. The bot hand-off processor consumes a swept component as it arrives. The component does not enter the stock.
What a machine has consumed. FMachineConsumedFragment counts it, kind by kind. The recipe execution processor counts a component when its run ends. The bot hand-off processor counts a swept component when it arrives.
| Fragment or tag | Trait | Where | Fields | Written by | Read by |
|---|---|---|---|---|---|
FMachineStockFragment |
UMachineRecipeAssignmentTrait, on a type whose recipes use the wildcard |
Server | FKindId Kind[24]; uint16 Room; uint8 Flags. Kind is the kind each buffer row is for, or none if the row has never been used. Room is the stock's room. Flags has one bit for each of six facts. The stock takes. The stock gives. The stock holds solids. The stock holds fluid. The type can take. The type can give. The first two follow the recipes that are turned on. The last four never change, and the trait sets them in the template. The last two decide how kinds share the room |
The topology processor writes Room and the first two bits of Flags. It gives a row to each kind the setting names. On a machine that the floor's supply fills, it gives a row to each kind in the stock's list. It gives a row to the fluid of a pipe network on an input port. Every processor that brings a kind to a stock, or reserves room for it, gives the kind its row |
The same processors, the settings processor, the recipe execution processor, the machine rep sync processor and the machine observer |
FMachineKindFilterFragment |
UMachineRecipeAssignmentTrait, on a type whose recipes use the wildcard |
Server | FKindId Kinds[16]; uint8 Count; bool bLimited. Kinds is the kinds the player has set, in the player's order. Count is how many there are. bLimited is false while the stock has no setting |
The settings processor | The topology processor, every processor that asks whether a stock takes a kind, and the machine rep sync processor |
FMachineConsumedFragment |
UMachineRecipeAssignmentTrait, on a type that runs recipes and has a recipe that makes nothing |
Server | FKindId Kind[16]; uint32 Count[16]; uint32 Other; uint8 Used. Kind is each kind the machine has consumed. Count is how many of that kind it has consumed. Used is how many entries of Kind are in use. Other counts every kind after the first 16 together |
The recipe execution processor, and the bot hand-off processor for a swept component | The machine rep sync processor |
FMachineStockRepStateFragment |
UMachineRecipeAssignmentTrait, on a type whose recipes use the wildcard |
Everywhere | FKindId Kind[24]; uint16 Held[24]; uint16 Room; uint16 Coming; FKindId FilterKinds[16]; uint8 FilterCount; uint8 Flags; FKindId ConsumedKind[16]; uint32 Consumed[16]; uint32 ConsumedOther. Kind and Room are copies of the server's fields. Held is how much of each kind the stock holds. It holds a fluid's amount in hundredths of a unit. Coming is how many components are on their way to the stock, of all kinds together. FilterKinds is the kinds the setting names. FilterCount is how many it names. Flags holds the six facts of FMachineStockFragment::Flags, and whether the stock has a setting. The three consumed fields are copies of FMachineConsumedFragment. A field that a type does not use stays zero |
The machine rep sync processor | The inspector |
FMachineStockCapacitySharedFragment |
UMachineStockCapacityTrait |
Const shared, everywhere | uint16 Capacity. Capacity is the stock's room. The trait reads it from the factory config, by the key value it names |
The trait | The topology processor and the windows |
FGroundSweepFragment |
UGroundSweepTrait |
Server | bool bSweep. bSweep is whether the machine is set to sweep. It starts false |
The settings processor | The bot dispatch processor and the machine rep sync processor |
FFloorSuppliedTag |
UMachineRecipeAssignmentTrait, on a type whose stock can give and can never take |
Server | The floor's supply fills the machine | The trait | The floor supply processor and the topology processor |
FRefusesFloorSupplyTag |
UFloorSupplyRefusalTrait |
Server | The machine takes nothing from a machine that the floor's supply fills, except by belt or pipe | The trait | The topology processor |
4.5 Fluid and pipes
Fluid is an amount held in machine buffers and moved through pipes, which hold nothing, as in the simulator; UMachineFluidTrait is on any machine type with a fluid port and UPipeTrait is on pipe sections. A pipe section also has the traits every section shares. They are UTiledMachineTrait, UFloorIndexTrait and UCarrierFlowTrait.
| Fragment or tag | Trait | Where | Fields | Written by | Read by |
|---|---|---|---|---|---|
FMachineFluidFragment |
UMachineFluidTrait |
Server | float Amount[48]. This is the fluid held in each fluid row of the machine's buffer, in units. The rows are the same rows as FMachineBufferFragment, which holds their shares. Every test on an amount allows a small tolerance (DFLogistics::FluidTolerance, proposed at a thousandth of a unit), so a recipe that takes 6 starts with 5.999 or more |
The floor supply processor, the recipe execution processor, the direct connection processor and the pipe flow processor | The same processors, and the machine rep sync processor |
FPipeSectionLinksFragment |
UPipeTrait |
Server | The network entity the section belongs to | The topology processor | The pipe rep sync processor |
FPipeFlowRepStateFragment |
UPipeTrait |
Everywhere | FKindId Fluid; uint8 Flow; bool bAtMax. Fluid is the fluid the network carries. Flow is how much has been passing through this section, smoothed, as a level for drawing. bAtMax is whether that is the pipe rate |
The pipe flow processor writes Flow and bAtMax. The pipe rep sync processor writes Fluid, and sends the fragment |
Pipe visualisation |
FCarrierFlowRepStateFragment |
UCarrierFlowTrait |
Everywhere | uint8 Ways[32]. For each tile, this holds the sides it leads out of. Belt, rail and pipe sections all have it. The tile's level says whether the tile passes under another belt or pipe (5.2) |
The spawn writes it. Server_Build works out each tile's exits over the whole route. It does this before it cuts the route into sections. Nothing works them out again for a section. The split processor moves them to the new sections. The topology processor reverses them when a run is flipped |
The topology processor, tiled visualisation, carrier visualisation and the client footprint |
FPipeTag |
UPipeTrait |
Everywhere | The section is a pipe | The trait | The topology processor |
Which fluid a pipe network carries. The topology processor works it out from the network's ports. A port takes a fluid by name when a recipe that is turned on takes that fluid through that port (4.2). It also takes a fluid by name when the setting of its machine's stock names that fluid. A port gives a fluid when a recipe that is turned on makes that fluid through that port. The network carries the one fluid that one port gives and another port takes by name. When no port takes a fluid by name, the network carries the one fluid that is given. In either case, when more than one fluid fits, the network is mixed and nothing flows. A stock that takes and has no setting takes no fluid by name. It takes whatever the network carries. A stock that the floor's supply fills gives each fluid the floor supplies to it (4.4).
The pipe network entity. There is one for each set of connected pipes. It has UCarrierNetworkTrait, which the rail network has too, and UPipeNetworkTrait. It exists only on the server and is not replicated. The pipe flow processor loops over these entities.
| Fragment or tag | Trait | Where | Fields | Written by | Read by |
|---|---|---|---|---|---|
FCarrierNetworkFragment |
UCarrierNetworkTrait |
Server | uint32 Age; uint16 Group. Age is the build serial of the network's oldest piece (5.1). It identifies the network and sets the order networks are processed in. Group is the set of networks that share a machine with it. A rail network has this fragment too |
The topology processor | The pipe flow processor, the rail advance processor and the rail load processor |
FPipeNetworkFragment |
UPipeNetworkTrait |
Server | FKindId Fluid; bool bMixed. Fluid is the one fluid the network carries. bMixed means its ports would mix two fluids, so nothing flows |
The topology processor | The pipe flow processor and the pipe rep sync processor |
FPipeNetworkGraphFragment |
UPipeNetworkTrait |
Server | Its junctions, its pipe runs and its ports. A pipe run goes between two junctions and records the section tiles it covers. A port records the machine, the port and whether it is an input or an output. This fragment holds arrays, so it opts out of trivial relocation, as FBuildGhostIntentFragment does |
The topology processor | The pipe flow processor |
FPipeNetworkFlowingTag |
None. The topology processor adds it | Server | The network carries a fluid, so the pipe flow processor visits it | The topology processor | The pipe flow processor |
4.6 Belts and rails
Belts and rails are the same thing with three differences. Both carry stacks along a run, and both store and replicate them the same way. A belt section and a rail section each have UCarrierStacksTrait and the traits every section shares (4.5). Each also has UCarrierItemsVisualTrait, as the splitter does. Section 4.8 covers the three things only a rail does.
Stacks are kept apart by the stack radius, which is a floor rule (4.11). It is 30 cm today, which is half the size the factory database gives a component. Two stacks on a run are never closer than twice the radius. A stack's place is stored as a distance along its section, in centimetres. Nothing is stored or limited per tile.
A run can span several sections. A stack is stored on the section it is over, and moves to the next section's fragment when it crosses the boundary.
| Fragment or tag | Trait | Where | Fields | Written by | Read by |
|---|---|---|---|---|---|
FCarrierStacksFragment |
UCarrierStacksTrait |
Server | FCarrierItem Items[64]; uint8 Count; bool bHeld. Items holds the stacks, front first. bHeld is whether the front stack could not move last frame |
The rail advance processor, the rail load processor, the belt transport processor and the split processor. The topology processor writes it when a run is flipped, or when a stack is re-addressed or dropped | The same, the carrier rep sync processor and the machine observer |
FCarrierLinksFragment |
UCarrierStacksTrait |
Server | Which run the section belongs to and where it sits in that run. For each tile, what each of its exits leads into, the port it passes or loads from, and what joins it there, in order. A belt tile has one exit. A rail tile can have up to three | The topology processor | The rail advance processor, the rail load processor, the belt transport processor and the carrier rep sync processor |
FCarrierStacksRepStateFragment |
UCarrierStacksTrait |
Everywhere | uint8 Count; FKindId Kinds[64]; uint16 At[64]; uint8 EndState; float SampleServerTime. At is in centimetres. EndState says whether the front of the section's run is moving, backed up or stuck. It is the same on every section of that run |
The carrier rep sync processor | Carrier visualisation and the inspector |
FCarrierItemsVisualFragment |
UCarrierItemsVisualTrait |
Rendering | The last few samples, and which mesh entry is showing which stack. Splitters have this too | Carrier visualisation | Carrier visualisation |
FCarrierFlipRequestFragment |
None. Server_FlipRun adds it |
Server, transient | A tile of the run to reverse | Server_FlipRun |
The topology processor, which then removes it |
FBeltTag |
UBeltTrait |
Everywhere | The section is a belt | The trait | The topology processor |
FCarrierBuildOrderFragment |
UCarrierFlowTrait |
Server | uint32 BuildSerial[32]. This is the order the section's tiles were built in. Belt, rail and pipe sections all have it. On a belt it decides which joining run goes first. The placement request gives a route's tiles their serials, in path order (5.1) |
The spawn writes it. The split processor moves it to the new sections | The topology processor |
These structs are used inside the fragments above. They are not fragments. Splitters, piles and bots use FComponentStack too, and so does a recipe.
| Struct | Fields |
|---|---|
FComponentStack |
FKindId Kind; uint16 Count; float Temperature. This is a count of one kind. Anything in transit is an FComponentStack. A recipe uses one for each kind it takes and one for each kind it makes. The struct of this name in FactoryRecipeBookData.h takes this definition, so there is one type. Every stack in transit holds one component to begin with. A recipe does not use the temperature. One or two more properties are expected later |
FCarrierItem |
FComponentStack Stack; float At. At is the distance along the section, in centimetres |
A merge is not a machine and not an entity. A merge is where one belt run ends against the side of another. The run being joined carries straight on, and the joining run adds a stack only where there is a gap. Where several runs join at one tile, the one built first goes first (FCarrierBuildOrderFragment). A merge has no state of its own.
A run is not an entity either. It is just its sections, which FCarrierLinksFragment links in order. The belt transport processor gathers each run's sections every frame and handles the oldest run first.
A cut or a join keeps what a section holds. UTiledMachineSplitProcessor destroys the section and spawns new ones, as it does today. It first moves the stacks, the flow and the build serials of the tiles that stay to the new sections. A cut takes a tile away. A stack over that tile stays on the old section, and UMachineLifecycleProcessor puts it down as a pile (3.2). Nothing else is put on the ground. A tiled machine with no carrier fragments, such as cradle track, is split as it is today.
4.7 Splitters
A splitter is a machine like any other. It is a UMachineData type on DA_MachineBase and is placed like a machine. It has UMachinePortsTrait for its four sides and UBeltSplitterTrait for its behaviour. It shares UFloorIndexTrait and UCarrierItemsVisualTrait with belt and rail sections. It is not a tiled machine and not part of a run. Belt runs start and end at its sides, and a machine port facing a side connects to it directly. The topology processor works out which sides are inputs and which are outputs from what is connected to them. A splitter does not have UMachineBufferTrait, and it has no bot port. It has no demand of its own, so nothing is delivered to it on demand and no bot visits it. It holds the stacks that fit across it, kept apart by the stack radius.
| Fragment or tag | Trait | Where | Fields | Written by | Read by |
|---|---|---|---|---|---|
FBeltSplitterSettingsFragment |
UBeltSplitterTrait |
Server | uint8 PrioritySides, one bit per side. A priority input is taken from before the other inputs. A priority output is used whenever it is not backed up |
The settings processor | The belt transport processor and the carrier rep sync processor |
FBeltSplitterFragment |
UBeltSplitterTrait |
Server | FSplitterItem Items[2]; uint8 Count; uint8 Deal[4]; uint8 Turn; bool bHeld. Items holds the stacks crossing it, front first. Deal records, for each input, which output it sends to next. Turn is which input is taken from next |
The belt transport processor | The belt transport processor, the carrier rep sync processor and the machine observer |
FBeltSplitterLinksFragment |
UBeltSplitterTrait |
Server | For each of its four sides, whether it is an input, an output or unused, and what is connected there. That is a section's tile or a machine's port | The topology processor | The belt transport processor |
FBeltSplitterRepStateFragment |
UBeltSplitterTrait |
Everywhere | uint8 PrioritySides; uint8 Count; FKindId Kinds[2]; uint8 Across[2]; uint8 SideOut[2]; float SampleServerTime. Across is in centimetres |
The carrier rep sync processor | Carrier and port visualisation, and the inspector |
FBeltSplitterTag |
UBeltSplitterTrait |
Everywhere | The machine is a splitter | The trait | The topology processor |
This struct is used inside FBeltSplitterFragment. It is not a fragment.
| Struct | Fields |
|---|---|
FSplitterItem |
FComponentStack Stack; float Across; uint8 SideIn; uint8 SideOut. Across is how far across the splitter the stack is, in centimetres. SideIn is the side it entered by. SideOut is the output chosen for it, which is picked as it passes the middle |
4.8 What only rails do
A rail section has URailTrait on top of the traits in 4.6. A rail does three things a belt does not.
- Each stack has a destination. A rail only loads a stack that a machine it reaches has demanded. Room is reserved for the stack at that machine.
- A rail can branch anywhere. At a branch, a stack takes the way to its destination. A belt only branches at a splitter.
- Rails take turns where they join. A belt lets a joining stack in only where there is a gap.
A rail also has its own speed, which is a floor rule.
| Fragment or tag | Trait | Where | Fields | Written by | Read by |
|---|---|---|---|---|---|
FRailStacksFragment |
URailTrait |
Server | FRailAddress To[64]; uint8 Turn[32]. To has one entry for each stack in FCarrierStacksFragment, in the same order. Turn is which incoming rail goes next, for each tile where rails join |
The rail advance processor, the rail load processor and the split processor. The topology processor writes it when a run is flipped, or when a stack is re-addressed or dropped | The same, plus the bot rail priority processor |
FRailSectionLinksFragment |
URailTrait |
Server | The network entity the section belongs to | The topology processor | The rail advance processor and the rail load processor |
FRailTag |
URailTrait |
Everywhere | The section is a rail | The trait | The topology processor |
This struct is used inside FRailStacksFragment. It is not a fragment.
| Struct | Fields |
|---|---|
FRailAddress |
uint16 ToMachine; uint8 ToPort. This is the machine and the port a stack is going to |
The rail network entity. There is one for each set of connected rails. It has UCarrierNetworkTrait and URailNetworkTrait. It exists only on the server and is not replicated. The two rail processors loop over these entities. A rail network's age and group are in FCarrierNetworkFragment (4.5), which UCarrierNetworkTrait adds.
| Fragment | Trait | Where | Fields | Written by | Read by |
|---|---|---|---|---|---|
FRailNetworkRoutesFragment |
URailNetworkTrait |
Server | Its rail runs, where a run goes from one join or branch to the next. For each run, the input ports it can reach and how far away each one is. The output ports the network loads from. The order its sections are processed in. This fragment holds arrays, so it opts out of trivial relocation, as FBuildGhostIntentFragment does |
The topology processor | The rail advance processor, the bot rail priority processor and the rail load processor |
4.9 Piles
A pile has UFloorPileTrait. Every pile starts unplaced. The trait also adds the surface membership fragments on the server. A replicated type needs them unless it has FHorizonAlwaysRelevantTag. A type with neither replicates to no one.
| Fragment or tag | Trait | Where | Fields | Written by | Read by |
|---|---|---|---|---|---|
FFloorPileFragment |
UFloorPileTrait |
Server | FComponentStack Stack; FIntPoint Cell; uint16 OutboundReserved. OutboundReserved is how many of the pile are held for bots on their way to collect them. Until the pile is placed, Cell is where it was dropped |
The spawn, the pile processor, the bot hand-off processor and the bot dispatch processor. The topology processor writes OutboundReserved when it recounts reservations |
The same |
FFloorPileRepStateFragment |
UFloorPileTrait |
Everywhere | FKindId Kind; uint16 Count; FIntPoint Cell. Cell is the tile the pile stands on. A client needs it, because Horizon sends an entity's transform only when it first sends the entity, and a pile is placed after it is spawned |
The pile processor, and the bot hand-off processor when a bot takes from the pile. The pile rep sync processor sends it | Pile visualisation |
FTransformFragment |
UFloorPileTrait |
Everywhere | Where the pile stands | The pile processor writes it on the server. Pile visualisation writes it on a client, from Cell |
Pile visualisation |
FActiveFloorPileTag |
UFloorPileTrait |
Server | The pile is live. An observer watches for its removal | The trait | The pile observer |
FFloorPileUnplacedTag |
UFloorPileTrait |
Server | The pile has no tile yet. Either it was just dropped, or a machine was placed on top of it | The spawn, and footprint registration | The pile processor, which removes it |
4.10 Bots
Bots keep ULogisticsBotTrait. It gains one field, which is the time a pick-up takes. The factory config gives a pick-up time too, and which one a bot uses is undecided (section 8). The job fragment replaces FLogisticsBotOrderFragment. A bot has FLogisticsBotOrderedTag while it has a job. The processors that write the job fragment add and remove the tag, deferred.
| Fragment | Trait | Where | Fields | Written by | Read by |
|---|---|---|---|---|---|
FLogisticsBotJobFragment |
ULogisticsBotTrait |
Server | The stack it is going to collect or is carrying. Its source, which is a machine or a pile. Its destination machine. The pick-up time left. Whether it is a sweep job | The bot rail priority processor, the bot hand-off processor and the bot dispatch processor. The topology processor re-addresses or drops a job that refers to a machine that has gone (5.1) | The same processors, bot state and the bot observer |
FLogisticsBotPathFragment |
ULogisticsBotTrait |
Server | The rest of the bot's computed path, as an array of cells, and the token of its latest path request. It holds an array, so it opts out of trivial relocation, as FBuildGhostIntentFragment does |
Path refill | Path refill and the monitor |
FLogisticsBotCargoRepStateFragment (existing) |
ULogisticsBotTrait |
Everywhere | The kind being carried. It used to be a part type | The bot hand-off processor. ULogisticsBotRepSyncProcessor sends it |
Bot visualisation |
4.11 The floor
There is one floor entity per surface. It has UFactoryFloorRulesTrait and UFactoryFloorTrait. AFactoryFloorManager spawns it for its surface and keeps its handle. A processor finds it with a second query. Observers, commands and RPC code ask the floor manager.
UFactoryFloorRulesTrait builds the floor's rules. One set applies to the whole floor. The rules come from DA_FactoryConfig, which the factory database import writes, so the trait has no fields. The trait turns them into a const shared fragment on every net mode, so nothing replicates them.
UFactoryFloorTrait also adds FHorizonAlwaysRelevantTag on every net mode, and the surface membership fragments on the server. The floor entity has no position, so nothing else would keep it relevant to a client.
| Fragment or tag | Trait | Where | Fields | Written by | Read by |
|---|---|---|---|---|---|
FFactoryFloorRulesSharedFragment |
UFactoryFloorRulesTrait |
Const shared, everywhere | float BeltSpeed; float RailSpeed; float StackRadius; float PipeRate; float Headroom; uint16 Space; uint8 BeltUnderpassLimit; uint8 PipeUnderpassLimit. BeltSpeed is in centimetres a second. RailSpeed is in centimetres a second. StackRadius is in centimetres. It is half the size the database gives a component, so it is 30 today. Two stacks on a belt or rail are never closer than twice the radius. PipeRate is the most fluid a pipe run carries in a second. Headroom is how many runs of each recipe a machine has room for. It is 3 today. Space is the room of a stock, unless the type has a room of its own (4.4). It is 12 today. BeltUnderpassLimit is the most tiles in a row that a belt can pass under. PipeUnderpassLimit is the most that a pipe can pass under. Both come from the defaults of the database's key values |
The trait | The processors that apply a rule, and placement |
FFloorTopologyDirtyTag |
None. It is added when the floor changes | Server | Something has been built, removed or changed since the topology was last rebuilt | The settings processor, footprint registration, the split processor, Server_FlipRun, and the machine and bot observers. All of them add it deferred |
The topology processor, which removes it |
FFactoryFloorSupplyFragment |
UFactoryFloorTrait |
Server | FKindId Kind[16]; float PerSecond[16]; double Credit[16]; uint8 Count. Kind is the kinds the floor supplies. Any kind can be supplied. PerSecond is the rate of each. Credit is the fraction of a solid component that has not arrived yet. A fluid does not need it. Count is how many kinds there are |
The settings processor writes Kind, PerSecond and Count. The floor supply processor writes Credit |
The floor supply processor and the topology processor |
FFactoryFloorSupplyRequestFragment |
None. Code that changes the floor's supply adds it | Server, transient | A kind and its new rate | In the first build, a test floor or a console command. What adds it in the game is not designed (section 8) | The settings processor, which then removes it |
FFactoryFloorRepStateFragment |
UFactoryFloorTrait |
Everywhere | The supply rates, and uint32 CatalogueHash |
The settings processor writes the rates when it applies a change to the supply. The spawn writes the hash. The floor rep sync processor sends the fragment | Clients |
4.12 Existing types that change
| Type | Change |
|---|---|
UMachineProcessTrait |
Becomes UMachineRecipeAssignmentTrait (4.3). It keeps the process list, which the factory database import writes. It loses SupportedVerbs and SlotCapacity. It no longer adds the slot, progress, supply-rate, modifier and utilisation fragments |
FMachineProcessSharedFragment |
Becomes FMachineRecipeSetSharedFragment (4.3). It loses the verbs and the slot count |
UArmMachineTrait |
Adds the utilisation fragment to arms. The arm processor writes it |
UMachinePortsTrait |
Keeps the typed ports and the two maps of chosen ports (4.2). The factory database import writes both. It loses its two older bot port fields. It works out the bot cells from its bot ports (4.2) |
FMachinePortsSharedFragment |
Holds the ports that belts, rails, pipes and other machines connect to. It also holds the two maps of chosen ports. The bot cells move to FMachineBotPortsSharedFragment |
ULogisticsBotTrait |
Gains the time a pick-up takes |
UTiledMachineTrait |
Unchanged. No tiled type has UMachineProcessTrait, so a section has no slots, progress, utilisation or supply rate today, and it gains none |
UMonorailTrackTrait |
Unchanged. The belt type no longer has it. The skyrail keeps it, because cradles travel on a skyrail |
FMachineRepStateFragment |
Loses SlotCapacity, SlotStates, ActiveProgress, ActiveEffectiveDuration and SlotOutputs |
FMachineIdentityFragment |
Loses SlotCapacity, InputPortCell and OutputPortCell |
FMachineVisualFragment |
Loses the per-slot client progress |
FMachineTilesRepStateFragment |
Each tile gains a level (5.2) |
FTileShapesSharedFragment |
Unchanged |
FActiveMachineTag |
Kept |
FMachinePendingFootprintTag |
Kept |
FResourceRetrievalMachineTag |
Kept |
FHorizonRepDirtyTag |
Kept. UMachineRepSyncProcessor and UFactoryProxySendProcessor both remove it, deferred |
| The bot and waypoint tags | Kept |
FSupplyMachineTag |
Removed |
FActiveBinTag |
Removed |
FMachineWorkingTag |
Removed |
| The operation state tags | Removed |
🗺 5. Topology and subsystems
5.1 Topology
How the floor is connected is worked out from what has been built and which recipes are enabled. This exists only on the server. There is no single topology object. Each derived fact is stored on the entity it describes, and ULogisticsTopologyProcessor is the only thing that writes any of it, except the last row.
| Derived fact | Where it is stored |
|---|---|
| What each machine port is connected to, and the kinds a built connection already delivers to it or from it | FMachineLinksFragment on the machine |
| A belt or rail run. That is its sections in order, what each tile leads into, the ports it passes or loads from, and what joins it | FCarrierLinksFragment on each of its sections |
| A splitter's inputs and outputs | FBeltSplitterLinksFragment on the splitter |
| Rail routing. That is which input ports each stretch of rail can reach and how far away they are. It also covers the order sections are processed in | FRailNetworkRoutesFragment on the rail network entity |
| A pipe network's junctions, pipe runs and ports, and the one fluid it carries | The pipe network entity |
| Which island of connected machines an entity is in | FFloorIndexFragment::Island |
| Tiles closed to bots. These are a tile that passes under another, its ramps and a splitter. Every other belt, rail and pipe tile is open to bots | Blocked cells in the occupancy grid. Footprint registration marks them from the tiles' levels when it adds the tiles, on the game thread. The navigation grid already reads these |
Indexes. A link refers to a machine, section or splitter by its floor index. The topology processor gives each new entity an index. There is no table of indexes. If no live entity has a given index, nothing is found for it that frame. When the floor is dirty, the processor first finds every rail stack and bot job that refers to an index with no entity behind it, and re-addresses or drops them. Only then does it reuse that index.
Oldest means the lowest build serial. Every machine has a build serial, and so does every tile of a section. AFactoryFloorManager::RequestMachinePlacement stamps them from a counter the floor manager keeps for its surface. A route's tiles are stamped in path order. AMachinePlacementActor and Debug_SpawnMachine spawn a machine without the placement request. They take a serial from the same counter. Nothing has a build serial today. The serial decides the order among machines of equal priority that have waited equally long. It also decides which joining run goes first and which network is the older.
A network is only created or destroyed when something is built or removed. A network is identified by its oldest piece. When two networks are joined, the one with the older piece is kept and the other is destroyed. When a network is cut in two, the half with the oldest piece keeps the entity and the other half gets a new one. A new network is spawned deferred, so it starts working on the frame after it is built. A network holds no components and no fluid, so nothing has to be moved when one is created or destroyed.
Reservations are recounted. When a machine, section or bot is removed, nothing has to release its reservations one by one. The floor is tagged dirty, and the topology processor recounts every reservation from the rail stacks and bot jobs that still exist. The reservations are InboundEnroute and OutboundReserved on machines, and OutboundReserved on piles.
5.2 What is a subsystem, and why
A subsystem is only used where the data has no entity to live on, or where something has to look the data up without going through an entity. This plan adds one subsystem and keeps one for a single job. It deletes four (section 7). The table shows the largest of them.
| Store | Verdict | Why |
|---|---|---|
UFactoryCatalogueSubsystem (new, game instance) |
Subsystem | It is a content index. Its tables are built once, on the game thread, from the factory database's assets, the first time anything asks for them. The subsystem asks at start-up, so a game never builds them in the middle of play. A world with no game instance builds them through the same static functions. It reads each kind's id from the kind's asset, where the import wrote it, and that id never changes. It gives each process and recipe an id in sorted order. It has a hash, which a client checks when it joins. Static functions read it, because a trait is built in worlds that have no game instance. The type snapshot commandlet makes such worlds. It is not simulation state and no entity owns it. It is read before any floor exists. The build bar, the Forgepedia, traits and saves all read it. UDFMassEntityRegistrySubsystem is the same kind of thing |
UFactoryFloorSubsystem (existing, world, everywhere) |
Subsystem, for occupancy only | It is a spatial index. It records which entity stands on each cell, by level. Placement, routing and previews look things up by cell. They do this from player input and RPC code, outside any processor, on every net mode. A lookup by cell cannot be an entity query. It has no levels today, so it gains them. A cell has four, which are ground, over, under and rail. A machine is on the ground level, and so is a splitter. A belt or pipe tile is on the over level, or on the under level where it passes under another. A rail tile is on the rail level. Over, under and rail are the carrier levels. Each tile carries its own level in FMachineTile, so the level is replicated and saved with the tiles. Registration, lookup and placement take a level, and there is a placement rule for each pair of levels. Everything that exists today registers on the ground level. Routing reads the carrier levels to find what the end of a route can join. The monorail grid answers that today. In this design belts are not in the monorail grid. The subsystem loses the slot mutators and the operation spawner. It also loses its record of each machine's ports, which moves to FMachineBotPortsSharedFragment. It has Mass subsystem traits that allow reads from a worker, and each processor that uses it declares it. Everything that writes it stays on the game thread |
ULogisticsSimulationSubsystem (existing, world, server) |
Deleted | Everything it holds is either entity state or a copy of it. See the table below |
UFactoryNavigationGrid (existing, a component of the floor manager) |
Kept. It is a component, not a subsystem | It is a spatial grid worked out from the floor tiles and occupancy, and it runs the path solves over that grid. It gains a region label per cell, so checking whether a bot can get somewhere is a comparison of two labels. It also holds each finished path until the path refill processor collects it. Only game-thread processors touch it. They are the path refill processor, the bot state processor, ULogisticsBotNavRetryProcessor, the bot rail priority processor and the bot dispatch processor |
This table shows what ULogisticsSimulationSubsystem held, and where each thing goes.
| It held | Goes to |
|---|---|
| Bot registry (id to entity) and bot count | A query on FLogisticsBotActiveTag |
| The set of free bots | A query for bots without FLogisticsBotOrderedTag. The set was a copy of state the bot already has |
| Free-bot spatial index | Dispatch sorts the idle bots it queried into buckets, once per pass |
| Each bot's computed path and request token | FLogisticsBotPathFragment on the bot |
| The path-finished callback. It wrote fragments from outside a processor | The navigation grid holds a finished path until ULogisticsBotPathRefillProcessor collects it |
| Pending deliveries | Removed. The job is FLogisticsBotJobFragment on the bot |
IssueBotOrder, CancelBotOrder, DespawnBot |
The bot rail priority and bot dispatch processors, the bot hand-off processor and DF::Mass::DestroyDeferred |
RequestPath and GetNavGridVersion, which the bot state processor and ULogisticsBotNavRetryProcessor call |
The navigation grid |
| Grid size and floor height | DFGrid::CellSize and the floor entity |
UFactoryFloorSubsystem also spawns machines, saves and restores them, and keeps each machine's footprint cells in a map keyed by entity. None of that is an index, and this plan does not move it. The spawn gains two things. They are the build serial and each tile's exits. A save gains each machine's settings. They are its recipes, its kinds, its priority, whether bots serve it and whether it sweeps. A save also gains splitter sides, run direction and build serials, and the floor's serial counter and supply rates. A save does not hold what a machine contains, as today.
📡 6. Replication and commands
Everything in the chain exists only on the server. A client gets copies of what it needs in replicated fragments, and a listen host draws from those same fragments.
What is on a belt or rail is sent as a sampled state, not as a stream of events. A few times a second, while it is changing, each section sends the kind of each stack, where it is, and the server time of the sample. The client slides each stack between samples at its render time.
A machine's run is sent as the server time it started and its length. The client works out progress from those. Nothing on a client follows a handle to another entity.
A section is complete in itself on a client. A belt, rail or pipe is cut into sections so that a client needs only the sections relevant to it, never the whole run. So everything a client draws or shows for a section comes from that section's own replicated state. A fact that belongs to the whole run or network is copied onto each of its sections by the rep sync. Examples are the fluid a pipe carries and whether a belt's front is stuck. Networks, and anything else that groups sections, stay on the server.
There is one proxy for each set of replicated fragments. Horizon picks a type's proxy by the exact set of replicated fragments the type has, and it asserts at the first spawn when no proxy matches. So a replicated fragment and its proxy are always added together. A machine proxy derives from UMachineEntityProxy, and a section proxy derives from UTiledMachineEntityProxy. The existing rep sync processors for machines and for tiles look for those two classes. The proxies that exist today keep their sets of fragments. Three are for machines. They are a tiled machine with no carrier fragments, an arm and a retrieval machine. The other three are for a cradle, the factory monitor and the build ghost. UMachineEntityProxy and UTiledMachineEntityProxy are regenerated, because FMachineRepStateFragment loses five fields and each tile gains a level (4.12).
| Proxy | Fragments |
|---|---|
UMachineEntityProxy (existing, regenerated) |
Machine |
URecipeMachineEntityProxy |
Machine, production, recipe assignment. Every machine that holds components and has no stock uses it |
UStockMachineEntityProxy |
Machine, production, recipe assignment, stock. Every machine with a stock uses it |
UCarrierSectionEntityProxy |
Machine, tiles, flow, stacks. Belt sections and rail sections both use it |
UPipeSectionEntityProxy |
Machine, tiles, flow, pipe flow |
UBeltSplitterEntityProxy |
Machine, splitter |
UFloorPileEntityProxy |
Pile |
UFactoryFloorEntityProxy |
Floor |
ULogisticsBotEntityProxy (existing) |
Bot cargo, with a kind |
A retrieval machine that also holds components needs one more proxy. The benchmark floor goes from about 7,800 proxies to perhaps 13,000.
Iris allows 65,536 replicated objects by default. That is a setting on the net driver (MaxReplicatedObjectCount), not a hard limit, so it can be raised. It counts everything the server replicates, on every surface, whether or not a client can see it.
Player changes are Server_ RPCs on UPlayerBuildingHandler. Each one is validated and then raised as a request fragment, which the settings processor and the topology processor consume. Pasting one machine's settings onto another uses the same calls. These are the RPCs.
Server_SetMachineRecipesServer_SetMachineKindsServer_SetMachineLogisticsServer_SetSplitterPriorityServer_FlipRun
Server_SetMachineRecipes sets which of a machine's recipes are on, and their order. Server_SetMachineKinds sets the kinds a machine's stock is limited to, or removes the limit (4.4). Server_SetMachineLogistics sets a machine's priority, whether bots serve it and whether it sweeps.
🗑 7. What is deleted
| Type | Kind |
|---|---|
UOperationEntityTypeAsset |
Entity type |
UOperationSpawner |
Entity type |
UOperationServerTrait |
Entity type |
UItemBinServerTrait |
Entity type |
FMachineSlotsFragment |
Fragment |
FMachineProgressFragment |
Fragment |
FSupplyRateFragment |
Fragment |
FItemBinFragment |
Fragment |
FItemBinSharedFragment |
Fragment |
| The four operation fragments | Fragment |
FPartHandle |
Struct |
FLogisticsBotOrderFragment |
Fragment |
UMachineDispatchProcessor |
Processor |
UMachineWorkProgressProcessor |
Processor |
USupplyCooldownProcessor |
Processor |
| The four operation processors | Processor |
ULogisticsHandoffProcessor |
Processor |
UPartRegistryAuditProcessor |
Processor |
UItemBinLifecycleProcessor |
Processor |
UBotOrderSubsystem |
Subsystem |
UPartRegistrySubsystem |
Subsystem |
UPartsBinSubsystem |
Subsystem |
ULogisticsSimulationSubsystem |
Subsystem |
The delivery scheduler in UAssemblyLineManager |
Subsystem |
MAX_MACHINE_SLOTS |
Constant |
MAX_OPERATION_INPUTS |
Constant |
MAX_OPERATION_OUTPUTS |
Constant |
MAX_OPERATION_DOWNSTREAM |
Constant |
EAssemblyVerb, with SupportedVerbs and SlotCapacity on the process trait |
Enum and fields |
USupplyMachineTrait |
Entity type |
FSupplyMachineSharedFragment |
Fragment |
AFactoryFloorTestHarnessActor, the benchmark's factory scenario (UDFFactoryBenchScenario) and the order progress window are built on these types, so they go with them.
What takes the place of a parts bin is not designed (section 8).
❓ 8. Open
- The cost of a frame is an estimate until a benchmark measures it. The benchmark should measure each stage on a floor that is filling and on one that is full. It should compare a full topology rebuild with a rebuild of one island. It should report the largest group's share of each stage. It should time dispatch matching without path finding. It should also time finding the entities a processor's links refer to. The results decide whether any carrier stage is worth spreading across threads.
- Nobody has measured what running the chain on a worker gains. A dedicated-server build runs every processor on the main thread, so it gains nothing there. The editor makes the game thread wait until the phase is done, so the gain only shows in a packaged build. Processors still run on workers in the editor, so a race between two processors can show there. A race between a worker processor and code outside Mass only shows in a packaged build. Most of the processors write
FMachineBufferFragmentorFMachineLedgerFragment, so Mass still runs most of them one at a time. Declaring dependencies removes ordering that nothing needs. It does not by itself make the chain parallel. - The dependency table in 3.1 has not been tested. It was worked out by reading the simulator. A missing dependency does not crash. It gives a result that differs from the simulator's, and only on some runs, because the order Mass picks can differ from one frame to the next.
- A game-thread processor sits between two worker processors.
ULogisticsBotRailPriorityProcessorreads the navigation grid's region labels, so it has to run on the game thread. It also has to run betweenURailAdvanceProcessorandURailLoadProcessor. Those two wait for the game thread at that point.ULogisticsBotDispatchProcessorreads the same labels and is on the game thread for the same reason. Both could move to a worker if the labels were stored on entities. That would be a new system, and it is not part of this plan. - Four processors do the same job for different types. They are the pipe, pile and floor rep sync processors and
UFactoryProxySendProcessor. Today each rep-sync processor fills the replicated fragment, copies it into the proxy and marks the proxy, all on the game thread. One processor in Horizon could send every type, if each generated proxy could copy from its entity through one virtual function. That is a change to the rep-proxy recipe. - A carrier sample reaches its proxy a frame late.
UCarrierRepSyncProcessorcannot add a tag within the frame, soUFactoryProxySendProcessorsends the sample on the next one. Each sample carries its own server time. A flag in a fragment would remove the delay. - Code outside Mass reads the occupancy grid on the game thread. In a packaged build it can run while a worker processor runs. That is safe only while everything that writes the occupancy grid stays on the game thread. Footprint registration writes it today.
UFloorPileProcessorcan run on a worker only while piles are not stored in the occupancy grid. It finds the piles on a tile with a query. If piles are added to the occupancy grid, something on the game thread has to add them.UMachineDatastill holds config. A machine type's name, description, icon and category are fields on the type asset. So are its placement flags and its screenshot settings. By the rule in section 1.1 they belong on traits. This plan does not move them. TheDFMoveFieldsToTraitcommandlet exists for moves of this kind.- One set of rules applies to the whole floor. The rules come from the factory database's config asset. That is the simulator's model too. A rule could instead be set on the trait of the type it applies to, such as belt speed on
UBeltTrait. Two types whose values must agree would then each hold a copy. A belt and a splitter are an example. - Network entities are untried. A processor creates and destroys them. A player does not place them. A new one starts a frame late. A destroyed one has to do nothing until it is gone. Every link to one is rewritten in the same pass. A network's routes or junctions are stored in a fragment that holds arrays, and a rail network that joins a whole floor makes that fragment large.
- The order of rail networks that share a machine. The simulator has no rail networks. It works through every rail tile on the floor in one order. Processing networks one at a time, oldest first, can load a contested destination from a different source than the simulator would. Pipes do not have this problem, because the simulator already processes pipe networks one at a time in that order.
- Belt runs are not entities, so state that belongs to a run is stored on a section. Whether a run's front is stuck is stored on the section holding the run's end (
FCarrierStacksFragment::bHeld). A run entity would be the consistent place for it. - Rails are stored like belts, and the simulator's are not. Both keep stacks apart by the stack radius. When it runs on the database's config, the simulator's belt also keeps stacks one component's size apart, which is twice the radius, so a belt agrees with it. The simulator's rail holds one stack per tile. So a rail cannot be checked against the simulator exactly.
- A rail stack's destination is stored beside the stack, not in it.
FRailStacksFragment::Tohas to stay in the same order as the stacks inFCarrierStacksFragment. Keeping two lists in step is a risk. The other choice is a destination field on every stack, which belts would ignore. - Recounting reservations is this plan's idea, not the simulator's. The simulator releases each reservation when its stack or job goes. A recount gives the same result. It costs a pass over the rail sections and bots on every frame the floor is dirty.
- Bot ids. Bots get an id from
UFactoryObjectIDRegistrytoday. With no bot registry, we need to find what still uses that id. The server can refer to a bot by its entity, and a client by its NetID. - The rest of
UFactoryFloorSubsystem. Its spawn, save and restore functions and its per-entity footprint map are not an index. This plan does not move them. Section 5.2 says what the spawn and a save gain. - Islands on real floors. How much players connect their floors into one network decides how much there is to gain from processing parts of the floor in parallel.
- Push model. The proxies are written for the push model, but the game target does not set
bWithPushModeland nothing setsNet.IsPushModelEnabled. If that is right, Iris compares every property of every relevant proxy on every update, and the server cost of the new arrays goes up. This was read from the configuration. It has not been observed running. While push model is off, a mark does nothing, and the processors that send to a proxy only copy plain values into it. They stay on the game thread so that turning push model on changes nothing in the chain. - More than one factory surface. This plan has one floor per surface. The navigation grid is set up for surface 0 only today, and the logistics subsystem holds one path provider. The first build supports surface 0 only. New spawners and commands take a surface id from the start.
- Unplaced piles and replication. It is undecided whether a pile is held back from replication until it has a tile. Holding it back would stop a client seeing a pile that is about to merge into another.
UPipeFlowProcessorwrites to pipe sections as well as machines. It writesFPipeFlowRepStateFragment, and it does so from a worker thread.- The cost of a stack sample. Each stack on a belt or rail is at its own distance. So a section's sample is a kind and a distance for each stack, where it used to be a kind for each tile. This has not been measured.
- Storing the gaps between stacks instead of their positions. Factorio does this for its belts (Friday Facts 176, https://www.factorio.com/blog/post/fff-176). While a belt moves freely the gaps do not change, so very little would need to be sent to clients. This could replace the belt sample. The details have not been worked out.
- What happens to a stack's state inside a machine. A buffer row is a count, so a stack's temperature has nowhere to go when the stack is delivered. Either the temperature is dropped and set again on what the machine makes, or each row keeps an average. What sets the state of a stack that a machine gives out is undecided too.
- Stacks of more than one. While every stack holds one component, a stack is loaded and delivered whole. A larger stack needs a rule for a machine that has room for only part of it. It also needs a rule for two stacks joining. Two piles of the same kind join today. Their quantities add up, and their state has to be combined somehow.
- State that changes in transit. If a stack cools as it travels, its temperature can be stored as a value at a time. Anything can then work out the current temperature, and nothing has to be updated every frame.
- What a client is sent about a stack. The samples carry a kind for each stack. Quantity and temperature are only added if they are drawn.
- The simulator moves bare kinds. With stacks of one, this plan can still be checked against the simulator on kind and position. The simulator cannot check a stack's state.
- Fluid amounts will not match the simulator exactly. This plan compares fluid amounts with a tolerance. The simulator rounds them to a millionth. So fluid amounts can only be checked against the simulator to within that tolerance, and a run can start up to a thousandth of a unit short of what its recipe needs.
- A splitter is different from the simulator's. In the simulator it is a belt tile. Here it is a machine. It still holds the stacks that fit across it and sends them out the same way, so it can still be checked against the simulator.
- A stock is this design's reading of the database's three wildcard recipes. The simulator has no wildcard. It makes a recipe for each kind a machine is set to. It also knows four machine types by name. Section 4.4 gets the same behaviour from the recipes and a few small traits.
- Where a stock behaves differently from the simulator.
- A stock with no setting accepts every kind. The simulator's machine accepts none until a kind is set.
- A debug sink consumes the oldest component it holds. The simulator's line consumes in the order of its kinds.
- A debug sink takes 1 second for each component, because
r-storehas no time in the database. The simulator takes 0.25 seconds. - A storage has rows for at most 24 kinds at once, and its three recipes can be turned on and off.
- A debug sink with no setting on a pipe network takes the network's fluid. It is served before a machine that makes things, because it starts at priority 18, as a line does in the simulator.
- What the simulator shows that this design does not keep. The simulator's airlock lists the last ten things it received. Its floor shows one count of everything that was delivered. Here each machine counts what it has consumed, and nothing adds those counts up.
- Storing a machine's recipes by index is this plan's idea, not the simulator's. The simulator keeps what a machine holds in a map keyed by recipe, which has no fixed size. Here a machine has 16 indexes, so a machine type can have no more recipes than that.
- Each machine stores where its recipes' rows start.
RowStartis the same for every machine of a type, so 16 bytes that are fixed per type are copied on to each machine. The other choice is a row table on the type. - Finding a kind's rows reads the catalogue. A processor goes through up to 16 recipes and reads each one's kinds from the catalogue, often on a worker thread. This has not been measured.
- A machine that only consumes gains the modifier and utilisation fragments.
UMachineRecipeExecutionTraitadds both, and the two debug sinks and the airlock have that trait. Whether the monitor should list such a machine is undecided. - The factory database gives more than this design uses. A machine can have a fuel, and only the smelter has one. A machine can have key values of its own, such as the storage's capacity and a belt's speed. The import does not write either yet. Until it does, this design has no fuel, and the floor's rules use the defaults of the key values.
- Bot speed and pick-up time have two sources. The factory config gives a bot speed of 4 tiles a second, which is 400 cm a second, and a pick-up time.
ULogisticsBotTraithas a speed of its own, which is 375 cm a second. Which one a bot uses is undecided. - What feeds the floor's supply in the game. In the first build a test floor sets the rates. A supply machine takes a supply interval from
USupplyMachineTrait, and a retrieval machine works one out from its yield. Both keep it onFSupplyRateFragment, which this design deletes. How either feeds the floor's supply is not designed. What replaces a parts bin is not designed either. How a part type maps to a kind is not designed either. - How an authority draws moving stacks. A listen host and a standalone game have no render delay, so they never have a sample either side of the render time. One answer is for
UCarrierRepSyncProcessorto write the replicated fragment every frame there, and to limit only what is sent. - Where a pipe tile's drawn values go.
UPipeVisualizationProcessorneeds three values on a tile. A mesh instance has two custom floats, and a tiled machine already uses both. - How an underpass and its ramps get a mesh. A tile's shape is chosen from its directions alone today.
- A bot's job when its path fails. What the job does then is undecided.
- How an observer tells a cleared floor from a removed machine. Both arrive as the same tag being removed. Clearing a floor has to put nothing on the ground (3.2).
- The floor entity is always relevant and also bound to a surface. Horizon's code allows both together. No existing type does it.
- Nothing in the project declares a subsystem requirement or an indirect requirement today. No processor spawns an entity from a worker thread either. The engine allows that through the processor's own command buffer, with the template fetched on the game thread first. In a build with slow checks on, the engine checks that an indirect access was declared. No other build checks it.
Docs/Design/FactoryLogisticsMassArchitecture.md in the Dropforge workspace.