Skip to content
Edit on GitHub

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.

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}$.

Industry apparatuses bridge four major subsystems:

  1. VoxelSandbox.Data: Supplies authored SubstanceCatalog assets.
  2. VoxelSandbox.Chemistry: Headless physics core that advances reactions and enthalpy.
  3. VoxelSandbox.World: Supplies IVoxelBlockWorld to verify that placement volume is clear air and resting on solid ground.
  4. 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;
  1. No prefabs: Placed apparatuses have no .prefab asset on disk. VesselFormFactory creates the GameObject, procedural cylinder mesh, and CapsuleCollider dynamically.
  2. Immutability of definitions: VesselApparatusDefinition holds static authoring defaults. Live runtime state (temperature, moles, pressure, valve status) lives solely in VesselDomain and is serialized via VesselSaveRecord.
  3. Footprint containment: bodyHeightMetres must not exceed footprintBlocks.y ($1.0\text{ m}$), ensuring apparatus geometry never clips through ceilings.

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.
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.

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 snippet
var 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: 2048

Assign 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.cs
public 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.

When VesselRuntime instantiates the domain in EnsureInitialized(), verify that it supplies the hearth parameters:

// In Assets/_Game/Scripts/Industry/Vessels/VesselRuntime.cs
private 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);
}

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.");
}
}
}

Execute the industry test category via Unity batchmode:

Terminal window
/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.log

Confirm all apparatus geometry and domain tests pass cleanly with failures="0".

  1. Enter Play Mode.
  2. Target a solid limestone or basalt floor with the hearth apparatus equipped.
  3. Verify that placement succeeds, procedural mesh renders, and heating the hearth past $750\text{ K}$ triggers red incandescence via VesselThermalGlowRuntime.

Use this checklist whenever adding a new Industry apparatus:

  1. Define the footprint box:
    • Small hearth: (1, 1, 1).
    • Standing furnace: (1, 2, 1).
    • Large blast smelter: (2, 3, 2).
  2. Keep geometry inside the envelope:
    • bodyHeightMetres <= footprintBlocks.y.
    • bodyRadiusMetres * 2 <= min(footprintBlocks.x, footprintBlocks.z).
  3. 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}$.
  4. Never create a prefab: Rely on VesselFormFactory procedural generation.
  5. Add geometry containment tests: Assert that height and diameter never exceed the block box.

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 bodyHeightMetres to strictly less than footprintBlocks.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 VesselApparatusDefinition during play.
  • Fix: Treat VesselApparatusDefinition as strictly read-only authoring parameters. Dynamic state lives in VesselDomain.
  • Symptom: Apparatus hangs in mid-air or can be placed floating over water.
  • Cause: Bypassing VesselFormFactory.TryPlace ground 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.

  1. Loading notes…