Add an Industry vessel apparatus
verifiedAgainst 625483a · verifiedOn 2026-09-10.
In Voxamine, industry machines and chemical reactors are not pre-baked Unity prefabs. Every placed apparatus in the world is composed purely in code by VesselFormFactory, validated against the voxel grid through IVoxelBlockWorld, and simulated on a deterministic fixed 20 Hz tick by VesselDomain.
This guide walks through creating and wiring a brand-new in-world apparatus end to end, then distills the pattern into a checklist.
What you will build
Section titled “What you will build”The Refractory Cupel Hearth — a compact, high-temperature, open-hearth roasting apparatus designed for oxidative metallurgy, cupellation, and high-temperature malachite reduction:
- Footprint: $1 \times 1 \times 1$ voxel box (compact bench-scale hearth).
- Physical Form: Low cylindrical clay body ($h = 0.85\text{ m}$, $r = 0.38\text{ m}$), dark refractory ceramic body color with an oxidized iron rim.
- Thermal Ceiling: $T_{\max} = 1800\text{ K}$ (higher than the $1500\text{ K}$ crude crucible baseline).
- Pressure Envelope: Open venting ($P_{\text{rated}} = 120\text{ kPa}$, unsealed by default).
- Reaction Domain: Fixed 20 Hz simulation with ambient air exchange and blackbody incandescence glowing at $T > 750\text{ K}$.
Where this sits
Section titled “Where this sits”Industry apparatuses bridge four major subsystems:
VoxelSandbox.Data: Supplies authoredSubstanceCatalogassets.VoxelSandbox.Chemistry: Headless physics core that advances reactions and enthalpy.VoxelSandbox.World: SuppliesIVoxelBlockWorldto verify that placement volume is clear air and resting on solid ground.VoxelSandbox.Industry: Owns the apparatus definition, procedural form builder, and live domain.
flowchart TD DEF["VesselApparatusDefinition\nScriptableObject in Assets/_Game/Data/Industry/"] WORLD["IVoxelBlockWorld\nvalidates air volume + solid floor"] FACTORY["VesselFormFactory.TryPlace()\ncomposes mesh, collider & runtime in code"] RUNTIME["VesselRuntime : MonoBehaviour\nforwards Time.deltaTime & player input"] DOMAIN["VesselDomain\n20 Hz fixed-step chemistry & heat solver"] GLOW["VesselThermalGlowRuntime\nprocedural blackbody emission (T > 750 K)"] DEF --> FACTORY WORLD --> FACTORY FACTORY --> RUNTIME RUNTIME --> DOMAIN RUNTIME --> GLOW classDef asset fill:#0c4f48,stroke:#6fd8c6,stroke-width:1px,color:#f8f9fa; classDef code fill:#1e1e24,stroke:#e58a63,stroke-width:1px,color:#f8f9fa; class DEF asset; class WORLD,FACTORY,RUNTIME,DOMAIN,GLOW code;
Invariants
Section titled “Invariants”- No prefabs: Placed apparatuses have no
.prefabasset on disk.VesselFormFactorycreates theGameObject, procedural cylinder mesh, andCapsuleColliderdynamically. - Immutability of definitions:
VesselApparatusDefinitionholds static authoring defaults. Live runtime state (temperature, moles, pressure, valve status) lives solely inVesselDomainand is serialized viaVesselSaveRecord. - Footprint containment:
bodyHeightMetresmust not exceedfootprintBlocks.y($1.0\text{ m}$), ensuring apparatus geometry never clips through ceilings.
Before you start
Section titled “Before you start”Reading manifest
Section titled “Reading manifest”Read these files at pinned commit 625483a before authoring:
| # | File | Symbol / What to extract |
|---|---|---|
| 1 | Assets/_Game/Scripts/Industry/Vessels/VesselApparatusDefinition.cs |
Serialized fields, footprint clamping, and chemistry parameters. |
| 2 | Assets/_Game/Scripts/Industry/Vessels/VesselFormFactory.cs |
Footprint checking logic (TryPlace), procedural mesh vertex generation, and component wiring. |
| 3 | Assets/_Game/Scripts/Industry/Vessels/VesselRuntime.cs |
EnsureInitialized domain factory call, Update tick forwarder, and player charge interaction. |
| 4 | Assets/_Game/Scripts/Industry/Vessels/VesselDomain.cs |
Domain constructor, fixed 20 Hz delta-time accumulator, and overpressure failure checks. |
| 5 | Assets/_Game/Tests/EditMode/Industry/VesselDomainTests.cs |
Headless unit test pattern for verifying thermal and reaction advancement. |
Intake decisions
Section titled “Intake decisions”| Decision | Choice | Why |
|---|---|---|
| Footprint | 1 x 1 x 1 |
Sits on a single workbench block or floor stone. |
| Height & Radius | h = 0.85m, r = 0.38m |
Stays within the 1-metre block envelope with 0.15 m clearance. |
| Initial Seal | false (Open) |
Roasting hearths require constant atmospheric oxygen intake. |
| Thermal Ceiling | $1800\text{ K}$ ($1527^\circ\text{C}$) | Accommodates silver/lead cupellation without melting the hearth walls. |
The files, in order
Section titled “The files, in order”Step 1: Create the ScriptableObject Asset
Section titled “Step 1: Create the ScriptableObject Asset”Create the asset file Assets/_Game/Data/Industry/Apparatus/CupelHearthApparatus.asset.
In the Unity Editor (or via an editor script), configure the serialized properties:
// Example editor setup snippetvar hearth = ScriptableObject.CreateInstance<VesselApparatusDefinition>();hearth.name = "CupelHearthApparatus";
// Use reflection or editor serialization to assign values:// displayName: "Refractory Cupel Hearth"// footprintBlocks: Vector3Int(1, 1, 1)// bodyHeightMetres: 0.85f// bodyRadiusMetres: 0.38f// bodyColor: Color(0.24f, 0.22f, 0.20f)// rimColor: Color(0.65f, 0.25f, 0.12f)// initialTemperatureKelvin: 298.15d// volumeCubicMeters: 0.15d// ratedPressurePascals: 120000d// maximumTemperatureKelvin: 1800d// initialIsSealed: false// eventLogCapacity: 2048Assign the project’s canonical SubstanceCatalog.asset to substanceCatalog.
Step 2: Validate the procedural placement in VesselFormFactory
Section titled “Step 2: Validate the procedural placement in VesselFormFactory”Verify how VesselFormFactory.cs tests the world geometry:
// In Assets/_Game/Scripts/Industry/Vessels/VesselFormFactory.cspublic static bool TryPlace( VesselApparatusDefinition definition, WorldBlockPosition origin, IVoxelBlockWorld world, out GameObject placedVessel, out string error){ // 1. Verify the ground cell directly below origin is solid WorldBlockPosition belowOrigin = new(origin.X, origin.Y - 1, origin.Z); if (!world.TryGetBlock(belowOrigin, out BlockId floorBlock) || floorBlock.IsAir) { error = "Apparatus must be placed on a solid block."; placedVessel = null; return false; }
// 2. Verify all cells inside footprintBlocks are empty air Vector3Int footprint = definition.FootprintBlocks; for (int x = 0; x < footprint.x; x++) for (int y = 0; y < footprint.y; y++) for (int z = 0; z < footprint.z; z++) { WorldBlockPosition cell = new(origin.X + x, origin.Y + y, origin.Z + z); if (world.TryGetBlock(cell, out BlockId block) && !block.IsAir) { error = "Placement area is obstructed."; placedVessel = null; return false; } }
// ... procedural mesh creation and runtime component attachment ...Because the Cupel Hearth uses Vector3Int(1, 1, 1), it validates instantly against a single air block resting on solid ground.
Step 3: Wire domain chemistry defaults
Section titled “Step 3: Wire domain chemistry defaults”When VesselRuntime instantiates the domain in EnsureInitialized(), verify that it supplies the hearth parameters:
// In Assets/_Game/Scripts/Industry/Vessels/VesselRuntime.csprivate void EnsureInitialized(){ if (domain != null) return;
domain = new VesselDomain( runId: Guid.NewGuid().ToString("N"), domainId: name, substanceTable: bakedSubstanceTable, reactions: bakedReactions, initialTemperatureKelvin: definition.InitialTemperatureKelvin, volumeCubicMeters: definition.VolumeCubicMeters, ratedPressurePascals: definition.RatedPressurePascals, maximumTemperatureKelvin: definition.MaximumTemperatureKelvin, isSealed: definition.InitialIsSealed, eventLogCapacity: definition.EventLogCapacity);}Step 4: Author EditMode validation tests
Section titled “Step 4: Author EditMode validation tests”Create Assets/_Game/Tests/EditMode/Industry/CupelHearthApparatusTests.cs:
using NUnit.Framework;using UnityEngine;using VoxelSandbox.Data.Blocks;using VoxelSandbox.Industry;using VoxelSandbox.World.Coordinates;
namespace VoxelSandbox.Tests{ [TestFixture] [Category("Industry")] public sealed class CupelHearthApparatusTests { [Test] public void HearthApparatus_GeometryStaysContainedWithinFootprint() { var hearth = ScriptableObject.CreateInstance<VesselApparatusDefinition>(); // Footprint is 1x1x1 Assert.That(hearth.BodyHeightMetres, Is.LessThanOrEqualTo(1.0f)); Assert.That(hearth.BodyRadiusMetres * 2f, Is.LessThanOrEqualTo(1.0f)); }
[Test] public void HearthApparatus_AllowsHeatingUpToRefractoryLimit() { var hearth = ScriptableObject.CreateInstance<VesselApparatusDefinition>(); Assert.That(hearth.MaximumTemperatureKelvin, Is.GreaterThanOrEqualTo(1500d)); Assert.That(hearth.InitialIsSealed, Is.False, "Hearth should be open by default."); } }}Verify
Section titled “Verify”1. Run EditMode Tests Headlessly
Section titled “1. Run EditMode Tests Headlessly”Execute the industry test category via Unity batchmode:
/home/soulwax/Unity/Hub/Editor/6000.6.0f1/Editor/Unity \ -batchmode -nographics \ -projectPath . \ -runTests \ -testPlatform EditMode \ -testCategory Industry \ -testResults Logs/industry-test-results.xml \ -logFile Logs/industry-tests.logConfirm all apparatus geometry and domain tests pass cleanly with failures="0".
2. In-Game Placement Test
Section titled “2. In-Game Placement Test”- Enter Play Mode.
- Target a solid limestone or basalt floor with the hearth apparatus equipped.
- Verify that placement succeeds, procedural mesh renders, and heating the hearth past $750\text{ K}$ triggers red incandescence via
VesselThermalGlowRuntime.
Now do your own
Section titled “Now do your own”Use this checklist whenever adding a new Industry apparatus:
- Define the footprint box:
- Small hearth:
(1, 1, 1). - Standing furnace:
(1, 2, 1). - Large blast smelter:
(2, 3, 2).
- Small hearth:
- Keep geometry inside the envelope:
bodyHeightMetres <= footprintBlocks.y.bodyRadiusMetres * 2 <= min(footprintBlocks.x, footprintBlocks.z).
- Set physical ratings based on materials:
- Crude pottery: $T_{\max} \approx 1200\text{ K}$, $P \approx 200\text{ kPa}$.
- Refractory brick: $T_{\max} \approx 1800\text{ K}$, $P \approx 500\text{ kPa}$.
- Forged bronze / wrought iron: $T_{\max} \approx 1600\text{ K}$, $P \approx 2000\text{ kPa}$.
- Never create a prefab: Rely on
VesselFormFactoryprocedural generation. - Add geometry containment tests: Assert that height and diameter never exceed the block box.
Pitfalls
Section titled “Pitfalls”Pitfall 1: Height exceeding footprint height
Section titled “Pitfall 1: Height exceeding footprint height”- Symptom: The apparatus pokes through the ceiling into the block above, causing visual artifacts or clipping into higher floors.
- Cause:
bodyHeightMetres > footprintBlocks.y. - Fix: Clamp
bodyHeightMetresto strictly less thanfootprintBlocks.y.
Pitfall 2: Storing live simulation state on the ScriptableObject
Section titled “Pitfall 2: Storing live simulation state on the ScriptableObject”- Symptom: Temperature or inventory changes persist across play sessions in the editor project files.
- Cause: Modifying fields on
VesselApparatusDefinitionduring play. - Fix: Treat
VesselApparatusDefinitionas strictly read-only authoring parameters. Dynamic state lives inVesselDomain.
Pitfall 3: Placing on non-solid blocks
Section titled “Pitfall 3: Placing on non-solid blocks”- Symptom: Apparatus hangs in mid-air or can be placed floating over water.
- Cause: Bypassing
VesselFormFactory.TryPlaceground block checks. - Fix: Always validate that the block directly below the footprint is solid and not air or fluid.
User-contributed notes
Corrections, clarifications, and practical tips for this page. Anonymous is fine — a name is optional. Basic Markdown works:
**bold**,*italic*,`code`, and links.Notes policy
Notes are lightly filtered for spam and may be edited or removed. Keep them about this page — no support requests, no personal data, nothing you would not publish. Links are limited and marked
nofollow.