Catalogues & authoring
verifiedAgainst 9a2d070 · verifiedOn 2026-09-10 · the automated staleness banner is planned, not built.
In one paragraph
Section titled “In one paragraph”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.
The pieces
Section titled “The pieces”| 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 |
The flow
Section titled “The flow”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.
Invariants you must not break
Section titled “Invariants you must not break”- A stable id is permanent.
BlockDefinition.Initialize(BlockDefinition.cs:82) says released definitions are deprecated, never renumbered — old saves resolve ids to blocks. Always takeGetNextAvailableId(). Same for items and forSubstanceId. - 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. - 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 —GetNextAvailableIdthrowsInvalidOperationExceptionwhen genuinely exhausted). Match this; don’t add exception-based control flow. - The bake is all-or-nothing.
TryBuildTable/TryBuildReactionsfail 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. - Chemistry never sees a ScriptableObject. The dependency is Data → Chemistry. If you find yourself wanting
using UnityEnginein aChemistry/file to read a catalogue, the design is inverted — bake it in Data and pass the immutable type.
Where it connects
Section titled “Where it connects”- Upstream:
VoxelSandbox.Chemistryowns the baked targets (SubstanceTable,ChemicalReaction) and all the math that validates a definition (ShomateThermodynamics,AtomBalance,ChargeBalance). Data references Chemistry; not the reverse. - Downstream:
VoxelSandbox.WorldreadsBlockCatalogfor generation and meshing;VoxelSandbox.InventoryreadsItemCatalog(asIItemStackRules) and usesBlockDefinition.Compositionfor bulk-material charging;VoxelSandbox.Industry’sVesselRuntimetakes optionalSubstanceCatalog/ReactionCatalogreferences 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.
See also
Section titled “See also”- Add a scientific route — a worked example authoring a
SubstanceDefinitionand the firstReactionDefinitionthrough this seam. - Add a voxel block type — the
BlockCatalog/BlockDefinitionside, 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 everySubstanceDefinitioncitation 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.