Factory: Mass Architecture
Greenfield Mass Entity Core Systems Architecture

Factory: Mass Architecture

Greenfield rewrite of the factory simulation. Machines and Operations become Mass entities; actors are a thin representation layer. Targets 1k+ machines and 10k+ concurrent operations, server-authoritative with Iris/Horizon replication.

🗺 Overview

Target Machines
1k+
Concurrent Ops
10k+
Operation States
8
Migration
One swap

The current FactoryFloor/ module is actor-based: AMachineBase subclasses each run their own tick/state-machine, coordinated by AFactoryFloorManager and a suite of subsystem managers. At target scale this doesn't hold up.

The new module (FactoryFloor2/) moves everything into Mass. Machines and Operations are entities; actors become a thin visual/input layer. Parts stay as handles behind a central registry. The scheduler runs as an ordered chain of server-side processors.

Migration style: FactoryFloor2/ is built green alongside the old module. Both compile until a single cutover PR removes FactoryFloor/. No incremental migration — one clean swap.

What stays the same

LogisticsBots are already on Mass (Source/Dropforge/Mass/Logistics/ + Mass/Navigation/) and replicate via the HorizonReplication plugin. The new module reuses those exact patterns. Logistics-bot code itself is not rewritten.

Scheduling doc

The 8-state operation lifecycle, MWKR dispatch algorithm, and JIT-pull scheduler are specified in the Production Scheduling doc. This doc covers the Mass implementation of that design.

Networking

Server-authoritative simulation. Operations are server-only. Clients receive a compact machine-state snapshot via Horizon proxies and reconstruct visible work state from that alone.

📁 Module Layout

The new module lives entirely under Source/Dropforge/FactoryFloor2/.

Source/Dropforge/FactoryFloor2/
  Actors/
    MachineActor.h/.cpp                 // thin AActor: mesh, click, footprint, FMassEntityHandle back-ref
    MachineActorRepresentation.h/.cpp   // arm-pose interpolation; ISM/anim driven by fragment reads
  Mass/
    Fragments/
      MachineFragments.h                // identity, slots, progress, modifiers, actor ref, rep state
      OperationFragments.h              // operation data, DAG links, progress, MWKR cache
      PartHandleFragment.h              // FPartHandle (uint32) + small inline arrays
    Tags/
      MachineTags.h                     // FActiveMachineTag, FMachineWorkingTag, FHorizonRepDirtyTag
      OperationTags.h                   // one tag per Operation state (Queued, InProgress, ...)
    Traits/
      MachineServerTrait.h/.cpp
      MachineClientTrait.h/.cpp
      OperationServerTrait.h/.cpp       // ops are server-only; not replicated
    Processors/Server/
      OperationReadinessProcessor       // WaitingForParts → Queued when inputs land
      MWKRRecomputeProcessor            // memoised DAG walk; runs only on op-graph changes
      MachineDispatchProcessor          // the JIT-pull scheduler loop
      MachineWorkProgressProcessor      // parallel: accumulate progress on InProgress slots
      OperationCompletionProcessor      // InProgress → SenderAwaitingExport; register output handles
      LogisticsHandoffProcessor         // SenderAwaitingExport ↔ ReceiverAwaitingDelivery; floor-part retry
      MachineLifecycleProcessor         // on machine entity removal: reset ops, drop parts to floor
    Processors/Client/
      MachineRepresentationProcessor    // drive ISM/animation from replicated fragment state
    Registry/
      PartRegistry.h/.cpp               // UPartRegistrySubsystem (see Part Safety section)
    Spawner/
      MachineSpawner.h/.cpp             // replaces FactoryFloorManager placement entity path
  Management/
    FactoryFloorSubsystem.h/.cpp        // grid/placement only — no work dispatch
    BotOrderSubsystem.h/.cpp            // ingest player Bot Orders, build Operation DAGs
  Replication/
    MachineEntityProxy.h/.cpp           // extends UHorizonEntityProxy
  DataTypes/
    OperationData.h                     // FOperationState enum, FRecipeStep, FProcessSpeed
    MachineData.h                       // ported UMachineData (verbs, slots, layout, ISM)

⚙️ Machine Entity

One Mass entity per machine. All fragments are server-side unless noted. The actor is a thin back-reference — no game logic lives there.

Fragments

