Skip to content
Edit on GitHub

Catalogues & authoring

verifiedAgainst 9a2d070 · verifiedOn 2026-09-10 · the automated staleness banner is planned, not built.

Every game element with an identity — a block, an item, a substance, a reaction — is an authored ScriptableObject in VoxelSandbox.Data, collected into a catalogue. All four catalogues share one shape: a flat serialized List<…Definition>, a ValidateCatalog() pass that returns a list of structured …CatalogIssue values instead of throwing, and a build/query step the runtime calls once. Blocks and items are queried directly (an id → definition dictionary). Substances and reactions are baked: SubstanceCatalog produces an immutable SubstanceTable and ReactionCatalog an immutable ChemicalReaction[], both of which live in the headless VoxelSandbox.Chemistry core and carry no reference back to the authoring layer. The catalogue is where a misconfiguration is meant to surface — in the Voxel Workshop, before it can corrupt a save or reach the solver.

Symbol File Responsibility
BlockCatalog Data/Blocks/BlockCatalog.cs:12 Block registry: TryGet(BlockId) (:21) via a lazily-built dictionary, GetNextAvailableId() (:76), ValidateCatalog() (:98)
BlockDefinition Data/Blocks/BlockDefinition.cs:13 One block’s identity: stable id, render class, solidity, tool gate, face textures, and the optional Composition (:76) bridge to Chemistry
ItemCatalog Data/Items/ItemCatalog.cs:13 Item registry; also implements IItemStackRules — TryGetMaximumStackSize (:41), TryGetItemForBlock (:54)
ItemDefinition Data/Items/ItemDefinition.cs:11 One item’s identity: stable id, max stack size, the block it places, tool type, icon
SubstanceDefinition Data/Chemistry/SubstanceDefinition.cs:16 One authored (substance, phase) thermodynamic record; TryBuildDefinition (:97) bakes it, requiring a sourceCitation
ShomateCoefficientsAuthoring Data/Chemistry/ShomateCoefficientsAuthoring.cs:15 Serializable mirror of the core’s ShomateCoefficients struct — Data carries [SerializeField], Chemistry stays Unity-free
SubstanceCatalog Data/Chemistry/SubstanceCatalog.cs:14 ValidateCatalog() (:46), TryBuildTable() (:87) → SubstanceTable, all-or-nothing
ReactionDefinition Data/Chemistry/ReactionDefinition.cs:16 Authored reaction: signed-coefficient terms + Arrhenius A/Ea; TryBuildReaction (:90)
ReactionTermAuthoring Data/Chemistry/ReactionTermAuthoring.cs:13 One species in a reaction: SubstanceDefinition ref + signed coefficient + rate order
ReactionCatalog Data/Chemistry/ReactionCatalog.cs:13 ValidateCatalog() (:46), TryBuildReactions() (:69) → ChemicalReaction[], enforces id uniqueness at bake time
SubstanceTable Chemistry/SubstanceTable.cs:55 Immutable baked target — a key-sorted array; TryCreate (:107) rejects a duplicate (SubstanceId, Phase); iterate with DefinitionAt (:67), never a dictionary (§28.8)
ChemicalReaction Chemistry/ChemicalReaction.cs:193 Immutable baked target; TryCreate proves atom and charge conservation from the supplied formulae — an unbalanced reaction cannot be constructed
…CatalogIssue e.g. Data/Chemistry/SubstanceCatalogIssue.cs:10 (Severity, Definition, Message) — the structured problem report every ValidateCatalog() returns
flowchart LR
  BD["BlockDefinition / ItemDefinition\nauthored ScriptableObject"]
  SD["SubstanceDefinition / ReactionDefinition\nauthored ScriptableObject"]
  BC["BlockCatalog / ItemCatalog\nList + ValidateCatalog()"]
  CC["SubstanceCatalog / ReactionCatalog\nList + ValidateCatalog()"]
  DICT["id -> definition dictionary\nlazily built, queried live"]
  BAKE["TryBuildTable / TryBuildReactions\nall-or-nothing"]
  ST["SubstanceTable / ChemicalReaction[]\nimmutable, key-sorted"]
  GAME["World, Inventory, Player\nblock & item lookups"]
  SOLVER["VesselSolver\n(VoxelSandbox.Chemistry)"]

  BD --> BC --> DICT --> GAME
  SD --> CC --> BAKE --> ST --> SOLVER
  CC -. issues block the bake .-> BAKE

