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
.csfiles 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:
- Write generated code → Unity compiles → domain reload.
- 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;@descbecomes the XML doc comment and inspector tooltip.FooDatabase : DefinitionDatabase— the per-tab container SO withRecords, lazy id lookups, and aSchemaFingerprint.
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
SheetForgeat 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.0↔1is 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
| Artifact | Policy |
|---|---|
| Sheets (local files) / Google sheet | The 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 asset | Gitignored, self-healing. Don't commit the one-line settings diff its first creation makes. |
A domain package's own Generated folder | The package's own choice. The bundled SheetForge.PluginDemo sample commits its generated code so the demo compiles immediately on import. |
| Import settings asset | Yours 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.
Related pages
- Sheet Syntax — the marker and type grammar the parser reads
- Data Studio — authoring on top of this model
- Sources, Export & Push — round-trip mechanics
- Authoring Kernel — the engine underneath the authoring window