FragmentContents
FMachineIdentityFragmentMachineDataPtr, supported verbs bitmask, SlotCapacity, base speed multipliers per process, grid cell
FMachineSlotsFragmentFixed-size array of {FMassEntityHandle Op, uint8 State} — size = SlotCapacity, capped at a compile-time max (e.g. 8) to keep the fragment POD
FMachineProgressFragmentfloat Progress[SlotMax] and float EffectiveDuration[SlotMax]
FMachineModifiersFragmentCurrent power/temp/adjacency multipliers; updated by environment processors
FMachineActorFragmentTWeakObjectPtr<AMachineActor> — back-ref for click handling and visuals (mirrors FLogisticsBotActorFragment)
FMachineRepStateFragment (replicated)Compact slot/progress snapshot for clients, mirrored via Horizon proxy

Tags

FActiveMachineTag — machine is powered and eligible for dispatch.
FMachineWorkingTag — presence-only; drives the work-progress query.
FHorizonRepDirtyTag — existing pattern; marks the proxy for replication.

🔗 Operation Entity

One Mass entity per operation step. Server-only — operations do not replicate; clients infer work state from machine fragments.

Fragments

FragmentContents
FOperationFragmentBotOrderID, RecipeStep, RequiredVerb, PredictedDuration
FOperationLinksFragmentTArray<FPartHandle, TInlineAllocator<4>> Inputs/Outputs; TArray<FMassEntityHandle, TInlineAllocator<2>> Upstream/Downstream op handles
FOperationProgressFragmentProgress, AssignedMachine: FMassEntityHandle, SlotIndex
FOperationMWKRFragmentCached MWKR value + dirty flag

State Tags

Exactly one tag per operation at all times — mutually exclusive. Tag transitions are the primary mechanism processors use to filter their work.

TagCorresponding State
FOpWaitingForPartsTagWaitingForParts
FOpReceiverAwaitingDeliveryTagReceiverAwaitingDelivery
FOpWaitingForMachineTagWaitingForMachine
FOpQueuedTagQueued
FOpInProgressTagInProgress
FOpSenderAwaitingExportTagSenderAwaitingExport
FOpCompleteTagComplete

🔐 Part Safety

Parts are handles, not entities. UPartRegistrySubsystem (a UWorldSubsystem) is the sole source of truth for every part in the factory. This gives the performance of handles — no Mass-entity overhead at 10k+ parts — with stronger lifetime guarantees than the current implicit-ownership TArray soup.

Record structure

struct FPartRecord {
  EPartType         Type;
  EPartOwner        OwnerKind;          // Slot | Bin | BotCargo | Floor
  uint64            OwnerKey;           // packed: (entity handle | bin id | bot entity); unused when Floor
  uint32            Generation;         // increments on every owner change

  // valid only when OwnerKind == Floor:
  FVector           FloorPosition;
  FMassEntityHandle PendingDestination; // receiving op handle; null = orphaned → needs re-dispatch
};
TSparseArray<FPartRecord> Records;

FPartHandle = {uint32 Index, uint32 Generation}. Every read goes through Resolve(FPartHandle) → FPartRecord*. A mismatched generation is a stale handle — hard assert in Dev, log-and-drop in Shipping.

Safety invariants

  1. 1Every live FPartHandle resolves to exactly one record.
  2. 2Every record's OwnerKey is reachable from that owner (round-trip check).
  3. 3PartRegistryAuditProcessor runs every N ticks in Dev/Test builds: walks all machine slots, bins, and bot cargo fragments; rebuilds the owner→part map; asserts it matches the registry. Orphans are logged loudly.
  4. 4Ownership transfers go through a single TransferOwnership(Handle, NewOwner) call that bumps generation — no field-poking.

Floor recovery API

MethodPurpose
DropToFloor(FPartHandle, FVector)Sets OwnerKind=Floor, stores position, clears PendingDestination
GetOrphanedFloorParts() → TArray<FPartHandle>Returns floor parts where PendingDestination is null or the destination op is no longer live; called by LogisticsHandoffProcessor each tick to re-issue pickup orders
AssignFloorPartToBotOrder(FPartHandle, FMassEntityHandle DestOp)Sets PendingDestination; prevents double-dispatch

📐 Scheduler Processor Chain

The scheduler is an ordered chain of server-side processors. Order is fixed. All run single-threaded except MachineWorkProgressProcessor.

