Skip to content
Edit on GitHub

Add a Voxel Workshop module

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

This page adds one focused Editor tool end to end, then reduces it to a reusable checklist. Read it with The Voxel Workshop: it assumes the workbench’s single-window, UI Toolkit, module-registration model.

Substance Table — a read-only Voxel Workshop module that selects a SubstanceCatalog, bakes it through the real TryBuildTable path, and lists each immutable (SubstanceId, Phase) row in the exact ascending order the chemistry solver uses. It shows validation failures instead of a partial table, changes no asset or runtime state, and is useful whenever authored chemistry looks right but the baked solver input needs inspection.

  • The implementation belongs at Assets/_Game/Editor/VoxelWorkshop/Modules/SubstanceTable/SubstanceTableModule.cs, inside the editor-only VoxelSandbox.VoxelWorkshop.Editor assembly.
  • Its input is VoxelSandbox.Data.Chemistry.SubstanceCatalog; its output is VoxelSandbox.Chemistry.SubstanceTable. The dependency already points editor → Data → Chemistry. No runtime assembly references the module.
  • It is a diagnostic lens, not a second chemistry model: the only bake is SubstanceCatalog.TryBuildTable, and the only enumeration is SubstanceTable.DefinitionAt.
flowchart TD
  REGISTRY["WorkshopModuleRegistry\nfactory list"]
  MODULE["SubstanceTableModule\nIWorkshopModule"]
  VIEW["CreateView\nUI Toolkit hierarchy"]
  REFRESH["Refresh\nread selected catalogue"]
  CATALOG["SubstanceCatalog\nTryBuildTable"]
  TABLE["SubstanceTable\nimmutable sorted rows"]
  ROWS["ScrollView\nrow labels"]

  REGISTRY --> MODULE --> VIEW
  VIEW --> REFRESH --> CATALOG --> TABLE --> ROWS
  CATALOG -. validation issue list .-> ROWS

Read The Voxel Workshop, then the exact contracts this module uses:

Intake decisions: This is read-only; it needs no Play Mode state or persisted settings. It needs one object picker, a status label, and a scrollable row list. Put it beside Chemistry Lab in the registry: both inspect the same catalogue, and the registry order is the user-visible menu order.

Start from ChemistryLabModule, but keep this tool to one action: bake and list. CreateView() builds widgets and hooks callbacks; it may call Refresh() once at the end so the first active view is populated. It must not author or save an asset.

// illustrative — Assets/_Game/Editor/VoxelWorkshop/Modules/SubstanceTable/SubstanceTableModule.cs
using UnityEditor;
using UnityEngine.UIElements;
using VoxelSandbox.Data.Chemistry;
using VoxelSandbox.VoxelWorkshop.Core;
namespace VoxelSandbox.VoxelWorkshop.Modules.SubstanceTable
{
internal sealed class SubstanceTableModule : IWorkshopModule
{
private const string DefaultCatalogPath = "Assets/_Game/Data/Chemistry/SubstanceCatalog.asset";
private SubstanceCatalog catalog;
private ObjectField catalogField;
private Label summary;
private ScrollView rows;
public string Id => "substance-table";
public string DisplayName => "Substance Table";
public string Description => "Bake a substance catalogue and inspect the immutable rows supplied to chemistry.";
public VisualElement CreateView()
{
catalog = AssetDatabase.LoadAssetAtPath<SubstanceCatalog>(DefaultCatalogPath);
var root = new VisualElement { style = { flexGrow = 1 } };
root.Add(new Label(Description) { style = { whiteSpace = WhiteSpace.Normal, marginBottom = 8 } });
catalogField = new ObjectField("Substance Catalog")
{
objectType = typeof(SubstanceCatalog),
allowSceneObjects = false,
value = catalog,
};
catalogField.RegisterValueChangedCallback(change =>
{
catalog = change.newValue as SubstanceCatalog;
Refresh();
});
root.Add(catalogField);
summary = new Label { style = { marginTop = 8, unityFontStyleAndWeight = UnityEngine.FontStyle.Bold } };
rows = new ScrollView { style = { flexGrow = 1, marginTop = 4 } };
root.Add(summary);
root.Add(rows);
Refresh();
return root;
}

VoxelWorkshopWindow.Activate (Editor/VoxelWorkshop/Core/VoxelWorkshopWindow.cs:99) clears the host and calls only module.CreateView(). Calling Refresh() after the fields exist is therefore what makes a module display initial state. The window’s toolbar calls activeModule?.Refresh() (:80) later.

2. Refresh through the production bake path

Section titled “2. Refresh through the production bake path”

Clear the old rows on every refresh. TryBuildTable first runs catalogue validation and returns every issue when it fails; displaying those facts is better than attempting to reproduce the bake rules in editor UI. On success, iterate by index: SubstanceTable deliberately exposes no mutable collection or dictionary enumeration.

// illustrative — continuation of SubstanceTableModule
public void Refresh()
{
if (catalogField == null)
{
return;
}
catalogField.SetValueWithoutNotify(catalog);
rows.Clear();
if (catalog == null)
{
summary.text = "No substance catalog selected.";
return;
}
if (!catalog.TryBuildTable(out SubstanceTable table, out IReadOnlyList<SubstanceCatalogIssue> issues))
{
summary.text = $"Cannot bake: {issues.Count} validation issue(s).";
foreach (SubstanceCatalogIssue issue in issues)
{
rows.Add(new Label($"[{issue.Severity}] {issue.Message}") { style = { whiteSpace = WhiteSpace.Normal } });
}
return;
}
summary.text = $"{table.Count} immutable row(s), ascending by (SubstanceId, Phase).";
for (int index = 0; index < table.Count; index++)
{
ThermodynamicPhaseDefinition definition = table.DefinitionAt(index);
rows.Add(new Label(
$"{definition.SubstanceId.Value}/{definition.Phase} {definition.Formula} M = {definition.MolarMassGramsPerMol:0.####} g/mol"));
}
}
}
}

