Crafting

Module PulseCrafting (depends on PulseInventory). Players turn items into other items: by hand, or at stations such as a workbench or a forge. Crafting takes time, runs in a queue, gives the ingredients back when a job is cancelled, and is saved with the player.

Overview

Type What it is
UPulseRecipe Data asset (Primary Asset Type PulseRecipe, registered in code for /Game and the content of every plugin; a project entry must have exactly this base class, no Blueprint classes, not editor only): ingredients, tools, outputs, duration, station tags, conditions
UPulseRecipeBook Data asset (Primary Asset Type PulseRecipeBook, registered the same way): a list of recipes given to crafters as a whole
UPulseCraftingComponent Pulse Crafting: known recipes and the job queue of one player
UPulseCraftingStationComponent Pulse Crafting Station: makes an actor a station (Station Settings: tags, speed, range, own inventory; a station id for saves)
UMVVMPulseCrafting View model of a crafting menu (recipes, ingredients, queue)

Setup

  1. Add Pulse Crafting to an actor the player owns: the pawn, the controller or the player state. The player also needs an inventory (UPulseInventoryComponent) on one of those actors; ingredients come from it and crafted items go into it.
  2. Create recipes (Miscellaneous > Data Asset > Pulse Recipe) and put them into recipe books.
  3. Give the crafter the recipes: books in RecipeBooks of the component, books every crafter knows in Project Settings > Pulse > Core > Crafting > Default Recipe Books, or unlock recipes at runtime (UnlockRecipe, UnlockRecipeBook), e.g. as a quest reward. Turn off bRequireKnownRecipes for a sandbox where every recipe works.
  4. For stations, add Pulse Crafting Station to an actor (a workbench) and give it station tags in its Station Settings (Pulse.Crafting.Station.*). Recipes with RequiredStationTags can only be crafted at a station that has all of them; recipes without station tags work by hand and at every station.
  5. Start crafting from the UI with RequestCraft(Recipe, Count, StationActor). On a client the request goes to the server.

Recipes

Property Meaning
DisplayName, Description, Icon Shown in menus. Empty name or icon: those of the first output.
Category, SortOrder Groups and orders recipes in menus (Pulse.Crafting.Category.*).
Ingredients Item and count per round. Leave Item empty and set ItemTags for “any item with these tags” (any wood). bIsTool: must be there once, is not used up (hammer).
Outputs Item and count per round. Chance for by-products; bRollRandomStats rolls random stats for each crafted item (see Loot and random stats).
Duration Seconds per round; 0 crafts at once.
RequiredStationTags The station needs all of them. Empty: by hand or at any station.
Conditions Pulse conditions (FPulseConditionList, see Conditions, costs and rewards): Actor Tag Query on the crafter, Has Item, skills, quests, … The context: the crafter is Player and Instigator, the owner of the station Target, the recipe Source, the number of rounds Amount, and FPulseCraftingContext the extra data.

Fixed ingredients are planned before tag ingredients, so “any wood” does not use up the oak a fixed ingredient needs. Data validation reports recipes without outputs and ingredients without item and tags.

Stations

Everything a station allows is one value, Station Settings (FPulseCraftingStationSettings): StationTags, SpeedMultiplier, UseRange, bUseStationInventory, bOutputToStationInventory. Blueprint reads the property and writes it with Set Station Settings; C++ uses GetStationSettings / SetStationSettings (and GetStationTags, GetSpeedMultiplier). Setting it is allowed on the authority (and on every machine before BeginPlay); a change fires OnSettingsChanged / OnSettingsChangedNative there and on clients when it arrives. Equal settings fire nothing.

Station ids. A saved job finds its station again by the station id (GetStationId), made from the Save Identity of the station (FPulseSaveIdentity of Pulse Foundation, scope World by default):

  • Stations placed in a level work without setup: their id is made from the actor and component names and the streamed level they are in (so the same layout in two sublevels or level instances gives two ids). Renaming the actor or the component loses the station of saved jobs.
  • Stations spawned while playing need a Key (for example from the spawner’s own id), set before the actor finished spawning (deferred spawn) or with Set Save Identity. Without a Key the station has no id and its jobs are saved without station.
  • Two stations with one id (a Key used twice) find none of their saved jobs: the registry logs a warning, and such a job counts as having lost its station until one of them gets its own Key.

FindStationById(WorldContext, Id) finds a registered station (authority; stations register in BeginPlay).

Jobs

StartCrafting(Recipe, Count, Station) (authority) or RequestCraft (any machine) checks the recipe and queues a job:

  • All or nothing: the ingredients of all rounds are taken at once, from the player’s inventory and, if the station allows it (bUseStationInventory), from the station’s inventory. One plan covers all rounds (Pulse::Crafting::PlanRounds), so every round finds the items the check found; it is split per round for exact refunds. If anything is missing, nothing is taken. Each source inventory fires its events once, after the job is queued.
  • One after the other: only the first job of the queue runs; one round after the other. A station’s SpeedMultiplier shortens the rounds.
  • Output: the crafted items go into the player’s inventory, or the station’s (bOutputToStationInventory). What does not fit is dropped next to the station or the crafter (Overflow = Drop), or the job waits until there is room (Overflow = Wait).
  • Cancel: CancelJob / RequestCancelJob gives back exactly the items the unfinished rounds took; items that were already crafted are handed out.
  • Station gone: with bCancelWhenStationLost a job whose station was destroyed is cancelled when its round ends; so is a loaded job whose station id is not registered (or shared by two stations) at that moment. Without it the job finishes without its station (into the player’s inventory). A loaded job whose station registers later finds it.
  • Empty rounds: a round whose outputs all failed their chance still counts (OnRoundCrafted), the job does not wait.
  • MaxQueueLength limits the jobs, MaxRoundsPerJob (settings) the rounds of one job.

