Assemblies & boundaries
In one paragraph
Section titled “In one paragraph”The project does not have a single linear stack of assemblies. It has a directed acyclic graph (DAG) with two independent roots: Unity-capable VoxelSandbox.Core and strictly headless VoxelSandbox.Chemistry. VoxelSandbox.Data is the bridge that turns Unity-authored catalogues into Chemistry values; World and Inventory consume that bridge in different ways; Industry and Player are broad feature consumers; and UI is intentionally the broadest runtime leaf. Two editor-only leaves—the Voxel Workshop and release-gate harness—may aggregate runtime assemblies but are never allowed back into a player build. The quirk is deliberate: dependency direction describes who may know whose types, not a simplistic order of gameplay importance.
Read the graph correctly
Section titled “Read the graph correctly”In this diagram, A → B means assembly A has B in its .asmdef references array. Arrows therefore point from a consumer to the provider whose public types it may use. It is the structural spine, not every transitive/direct edge—the table below is the complete direct-dependency lookup. Dashed arrows do not mean a weak runtime dependency; they mark the hard absence that protects a boundary.
flowchart TD CORE["VoxelSandbox.Core\nstartup composition"] CHEM["VoxelSandbox.Chemistry\nheadless math"] DATA["VoxelSandbox.Data\nauthored catalogues"] WORLD["VoxelSandbox.World\nterrain + presentation"] INV["VoxelSandbox.Inventory\nitems + bulk matter"] IND["VoxelSandbox.Industry\nvessels in world"] PLAYER["VoxelSandbox.Player\ninput + interaction"] UI["VoxelSandbox.UI\nHUD + menus"] WORK["VoxelWorkshop.Editor\nEditor-only workbench"] GATES["Editor.Gates\nEditor-only acceptance"] DATA --> CORE DATA --> CHEM WORLD --> DATA WORLD --> CHEM INV --> DATA INV --> CHEM IND --> INV IND --> WORLD PLAYER --> IND PLAYER --> INV PLAYER --> WORLD UI --> IND UI --> PLAYER WORK --> UI GATES --> WORK CORE -. never references .-> WORK UI -. never references .-> WORK
The source of truth is the .asmdef JSON, not this picture: VoxelSandbox.Data.asmdef:4, VoxelSandbox.World.asmdef:4, VoxelSandbox.Player.asmdef:4, and VoxelSandbox.UI.asmdef:4 are the runtime edges. The Workshop is explicitly Editor-only (VoxelSandbox.VoxelWorkshop.Editor.asmdef:17); the gates assembly is another Editor-only consumer (VoxelSandbox.Editor.Gates.asmdef:17).
The pieces
Section titled “The pieces”| Assembly | What it owns | Direct assembly dependencies | Boundary that matters |
|---|---|---|---|
VoxelSandbox.Core |
Startup root and a very small application-service registry | — | It is not the universal gameplay service locator. |
VoxelSandbox.Chemistry |
Deterministic thermodynamics, kinetics, charge, and reaction types | — | noEngineReferences: true; no Unity type is legal here. |
VoxelSandbox.Data |
ScriptableObject catalogues and generation authoring | Core, Chemistry | Converts authored Unity data to immutable Chemistry values; Chemistry never sees Data. |
VoxelSandbox.World |
Coordinates, generation, chunks, persistence, presentation | Core, Chemistry, Data + Unity Burst/HDRP packages | It owns the mutable voxel world, not Player input or UI. |
VoxelSandbox.Inventory |
Item stacks, rules, bulk material | Core, Chemistry, Data | It can exist with no loaded world. |
VoxelSandbox.Industry |
Vessels and world-embedded process runtime | Core, Chemistry, Data, Inventory, World | It consumes a world and matter; World does not know about machines. |
VoxelSandbox.Player |
Movement, targeting, interaction, session coordination | Core, Chemistry, Data, Industry, Inventory, World | Player translates input to calls on lower systems; it owns neither terrain nor vessel math. |
VoxelSandbox.UI |
Code-built HUD, menus, presentation formatting | Core, Chemistry, Data, Industry, Inventory, Player, World + Input System | A broad leaf is acceptable because it reads state and owns no domain state. |
VoxelSandbox.VoxelWorkshop.Editor |
Authoring/diagnostic modules | every runtime assembly + Editor packages | Editor-only aggregate; runtime must never reference it. |
VoxelSandbox.Editor.Gates |
Editor acceptance/capture harnesses | Workshop + every runtime assembly + HDRP | Editor-only aggregate for proof, not gameplay. |
The design quirks—and why they are intentional
Section titled “The design quirks—and why they are intentional”Two roots, not one
Section titled “Two roots, not one”Core and Chemistry both have empty references arrays, but they serve opposite purposes. Core is allowed to use UnityEngine: GameBootstrap makes a persistent scene object before scene load and initializes only root-level services (GameBootstrap.cs:9). Chemistry has noEngineReferences: true (VoxelSandbox.Chemistry.asmdef:13) and can run exactly the same solver in EditMode, a release build, or a future headless process.
Do not solve a Chemistry/Unity impedance mismatch by moving Chemistry into Core. That would make the easiest global root a dependency magnet and compromise the headless contract. Instead, put Unity authoring in Data or Unity runtime adaptation in World/Industry/UI, and pass Chemistry immutable values.
Data is a bridge, not “just configuration”
Section titled “Data is a bridge, not “just configuration””Data depends on both roots because it is the one intentional crossing from Unity serialization into solver-ready values. Catalogues validate ScriptableObject records and bake SubstanceTable / ChemicalReaction values that live in Chemistry. That makes this shape correct:
Unity Inspector asset → VoxelSandbox.Data validation/bake → VoxelSandbox.Chemistry immutable valueThe reverse is forbidden. A Chemistry file must never reach back to a catalogue or use UnityEngine. The Catalogues & authoring map covers the bake in detail.
World is deliberately not a pure domain assembly
Section titled “World is deliberately not a pure domain assembly”World references Unity Burst, Collections, Jobs, Mathematics, Core Render Pipeline, and HDRP alongside the project assemblies (VoxelSandbox.World.asmdef:4). That is an intentional compromise: World owns both pure-ish coordinate/generation policy and the practical streaming/presentation boundary that starts jobs, uploads meshes, and assigns colliders. Its internal answer is separation by type, not a second assembly for every algorithm:
ChunkScheduler, terrain field math, and persistence models stay unit-testable.ChunkStreamingRuntime,ChunkView, and HDRP-aware presentation are Unity-facing owners.- A caller crosses through interfaces and public runtime APIs such as
IVoxelBlockWorld; it does not reach into aChunkDatadictionary.
Chunk streaming and Terrain generation show the two sides meeting at the job-output → loaded-world-data hand-off.
Industry, Player, and UI are intentionally broad leaves
Section titled “Industry, Player, and UI are intentionally broad leaves”The old description of Industry as disconnected is stale. Player now directly references Industry (VoxelSandbox.Player.asmdef:8); for example, block targeting and interaction recognize vessel affordances. UI also references Industry (VoxelSandbox.UI.asmdef:4) so VesselRunViewRuntime can present an industry run without moving that presentation logic into the solver or vessel domain.
This is not permission for every feature to reference everything. It works because these three are leaf consumers: they translate input, compose a user-facing result, or format read-only state. They must not be referenced by World, Inventory, Industry, Data, or Chemistry. If a lower layer needs a Player/UI/Industry concept, extract a lower-level value, interface, event, or command instead of adding a back-edge.
Editor aggregation is a sanctioned exception
Section titled “Editor aggregation is a sanctioned exception”The Workshop and Gates assemblies can reference the whole runtime graph because they are compile-excluded outside the Editor. They are diagnostic/authoring/proof surfaces, not architectural owners. This lets a Workshop module validate a Data catalogue, drive a World preview, or evaluate a Chemistry table through the same public APIs as gameplay—without a runtime assembly ever depending on UnityEditor.
Treat this exception as one-way. Putting a runtime type in Assets/_Game/Editor/, or adding VoxelSandbox.VoxelWorkshop.Editor to a runtime .asmdef, makes a player build depend on an Editor assembly and is a build-breaking design error.
Tests are selective adjacency checks, not assembly mirrors
Section titled “Tests are selective adjacency checks, not assembly mirrors”Every runtime area has an EditMode test assembly, but its references list should be only the code directly exercised plus dependencies that its fixtures construct. For example, VoxelSandbox.UI.Tests names Chemistry, Inventory, and UI—not every runtime assembly UI itself can consume (VoxelSandbox.UI.Tests.asmdef:4). Chemistry tests repeat the headless boundary with noEngineReferences: true (VoxelSandbox.Chemistry.Tests.asmdef:17).
This prevents an apparently convenient test reference from becoming an accidental endorsement of a runtime coupling. Start a test assembly small; widen it only for a real fixture dependency.
Put a new type in the right assembly
Section titled “Put a new type in the right assembly”Folder names are only useful after the ownership decision. These examples cover the common cases that otherwise tempt an unnecessary reference:
| New concern | Correct home | Why | Avoid |
|---|---|---|---|
| A reaction extent calculation | Chemistry | It runs from immutable values and needs no Unity type. | Putting it in World because a vessel will call it. |
| A ScriptableObject field and its validation/bake | Data | Inspector authoring becomes a lower-level value here. | Passing a catalogue asset into Chemistry. |
| A generated terrain rule or edit-overlay mutation | World | World owns coordinates, chunks, and canonical voxel changes. | Letting Player maintain a parallel voxel cache. |
| A compositional carrying/merge rule | Inventory | It should work with no loaded scene or world. | Making Inventory depend on Player or World presentation. |
| A machine checkpoint DTO | Industry | Industry owns its format and validation. | Adding an Industry type reference to World persistence; use World’s generic save mechanism instead. |
| A ray/input adapter or interaction command | Player | It translates device state to calls on lower owners. | Placing a MonoBehaviour input reader in World or Chemistry. |
| A label, panel, or formatting policy | UI | It reads existing state and owns user-facing presentation. | Storing the canonical decision only in UI. |
| An authoring or proof utility | Workshop / Gates | Editor-only aggregation is sanctioned here. | Importing UnityEditor into a runtime assembly. |
When two owners both seem plausible, place the type with the invariant it must enforce. For example, a vessel save record belongs to Industry because it knows which fields form one valid checkpoint; SaveGameService belongs to World because it owns the atomic slot layout. The dependency between them is a generic persistence call, not a reverse assembly reference.
Dependency changes need a proof, not a hunch
Section titled “Dependency changes need a proof, not a hunch”Before adding an .asmdef reference, write the proposed edge as consumer -> provider, then test it against this sequence:
- Name the value or operation needed. If it is an immutable value, a command, or a narrow interface, it may belong below both current assemblies.
- Trace the return path. Search the provider’s
.asmdefdependencies and their direct consumers. A return path is an assembly cycle even if folders look unrelated. - Check the authority. A new reference should let the consumer ask an owner to act or read its state, not let it reach inside private storage.
- Check platform scope. An Editor-only consumer may aggregate runtime assemblies; no runtime provider may reference that consumer.
- Add the narrow test reference only after the runtime boundary is sound. Tests validate a real relationship; they must not be used to normalize one.
For example, Player may depend on Industry to resolve and invoke a vessel affordance because Player is a broad command leaf and Industry owns the apparatus. World must not depend on Industry merely to save a vessel record: World exposes a generic atomic DTO path while Industry validates its own record. This preserves the one-way graph and makes both systems independently testable.
Invariants you must not break
Section titled “Invariants you must not break”- The runtime graph stays acyclic. Unity catches an actual assembly cycle, but it cannot decide whether a new dependency is a good boundary. Inspect the whole path before adding an edge.
- Chemistry remains Unity-free. Preserve
noEngineReferences: true; adapt at Data/World/Industry/UI instead. - No runtime → Editor reference. The Workshop and Gates depend on runtime; never reverse that arrow.
- Keep the Data → Chemistry bake one-way. A solver receives immutable Chemistry types, never a catalogue asset.
- Broad leaves only read/translate. Player/UI may depend widely; they do not become owners of world generation, inventory rules, or vessel thermodynamics.
- The service registry stays at the composition root.
ServiceRegistryexplicitly says gameplay objects receive dependencies from their owning feature rather than locating them globally (ServiceRegistry.cs:6). - Test references remain minimal. A test-only assembly is not a loophole for coupling unrelated runtime modules.
A dependency decision checklist
Section titled “A dependency decision checklist”Before editing an .asmdef, answer these in order:
- Does the caller truly need the provider’s concrete type, or only a value/command/interface that belongs lower in the graph?
- Is the caller a runtime owner, a broad leaf that only translates state, or an Editor-only diagnostic surface?
- Would the edge create a return path through an existing dependency? Trace both paths; do not guess from folders.
- Can the dependency be inverted by placing a small abstraction beside the data it represents, instead of in Core by default?
- Which smallest test assembly proves the new seam without importing unrelated areas?
See also
Section titled “See also”- Catalogues & authoring — the Data → Chemistry bridge.
- The chemistry solver — why headless Chemistry is protected so aggressively.
- Terrain generation and Chunk streaming — the mixed pure/Unity shape inside World.
- The runtime UI — the deliberate broad-leaf, thin-runtime conventions.
- The Voxel Workshop — why the Editor-only aggregate may see the entire runtime graph.
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.