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
- 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. - Create recipes (Miscellaneous > Data Asset > Pulse Recipe) and put them into recipe books.
- Give the crafter the recipes: books in
RecipeBooksof 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 offbRequireKnownRecipesfor a sandbox where every recipe works. - For stations, add Pulse Crafting Station to an actor (a workbench) and give it station tags in its Station Settings (
Pulse.Crafting.Station.*). Recipes withRequiredStationTagscan only be crafted at a station that has all of them; recipes without station tags work by hand and at every station. - 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
SpeedMultipliershortens 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/RequestCancelJobgives back exactly the items the unfinished rounds took; items that were already crafted are handed out. - Station gone: with
bCancelWhenStationLosta 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. MaxQueueLengthlimits 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,bCanCraftwithReason,MaxCraftable,Ingredients(available and needed per ingredient) andOutputs.Jobs(UMVVMPulseCraftingJob): recipe, rounds left, running or waiting for room;GetProgressandGetTimeLeftfor 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 withPulseDebugDump Crafting. - Queued, crafted and cancelled jobs are timeline events (source
Crafting). - Cheats and
Pulse.Crafting.Instant: see Console.