This page describes the runtime skeleton of a machine and the recipe execution architecture in PrototypeMachinery.
Docs follow the current implementation. If you find a mismatch, treat the code as the source of truth.
Chinese original:
-
Types and runtime instances:
src/main/kotlin/api/machine/*src/main/kotlin/impl/machine/*- Block entity:
src/main/kotlin/common/block/entity/MachineBlockEntity.kt
-
Component system (ECS variant):
src/main/kotlin/api/machine/component/*src/main/kotlin/impl/machine/component/*
-
Recipes and processes:
src/main/kotlin/api/recipe/*src/main/kotlin/impl/recipe/*
MachineType: the definition of "what this machine is" (structure, component types, metadata, ...)MachineBlockEntity.initialize(machineType): creates and binds aMachineInstanceImpl- Formed state: when a multiblock match succeeds, the instance updates its formed status, used by rendering and runtime permission checks.
- Components are declared and constructed via
MachineComponentType. MachineSystemprovides tick-driving and system ordering (dependency relations -> topological sorting).- Some components can have
system == null, meaning they are data-only and not ticked.
MachineRecipe: the recipe definition, primarily a list of requirements grouped by requirement type.RecipeProcess: a runtime process instance.- Holds a seed to allow reproducible randomness.
- Has a dedicated attribute overlay (
attributeMapoverlay per process).
RecipeRequirementType is bound to a RecipeRequirementSystem.
Core API:
src/main/kotlin/api/recipe/requirement/component/system/RecipeRequirementSystem.ktstart(process, component): RequirementTransactionacquireTickTransaction(process, component): RequirementTransaction(optional; for tickable requirements)onEnd(process, component): RequirementTransaction
-
Obtaining a transaction (start/tick/end) may produce immediate side effects, e.g.
- reserving items
- draining energy
- writing temporary state
-
The caller must decide based on
RequirementTransaction.result:Success: must callcommit()Blocked: must callcommit()as well- and the system must guarantee "no side effects" or "recoverable side effects" semantics to avoid deadlocks
Failure: must callrollback()
-
The executor must treat a set of requirements within the same stage as one atomic stage. If any requirement returns
Blocked/Failure, it must rollback all transactions already acquired in this stage.
Default executor implementation:
src/main/kotlin/impl/machine/component/system/FactoryRecipeProcessorSystem.kt- stages: START -> (TICK...)* -> END
- collects transactions per stage; commits all on success; rolls back (reverse order) on failure/blocked
- stable ordering (by type id) to keep behavior reproducible and tests deterministic
This layer is the main extension point for complex behaviors (input/output, chance, multipliers, candidates, ...).
Each RecipeProcess can carry an overlay; requirement execution resolves the "effective component" before running.
- Entry:
src/main/kotlin/impl/recipe/requirement/overlay/RecipeRequirementOverlay.kt - Overlay components:
impl/recipe/process/component/RecipeOverlayProcessComponent*
Typical uses:
- Apply different consumption multipliers / filters per process instance
- Provide independent parameter views for parallel processes
RecipeProcess also supports a component system.
Each RecipeProcessComponentType may bind a process system, executed each machine tick (pre/tick/post).
FactoryRecipeProcessorSystemcallstickProcessComponents(process, Phase.*)per tick- Lifecycle helper component:
impl/recipe/process/component/RecipeLifecycleStateProcessComponent*(e.g. flags like started)
To avoid scanning all recipes frequently, the project uses indexing:
IRecipeIndexRegistry/RecipeIndexRegistryRequirementIndexFactorybuilds indexes for each requirement type
Note: this “recipe indexing” refers to runtime scanning acceleration (shrinking the candidate set), not JEI
IIngredientsindexing (search / lookup).
- Goal: reduce the number of recipes that need expensive simulate-based checks in
FactoryRecipeScanningSystem. - Constraint: indexes must be conservative (prefer false positives; avoid false negatives).
- Only use cheap, observable machine state:
- Item / Fluid: prefer storage-backed key-level containers (can enumerate and count keys).
- Capability-backed containers without enumeration should yield “no opinion” (
lookup() == null) or disable indexing.
RequirementIndex.lookup(machine):null: no opinion (e.g. machine has no observable containers of that type).emptySet(): strong filter meaning “no recipes match current state”.
RecipeIndex.lookup(machine)intersects non-null results.- If all indices return
null, the caller should treat it as “index unavailable” and fall back to normal scanning.
- If all indices return
- Key:
PMKey<ItemStack>prototype equality (count ignored). - Build:
recipesByInputKey: Map<PMKey<ItemStack>, Set<MachineRecipe>>- (optional)
requiredByRecipe: Map<MachineRecipe, Map<PMKey<ItemStack>, Long>>for amount-level filtering
- Lookup:
- Read only PortMode.OUTPUT item sources.
- Use only storage-backed containers (enumerate keys + amounts).
- Union by available keys, then (optionally) check totals against
requiredByRecipe.
- Key:
PMKey<FluidStack>prototype equality. - Build:
- Use
inputs+inputsPerTickfor coarse “presence” filtering. - Prefer amount-level filtering only for
inputsinitially, to avoid false negatives.
- Use
- Lookup:
- Read only PortMode.OUTPUT fluid sources.
- Use only storage-backed containers.
- Build a simple threshold table per recipe (e.g.
input, optionallyinput + inputPerTick). - Lookup sums PortMode.OUTPUT
StructureEnergyContainer.storedand filters by threshold.
CheckpointRequirementComponent(requirement=...): unwrap and index the inner requirement.SelectiveRequirementComponent(candidates=[...]): union candidates into indices (conservative).
This is a design proposal to be implemented later.
- During checks (simulate): treat chance as 100%.
- During actual IO (execute): apply the effective chance.
- If chance and fuzzy IO coexist:
- checks still run fuzzy checks with chance=100%
- actual IO first decides chance; if it fails, skip the fuzzy operation.
Use RecipeProcess.seed via RecipeProcess.getRandom(salt).
For per-tick randomness, introduce a persisted tick counter process component and include it in the salt.
Let parallelism be
Split:
Define “number of successful executions” as:
This avoids the “all or nothing” extreme while staying stochastic and reproducible.
- During checks: pick the first satisfiable candidate and lock it.
- Locks must be rollbackable and thus stored in a process component (e.g.
locks[(requirementId, groupIndex, stage)]).
- During checks: perform a strict capacity check (chance=100%).
- Conservative approach: validate the worst-case selection (top-N by required count).
- During execution: pick
Ndistinct candidates via weighted sampling without replacement using the process RNG.