Add using System.Collections.Generic;, using VoxelSandbox.Chemistry;, and using UnityEngine; to the illustrative file. The line order is not cosmetic: SubstanceTable.TryCreate sorts by (SubstanceId, Phase) to make chemistry iteration deterministic. The UI is showing solver truth, not a friendlier authoring order.

The module does not exist until the central registry creates it. Add its namespace import and one factory adjacent to ChemistryLabModule; registration is intentionally flat, explicit, and reviewable.

// illustrative — WorkshopModuleRegistry.cs
using VoxelSandbox.VoxelWorkshop.Modules.ChemistryLab;
using VoxelSandbox.VoxelWorkshop.Modules.SubstanceTable;
// ...
() => new ChemistryLabModule(),
() => new SubstanceTableModule(),
() => new AssayLabModule(),

Do not add another [MenuItem]. The sole workshop menu entry is VoxelWorkshopWindow.Open (Editor/VoxelWorkshop/Core/VoxelWorkshopWindow.cs:26); the module appears in its existing chooser.

  1. Open Tools ▸ Voxel Sandbox ▸ Voxel Workshop, choose Substance Table, and confirm the default SubstanceCatalog.asset is selected.
  2. The summary reports the baked row count. Confirm the rows rise by (SubstanceId, Phase) rather than by display name, and compare one row’s formula/molar mass to its authored SubstanceDefinition.
  3. Temporarily assign a duplicate (SubstanceId, Phase) in a disposable catalogue asset. Click the existing Refresh toolbar button: the module must display the validation issue and no partial rows. Undo the asset change.
  4. Close and reopen the Workshop. The host restores the last module through EditorPrefs; the module still builds and refreshes normally.

Run the full EditMode suite after implementing an editor module. This repository has no dedicated Workshop test assembly; the module’s confidence comes from compiling the editor assembly, exercising the real bake path, and the existing Data/Chemistry tests covering its inputs.

cd /home/soulwax/workspace/engines/unity/minecraft/Minecraft-HD
"/path/to/Unity" -runTests -batchmode -projectPath . -testPlatform EditMode \
-testResults ./Logs/editmode-results.xml -quit

Done means the module is reachable in the one Workshop window, lists only a successfully baked table, and reports every invalid-catalogue issue without mutating project state.

  1. Choose one narrowly useful editor question and identify the existing public runtime API that answers it.
  2. Add one internal IWorkshopModule under Editor/VoxelWorkshop/Modules/<Topic>/; make Id stable, DisplayName concise, and Description an accurate statement of scope.
  3. Construct UI Toolkit widgets and callbacks in CreateView(), then call Refresh() once after every field the refresh path needs exists.
  4. Put current-state reads, list rebuilding, and validation in Refresh(); clear stale output before rendering the next result.
  5. Reuse production validation/bake/query APIs. Do not clone domain rules, create shadow data, or mutate a save/runtime system merely to preview it.
  6. Import and register exactly one factory in WorkshopModuleRegistry, in a deliberate user-facing order.
  7. Exercise success, absent input, and one invalid-data path in the Editor; run the matching EditMode tests or the full suite when no editor fixture exists.
  8. Capture one post-change Workshop screenshot and update the capture queue; bump the game VERSION and CHANGELOG.md when the feature lands.

Decisions that vary: read-only diagnostic versus an explicit authoring action; a required project asset versus a user-selected asset; an instant refresh after input change versus an explicit button for expensive work; whether a pure helper deserves a focused EditMode fixture.

Work happens only in CreateView(). Symptom: the toolbar Refresh button changes nothing, or a selected new asset shows stale rows. Cause: CreateView() is only called on activation; the host’s Refresh button calls Refresh(). Fix: build controls once, retain their references, and make Refresh() the complete idempotent rebuild path.

The module rebuilds the table itself. Symptom: the Workshop accepts a catalogue the solver rejects, or row ordering differs from runtime behavior. Cause: editor code copied a validation or sorting rule instead of calling TryBuildTable. Fix: treat the Data-to-Chemistry bake as the single source of truth; display its returned issue list.

A separate menu command is added. Symptom: tooling is scattered across Unity menus and the preferred-module behaviour cannot find the new tool. Cause: a new [MenuItem] bypassed WorkshopModuleRegistry. Fix: add one registry factory. VoxelWorkshopWindow remains the only project-authored entry point.

A diagnostic module mutates project state. Symptom: opening a view dirties an asset, changes an authored catalogue, or makes a save difficult to trust. Cause: a preview writes its result back to an authoritative object. Fix: use local labels and scratch values only. Make any real authoring operation an explicit, separately reviewed command.

Rows are not cleared before refresh. Symptom: repeated refreshes duplicate entries, and an invalid catalogue leaves old successful rows visible. Cause: the existing ScrollView was appended to rather than rebuilt. Fix: call rows.Clear() before handling null, failure, or success.

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…