CanCraft runs the same checks without changing anything and returns the reason as EPulseCraftingResult and as text for the player: unknown recipe, wrong station, too far away (UseRange of the station), a condition that fails (its line, e.g. “Needs the forge key”), queue full, missing ingredients. GetMaxCraftable says how many rounds the ingredients are enough for.

Override GetSourceInventories and GetOutputInventory (Blueprint native events) for other rules, e.g. a shared storage of the base. Crafting and the view model use GetCraftingSources: the sources without empty entries and duplicates, so an inventory returned twice is counted and taken from once. When a station inventory is used, it must allow the player to change it (UPulseInventoryComponent::CanRequesterModify), otherwise client requests are rejected.

Events and messages

Every change of the queue is one operation (start, cancel, cancel all, a finished round, a delivery after room was made, an import). Its events fire after the whole change, when the queue holds its final state: first the inventory events of the crafter’s own changes (one OnInventoryChanged per applied change, each with its stack snapshot), then the queue events in the order they happened, then OnQueueChanged once. A listener that calls back into the crafter (StartCrafting, CancelJob) gets its result at once; the events of its call fire after the running ones.

Event (native and Blueprint) When
OnQueueChanged Job added, round finished, job finished or cancelled: once per operation on the authority, on the owner when the queue arrives
OnRoundCrafted A round was finished and its items handed out (authority)
OnJobCompleted, OnJobCancelled A job finished all its rounds, or was cancelled (authority)
OnRecipeUnlocked A recipe was unlocked (authority and owner)

Message bus channels (payload FPulseCraftingMessage, sent on the server): Pulse.Message.Crafting.Queued, .Crafted, .Completed, .Cancelled, .RecipeUnlocked. Their context names the crafter as Player and Instigator, the station’s owner as Target, the recipe as Source and the rounds as Amount (1 for .Crafted and .RecipeUnlocked). Quests and achievements can count crafted items with them without a bridge.

Multiplayer

The server decides everything. The queue and the unlocked recipes replicate to the owner only; the Station Settings replicate to everybody as one property (push model). Jobs replicate their station id and the station reference. Progress (GetRoundProgress, GetRoundTimeLeft) is computed from the server time, so nothing ticks; on clients it can lag a little behind the server.

Crafting menus

UMVVMPulseCrafting feeds a menu: call SetCrafter (with FindCraftingComponent of the player) and SetStation.

  • Recipes (UMVVMPulseRecipe): name, icon, category, duration at this station, bCanCraft with Reason, MaxCraftable, Ingredients (available and needed per ingredient) and Outputs.
  • Jobs (UMVVMPulseCraftingJob): recipe, rounds left, running or waiting for room; GetProgress and GetTimeLeft for a progress bar, RequestCancel.
  • Categories, SetCategoryFilter, bQueueFull, RequestCraft.

The view model refreshes when the queue, the unlocked recipes or the source inventories change.

Saving

ExportSaveData / ImportSaveData and SaveToBytes / LoadFromBytes (versioned) save the unlocked recipes and the queue, including the items each unfinished round took and the progress of the running round. A loaded job continues where it stopped (the first job with the progress of its own saved entry) and finds its station by its station id (save data version 3; see Stations). A job whose recipe no longer exists is refunded; the import first replaces recipes and queue in one step and gives the refunds back afterwards, so inventory listeners already see the loaded queue. Recipes and items are saved by their primary asset id (PulseRecipe:<name>, PulseItem:<name>) with the path as fallback, so moved assets still load (save data version 2: every unlocked recipe is one FPulseCraftingSavedRecipe entry in Recipes with id and path; version 1 data still loads from UnlockedRecipes by path). Recipe and recipe book asset names must be unique within their type, like item names. With PulseSave use UPulseSaveCraftingComponent from PulseCoreSaveBridge, and load it after the inventories.

Rules without a world

The rules of crafting are plain functions in Pulse::Crafting, so specs and other code can use them without components:

Header Functions
PulseCraftingRules.h PlanRounds (one plan for all rounds of a job, split per round, tools apart), PlanIngredients (its total), GetMaxRounds, GetIngredientStatus, IsStationSuitable, RollRoundOutputs (chances and random stats of one round), GetRoundDuration (station speed, instant crafting), IsWithinUseRange
PulseCraftingQueue.h FPulseCraftingQueue (jobs, next id) with CheckEnqueue, Enqueue, Next (the state machine of the first job: wait, craft, deliver), FinishRound, ConfirmDelivery, Cancel, GetStationAvailability, GetRoundProgress, GetRoundTimeLeft, ToSavedStack, MakeSavedJobs, QueueFromSave; the functions report what happened as events in order

Specs: PulseCrafting.Plan and PulseCrafting.Queue.

Settings

Project Settings > Pulse > Core > Crafting (UPulseCraftingSettings):

Setting Default Meaning
DefaultRecipeBooks empty Recipe books every crafter knows.
MaxRoundsPerJob 99 Most rounds of one job.
DropScatterRadius 60 Spread of dropped items (cm).

Debugging

  • Every crafting component is a snapshot provider (category Crafting): queue, progress and waiting jobs show on the Live page of the Pulse Debugger and with PulseDebugDump Crafting.
  • Queued, crafted and cancelled jobs are timeline events (source Crafting).
  • Cheats and Pulse.Crafting.Instant: see Console.

PulseCore plugin documentation.

This site uses Just the Docs, a documentation theme for Jekyll.