1 · OperationReadinessProcessor

Queries FOpWaitingForPartsTag. For each op, checks all Inputs[] are present and not in transit. On success: flip to FOpWaitingForMachineTag; mark MWKR dirty for self and entire upstream chain.

2 · MWKRRecomputeProcessor

Only walks ops with a dirty MWKR flag. Recursive memoised formula:
mwkr(op) = duration × (1 − progress) + max(mwkr(downstream))
Uses predicted duration, not actual — result is deterministic across modifier changes.

3 · MachineDispatchProcessor

The JIT-pull loop. Gathers FOpWaitingForMachineTag ops; stable-sorts by (BotPriority ASC, MWKR DESC, OperationHandle ASC) — the final key guarantees determinism. For each op: query eligible machines (verb match, slot free, enabled, modifier > 0); pick the one where effectiveRemaining + op.PredictedDuration is minimum; tiebreak by proximity, then MachineHandle ASC. Write AssignedMachine + SlotIndex, occupy slot, flip tag. Single-threaded by design — the global sort/assign is inherently sequential.

4 · MachineWorkProgressProcessor parallel

Parallel chunked query over FOpInProgressTag. For each op: look up the assigned machine's modifier; accumulate Progress += DeltaTime × BaseSpeed × MachineMod × ProcessMod / PredictedDuration. Sets FHorizonRepDirtyTag on the machine when slot state changes visibly. No cross-entity writes → safe to parallelise.

5 · OperationCompletionProcessor

Queries FOpInProgressTag with Progress ≥ 1.0. Registers output parts via UPartRegistrySubsystem::CreatePart(slot owner). Flips to FOpSenderAwaitingExportTag. Slot stays occupied — it frees only when the bot picks up the output.

6 · LogisticsHandoffProcessor

Bridges to the existing ULogisticsSimulationSubsystem: emits delivery orders for SenderAwaitingExport → ReceiverAwaitingDelivery pairs. On bot dropoff callback, transfers part ownership to the receiving op's slot and frees the sender slot.

♻️ Destruction & Recovery

Invariant: parts are never lost. Machine dismantled or bot destroyed → ownership transfers to the floor at the owner's last known world position. All pending deliveries are retried automatically by a new bot order.

Machine destroyed

MachineLifecycleProcessor implements a pre-removal hook and runs synchronously before the entity is removed:

  1. 1Read world position from FMachineActorFragment::Actor->GetActorLocation() before teardown — the actor may already be marked pending-kill.
  2. 2For each occupied slot: call PartRegistry.DropToFloor(handle, worldPos) for any slot-owned parts. Reset InProgress/Queued ops to WaitingForMachine (clear assignment, reset Progress, mark MWKR dirty). Leave SenderAwaitingExport ops as-is — the part is now on the floor; LogisticsHandoffProcessor re-dispatches pickup on the next tick.
  3. 3Set FHorizonRepDirtyTag for a final replication push before entity removal so clients see slots clear.
AMachineActor GC is independent. The TWeakObjectPtr in FMachineActorFragment goes null safely; no processor reads it after entity removal.

Bot destroyed mid-transit

ULogisticsSimulationSubsystem fires OnBotDestroyed(FMassEntityHandle, FVector LastPosition). LogisticsHandoffProcessor subscribes and for each part the bot was carrying:

  1. 1Call PartRegistry.DropToFloor(handle, lastPosition).
  2. 2Leave the receiving op in FOpReceiverAwaitingDeliveryTag — it is still waiting for the same part, just needs a new carrier.
  3. 3On the next tick, GetOrphanedFloorParts() surfaces the handle; LogisticsHandoffProcessor re-emits a pickup bot order (floor position → original destination slot) and calls AssignFloorPartToBotOrder.
  4. 4Cancel the old in-flight order record.

No op tag change is needed — FOpReceiverAwaitingDeliveryTag already means "waiting for this part to arrive"; the new bot order satisfies it identically.

🎨 Actor Representation Layer

AMachineActor is a thin AActor created once per machine. It owns: scene root, mesh, ISM handles, click collision, and a FMassEntityHandle MyEntity back-ref set during spawn.

No game logic in actors. Tick is disabled by default. All decisions live in processors. The actor is a display surface only.
MachineRepresentationProcessor

