Skip to content
SheetForge

Core Concepts

The sheet is the single source of truth

There is exactly one canonical form of your data: the sheet. Everything else is derived:

  • The IR (immutable Definitions) is the validated, assembled form of the sheet.
  • The generated C# classes are the IR's schema, made strongly typed.
  • The baked ScriptableObjects are the IR's values, made loadable — a lookup cache, never an independent truth.

All modification goes through the sheet and must pass re-import validation to become real. Editing a baked SO directly would create a second truth and bypass validation, so the product deliberately does not support it as a workflow.

(The inspector's "test edit" toggle exists for temporary runtime experiments. It is never written back, and re-import erases it.)

Why this matters: a project that treats the SO as the source of truth ends up with un-validated data drifting from the sheet and no way to reconcile the two. Here, reconciliation is structural — you regenerate from the sheet, always.

The IR — an immutable, validated assembly

The IR is what validation produces. For each tab it holds a SheetTable (schema + records) whose cells are already typed values: int, float, enum values, record references, asset references, lists, custom plugin types.

Key properties:

  • No partial assembly. If a single error exists anywhere, the IR is not built (ImportResult.Success == false ⇔ Registry == null — a hard invariant).
  • No nulls. An empty optional cell materializes its type default immediately, flagged IsDefaulted; consumers never null-check.
  • Immutable. The IR is read-only after assembly; the exits (codegen, bake, export) read it, never mutate it.

The pipeline

fetch → parse markers/schema → parse cells → validate (keys, references,
@overlap, asset keys, domain rules) → assemble IR → codegen (.cs) → bake (SO)
       └──────────────── collect ALL diagnostics ────────────────┘
  • Validation collects everything. You get the complete list of problems in one run — where / what / why / how, per error — instead of fixing one error per re-import.
  • Codegen is the last stage, after validation and value assembly, because writing .cs files triggers a domain reload. The pipeline is structured so the reload is safe and the chain resumes automatically after it.
  • Errors are structured objects, rendered as sentences. Each error carries the tab, the 1-based row, and the column letter and field name. It also carries the offending value, the violated rule, and an actionable suggestion (with nearest-match proposals for typos). The same objects also render as machine coordinates for logs and CI.

The automatic import chain

When a schema is new or changed, one import run internally does two things:

  1. Write generated code → Unity compiles → domain reload.
  2. After the reload, the chain resumes by itself and completes the bake.

You never re-trigger anything manually. If compilation fails (for example, your game code references a field that a rename just changed), the chain safe-aborts with an actionable console sentence instead of looping (attempt cap 3, resume log).

Strong typing, no runtime parsing

Codegen reads @name / @type / @desc and emits, per tab Foo:

  • FooDefinition — a strongly-typed record class, one field per column; @desc becomes the XML doc comment and inspector tooltip.
  • FooDatabase : DefinitionDatabase — the per-tab container SO with Records, lazy id lookups, and a SchemaFingerprint.

The bake writes real typed fields — zero runtime text parsing, no runtime reflection — which makes it IL2CPP-safe (no stripping hazards).

Address loading — how the cache stays shareable

Baked SOs are per-machine caches with per-machine GUIDs, so direct scene references to them would break across machines. Instead:

  • Import auto-registers each Database SO to the Addressables group SheetForge at the stable address "SheetForge/{tab}" (re-bakes re-link the new GUID to the same address; deleted tabs are cleaned up).
  • Game code loads by address: SheetForgeDatabases.LoadAsync<FooDatabase>("Foo").
  • The Addressables group asset is gitignored and self-healing (import recreates it when it is missing).

Baselines — how round-trip preserves your sheet

On import, a normalized snapshot of each tab's structure (marker rows, column order, comments, human-written text) is stored as the baseline. Export then swaps current SO values into the baseline structure.

So a sheet → import → Export → sheet round-trip preserves your sheet 100% structurally, and preserves values semantically:

  • 1.01 is allowed because the value is identical.
  • Floats use the shortest round-trip format.
  • The decimal separator is always ., locale-independent.

What is committed and what is regenerated

ArtifactPolicy
Sheets (local files) / Google sheetThe truth. Committed / shared.
Baked Database SOs (Assets/SheetForgeBaked)Gitignored per-machine cache — regenerate by running an import.
Generated code (Assets/SheetForgeGenerated)Commit it (the recommendation). It is your project's source, it lives outside Assets/SheetForge so reinstalling the product cannot delete it, and committing it means a fresh clone compiles before anyone runs an import. The output is deterministic, so teammates' imports produce identical bytes. Gitignoring it is a valid alternative; the next import regenerates it. A project that predates this default keeps generating into Assets/SheetForge/Runtime/Generated until that folder is empty; see Getting Started.
Addressables SheetForge group assetGitignored, self-healing. Don't commit the one-line settings diff its first creation makes.
A domain package's own Generated folderThe package's own choice. The bundled SheetForge.PluginDemo sample commits its generated code so the demo compiles immediately on import.
Import settings assetYours to manage; keep service-account key paths out of the repo (use the SHEETFORGE_SHEETS_KEY environment variable).

Extension without modification

Registration contracts let plugins join the pipeline with zero Core edits:

  • cell-type parsers (including wrapper types), domain validators, edge contributors;
  • custom structural markers, "Create sheet" templates, import-source providers;
  • the Data Studio's canvas overrides, code registries, widgets, actions, cell widgets, colour presets and UI strings.

Core never references a domain package; the one-way dependency is enforced by the compiler. The authoritative list — and its count — lives in Plugin Authoring.