Skip to content
Edit on GitHub

Assemblies & boundaries

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.

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

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”

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 value

The 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 a ChunkData dictionary.

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.

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:

  1. 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.
  2. Trace the return path. Search the provider’s .asmdef dependencies and their direct consumers. A return path is an assembly cycle even if folders look unrelated.
  3. 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.
  4. Check platform scope. An Editor-only consumer may aggregate runtime assemblies; no runtime provider may reference that consumer.
  5. 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.

  1. 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.
  2. Chemistry remains Unity-free. Preserve noEngineReferences: true; adapt at Data/World/Industry/UI instead.
  3. No runtime → Editor reference. The Workshop and Gates depend on runtime; never reverse that arrow.
  4. Keep the Data → Chemistry bake one-way. A solver receives immutable Chemistry types, never a catalogue asset.
  5. Broad leaves only read/translate. Player/UI may depend widely; they do not become owners of world generation, inventory rules, or vessel thermodynamics.
  6. The service registry stays at the composition root. ServiceRegistry explicitly says gameplay objects receive dependencies from their owning feature rather than locating them globally (ServiceRegistry.cs:6).
  7. Test references remain minimal. A test-only assembly is not a loophole for coupling unrelated runtime modules.

Before editing an .asmdef, answer these in order:

  1. Does the caller truly need the provider’s concrete type, or only a value/command/interface that belongs lower in the graph?
  2. Is the caller a runtime owner, a broad leaf that only translates state, or an Editor-only diagnostic surface?
  3. Would the edge create a return path through an existing dependency? Trace both paths; do not guess from folders.
  4. Can the dependency be inverted by placing a small abstraction beside the data it represents, instead of in Core by default?
  5. Which smallest test assembly proves the new seam without importing unrelated areas?

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…