Runs on client (and server-listen-host). Reads FMachineSlotsFragment and FMachineProgressFragment; drives arm-pose interpolation. Replaces every per-arm IdleTick, JobRangeCheck, and MoveToTargetBehaviour call from the old ARobotArmBase.

Arm state machine collapse

The old ARobotArmBase state enum, timers, and behaviour functions collapse to a pure function of (slot state, progress, target-part position) — pose-only, no decisions. All decision logic moves into processors.

📡 Replication

Reuses the existing Plugins/Horizon/ patterns from the logistics bot implementation.

WhatHow
Machine stateUMachineEntityProxy : UHorizonEntityProxy — UPROPERTY-replicated mirror of FMachineRepStateFragment (slot occupancy, progress, working flag). Marked dirty by server processors via FHorizonRepDirtyTag.
Client updateClient MachineRepresentationProcessor consumes proxy data → updates client fragments → drives visuals.
OperationsNot replicated. Clients reconstruct visible work state from machine fragments only.
Interest filteringHorizonSpatialRepSyncProcessor provides scale-appropriate interest management per the existing logistics pattern.

🚀 Cutover Plan

Eight steps from skeleton to full cutover. Both modules compile in parallel throughout. No incremental migration — the old module is deleted in a single PR at step 8.

  1. 1Stand up FactoryFloor2/ module skeleton — traits and empty processors. Builds alongside FactoryFloor/.
  2. 2Implement UPartRegistrySubsystem + audit processor with unit-like dev tests.
  3. 3Implement Operation entity + full scheduler chain. Validate with a synthetic harness (no actors) — determinism gate and audit processor at every tick.
  4. 4Implement Machine entity + AMachineActor + representation processor. Port one machine type end-to-end — recommend Fabricator (cleanest single-slot work loop).
  5. 5Port remaining machine types (RobotArm variants, ItemBin, ItemSpawner, Monorail). Flag: Monorail conveyors may need a dedicated movement processor — treat as a sub-design when reached.
  6. 6Bridge ULogisticsSimulationSubsystem callbacks to the new LogisticsHandoffProcessor.
  7. 7Migrate placement/grid responsibilities from AFactoryFloorManager into UFactoryFloorSubsystem. Drop the work-dispatch managers entirely.
  8. 8Replace all spawn sites producing old AMachineBase with UMachineSpawner. Delete FactoryFloor/ in the same PR.

📎 Critical Reference Files

These existing files are read-only references — pattern sources, not files to modify during FactoryFloor2 development.

FileWhy it matters
Mass/Logistics/LogisticsBotFragments.hFragment design template for the new machine and operation fragments
Mass/Logistics/Traits/LogisticsBotServerTrait.cppTrait pattern to follow for MachineServerTrait
Mass/Logistics/Processors/Server/LogisticsBotStateProcessor.cppState-tag processor pattern; this is what the scheduler chain processors should look like
Plugins/Horizon/…/HorizonEntityProxy.hBase class for UMachineEntityProxy
Plugins/Horizon/…/HorizonSpatialRepSyncProcessor.hInterest filtering at scale
FactoryFloor/DataTypes/MachineData.hPort the shape of this into the new MachineData.h
FactoryFloor/DataTypes/WorkOrderData.hInforms the Operation and BotOrder data model

Verification

Determinism gate

Synthetic harness (no rendering): seed N=1024 machines, 4096 ops; run two identical runs; assert byte-equal slot assignment sequences. No actors required — validates the scheduler chain in isolation.

Part registry audit

Enable PartRegistryAuditProcessor at every tick in tests. Run a 60s simulated session with machine and bot churn. Zero orphaned parts expected.

Performance gate

ProfileGPU + Unreal Insights: full scheduler chain ≤ 2ms server frame at target scale (1k machines, 10k ops). Verify parallel scaling of MachineWorkProgressProcessor with thread count.

ImGui overlay

Reuse the ReceiveImGuiDrawcall_Implementation pattern from AMachineBase. Live view: op states per machine, slot occupancy, MWKR values, registry size, orphan count.

Net soak test

4-client soak: clients must show correct slot/progress visuals at 10Hz cadence; no actor desync across a sustained machine churn session.

Cutover smoke test

Load an existing save (flag as a sub-task if save-compat is required) and confirm all machines respawn correctly under the new module. No orphaned parts on load.