The generator pipeline
ICodeGenerator, the twelve domain generators and the order they run in, and the four ways to change what a sync writes without forking the plugin.
All documentation — Extending
Start here
Concepts
Authoring for a game
Playing a story
The generated code
The plugin and sync
On this page
A sync builds one ProjectModel and then runs a list of generators over it. Each returns files; the
pipeline writes them. That list is the seam — you can skip an entry, replace one, append your
own, or post-process everything.
ICodeGenerator
| Member | |
|---|---|
string Id | Unique, and how a config names you for a skip or a replacement. Typically the class name. |
List<GeneratedFile> Generate(ProjectModel model, GeneratorOptions options, List<Diagnostic> diagnostics) |
diagnostics is a mutable list you append to — it is not a channel and there is no callback.
Append rather than throw: a generator that throws takes the whole sync with it, and one bad enum is
not worth a failed sync.
GeneratedFile
| Member | |
|---|---|
RelativePath | Within the output directory — Enums/Gender.cs, Data/Character/RynZhao.json. Forward slashes, on every platform. |
Content | |
IsJson | Whether the writer applies code formatting |
GeneratorOptions
| Member | Default | |
|---|---|---|
Namespace | TaleShard.Generated | Generators append their own sub-namespace — .Enums, .Entities |
OutputPath | ./output | Each generator makes its own subdirectory under it |
MediaOutputPath | Media | Relative to the output. The editor’s folder hierarchy is preserved inside it. |
Diagnostic
Severity · Code · Message, with Info, Warning and Error factories and a formatted
ToString.
DiagnosticSeverity is Info · Warning · Error — the last meaning generation cannot be
correct.
Give a code, not just a message — ENUM001 is the shape. It is what makes a diagnostic
matchable rather than merely readable, and it is the same argument as the runtime’s own
diagnostic codes.
The twelve domains, in order
Order is dependency order, not preference: several read model properties an earlier domain populated. A generator you insert has to respect it.
| # | Id | Produces |
|---|---|---|
| 1 | TagDomain | Tags |
| 2 | EnumDomain | One C# file per enum, plus the enum registry |
| 3 | MediaDomain | The media constants class and the lookup index |
| 4 | EntityDomain | The entity base, per-definition classes, per-item JSON, the loader, the store and the project database |
| 5 | TraitDomain | One class per unique trait code |
| 6 | TableDomain | A typed row class, per-table JSON and a typed loader |
| 7 | CommandDomain | The command handler partial and the dispatcher implementation |
| 8 | VariableDomain | The variables class and its store |
| 9 | NodeTypeDomain | One typed class per node type the project declares |
| 10 | BoardDomain | The board registry and the boards class |
| 11 | LocalizationDomain | Per-language JSON, the localization registry, per-language UI strings, and project settings |
| 12 | ScaffoldDomain | The session, bootstrapper, behaviours and data facade — plus the copy of runtime.json the game plays from |
Three of those are worth a sentence more:
TriggerDomain does not exist. Triggers are options on a reusable enum, so EnumDomain emits
them as a C# enum plus a registry entry.
NodeTypeDomain is why game code never matches on a type name. A user-authored node type becomes
a real class with one member per declared field and a list per repeater.
TableDomain evaluates nothing. The sync server materializes spreadsheet cells — computed
values, with entity and asset references already resolved — so no formula evaluation and no
id-to-code resolution happen at generation time.
BoardDomain reads the board tree — names, folders, order — from codegen.json and board
content from runtime.json. Two files, two purposes.
The three toggles
The sync window has three checkboxes, and they are the ordinary way to turn generators off — no code required. Each owns a set of generators, and turning one off adds their ids to the skip list.
| Toggle | Owns | Turn it off when |
|---|---|---|
| Data structures | The ten that produce typed C# for project content — enums, entities, traits, variables, commands, tags, media, localization, node types, tables | You only want the data files |
| Runtime integration | BoardDomain — board loading and the registries that wire data in | Never, for a project using the dialogue runtime |
| Unity bootstrap | ScaffoldDomain — the scene-level scaffolding | You have your own game initialization |
Unity bootstrap is the one integrators actually turn off. It is the only layer whose output is opinionated about how your game starts; the other two produce the code everything else is written against.
Changing the list
GeneratorPipelineConfig is the whole surface, and the four members do not overlap.
| Member | |
|---|---|
IGeneratorProfile? Profile | The base set. Null means the default profile. |
HashSet<string> SkipGenerators | Ids to exclude |
Dictionary<string, ICodeGenerator> ReplacementGenerators | By id. A replacement keeps the original’s position. |
List<ICodeGenerator> AdditionalGenerators | Appended after the profile’s |
List<Func<List<GeneratedFile>, List<GeneratedFile>>> PostProcessors | Run in order, after every generator |
A post-processor receives the full list of generated files and returns a list. That is the place to stamp a header, drop a file, or rewrite a path across everything at once.
IGeneratorProfile
A named, ordered set of generators: TargetEngine and GetGenerators().
The seam is live — the pipeline runs whatever GetGenerators returns, and the sync service builds a
config around it.
IOutputWriter
void WriteFiles(List<GeneratedFile> files, string outputPath) — where generated files land.
Replaceable for a build that writes somewhere other than the file system.
See also
Post-sync hooks run after the whole sync, on the finished output. Reach for a hook when you want to react to a sync, and for this pipeline when you want to change what it produces.