Blocks and items. BlockCatalog holds a List<BlockDefinition> and builds an id → definition dictionary on first query (EnsureLookup), invalidated on every add. TryGet (BlockCatalog.cs:21) returns false for air rather than throwing. TryAddDefinition rejects both a duplicate object and a duplicate stable id. GetNextAvailableId (:76) walks from BlockId.FirstContentValue and is the only sanctioned way to pick a new id. ItemCatalog is the same shape and additionally implements IItemStackRules, so inventory code depends on the catalogue interface, not the concrete asset.

Substances and reactions. SubstanceCatalog.ValidateCatalog (SubstanceCatalog.cs:46) reports null entries, duplicate (SubstanceId, Phase) pairs, and any definition that fails its own TryBuildDefinition (bad formula, molar mass off by >1 %, missing citation, unordered Shomate range). TryBuildTable (:87) runs that validation first and returns false with the issue list if anything is wrong — a caller never receives a partially-baked table. The bake target SubstanceTable.TryCreate (SubstanceTable.cs:107) key-sorts the entries and rejects a duplicate again — the solver iterates it by index for determinism (§28.8), never by hash. ReactionCatalog mirrors this; ChemicalReaction.TryCreate (ChemicalReaction.cs:193) additionally runs AtomBalance and ChargeBalance on the formulae, so a baked ChemicalReaction is a conservation proof by construction.

The authoring/core seam. ShomateCoefficientsAuthoring exists only because VoxelSandbox.Chemistry cannot carry [SerializeField] — it is a field-for-field mirror of the core’s ShomateCoefficients, converted at bake time. This is the pattern for every authored → core value: the ScriptableObject lives in Data, the immutable struct lives in Chemistry, and one TryBuild… call crosses the line.

  1. A stable id is permanent. BlockDefinition.Initialize (BlockDefinition.cs:82) says released definitions are deprecated, never renumbered — old saves resolve ids to blocks. Always take GetNextAvailableId(). Same for items and for SubstanceId.
  2. Validation is the contract, not a nicety. When a catalogue gains a field or rule, extend its ValidateCatalog() in the same change. An unvalidated field is one that fails silently in a save or a solver tick instead of loudly in the Workshop.
  3. Fallible operations return bool TryXxx(out result, out error). The catalogues never throw for an expected failure (missing id, bad formula, full id space is the one exception — GetNextAvailableId throws InvalidOperationException when genuinely exhausted). Match this; don’t add exception-based control flow.
  4. The bake is all-or-nothing. TryBuildTable / TryBuildReactions fail the whole batch on one issue. Do not add a “skip the bad entry and continue” path — a solver running against a table that is silently missing a species is worse than a solver that won’t start.
  5. Chemistry never sees a ScriptableObject. The dependency is Data → Chemistry. If you find yourself wanting using UnityEngine in a Chemistry/ file to read a catalogue, the design is inverted — bake it in Data and pass the immutable type.
  • Upstream: VoxelSandbox.Chemistry owns the baked targets (SubstanceTable, ChemicalReaction) and all the math that validates a definition (ShomateThermodynamics, AtomBalance, ChargeBalance). Data references Chemistry; not the reverse.
  • Downstream: VoxelSandbox.World reads BlockCatalog for generation and meshing; VoxelSandbox.Inventory reads ItemCatalog (as IItemStackRules) and uses BlockDefinition.Composition for bulk-material charging; VoxelSandbox.Industry’s VesselRuntime takes optional SubstanceCatalog / ReactionCatalog references and bakes them at startup.
  • Editor: the Block/Item Library and Chemistry Lab Voxel Workshop modules are thin views over ValidateCatalog() — they render the issue list and, for Chemistry Lab, evaluate one substance’s Shomate state (the The Voxel Workshop orientation page is still to be written).
  • See Assemblies & boundaries for the one-directional rule this seam depends on.
  • Add a scientific route — a worked example authoring a SubstanceDefinition and the first ReactionDefinition through this seam.
  • Add a voxel block type — the BlockCatalog / BlockDefinition side, end to end.
  • Docs/MASTERPLAN.md §36.1 (the authoring layer), §28.8 (deterministic iteration over the baked table); Docs/CHEMISTRY_SOURCES.md (the sourcing bar every SubstanceDefinition citation meets).

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…