In Starbud Station, machines do a lot of heavy lifting. A trimmer takes raw flower and produces trimmed product. A press compresses material into concentrated form. An extractor runs multi-stage processes with configurable parameters. A packager outputs finished inventory ready for sale. Each machine is fundamentally the same thing at the system level — it accepts inputs, runs a timed process, and emits outputs — but the variations in configuration, timing, and output logic are significant enough that a naive implementation would have become a maintenance problem fast.

The goal was a single machine architecture that any processing device in the game could use without duplicating logic. Every machine is an actor with a UMachineComponent attached. The component owns the job queue, the active timer, the input and output slot references, and the replication state. The actor itself is just a mesh and a Blueprint child class for tuning parameters in the editor.

The Job Queue

Every operation a machine performs is represented as an FMachineJob. This is a simple struct carrying the input item reference, the recipe being used, the duration in seconds, the output definition, and a unique job ID for tracking across network boundaries.

USTRUCT(BlueprintType)
struct FMachineJob
{
    GENERATED_BODY()

    UPROPERTY() FGuid          JobId;
    UPROPERTY() FName          RecipeId;
    UPROPERTY() FInventorySlot InputSlot;
    UPROPERTY() float          DurationSeconds;
    UPROPERTY() float          ElapsedSeconds;
    UPROPERTY() bool           bIsActive;
};

The UMachineComponent maintains a TArray<FMachineJob> as its job queue. Jobs are pushed from server-side logic only — a player interacting with the machine UI sends an RPC to the server, which validates the inputs, constructs the FMachineJob, and enqueues it. The client never writes directly to the queue. This keeps the system server-authoritative from the first interaction.

Timer Management

Processing timers in a networked game have a specific problem: the server needs to be the authority on when a job completes, but clients need to show accurate progress without pinging the server every frame. We handle this with a hybrid approach. The timer runs on the server using Unreal's FTimerManager. Elapsed time is replicated on the active job struct so clients can display accurate progress bars by reading the replicated value. Completion fires a server-side delegate and triggers a NetMulticast notification to all clients.

void UMachineComponent::StartNextJob()
{
    if (JobQueue.IsEmpty() || bIsProcessing) return;

    ActiveJob = JobQueue[0];
    ActiveJob.bIsActive = true;
    bIsProcessing = true;

    FTimerDelegate Delegate;
    Delegate.BindUObject(this, &UMachineComponent::OnJobCompleted);

    GetWorld()->GetTimerManager().SetTimer(
        ProcessingTimerHandle,
        Delegate,
        ActiveJob.DurationSeconds,
        false
    );
}

When the timer fires, OnJobCompleted resolves the output using the recipe definition, pushes the result to the machine's output inventory slot, removes the job from the queue, broadcasts OnProductionCompleted, and calls StartNextJob again. The queue drains itself automatically — as long as there are jobs queued and inputs available, the machine keeps running without any external polling.

Recipe Definitions

Recipes are defined in a UMachineRecipeData DataAsset. Each recipe specifies the input item type and quantity, the output item type and quantity range, the base processing duration, any quality multipliers from input traits, and an optional secondary output chance. This lives in data, not code — designers can add, modify, or balance recipes without touching C++.

The machine component holds a reference to the recipe dataset and resolves the active job's output at completion time rather than at enqueue time. This matters for quality-dependent outputs: a machine processing higher-quality inputs calculates a better output at completion, not when the job was submitted. This means a quality upgrade mid-run applies to any queued jobs, which is an intentional design choice that rewards players who invest in their inputs.

Replication Strategy

The job queue and active job are both replicated using Unreal's standard property replication. The queue is marked with UPROPERTY(Replicated) and triggers a OnRep_JobQueue callback on clients to update the UI. The active job's ElapsedSeconds field is updated on the server each tick only when a job is active, keeping replication overhead minimal during idle states.

One design decision we made early: machines never replicate their internal timer handle state directly. Clients reconstruct progress from the replicated elapsed time and the job duration. This avoids timer synchronisation issues entirely and means clients always show accurate progress even after joining mid-session.

When a client joins an active game session, the replicated job queue and active job arrive with their current state intact. The client reads ElapsedSeconds and DurationSeconds to reconstruct where in the process the machine is. There's no catch-up logic needed — the data is already there.

Multi-Stage Processing

Some machines in Starbud Station run multi-stage jobs — an extractor, for example, goes through a preparation phase, an active extraction phase, and a cooldown phase before the output is accessible. These are implemented as job chains rather than a single complex job. When the server completes stage one, it automatically enqueues the stage two job. From the player's perspective it looks like a single continuous process. Under the hood it's a sequence of discrete FMachineJob entries, each with their own duration and intermediate state.

This turned out to be much cleaner than trying to encode multi-stage logic into a single job struct. Each stage is independently cancellable, independently inspectable, and the progression between them fires all the same delegates that single-stage jobs fire. The UI just sees a queue draining and doesn't need to know whether a job is a standalone process or part of a chain.

What's Still Being Built

The machine system foundation is complete and running. The next phase covers machine upgrade logic — higher-tier machines reduce processing time and unlock recipe variants — and the automation layer, where players can wire machines together into production pipelines that run without manual intervention. The automation design is built on top of the same job queue architecture, just with the job submission triggered by sensor components rather than player interaction.