API Reference — The Public Surface
This page lists every public type in the product assemblies. Anything not listed here is internal by design; the public surface is deliberately narrow.
- Core (
SheetForge.Core+SheetForge.Core.Tooling): 137 public types (Core: 131, Core.Tooling: 6). Core.Tooling is the editor-only half holding import-time services such as reporting and push planning — none of it ships in player builds. - Editor: 53 top-level public types plus their public nested types.
- Runtime: 7 types, plus generated outputs.
This is exactly the surface the consumer-simulation tests (no InternalsVisibleTo) compile against.
Detection contract (not a type): a separate asset can also detect that SheetForge is installed at compile time via the
SHEETFORGEscripting-define symbol the Editor assembly self-registers. It is a define, not a public type, so it is not listed in the tables below — see Plugin Authoring ▸ Detecting SheetForge from another asset. (Distinct fromSHEETFORGE_ADDRESSABLES, an internal version-define that only marks whether the Addressables package is present.)
Conventions: signatures are abbreviated (… = see source XML docs); "pure" means no UnityEngine / no IO.
Core assembly (SheetForge.Core) — pure C#
No UnityEngine, no IO, no network, no domain knowledge. Compiler-enforced: Core references nothing.
Plugin registration contracts (SheetForge.Core.Plugins)
| Type | Kind | Role & key members |
|---|---|---|
ISheetForgePlugin | interface | The base domain-plugin contract. string Name · RegisterEnums(EnumRegistry) · RegisterCellParsers(CellParserRegistry) |
ISheetForgeValidatorPlugin | interface | Opt-in add-on for validation rules. RegisterValidators(DomainValidatorRegistry) |
ISheetForgeEdgePlugin | interface | Opt-in add-on for edge declarations. RegisterEdgeContributors(EdgeContributorRegistry) |
ISheetForgeMarkerPlugin | interface | Opt-in add-on for custom structural markers. RegisterStructuralMarkers(MarkerRegistry) |
ISheetForgeTemplatePlugin | interface | Opt-in add-on for "Create sheet" templates. RegisterTemplates(TemplateRegistry) |
ISheetForgeGraphPlugin | interface | Opt-in add-on registering per-tab Data Studio canvas overrides. RegisterGraphShapes(GraphShapeRegistry) |
ISheetForgeCodeRegistryPlugin | interface | Opt-in add-on for code-owned reference targets (locked virtual tabs). RegisterCodeRegistries(CodeRegistryCatalog) |
ISheetForgeThemePlugin | interface | Opt-in add-on for window colour presets. RegisterThemes(ThemeRegistry) |
ISheetForgeStudioPlugin | interface | Opt-in add-on for declarative authoring surfaces (actions, panels, column badges, cell-editor hints). RegisterStudioUi(StudioUiRegistry). In Core rather than Editor so one registration renders in both the UIToolkit editor and the browser |
ISheetForgeStringsPlugin | interface | Opt-in add-on registering the pack's own UI strings per language. RegisterStrings(StringOverlayRegistry). Replaces the retired Editor-side ISheetForgeLocPlugin / PluginLocRegistry pair, which could only reach the editor |
ISheetForgePipelinePlugin | interface | Opt-in add-on registering pipeline observers. RegisterPipelineObservers(PipelineObserverRegistry) |
Composition and compatibility (SheetForge.Core.Plugins)
Discovery is per host — Unity's TypeCache in the editor, the browser's uploaded-assembly scan on the web. Everything after it (instantiation, ordering, isolation and the compatibility gate) is one shared Core function, which is what keeps the two hosts from drifting slot by slot.
| Type | Kind | Role & key members |
|---|---|---|
PluginComposition | static class | The single assembly path. One instance per type, cast to every contract it implements. The two members and the diagnostic split are below the table |
PluginSet | sealed class | The assembled result — twelve slots: Enums · Parsers · Validators · EdgeContributors · Markers · Templates · GraphShapes · CodeRegistries · Themes · Strings · StudioUi · PipelineObservers. A new slot reaches both hosts by being added here |
SheetForgePluginCompatAttribute | sealed attribute (assembly) | [assembly: SheetForgePluginCompat(SheetForgePluginFormat.Current, MinHostVersion = "…", PluginVersion = "…")]. int FormatVersion · string MinHostVersion (numeric dotted comparison; null/empty = no requirement) · string PluginVersion (display only, never compared). Read without instantiating anything, and judged per assembly — a refused assembly loses every registration rather than half-loading. Absent = generation Minimum, no host requirement |
SheetForgePluginFormat | static class | The generation constants: const int Current · const int Minimum. Moves only if the plugin format itself is replaced — purely additive growth keeps the number where it is |
PluginComposition — the two members:
IReadOnlyList<Type> ContractTypes— the discovery filter. Its order is fixed, because it decides the order diagnostics appear in.PluginSet Compose(IReadOnlyList<Type> candidateTypes, string hostVersion, ErrorCollector errors, ICollection<string> failures, Func<string,bool> isProductKey = null)— the assembly call itself.
Two kinds of trouble are kept apart. Registration conflicts and compatibility refusals become structured diagnostics in errors; implementation bugs — construction failure, a throwing callback — become English lines in failures, and passing null there discards them.
The trailing isProductKey predicate is how the string overlay's "a plugin may not overwrite a product key" rule is enforced without Core ever seeing the language tables: the rule lives here, the host supplies only the material. Omit the predicate and only that one rule is skipped.
Registries (SheetForge.Core.Model / .Validation / .Edges)
| Type | Role & key members |
|---|---|
EnumRegistry | Enum name → CLR type (codegen material). Register<TEnum>() · Register(name, memberNames) · TryGetMembers · TryGetClrTypeName · TryGetClrAssemblyName · RegisteredEnumNames. Three further members are detailed below the table |
CellParserRegistry | Type name → cell parser (open-closed). Duplicate registration throws. Register(ICellValueParser) · TryGet · TryGetCustomRenderer · RegisteredTypeNames · RegisterWrapper(ICellWrapperType) · TryGetWrapper · RegisteredWrapperNames (wrapper types) |
DomainValidatorRegistry | Append-only validator list, order preserved. Register(IDomainValidator) · Validators |
EdgeContributorRegistry | Append-only contributor list, order preserved. Register(IEdgeContributor) · Contributors |
MarkerRegistry | Marker name (no @) → custom structural marker. Collision with a built-in marker (SheetSyntax.ReservedMarkers — @name/@type/@desc/@overlap/@style/@enum/@loc) / duplicate / invalid identifier throws. Register(IStructuralMarkerDefinition) · TryGet · IsEmpty · RegisteredMarkerNames · AppendMarkerTokens |
TemplateRegistry | "Create sheet" template key → template. Empty/duplicate key, empty display, zero tabs, empty tab TSV throw. Register(DataTemplate) · TryGet · Templates · IsEmpty |
EnumRegistry — the three members in detail:
EnumRegistry(EnumRegistry parent)— a child that reads through to a parent and registers only on itself. The parent holds plugin-registered CLR enums for the whole domain reload, the child this import's sheet-defined ones, so an import never mutates the shared cache. Registering a name the parent already owns throws rather than shadowing it.Contains(name)— own then parent, Ordinal.SetClrTypeName(name, fullTypeName)— fill in a CLR name after a string-only registration. The assembly name stays empty because the type does not exist yet.
"Create sheet" templates (SheetForge.Core.Model)
| Type | Role & key members |
|---|---|
DataTemplate | A plugin-registered template: string Key (registry identity) · string DisplayName (plugin-owned text) · IReadOnlyList<DataTemplateTab> Tabs (one or more) |
DataTemplateTab | One tab of a template: string TabName · string Tsv (a complete normalized TSV — marker rows plus example data) |
Custom cell types (SheetForge.Core.Model)
| Type | Role & key members |
|---|---|
ICellValueParser | Parses one scalar cell. Failure = collect into context.Errors + return false (never throw). string TypeName · bool TryParse(CellParseContext, string, out object) |
ICustomCellType | Optional codegen/round-trip helper. Type ValueType · bool TryRender(object, out string text, out string reason) |
IReferencingCellType | Opt-in capability a registered ICellValueParser may also implement so a key buried in its own notation gets full RecordId@Tab treatment — integrity + suggestions, key-rename propagation with the payload preserved, graph edges and ports, the ▾ picker, orphan detection, exported dropdown rules. Found by casting the registered parser (no separate registration). bool TryGetTokenKey(elementText, out key) · string MakeToken(key) · bool TryRetargetToken(elementText, newKey, out newText) · bool TryRemoveToken(elementText, key, out newText) (empty result = the element disappears) · bool TryRewriteKeys(elementText, IReadOnlyDictionary<string,string> renames, out newText). One call = one element: the whole cell, or one ;-separated element. So a payload may not contain ;. @target must name a real sheet tab (UnknownTargetTab otherwise). Never throws — false/null means "cannot interpret", and rewrites preserve the residue |
IRefBearingValue | The value-side half of the above, implemented by the parsed value: IEnumerable<string> ReferencedKeys (declaration order = diagnostic and suggestion-budget order; null/empty entries are skipped). The scanner reads this; the text hooks above rewrite the cell. Both are needed — a parsed value cannot restore the author's notation, and text cannot be validated without being read |
ICellWrapperType | A generic wrapper value shape MyWrapper<T> (e.g. Pair<int> = 1~2) — the wrapper owns the outer syntax and the Core parses the inner type recursively. string Name · bool TrySplit(string, out IReadOnlyList<string> pieces, out string reason) · string JoinCanonical(IReadOnlyList<string>) · Type OpenClrType · object Assemble(IReadOnlyList<object>, Type closed) · bool TryDisassemble(object, out IReadOnlyList<object>, out string reason) |
WrapperValue | A wrapper cell's parsed IR — carries the wrapper strategy + exposes the inner CellValues (so references inside pass through validation, key/tab rename, and Export). ICellWrapperType Wrapper · IReadOnlyList<CellValue> Inner |
IStructuralMarkerDefinition | A custom @marker row (per-column values, validated column by column — generalizes @overlap). string MarkerName (no @) · string Description · void ValidateCell(MarkerCellContext) |
MarkerCellContext | One marker-cell validation call. string MarkerName · string RawText · string FieldName · CellCoordinate Coordinate · void Reject(string reason, string suggestion = null) (→ MarkerCellInvalid) |
CellParseContext | One parse call's context. TypeToken Type · CellCoordinate Coordinate · ErrorCollector Errors · EnumRegistry Enums |
Sheet grammar constants (SheetForge.Core.Model)
A pack that reads or writes cell text works in the same grammar the importer does — splitting a list cell, composing an @type string, checking whether a name is already taken.
These constants are that grammar's single source of truth, so a pack never restates a separator of its own: a copied character drifts the day the grammar moves. The lists are handed out read-only, so nothing a pack does can change the grammar itself. The notation they spell out is documented in full on the Sheet Syntax page — this is the programmatic handle on it.
| Type | Kind | Role |
|---|---|---|
SheetSyntax | static class | The sheet grammar as constants, grouped below |
Markers
CommentPrefix(#) ·MarkerPrefix(@).- One constant per built-in marker row:
NameMarker·TypeMarker·DescMarker·OverlapMarker·StyleMarker·EnumMarker·LocMarker. RequiredMarkers— the three every sheet must carry.ReservedMarkers— every built-in name. Read it before naming a custom marker: a collision is refused at registration.
Separators
ListSeparator(;) — between list elements.EntrySeparator(,),FieldSeparator(:),SectionSeparator(|),KeyTimeSeparator(@) — the layers inside one value, which is why;never appears in a value's own text.StyleKeyValueSeparator(=) — inside a@stylecell.
@type notation
OptionalSuffix(?) ·DefaultSeparator(=) ·TargetSeparator(@, as inRecordId@Tab).ListTypeName·ListOpen(List<) ·ListClose(>).
Type names
- One constant per built-in name:
IntTypeName·FloatTypeName·BoolTypeName·StringTypeName·RecordIdTypeName·IntIdTypeName·AssetRefTypeName·LocRefTypeName·ColorTypeName·AnimationCurveTypeName·GradientTypeName·EnumTypeName. BuiltinScalarTypesandIsBuiltinScalarTypeName(name)— "is this name already a built-in?", answered before a parser is registered under it.StyleKeyNames(title,color) ·LocReservedColumns(smart,comment).
Values
TrueCanonical/FalseCanonical— canonicalbooltext.NumberCellStyles— theNumberStylesevery numeric cell is read with. Thousands separators are excluded and the culture is always invariant, so a locale decimal comma fails loudly instead of silently changing a number.
Domain validation (SheetForge.Core.Validation)
| Type | Role & key members |
|---|---|
IDomainValidator | Cross-column/cross-tab rule. Violations → ctx.Errors as DomainRuleViolation with all 4 elements. string Name · Validate(DomainValidationContext) |
DomainValidationContext | Tables (tab → SheetTable) · KeyIndices · AssetKeys (null = skipped) · Errors |
Edge seam (SheetForge.Core.Validation / .Edges)
| Type | Role & key members |
|---|---|
ReferenceScanner (static) | Single source of truth for reference-occurrence enumeration. Scan(tables) · ScanTable · ScanField · IsReferenceField(TypeToken), plus the two reference predicates detailed below the table |
RefKeyKind (enum) | Whether a reference matches the string (RecordId) or integer (IntId) key space. Returned by ReferenceScanner.GetReferenceKind; consumers branch on it. Append-only |
ReferenceOccurrence (struct) | One occurrence — Kind · FromTab · RowNumber · ColumnNumber · FieldName · TargetTab · TargetId · ToCoordinate() |
ReferenceOccurrenceKind (enum) | Scalar · ListElement · ExplicitDefault · WrapperElement · CustomElement (a reference an IRefBearingValue declared out of its own notation — cell-level coordinates, since the inner layout belongs to that type). Append-only, so existing values keep their meaning |
IEdgeContributor | Declares edges the scanner can't see. No diagnostics. string Name · ContributeEdges(EdgeContributionContext, ICollection<EdgeSpec>) |
EdgeSpec | One edge — FromTab/FromRecordId/ToTab/ToRecordId (+ optional FieldName, PayloadTab/PayloadRecordId for record-edges, Label) |
EdgeContributionContext | Read-only Tables + KeyIndices (no error collector — edges aren't validation) |
IAuthorableEdgeContributor | Opt-in capability an IEdgeContributor may also implement so its edge can be edited on the graph canvas. bool TryPlanConnect(EdgeAuthoringContext, fromTab, fromRecordId, toTab, toRecordId, out EdgeCellWrite) · bool TryPlanDisconnect(EdgeAuthoringContext, RecordEdge, out EdgeCellWrite) — false = nothing is staged and the affordance is disabled with a reason; both run inside try/catch |
IEdgeTokenEditor | Opt-in capability an IEdgeContributor may also implement so the leftover of its token (everything that is not the key) can be edited in the wire inspector. bool TryDescribeToken(EdgeAuthoringContext, RecordEdge, out EdgeTokenDescription) · bool TryPlanSetModifier(EdgeAuthoringContext, RecordEdge, string newModifier, out EdgeCellWrite) — both read the same cell (an edge knows where it points, not how it is spelled today), false = the row is hidden or honestly disabled; both run inside try/catch |
EdgeTokenDescription | What one token is and how to edit its leftover — TokenText (the fragment to highlight) · ModifierText · HasModifier · ModifierLabel · IsChoice · Options / OptionLabels. new EdgeTokenDescription(tokenText) = no leftover, so no row is drawn; the choice ctor falls back to free text when the option list is empty |
IBatchAuthorableEdgeContributor | Opt-in capability, sibling of IAuthorableEdgeContributor (not an inheritance): connect/disconnect plans as a list of cell writes, for data where one gesture must change several paired cells together. bool TryPlanConnectMany(EdgeAuthoringContext, fromTab, fromRecordId, toTab, toRecordId, out IReadOnlyList<EdgeCellWrite>) · bool TryPlanDisconnectMany(EdgeAuthoringContext, RecordEdge, out IReadOnlyList<EdgeCellWrite>) — the whole list is staged as one undo step or not at all; singular contributors keep working (fallback), and batch wins when one class implements both |
IVirtualNodeFactory | Opt-in capability: canvas "create" gestures that are not a new sheet row. IReadOnlyList<VirtualNodeKind> KindsFor(EdgeAuthoringContext, tab, recordId) (called per menu build — keep it light) · bool TryPlanCreate(EdgeAuthoringContext, tab, recordId, VirtualNodeKind, out IReadOnlyList<EdgeCellWrite>) — false = the session is untouched. A plan cannot target a record created in the same gesture |
VirtualNodeKind (struct) | One creatable kind — Id (returned verbatim on pick) · Label (already-translated menu text; / nests) · IsUsable. Null-safe, default-safe |
IEdgeSlotDeclarer | Opt-in capability: ports a (virtual) node opens without needing a live edge. IReadOnlyList<DeclaredSlot> DeclareSlots(EdgeAuthoringContext, nodeTab, nodeRecordId) — declared slots join the connect menu, the port picker and the card's port rows; called per render, so implementations must be light and side-effect free |
DeclaredSlot (struct) | One declared slot — FieldName (unique per node; must match the contributor edge's FieldName for wires to anchor) · TargetTab · IsList · IsUsable. Null-safe, default-safe |
EdgeAuthoringContext | The planning input — Tables + string CellText(tab, recordId, field), which returns the cell as it reads now (baseline plus staging), so two links made in a row see each other |
EdgeCellWrite (struct) | The plan: TabName · RecordId · FieldName · NewRawText (empty = clear) · IsAddressable. Addressed by key, not row number |
ReferenceScanner — the two reference predicates:
GetReferencedTab(TypeToken)— the one predicate every consumer asks, "is this a reference, and to where". It answers forRecordId@Tab, forIntId@Tab(the integer key space), for the inside of a wrapper, and for a custom type stampedIsCustomReference. That is why one opt-in — and, forIntId@Tab, one widening of this predicate — turns them all on at once.GetReferenceKind(TypeToken)→RefKeyKind— whether the reference compares against the string or the integer key space, so rename propagation, the dropdown and the picker branch correctly.GetReferenceKind(TypeToken, tables)— the table-aware overload.
A core reference states its own space (RecordId / IntId). A referencing custom type has no notation to say it in — MyType@Tab is the only spelling — so its space is derived from the target tab's identity: a RecordId self key means string space, IntId alone means integer space, and an unknown tab or null tables falls back to string space, which is the same answer the token-only overload gives. That derivation is what lets an existing IReferencingCellType implementation aim at an IntId-keyed tab without a single line changed.
Reference graph index (SheetForge.Core.Edges)
An immutable snapshot that merges core-scanned references and contributor edges into one model, indexed both ways. Display material — it never produces diagnostics (the projection's Diagnostics remain the only source of truth for problems).
| Type | Kind | Role & key members |
|---|---|---|
RecordEdge (struct) | value | One edge. RecordEdgeOrigin Origin · FromTab · FromRecordId (empty for field-level edges) · FieldName · RowNumber / ColumnNumber (1-based; 0 = field/tab level) · ToTab · ToRecordId (the intended id even when unresolved) · bool IsDangling (fixed at build time) · Label · PayloadTab / PayloadRecordId (record-edges) |
RecordEdgeOrigin (enum) | — | CoreReference (read from a RecordId@Tab cell — has coordinates) · Contributor (declared by an IEdgeContributor — record-level) |
ReferenceIndex | sealed class | The snapshot. static Build(tables, keyIndices, contributorEdges, codeRegistries, extraKeys = null) (the last three may be null; extraKeys = tab → keys that exist but are not parsed yet, e.g. rows an authoring surface just staged, so links to them are not drawn as broken) · AllEdges (deterministic order: from-tab Ordinal → row → column → occurrence) · OutEdges(tab, recordId) / InEdges(tab, recordId) (never null) · int InCount(tab, recordId) · bool TryGetRowKey(tab, rowNumber, out recordId) · DanglingEdges |
Data Studio record canvas (SheetForge.Core.Graphing)
The canvas decides what to draw on its own: it walks the reference index outward from the record you opened (the terminus) and lays the result out deterministically. A plugin does not replace that picture — it adds to it. Pure data throughout: columns are grid cells, not pixels, and colours are a free Category string the window maps to a palette.
| Type | Kind | Role & key members |
|---|---|---|
IRecordCanvasAugmenter | interface | One tab's canvas override, called after the closure is assembled. Augment(GraphBuildContext, CanvasAugmentBuilder, string terminusTab, string terminusRecordId). Adding nothing leaves the core picture as it is. A throw is caught by the window and becomes an English console warning. Identity belongs to the data — a virtual node loses to a real record of the same key. Presentation, the display hint, does not |
CanvasAugmentBuilder | sealed class | The write surface, four things only — members and rules below the table |
GraphShapeRegistry | sealed class | Tab name → canvas override. Register(tabName, IRecordCanvasAugmenter) (duplicate tab / empty name / null throw) · TryGet · IsEmpty |
GraphBuildContext | sealed class | The override's read-only input. Tables (tab → SheetTable) · ReferenceIndex References · IReadOnlyList<CodeRegistrySource> CodeRegistries (empty, never null). No error collector — a canvas is display, not validation |
GraphSpecBuilder | sealed class | The graph assembly helper. ctor (GraphBuildContext) · static NodeKey(tab, recordId) (the single truth wires point at) · AddNode(GraphNodeSpec) (first (Key, Column) wins) · AddWire(GraphWireSpec) · AddWire(fromKey, toKey, label, fromTab, fromRecordId, fieldName, isCyclic = false, cyclicNote = null) (the overload that also names the cell the link is written in, which is what makes the wire editable) |
GraphSpec | sealed class | The assembled result the canvas draws — Nodes · Wires (assembly goes through the builder; the ctor is internal) |
GraphNodeSpec | sealed class | One node. Key · Tab · RecordId · Title · Subtitle · Category · CellCoordinate Address · Column / Row (grid cells the canvas has already solved — carried, not chosen, here) · IsFocus (the terminus) · IsMissing · InCount · IsCyclic |
GraphWireSpec | sealed class | One wire. FromKey · ToKey · Label · IsCyclic · CyclicNote, plus the optional owning cell: FromTab · FromRecordId · FieldName · RecordEdge? SourceEdge (null = display-only wire; the canvas then says it cannot be edited). The five display arguments are unchanged, so existing calls compile and render identically |
IAuthorableGraphShape | interface | Opt-in capability an IRecordCanvasAugmenter may also implement. IReadOnlyList<string> CreatableTabs(GraphBuildContext, string tabName) — where the canvas may create a record (empty = nowhere). The defaults without it are below the table |
CanvasAugmentBuilder — the write surface. Four things only:
AddNode(tab, recordId, title = null, category = null)/AddNode(tab, recordId, title, category, CellCoordinate address)— a virtual node for an identity that is not a sheet record (an event key, a code atom); the tab may be empty.AddEdge(fromTab, fromRecordId, toTab, toRecordId, label = null, fieldName = null, fieldOnTarget = false, isCyclic = false, cyclicNote = null)— an extra edge the core scanner cannot see. NamingfieldNamesays which cell the link is written in,fieldOnTargetsays that cell sits on the arrival rather than the departure, and the cycle pair marks a loop for display with the note only the domain knows.SetLayer(tab, recordId, layer)— an absolute layer hint (0 = leftmost, negative = further left, everything shifts right to compensate).SetLayerRelative(tab, recordId, offset)— the same counted from the terminus (−1 = one column left of it), resolved against the terminus column before any hint moved it.SetSubtitle(tab, recordId, subtitle)— a display hint, the one thing that applies to records that already exist and to records that are not on screen (the connect picker reads those).
Items with an empty key are ignored, and what was collected is internal, because the merge rules live in one place. Every widening is trailing, so an override written against an earlier surface still compiles.
IAuthorableGraphShape — the two defaults without it:
- The creatable-tabs list this capability replaces — the axis that also decides whether a canvas opens at all and how far the pending-row sweep reaches — covers every tab reachable from the focus tab by following the schema transitively.
- The linking cascade the user actually sees starts from the tabs the ports currently drawn aim at.
Both drop code-registry tabs and tabs with no key column. A tab this returns that no drawn port accepts stays listed in the linking cascade with its reason attached, and the window's own gates still apply on top.
Colour presets (SheetForge.Core.Theming)
| Type | Role & key members |
|---|---|
ThemeRegistry | Preset id → theme. Blank ids, duplicates and the two reserved built-in ids throw. Register(SheetForgeTheme) · TryGet · Themes · IsEmpty · IsBuiltInId(id) · BuiltInDefaultId · BuiltInHighContrastId |
SheetForgeTheme | One colour preset. Id · DisplayName · DarkColors / LightColors (IReadOnlyDictionary<ThemeColorSlot, uint>, copied on construction) · TryGetColor(dark, slot, out rgb) · IsEmpty |
ThemeColorSlot | enum — the 33 colour roles a preset may override (surfaces, lines, text, semantic colours, staging marks, failure surfaces, scrim, graph). Colours are 0xRRGGBB: Core references no engine type, and translucent fills derive from a slot colour plus a fixed alpha. Append-only. |
A preset overrides only the slots it names; every other slot keeps the product default, so a preset stays valid as slots are added. Registering never applies a preset — the user picks one in Preferences ▸ SheetForge ▸ Theme.
Declarative authoring surfaces (SheetForge.Core.Studio)
A plugin describes what to show — shell as data, predicate and effect as delegates — and each host draws it with its own widgets: UIToolkit in the editor, React in the browser. No layout numbers appear anywhere. What to say is the plugin's, how to place it is the renderer's.
Every enum here is append-only, so a registration keeps its meaning as the vocabulary grows.
| Type | Kind | Role & key members |
|---|---|---|
StudioUiRegistry | sealed class | What RegisterStudioUi fills. AddAction(StudioActionDescriptor) · AddPanel(StudioPanelDescriptor) · AddColumnBadge(StudioColumnBadgeDescriptor) · AddCellEditorHint(StudioCellEditorHint) · Actions / Panels / ColumnBadges / CellEditorHints · IsEmpty |
StudioUiNode | sealed class | One described fragment, immutable, built through static factories — the factories, the readable properties and the URL rule are below the table |
StudioUiNodeKind | enum | The 13 kinds above (Row … Link) |
StudioActionDescriptor | sealed class | One verb. Id (unique) · LabelKey (a Loc key; unregistered shows verbatim) · StudioActionPlacement Placement · Func<StudioSurfaceContext,bool> AppliesTo · Action<StudioSurfaceContext> Execute · ConfirmKey (optional — the host asks this sentence first). The host re-checks AppliesTo at invocation, so a stale menu entry answers with an honest no-op and a redraw |
StudioActionPlacement | enum | Inspector · RowContextMenu · TopbarMenu · ColumnHeaderMenu · CanvasNodeMenu. Each seat fills different context fields — the row seat carries the record, the column seat the column name, the canvas seat that node's record |
StudioPanelDescriptor | sealed class | One panel in the Studio's right-hand pane. Id · TitleKey · Func<StudioSurfaceContext,StudioUiNode> Build — rebuilt each recompute tick, so it holds no state. With no panel registered the pane is not drawn at all |
StudioColumnBadgeDescriptor | sealed class | One badge beside a column header. Func<StudioSurfaceContext,string,string,StudioUiNode> Provide (context, tab, field) — null means nothing on that column |
StudioCellEditorHint | sealed class | "Use this built-in widget for this type" — picking a kind rather than supplying one. TypeName (an exact CellParserRegistry type name; a list cell is matched on its element name; wrapper cells keep the canonical text and are never matched) · StudioCellEditorArchetype Archetype · GetOptions (dropdown only — Func<context, tab, field, IReadOnlyList<StudioCellOption>>) · SliderMin / SliderMax · ToggleTrueValue / ToggleFalseValue. Four constructors, one per material shape. Consulted after a registered IStudioCellEditorProvider declines and before the built-in branches; a pack's hint is consulted before the built-in hints below, so registering one under Color, AnimationCurve or Gradient overrides the default editor for that type. A List<> whose element hint is ColorPicker, CurveEditor or GradientEditor becomes a chip editor in both hosts |
StudioCellEditorArchetype | enum | Dropdown · MultilineText · Slider · Toggle · ColorPicker (cell text #RRGGBB / #RRGGBBAA) · CurveEditor (cell text = the canonical CurveValue notation) · GradientEditor (cell text = the canonical GradientValue notation). Append-only — the two newest are 5 and 6 |
BuiltinCellEditorHints | static class | The three hints the Core itself declares — Color → ColorPicker, AnimationCurve → CurveEditor, Gradient → GradientEditor — travelling the same path as a pack's hints, so the editor and the browser cannot choose different widgets for them. IReadOnlyList<StudioCellEditorHint> All (fixed order) · bool TryGet(typeName, out hint) (Ordinal). Hosts consult StudioUiRegistry.CellEditorHints first and fall back to this table |
StudioCellOption | sealed class | One dropdown candidate — Value (the canonical text written to the cell) · Label (what a person reads; defaults to Value) |
StudioSurfaceContext | sealed class | The one seam an extension sees and acts through. Read: Tables · ReferenceIndex References · CodeRegistries · Tab · RecordId · Field · ActionArgument (the value an Input node committed). Mediated mutation, and nothing else: Action<string,string,string,string> StageCell · Action<IReadOnlyList<EdgeCellWrite>> StageCells (one Undo step, all-or-nothing) · Action<string,string> FocusRecord · Action RequestRebuild. Staging goes through the window's own gate, so a read-only source, a running pipeline or a workbook-backed tab blocks it with a reason (ctor internal: the host assembles it) |
StudioUiNode — factories, reads and the URL rule:
- Factories:
Row·Label·Chip·Badge·Button·Rule·Heading·KeyValue·Table(headerRow, rows)·List·Progress·Input·Link, plusWithTooltip(text), which returns a new node rather than changing this one. - Reads:
Kind·Text·Tooltip·ThemeColorSlot? Tone(never a hard-coded colour, so it follows the theme) ·ActionId·Detail·Ratio·Url·Children. - static
bool IsAllowedUrl(url)—http/httpsonly. One predicate both hosts ask, so they cannot disagree about what is safe to open.
Plugin UI strings (SheetForge.Core.Model)
| Type | Role & key members |
|---|---|
StringOverlayRegistry | Collector and lookup overlay for plugin-registered UI strings; Loc.Tr (editor) and t() (browser) consult it before the product tables. Register(key, language, value) · Register(key, IReadOnlyDictionary<string,string> byLanguage) · bool TryGet(key, language, out value) · RegisteredKeys. Language matching and the four refusals are below the table |
StringOverlayRegistry — matching and refusals. language is an IETF code ("en", "ko", "zh-Hans", "pt-BR", …), matched case-insensitively. Lookup falls back requested language → English → miss, and the fallback lives here so both hosts answer identically.
Four registrations are refused, each recording a developer-facing reason rather than failing silently:
- a product built-in key — an overlay may add keys, never overwrite the product's own sentences or menu paths;
- a key+language another pack already registered — first found wins, because otherwise install order would decide the screen;
- an empty key or value;
- a language code the product does not know, which is never folded into English.
Pipeline observation (SheetForge.Core.Plugins / .Model)
| Type | Role & key members |
|---|---|
IPipelineObserver | Read-only notification. void OnImportCompleted(PipelineRunView view) — once per explicit import cycle, at its end, success or failure. There is deliberately no hook that alters a value or adds a diagnostic (those belong to a cell type and to IDomainValidator), and none that runs on the staging pre-flight. A throw is isolated with its reason collected; the import output is unchanged. Future observation points arrive as sibling capability interfaces, cast from the registered observer, so an implementation written today keeps compiling |
PipelineObserverRegistry | Append-only observer list, order preserved. Register(IPipelineObserver) · Observers |
PipelineRunView | The immutable snapshot an observer receives — Success (validation, i.e. was a registry assembled; codegen/bake outcomes are read from the diagnostics) · Tables (tabs that parsed; on a failed run only the tabs that could not be parsed are missing, because no-partial-assembly is an output rule, not an observation rule) · Diagnostics (the same list the report shows) · SkippedTabs · EnumTabs. Collections are copied on construction, and the constructor is internal so no half-built snapshot can be handed to an observer |
Code registries (SheetForge.Core.Graphing)
Reference targets that live in code, exposed to the authoring surface as locked virtual tabs. Consumed by the Data Studio (sidebar / graph / inspector), not by the import validator.
| Type | Kind | Role & key members |
|---|---|---|
CodeRegistryCatalog | sealed class | Registration root. Register(CodeRegistrySource) (null / empty tab name / duplicate tab name throw) · TryGet(tabName, out source) · Sources · IsEmpty |
CodeRegistrySource | sealed class | One locked virtual tab. string TabName · IReadOnlyList<CodeRegistryEntry> Entries (registration order = display order) |
CodeRegistryEntry | sealed class | One entry. string Key (what a reference may point at) · string Label · IReadOnlyList<string> Raises (null normalises to empty). Core treats all three as opaque strings |
The IR read model (SheetForge.Core.Model)
| Type | Role & key members |
|---|---|
SheetTable | One tab's parse output. SheetSchema Schema · IReadOnlyList<SheetRecord> Records |
SheetSchema | string TabName · Fields · TryGetField(name, out FieldSchema) · SheetStyle Style (the sheet's @style display metadata) · bool IsLocalizationSheet (the @loc marker is present) · IReadOnlyList<LocaleColumn> LocaleColumns (the locale columns in original column order — empty on a sheet that is not a localization sheet, never null) · TryGetLocaleColumn(localeCode, out LocaleColumn) (lookup by code, case-insensitive) · TryGetSourceLocale(out LocaleColumn) (the first locale column; false when there is none) |
SheetStyle | The @style row's value — display metadata for one sheet. string Title (sidebar group label) · string ColorHex (#RRGGBB as written) · bool HasColor · static None (no styling). Never read by codegen, baking or the schema fingerprint |
LocaleColumn (struct) | One locale column of a localization sheet — what the @loc row wrote on that column. string Code (the code exactly as written; the Core validates the spelling shape, never whether the locale exists) · string FieldName · int ColumnNumber (1-based) · bool IsSource (the first locale column — the one inline previews read and minting writes to) |
SheetRecord | int RowNumber (original, 1-based) · Values (field → CellValue) · TryGet · indexer |
FieldSchema | Name · TypeToken Type · Description · ColumnNumber · DefaultValue · AllowOverlap · IReadOnlyDictionary<string,string> MarkerValues (custom-marker name → this column's cell text) |
TypeToken | Parsed @type cell. RawText · TypeName · TypeArgument · TargetName · IsList · IsOptional · HasExplicitDefault · DefaultValueText · AllowsEmptyCell · IsSelfKey · IsIntId (this tab's self integer key only — the IntId@Tab reference form is read through TargetName + ReferenceScanner.GetReferencedTab, the same as RecordId@Tab) · TypeToken InnerToken / IsWrapper (wrapper types — recursive inner) · IsCustomReference (this column is MyType@Tab where the parser implements IReferencingCellType; ReferenceScanner.GetReferencedTab is the single predicate that reads it, which is how every consumer lit up without a signature change) · AssetTypeName (the <Type> of AssetRef@Group<Type> as written, null when unrestricted; the Core stores the name only — resolving it is IAssetTypeResolver's job — and it is stamped on the AssetRef token inside a list or a wrapper too). The constructor's three trailing parameters (innerToken, isCustomReference, assetTypeName) are defaulted, so existing calls compile, and the earlier 8- and 10-argument constructors remain as overloads, so already-compiled plugin assemblies keep working without a rebuild |
CellValue (struct) | One typed cell value; no nulls (IsDefaulted marks materialized defaults). object Value · IsDefaulted · AsList · static Of / Defaulted |
RecordId (struct) | A key value (Ordinal equality). string Value · IsEmpty |
RecordRefValue (struct) | A RecordId@Tab cell's value. TargetTab · Id |
IntRefValue (struct) | An IntId@Tab cell's value — the integer-key twin of RecordRefValue. string TargetTab · int Id · bool IsEmpty · static Empty(tab) (an optional IntId@Tab? that points at nothing) |
LocRefValue (struct) | A LocRef@Tab cell's value — the localization twin of RecordRefValue, kept a separate type so a consumer knows from the value alone that it points at a string table. string TargetTab · string Key · bool IsEmpty · static Empty(tab) · ReferencedKeys. It implements IRefBearingValue, so the reference scanner treats it exactly like a core reference |
AssetRefValue (struct) | An AssetRef@Group cell's value. Group · Key (a sub-asset key is parent[sub]) |
EnumValue (struct) | An Enum<T> cell's value (string pair — CLR conversion is bake's job). EnumName · MemberName |
Typed asset references (SheetForge.Core.Model)
The <Type> in AssetRef@Group<Type> is resolved by the host — the Core knows neither the engine nor the project's assemblies — and the Core only judges the result. Everything here is pure data.
| Type | Kind | Role & key members |
|---|---|---|
IAssetTypeResolver | interface | AssetTypeResolution Resolve(string rawName) — one name in, one verdict out; the same name always gets the same answer (implementations may cache). Injected into ImportPipeline separately from AssetKeyIndex, so type names are resolved even in a project that has no Addressables settings yet; when no resolver is injected (headless, browser) the type-name diagnostics are simply not produced. The Editor's implementation resolves against the project's loaded UnityEngine.Object-derived asset types (no allow-list; components and editor-only types excluded) |
AssetTypeResolution | sealed class | The verdict for one name — RawName · AssetTypeResolutionStatus Status · FullName (CLR full name, nested types with +; Resolved and NotReferenceable only) · AssemblyName (the assembly the generated companion assembly must reference — set for assembly-definition types, null for engine modules and unresolved names) · Candidates (never null: the ambiguous candidates, or nearest-match suggestions for an unknown name). Factories Resolved(raw, fullName, assemblyName) · Unknown(raw, suggestions) · Ambiguous(raw, candidates) · NotReferenceable(raw, fullName, assemblyName) |
AssetTypeResolutionStatus | enum | Resolved · Unknown (no such type) · Ambiguous (the short name matches several types — write the full name) · NotReferenceable (the type lives in a predefined assembly such as Assembly-CSharp, which generated code cannot reference) |
Codegen reads the resolved dictionary the pipeline produces and emits AssetReferenceT<global::FullName> for a resolved name; a name it cannot find in that dictionary is never emitted verbatim — the field falls back to AssetReference and an AssetTypeUnresolvedFallback warning is collected. The resolved full name is also mixed into the schema fingerprint.
Visual value types (SheetForge.Core.Model)
Engine-free value models for the three built-in visual types. Each is immutable, IEquatable, and owns its own text form (TryParse / Render) — the same notation the Sheet Syntax page documents — so a plugin type that stores a colour, a curve or a gradient can reuse them instead of inventing a second notation. The Editor bakes them into UnityEngine.Color / AnimationCurve / Gradient and reads them back; the browser samples them through the evaluators below rather than re-implementing the maths.
| Type | Kind | Role & key members |
|---|---|---|
ColorValue | readonly struct | Four bytes R · G · B · A · static Default (#00000000) · static TryParse(text, out value, out error) (accepts #RGB / #RGBA / #RRGGBB / #RRGGBBAA) · Render() (upper-case, six digits when opaque) |
CurveValue | sealed class | Keys (time-ascending) · PreWrap / PostWrap · static Empty (no keys — the only state with no text form; Render() gives "") · static Create(keys, preWrap, postWrap) — the one construction path: sorts by time, rejects duplicate times, and applies CurveTangentSolver so "the mode wins" holds from the moment a curve exists · static TryParse (2/4/7/8-field keys, Once accepted as an alias of ClampForever, Infinity/-Infinity tangents) · Render() (8-field keys, wrap suffix only when needed) |
CurveKey | readonly struct | Time · Value · InTangent · OutTangent · InWeight · OutWeight · CurveWeightedMode WeightedMode · CurveTangentMode LeftMode / RightMode · bool Broken; a ten-argument constructor with no normalisation of its own |
CurveWrap | enum | ClampForever · Loop · PingPong · Default — Unity's wrap vocabulary by name (the value mapping to WrapMode belongs to the baker) |
CurveTangentMode | enum | Free = 0 · Auto = 1 · Linear = 2 · Constant = 3 · ClampedAuto = 4 — name-and-value identical to AnimationUtility.TangentMode, so the baker maps by name and never touches Unity's packed tangent bits |
CurveWeightedMode | [Flags] enum | None = 0 · In = 1 · Out = 2 · Both = 3 — which side of a key uses weighted (Bezier) tangents |
CurveTangentSolver | static class | CurveKey[] Apply(IReadOnlyList<CurveKey> sortedKeys) — derives the tangent numbers a mode dictates, applying the stages in the engine's order (Linear on its own side → ClampedAuto both sides → Auto both sides → Constant on its own side), leaving Free sides and weights untouched. CurveValue.Create calls it, so callers rarely do |
CurveEvaluator | static class | float Evaluate(CurveValue, float time) · float[] Sample(CurveValue, int count) (count ≥ 2, evenly spaced from the first to the last key) — Hermite between keys, weighted Bezier on the sides whose weight flag is set, a hold when a tangent is infinite, and the four wrap behaviours outside the key range; verified against AnimationCurve.Evaluate on random curves |
GradientValue | sealed class | ColorKeys · AlphaKeys (1 to 8 each, time-ascending) · GradientBlend Mode · GradientColorSpace ColorSpace · static Default (white, fully opaque, Blend) · static Create(colorKeys, alphaKeys, mode, colorSpace) (validates counts and 0…1 ranges, quantises times to 16 bits as Unity does, sorts stably) · static TryParse (three or four ` |
GradientColorKey | readonly struct | ColorValue Color (alpha ignored — alpha has its own keys) · float Time |
GradientAlphaKey | readonly struct | float Alpha · float Time |
GradientBlend | enum | Blend · Fixed · PerceptualBlend |
GradientColorSpace | enum | Uninitialized (not written; reads as Gamma) · Gamma · Linear — only PerceptualBlend is affected |
GradientEvaluator | static class | ColorValue Evaluate(GradientValue, float time) · ColorValue[] Sample(GradientValue, int count) — linear, stepped or perceptual (Oklab) blending with the alpha keys blended separately, rounded to bytes; verified against Gradient.Evaluate on random gradients |
Errors and results (SheetForge.Core.Model / .Reporting)
| Type | Role & key members |
|---|---|
ImportError | Structured, locale-neutral error. Code · Severity · Coordinate · ActualValue · Expected · Suggestion |
ImportErrorCode (enum, 105) | The complete "why" catalog — the families it covers are listed below the table. Append-only, because renderer tables key off the member values |
ImportSeverity (enum) | Error (blocks output) · Warning |
CellCoordinate (struct) | Tab · 1-based row · 1-based column · field; computes the spreadsheet column letter. ForTab / ForRow factories |
ErrorCollector | Collect-everything sink. All · HasErrors · ErrorCount · Add |
ImportResult | Pipeline output. Invariant: Success == false ⇔ Registry == null. Success · Registry · Diagnostics · SkippedTabs · EnumTabs (tabs read as enum definition sheets, so never parsed as data tables. They are kept apart from SkippedTabs, which means "no table written yet", so the report's skipped count stays true. Both are preservation sets that keep generated code, baked assets and addresses for those tabs) · static Succeeded / Failed |
ImportReport (.Reporting, assembly SheetForge.Core.Tooling) | Report renderers' input — Timestamp · SourceDescription · TabCount · RecordCount · Success · Diagnostics · ErrorCount · WarningCount · SkippedTabCount (how many of TabCount were empty sheets skipped rather than imported — the header prints it so a tab count is not mistaken for "all imported") |
ImportReportText (.Reporting, assembly SheetForge.Core.Tooling, static) | Renders a report to the product's own human-readable string. Nothing is written to the console, and no jump link or machine-coordinate line is appended — those belong to the console's own convention. string Render(ImportReport report, IReadOnlyDictionary<string,string> languageTable = null, string operationName = null) — omit the table for English. The operation name, when omitted, is read from the same table so the sentence never mixes two languages. Editor-side callers usually want SheetForgeActions.RenderReportText(report), which fills in the current editor language (a pure assembly cannot read EditorPrefs) |
ImportErrorCode — the families it covers:
- markers, schema, types, cells, keys/references and asset keys;
- sources/files, csv/xlsx, codegen identifiers, addressables, baseline/export, Google/auth/Push and templates;
- plugins —
PluginRegistrationConflict, plusPluginIncompatiblewhen an assembly's compatibility declaration falls outside what this host reads; - IntId —
DuplicateIntId, and forIntId@TabreferencesUnresolvedIntId·TargetTabHasNoIntId; @overlapandDomainRuleViolation;- enum definition sheets —
EnumSheetMarkerConflict·DuplicateEnumName·EnumSheetEmptyColumn·InvalidEnumIdentifier·InvalidEnumUnderlyingType·InvalidEnumMemberValue; DropdownNotSupportedByFormat, which is a warning rather than an error;- typed asset references —
UnknownAssetType·AmbiguousAssetType·AssetTypeNotReferenceable(once per column, on the@typerow),AssetTypeMismatchper cell, and the codegen warningAssetTypeUnresolvedFallback.
Indices and utilities (SheetForge.Core.Validation / .Model / .Parsing / .Unparse)
| Type | Role & key members |
|---|---|
TabKeyIndex | One tab's key info — the string key column plus the tab's IntId integer-key set, so both RecordId@Tab and IntId@Tab references resolve against it. TabName · KeyField · HasKeyColumn · Keys · Contains(id) |
KeyIndexBuilder (static) | Builds key indices (the string keys and the IntId integer-key set, in one pass), reports key errors, validates IntId columns. Build(SheetTable, ErrorCollector) · ValidateIntIdColumns |
AssetKeyIndex | Group → valid-key set (Editor fills from the Addressables catalog, sub-asset keys included; null injection = skip asset validation). Register(group, keys) · HasGroup · HasKey · KeysOf · GroupNames, plus the type layer used by AssetRef@Group<Type>: RegisterTyped(group, key, satisfiedTypeFullNames) (the key and the closure of type full names it can be loaded as — its own type, bases, interfaces, its sub-assets' types; re-registering unions the closure) · HasTypeInfo(group, key) · SatisfiesType(group, key, typeFullName). A key registered with plain Register has no closure and is exempt from the type check rather than failing it |
LocalizationCoverage (static) | Per-locale coverage and orphan keys for a localization sheet. A pure calculation that returns lists instead of collecting errors, because an untranslated cell and an unused key are normal states rather than exits to block. IReadOnlyList<LocaleCoverage> Compute(SheetTable) · IReadOnlyList<string> FindOrphanKeys(locTabName, tables) (keys nothing points at; deliberately conservative — every reference form the scanner knows counts as a use, so a live translation is never called an orphan) |
LocaleCoverage (sealed class) | One locale's coverage. LocaleColumn Locale · int TotalKeys · int TranslatedKeys · IReadOnlyList<string> MissingKeys (sheet row order, never null) · bool IsComplete |
TextSuggestion (static) | Nearest-match suggestions (bounded Levenshtein, deterministic). FindNearest · Distance · DistanceWithin |
BuiltinCellParsers (static) | CreateDefaultRegistry() — the 12 built-in parsers (int, float, bool, string, Enum, RecordId, AssetRef, IntId, LocRef, Color, AnimationCurve, Gradient). |
CanonicalValueRenderer (static) | Value → canonical cell string (Export/Push). TryRender(…) (delegates a ColorValue / CurveValue / GradientValue to its own Render(); a curve with no keys renders as the empty cell) · RenderFloat(float) (shortest round-trip) |
Push plan (SheetForge.Core.Unparse)
Public because IPushApprover.Approve(PushPlan) exposes them; pure data.
| Type | Role |
|---|---|
PushPlan (assembly SheetForge.Core.Tooling, as are the three rows below) | The whole send plan. Tabs · HasWork |
PushTabPlan | One tab: Writes · Appends · Deletes (key + row number; DeleteNotices remains the key-only view) |
PlannedCellWrite | One cell write — coordinates, baseline cell, new value/text, string-family flag |
PlannedRowAppend | One appended row — full cell texts + string-family columns |
Editor assembly (SheetForge.Editor)
Settings, localization, composition (SheetForge.Editor.Pipeline / .Localization)
| Type | Role & key members |
|---|---|
SheetForgeSettings (SO) | The settings asset. Fields: sourceProviderId (sole source-selection axis; empty = built-in LocalFile) · localFolderPath · bakeOutputFolder · generatedCodeFolder · generatedNamespace · exportFolderPath · exportFormat · spreadsheetId · googleAccessMode · serviceAccountKeyPath · gidMap (list of GidMapEntry { tabName, gid }). Effective* resolved properties. |
Loc (static) | Localization entry point. Tr(key) · TrContent(…) · Table · MenuRoot const. Tr resolves in four steps: plugin-registered string (current language, then English — the overlay owns that fallback, see StringOverlayRegistry) → built-in table (current language, then English) → the key itself. There is exactly one registration channel for plugin strings, so "which registration wins" never becomes a question |
PluginRegistry (static) | TypeCache-discovers plugins, then hands the candidates to PluginComposition.Compose. Build · BuildValidators · BuildEdgeContributors · BuildStructuralMarkers · BuildTemplates · BuildGraphShapes · BuildCodeRegistries · BuildThemes · BuildAll (bundle) · InvalidateCache() (drops the reload-lifetime cache — same convention as SourceProviderRegistry.InvalidateCache; it also drops the compatibility gate's cache, so a changed discovery set is re-judged). The bundle and slot isolation are below the table |
ImportEvents (static) | Editor-side event bus — a public contract: external assets may subscribe. event Action<ImportCompletedArgs> ImportCompleted · RaiseImportCompleted(ImportCompletedArgs) fire only when an import ran all the way through bake, so a subscriber may read the baked assets. event Action<BaselineUpdatedArgs> BaselineUpdated · RaiseBaselineUpdated(BaselineUpdatedArgs) fire whenever a sheet snapshot was saved — including a run that failed validation — which is how an authoring surface refreshes on a quarantined import. Two axes, deliberately not merged: one means "the sheets moved", the other "the assets moved" |
BaselineUpdatedArgs (sealed) | Baseline-save payload. IReadOnlyList<string> Tabs (the tabs written to the snapshot) · bool Quarantined (whether the snapshot just saved failed validation) |
SheetForgeActions (static) | The run facade — the same cycle a menu click runs, callable from a CI script, a build hook or your own button. RunImport() · RunExport() · RunPush() · RunHealthCheck() · RunLocalizationSync() (each delegates; settings resolution, the Addressables gate, mutual exclusion, confirmation modals, the progress bar and the codegen→compile→bake resume all stay inside the product) · bool IsBusy · bool TryBeginExclusiveScope(out IDisposable scope) (false + scope = null when something is already running; the scope is what releases, and a second Dispose cannot free somebody else's run) · string RenderReportText(ImportReport) (the product's own sentences in the current editor language, no console write). Completion semantics are below the table |
SheetForgeEditorInfo (static, namespace SheetForge.Editor) | Editor-assembly anchor — const Version, the mirror of SheetForgeRuntimeInfo for feature-gating against the editor-side surface |
ImportCompletedArgs (sealed) | Completion payload passed to subscribers. IReadOnlyList<string> Tabs (tabs baked by this completion) · string BakeFolder (Database-SO folder). Args-object pattern — future fields won't break the event signature. |
GoogleSheetAccessMode (enum) | SheetsApi (auth, writable) · ExportUrl (no auth, read-only) |
ExportFormat (enum) | Tsv · Csv · Xlsx · Json · MatchSource |
PluginRegistry — the bundle and slot isolation. The nested PluginBundle exposes the composed PluginSet Set — the twelve-slot single truth, which is how a newly grown slot is read without widening the bundle — plus nine convenience windows onto it: Enums · Parsers · Validators · EdgeContributors · Markers · Templates · GraphShapes · CodeRegistries · Themes. The earlier six- and eight-argument constructors remain as overloads that default the later registries to empty, behaving identically to the versions before those contracts existed.
Isolation is Core's, not this type's: a plugin that throws while registering is reported by name and skipped, and every other slot and plugin still registers.
SheetForgeActions — completion semantics. RunImport/RunPush are fire-and-forget. Their bodies are async void because the editor main thread cannot block on network IO, so the return is not completion — subscribe to ImportEvents.ImportCompleted for that. RunExport/RunHealthCheck/RunLocalizationSync complete synchronously — RunLocalizationSync runs the sheet → StringTable path an import completion runs, and without the Unity Localization package it shows the install notice and changes nothing.
Source-provider seam (SheetForge.Editor.Sources)
| Type | Role & key members |
|---|---|
ISheetSourceProvider | The provider contract. Id · DisplayNameKey · CreateTabSource(settings) · GetVisibility(settings) · CanAuthor(settings) · CreateReflectTarget(dispatcher, settings) |
ISourceReflectTarget | Write-back target. void Reflect() |
SourceVisibility | Which settings fields to show — 5 bool flags |
SourceProviderRegistry (static) | Discovery/resolution. All · ResolveActive(SheetForgeSettings) and ResolveActive(string providerId) (resolve straight from an id, without a settings asset in hand) · TryGet · InvalidateCache |
ITabSource | Fetch abstraction. Description · Task<TabSourceResult> FetchAsync() |
TabSourceResult | Tabs (name → raw TSV) + diagnostics + per-tab formats; partial output allowed. static Create |
TabSourceFormat (enum) | Tsv · Csv · Xlsx · GoogleSheet |
Data Studio extension points (SheetForge.Editor.Studio)
Editor-side because they hand back UIElements or touch window state — the same justified asymmetry as ISheetSourceProvider. All four contracts are discovered by TypeCache (parameterless constructor; no registration call), and all are called inside try/catch. The window itself (DataStudioWindow) is internal.
Anything expressible as data belongs in the Core ISheetForgeStudioPlugin vocabulary instead, which renders in the browser too. These are the ceiling-free escape hatches for what description cannot say.
The last four entries are not contracts but tools a mounted widget may use:
- the window's own read-only skin values, so it can look like it belongs;
- the key drop-down, so a cell widget picks keys the same way the built-in cell does;
- and the discovery-cache reset, so your own tests can re-discover a probe.
| Type | Kind | Role & key members |
|---|---|---|
IStudioGraphWidget | interface | A domain strip above the graph canvas (Core ships none). bool AppliesTo(StudioGraphContext) · VisualElement Create(StudioGraphContext) (re-created per graph rebuild — hold no state; null adds nothing) |
StudioGraphContext | sealed class | Read-only: Tab and FocusRecordId (the terminus) · SheetRecord FocusRecord (null when unresolved) · Tables · ReferenceIndex References · CodeRegistries. Two retired axes remain for signature compatibility and are marked [Obsolete]: ShapeId (always "record") and ModeId (always empty). Comparing either one compiles and is never true, so the compiler now says so instead of leaving a dead branch — delete the check. No staging surface — widgets display only (ctor internal: the window assembles it) |
IStudioCellEditorProvider | interface | Draws one grid cell for a named type. string TypeName (matches a CellParserRegistry type or wrapper name, Ordinal; empty opts out) · VisualElement CreateEditor(StudioCellEditorContext) — returning null declines that cell and the built-in widget takes over. A duplicate claim on the same type name warns and keeps the first found |
StudioCellEditorContext | sealed class | What the cell widget gets: Tab · FieldName · TypeToken Type · CurrentRawText (canonical text with staging applied) · Action<string> Commit (a one-shot act — its own undo step) · Action<string> CommitTyping (a keystroke burst — coalesces per cell) · Func<string,IReadOnlyList<string>> ReferenceKeys (the same candidate keys the built-in picker offers). Both commits pass the window's staging gate (ctor internal: the window assembles it) |
IStudioInspectorAction | interface | An extra button on the node inspector. string LabelKey (Loc key; unregistered = shown verbatim, empty = type name) · bool AppliesTo(StudioInspectorContext) · void Execute(StudioInspectorContext) |
StudioInspectorContext | sealed class | Read: Tab · RecordId · SheetRecord Record · Tables · References · CodeRegistries. Mediated mutation: Action<string,string,string,string> StageCell · Action<IReadOnlyList<EdgeCellWrite>> StageCells, both detailed below the table. Services: Action<string,int,string> FocusCell · Action RequestRebuild. The AuthoringSession is deliberately not exposed |
IStudioPanelProvider | interface | An arbitrary UIToolkit panel in the Studio's right-hand pane — the escape hatch beside the descriptive StudioPanelDescriptor. string Id · string TitleKey · bool AppliesTo(StudioSurfaceContext) · VisualElement CreatePanel(StudioSurfaceContext) (null draws nothing this tick). Register a descriptive panel under the same Id and each host takes what it can draw: the editor prefers this one, the browser draws the descriptive one — so "as far as the browser goes, all the way in the editor" needs no second contract. The element lives one recompute tick, so it holds no state |
StudioPalette | static class | Read-only colour, spacing and type values the window itself paints with, so a widget you mount matches the window instead of hard-coding hex. Each slot resolves at read time, so widgets follow the brightness mode and colour preset for free. Choosing the values (presets, brightness, defaults) stays internal — widgets follow the palette, they do not repaint it. The member list is below the table |
StudioTheme | static class | Four members only: CategoryColor(category) (same deterministic tone the window gives that category) · Np(text) (safe interpolation into a rich-text label) · Mono / ApplyMono(element) (the mono-font policy: keys, addresses and numbers only — mono fonts have no CJK glyphs). Everything else on this type is internal |
StudioKeyPicker | static class | One member: Show(Rect screenAnchor, string targetTab, IReadOnlyList<string> candidates, Action<string> picked, string acceptsLabel = null) — the same drop-down the built-in reference cell opens, for a cell widget that has to reach a key inside its own notation. It picks one key from the candidates you supply and hands it back. Creating a record, leaving the cell empty, multi-toggling a list and asking which port receives the pick are the built-in reference cell's own rules, so they are not on this facade. picked is required (ArgumentNullException before any window is made). With no candidates and nothing to offer it logs instead of opening an empty list. The window type itself stays internal |
StudioPluginRegistry | static class | One public member: InvalidateCache() — drops the per-reload discovery cache so a probe your own tests just enabled is found again (the same courtesy PluginRegistry and SourceProviderRegistry already offered; this one was the odd registry out). The discovered lists stay internal: nothing outside can read or replace what the window will mount |
StudioInspectorContext — the two staging delegates:
- StageCell takes tab, recordId, field and canonical raw text. The window registers the Undo step, bumps the projection generation and stages the logical address.
- StageCells does the same for several cells that must change together: one native Undo step, all-or-nothing. If any one cannot be staged the session is not touched at all.
A failure is silent on screen either way, and only the gate explains itself. A read-only source, a pipeline already running, or a workbook-backed tab writes its reason to the console. An empty list, a write missing its tab or field, and a record key that resolves to no row do nothing and say nothing.
StudioPalette — the members:
- 33 colour slots:
Canvas·Panel·Band·Chrome·Surface·Chip·Selection·PendingCell·Line·LineSoft·GridLine·LineHover·Text·TextMuted·TextFaint·RefText·OnAccent·Accent·AccentDim·Warning·Danger·Ok·SheetTone·CodeTone·EditedCell·NewRowCell·NewRowLine·DangerChip·DangerPanel·Scrim·Wire·WireDot·GridDot. IsDark.- Spacing:
SectionSpace·RowSpace·RuleHeight·ButtonHeight·PrimaryButtonHeight·GlyphWidth. - Type sizes:
HeadingFontSize·SectionFontSize·CaptionFontSize. FromRgb(uint)·ToHex(uint).
Push approval (SheetForge.Editor.Push)
| Type | Role |
|---|---|
IPushApprover | bool Approve(PushPlan, string humanSummary) · bool ApproveStructureRewrite(string, bool hasLiveConflicts) — reject = zero send |
AutoPushApprover | Always-approves (for tests/automation) |
Authoring engine (SheetForge.Editor.Structure / .Pipeline / .Export)
| Type | Role & key members |
|---|---|
AuthoringSession | Staging state owner (serializable — free Undo + reload survival). Edits · IsolatedEdits · NewRows · StructOps · Reorders · TabRenames · EnumMembers (staged enum-sheet member additions) · AssetRegistrations (staged Addressables registrations — project-level, so they take no part in the per-tab gates but do count for reflect entry, discard and the diff summary) · HasAssetRegistrations · StageAssetRegistration(r) (same guid, or same group for a group creation, replaces in place — the last intent wins; a registration without an identity is refused) · RemoveAssetRegistrationsWhere(predicate) · SetStaged · ResolveBaselineEdits · RemapFieldName/RecordId/Tab · StageTabRename · EffectiveStructOps · PendingStructCount · TabNames · TryGetBaselineTable · LastProjectionResult · ClearAll (clears the registrations too) |
AuthoringDispatcher | The reflect orchestrator. ctor (session, callbacks, baselines) · Reflect() · BuildProjectionResult() (side-effect-free projection query) · IReadOnlyDictionary<string,string> BuildProjectedTabs() (the same projection as TSV per tab — what a write-back target is about to send, previewable without writing) · void FinalizeReflectSuccess(IReadOnlyList<string> writtenTabs, IReadOnlyList<TabRenameEntry> committedRenames = null) (the ending a source's own write-back has to reach: retain-pruning for the tabs it wrote, the ClearUndo boundary, and the automatic re-import. The built-in paths run the same private body, so an external provider ends exactly the way they do. An empty list is a no-op that keeps staging intact) · Session · Callbacks · Baselines |
AuthoringDispatchCallbacks | 13 general view-concern delegates + IPushApprover — ResolveSettings · RenderReport (Action<ImportReport>, null-tolerated) · TriggerReimport · ConfirmKeyRenames · ConfirmTabRenames (null-tolerated) · ClearUndo · Rebuild · … Built-in Local/Google dialog delegates live in the opt-in BuiltInSourceDialogs bundle |
BuiltInSourceDialogs | Opt-in bundle of 14 built-in Local/Google source dialog delegates, separate from AuthoringDispatchCallbacks — external providers never need them. NotifyLocalDone takes five arguments; the last is the Addressables registration summary line for the completion dialog (null when nothing was staged) |
BaselineStore (.Export) | Per-tab normalized-TSV baseline snapshots |
Staging value types (SheetForge.Editor.Structure; StagedCellEdit/StagedNewRow are in SheetForge.Editor.Windows)
| Type | Role |
|---|---|
StagedCellEdit (struct) | One staged edit — TabName · RowOrdinal · FieldName · RawText · RecordId (logical key) |
StagedNewRow | One staged new row — TabName · FieldNames · CellTexts |
StructureOp | One structure op — Kind · coordinates · texts · Order permutation |
StructureOpKind (enum) | AddColumn · RemoveColumn · AddMarker · RemoveMarker · RemoveDataRow · ReorderColumns · ReorderDataRows · RenameColumn · EditColumnType · EditColumnDesc · SetColumnOverlap · SetSheetStyle |
TabReorderEntry | Per-tab reorder state — Tab · ColOrder · RowOrder |
TabRenameEntry (struct) | OldName · NewName |
StagedEnumMember (struct) | One staged "add this member to this enum" — TabName (which enum sheet; empty = search them all) · EnumName · Member. Session-level rather than a StructureOp, for the same reason a tab rename is: an enum sheet has no table, no schema and no key column, so a cell edit's (tab, record, field) address cannot name "the next member of this enum". Public only because AuthoringSession.EnumMembers is (CS0050) |
StagedAssetRegistration (struct) | One staged change to the project's Addressables settings, made by dropping or picking an asset into an AssetRef@Group cell — StagedAssetRegistrationKind Kind · Guid (the asset; a sub-asset stages its parent) · Group · FromGroup (moves only) · Address (the file name without extension for a new entry; an already-registered asset keeps its address) · AssetPath (for display). Factories Add(guid, group, address, assetPath) · Move(guid, fromGroup, group, address, assetPath) · CreateGroup(group). Executed after the sheet write succeeds, then cleared. Public only because AuthoringSession.AssetRegistrations is (CS0050), like StagedEnumMember |
StagedAssetRegistrationKind (enum) | Add · Move · CreateGroup |
TabBaselineAnchor (struct) | TabName · Fingerprint · RecordCount |
IsolatedEdit | A re-anchor-failed edit — Edit · Reason |
IsolationReason (enum) | External rename / external delete / key conflict |
Authoring helpers (SheetForge.Editor.Windows / .Structure)
| Type | Role |
|---|---|
KeyRenamePlanner (static) | Key rename + cross-tab propagation planning. Plan(…) · nested KeyRenamePlan · sibling struct KeyRename |
RecordIdMinter (static, pure) | Id suggestions. Suggest · DetectCommonPrefix · Uniquify · StagedNewRowKeys |
IntIdMinter (static, pure) | Next-IntId suggestion for a new record — Suggest(existingIds) → max + 1. A separate axis from RecordIdMinter, and it never re-uses a deleted gap |
ProjectionErrorMapper (static, pure) | Error coordinate → logical address. TryMap(…) · nested LogicalAddress |
EphemeralSoApply (static) | Staged-value SO overlay (temporary). Apply(…) · InvalidateIndex(…) · nested Report / SkipReason / SkippedEdit |
Runtime assembly (SheetForge.Runtime)
autoReferenced — usable from game code without an asmdef reference.
| Type | Role & key members |
|---|---|
SheetForgeDatabases (static) | The runtime loader — the sanctioned load path. const AddressPrefix = "SheetForge/" · AddressFor(tab) · LoadAsync(tab) · LoadAsync<TDatabase>(tab) · Release(handle) / Release<TDatabase>(db). The address helpers are plain strings and always compile. LoadAsync and Release exist only under SHEETFORGE_ADDRESSABLES, the version-define set when com.unity.addressables is installed — which is what lets the product compile without the package |
DefinitionDatabase (abstract SO) | Base of every generated per-tab Database. abstract TabName · abstract Count · virtual IReadOnlyList<object> RecordsUntyped · virtual InvalidateIndex(). RecordsUntyped is the sanctioned way to enumerate a baked tab without knowing its generated type. A second baker or an inspector that walks every tab used to have to reflect on the private records field, which made a field name into an undeclared contract that would break silently the day codegen renamed it. Treat the list as read-only (the sheet is canonical). It defaults to empty, so generated code from before this member existed still compiles and runs. A re-import emits the override |
RecordRef (struct) | The serialized reference value inside baked SOs (string id, resolved at lookup). Id · IsEmpty |
IntRef (struct) | The serialized integer-key reference value inside baked SOs — the twin of RecordRef for IntId@Tab fields. Because 0 is a valid id, a hasValue bit backs IsEmpty. Id · IsEmpty. Codegen emits an IntId@Tab field as IntRef, and TryGet(IntRef) on the generated Database consumes it |
LocRef (struct) | The serialized localization reference inside baked SOs — a LocRef@Tab cell. Table (the localization tab, which is the StringTable collection name) · Key · long KeyId (0 means "not resolved yet": an import bakes 0 and the bridge fills the real id in after a table sync, so a reference survives a key rename) · IsEmpty. It always compiles — generated code and baked assets never contain a localization-package type, which is what keeps the package optional |
LocRefExtensions (static) | One member: LocalizedString ToLocalizedString(this LocRef) — it points by KeyId when that is not 0 and by key name otherwise, and an empty reference converts to an empty LocalizedString. It exists only when com.unity.localization is installed, under the SHEETFORGE_LOCALIZATION version-define — the same arrangement SHEETFORGE_ADDRESSABLES uses for the Addressables layer |
SheetForgeRuntimeInfo (static) | const Version |
Generated types (pattern — per project, not shipped API)
For each tab Foo, codegen emits into your generatedNamespace:
public sealed partial class FooDefinition // one strongly-typed field per column; @desc → doc/tooltip
public sealed partial class FooDatabase : DefinitionDatabase
{
// TabName, Count, SchemaFingerprint, Records, RecordsUntyped override,
// lazy _byId/_byIntId lookups, InvalidateIndex override
}Load with SheetForgeDatabases.LoadAsync<FooDatabase>("Foo").
Both classes are emitted partial, so you can add derived members — a computed property, an interface implementation, an operator — in your own file next to the generated one, and a re-import will not overwrite it.
One boundary: add no serialized fields in your part. The baked ScriptableObject is rebuilt from the sheet on every import, so anything only your part serializes comes back at its default. If a value belongs to the data, it belongs in a column.
(The partial keyword does not touch SchemaFingerprint, which is computed from the schema alone, so making the classes partial did not invalidate a single existing bake.)
Other assemblies
-
SheetForge.Setup— the dependency-free Addressables-absence bootstrap. No public API (all internal; it exists to show a guidance window). -
SheetForge.PluginDemo(one merged asmdef + a Demo.Editor asmdef; content namespace remainsSheetForge.Skills) — the reference sample package, not product API. It holds:SkillsPlugin(seven plugin interfaces — base, validator, edge, template, graph, code registry, theme);Modifier+ModifierCellParser(custom cell type),ModifierStatEdgeContributor(edge contributor);ExamplePipelineAugmenter/ExampleReactiveAugmenter(canvas overrides),ExampleCodeAtoms(the_Refscode registry);ExampleStudioUi(declarative actions, panel, column badge and cell-editor hint),ExampleImportObserver(pipeline observer);ExampleStageStripWidget/ExampleInspectorAction/ExampleStudioPanel(Data Studio Editor extension points, painted from the public palette),ExampleLocStrings(registers those labels in two languages — in the main assembly, so the browser shows them too);- an assembly-level
SheetForgePluginCompatdeclaration; SkillRunner(consuming runtime), generatedExample*types in the defaultSheetForge.Generatednamespace (isolation is by theExample*prefix, not a separate namespace).
The plugin-free
SheetForge.CoreDemosample ships with zero asmdefs (compiles intoAssembly-CSharp).
Related pages
- Plugin Authoring — the contracts in use, with full examples
- Authoring Kernel — the engine types in context
- Capabilities & Limits — behavioral boundaries of these APIs