Skip to content

The typed C++ layer

What a schema change generates into your game module — native tags, enums, entity structs, row structs, the command class, field accessors, variables.

All documentation — The generated assets and code
On this page

A sync whose schema changed can emit C++ into your project’s own game module, under Source/<YourModule>/TaleShardGenerated/, for your C++ code to use. A content change never reaches this layer. Everything here is a view over the data layer: a generated accessor reads the index, the store or the session, and holds no storage of its own.

When it generates

Generate C++ Types, in Project Settings > Plugins > TaleShard and in the sync window, takes three values:

ValueGenerates
Automatic — the defaultwhen the project has a C++ game module, and not otherwise. A Blueprint-only project syncs with no C++ and no message about it
Onalways. In a project with no C++ module nothing is written, because files with nothing to compile them would break the build of a module added later; the sync window says so (CPP034) and how to add one: Tools > New C++ Class
Offnever. Everything below is still reachable by tag and by key through the plugin’s own libraries; this layer is the typed spelling of the same data

The generated code needs the module’s build rules to list GameplayTags, TaleShardCore and TaleShardRuntime. The sync adds whichever are missing to your module’s Build.cs itself, at its PublicDependencyModuleNames.AddRange(new string[] { ... }) line, before it writes the generated files, and says so in the sync window (CPP036). The next build compiles them in. When there is no such line to add to (CPP030), or the file cannot be written (CPP037), no C++ is written and the message gives the exact line to add by hand. The story content has synced either way: the sync window reads Synced — C++ not generated, with the reason beneath.

Names

Every generated name is the author’s code, minted into a C++ identifier by one set of rules:

RuleExample
PascalCase, split on punctuation and camel boundariesloot_count → LootCount
a character outside ASCII is dropped, never transliteratedcafé → Caf
a leading digit gets N3d model → N3dModel
a C++ keyword gets Valueclass → ClassValue
a name a base class already holds gets Valuea field name on a node type → GetNameValue, because GetName is UObject’s
a second code minting to the same name gets an ordinal, in order of stable idLootCount, LootCount2
a code with nothing left is named from its id

Every change is reported in the sync’s diagnostics, so a member nobody can guess the name of is at least a member somebody can look up.

The files

FileHolds
TaleShardGenerated.h/.cppnative gameplay tags: TaleShardTags::Field_<Code>, Command_<Code>, Variable_<Context>_<Code>, Enum_<Enum>_<Option> and the rest — compile-checked symbols for the same tags the ini registers
TaleShardEnums.hone ETaleShard<Enum> : uint8 per reusable option set, with the authored values, plus a FTaleShard<Enum>Options helper that parses and prints option codes
TaleShard<Definition>.h/.cppone FTaleShard<Definition> struct per entity definition, and a UTaleShard<Definition>Library that fills it from the entity index
TaleShard<Definition>Scripts.h/.cppfor a definition with a code or condition field, of its own or on a trait it applies, a UTaleShard<Definition>Scripts library with one typed call per such field
TaleShard<Sheet>Row.hone FTaleShard<Sheet>Row : FTableRowBase per typed spreadsheet, and the data table re-typed to it. A free-form sheet has no column contract, so it gets no row struct and no data table; its cells reach the game through its table store
TaleShardProjectCommands.h/.cppthe command handler class with one typed event per project command
TaleShard<NodeType>Fields.h/.cppone function library per node type, with a typed accessor per field
TaleShardVariablesLibrary.h/.cppa typed getter and setter per project variable
TaleShardGeneratedHelpers.hthe conversions the generated code shares
TaleShardGenerated.manifest.jsonwhat was emitted, by source id, with the schema fingerprint and the plugin version

Never hand-edit a generated file. The next schema change overwrites the folder as a whole.

Include a generated header by its folder, from anywhere in the module:

#include "TaleShardGenerated/TaleShardCharacter.h"

Enums

A reusable option set with 255 options or fewer becomes a UENUM, with each entry’s authored value. A bitmask set carries the Bitflags metadata. Beyond 255 the enum is not emitted and reported; the tag form stays.

An option set is what a variable or an entity field of that type becomes. A node field always reads as the stored option string, which is why the node accessors below return the enum by parsing it through the Options helper. Its Try Parse answers false for an option the project no longer declares; the plain Parse the accessors use answers the enum’s first entry for one.

Entity structs and libraries

FTaleShardCharacter carries one member per field the definition declares or inherits, typed from the field’s kind: text and rich text as FString, a number as double, a boolean as bool, a colour as FLinearColor, a vector as FVector, a time of day as FTaleShardTimeOfDay (Hour and Minute, parsed from the stored HH:MM), a select as its enum (or an array of it), an entity reference as a FGameplayTag (or a tag container), a node, board or tag reference as an FName. A trait’s fields arrive under the trait’s own key path, so two applications of one trait keep their values apart, and each is named from that path: a trait applied as stats gives its hp field the member StatsHp, beside the definition’s own Hp.

A formula field is generated as a BlueprintReadOnly member. Its value is worked out from the entity’s other fields when the project syncs, and the session refuses a write to it: Set Entity Field Value on a formula field fails with '<entity>.<field>' is a formula field. Write the fields it is computed from instead.

UTaleShardCharacterLibrary:

Function
GetCharacter(Index, Code, OutEntity, Result)by code tag, answering Found or Not Found
FindCharacter(Index, Code, OutEntity)the same as a boolean
FindCharacter(Session, Code, OutEntity)the same, through the session’s entity index
GetCharacterById(Index, EntityId, OutEntity)by id: what a payload reference and a mention carry
GetCharacterById(Session, EntityId, OutEntity)the same, through the session’s entity index
GetCharacterFromRow(Index, Row)from a row the entity index already handed you
SpawnCharacter(Session, Template, OutInstance, OutReason)a copy of a Character on the session, which changes on its own — see below

Every read fills the struct from a store row; the struct is a copy. A value the story changed at play time lives in the session’s overlay, which the session’s Get Entity reads — this library is the authored data.

No entity index has to be wired in. The payload asset names the entity index the same sync generated, and the session’s GetEntityIndex() loads it, so the overloads that take a session need nothing else. They answer false when the session is null or its payload names no index — a session built from a sync output directory, or a payload from a sync that generated no assets. The overloads that take an index are for code that holds one of its own.

FTaleShardCharacter Kim;
if (UTaleShardCharacterLibrary::GetCharacterById(Session, KimId, Kim))
{
UE_LOG(LogTemp, Display, TEXT("%s has %f hit points"), *Kim.Label, Kim.Hp);
}

SpawnCharacter takes the session and a Character from the reads above, spawns a copy of it, and answers the struct again carrying the copy’s id. The members stay the template’s authored values; the copy’s own are read through the session by that id. What a copy is and how it is saved is Entity scripts and instances.

Entity scripts

A definition with a code or condition field gets UTaleShard<Definition>Scripts, holding one call per such field: Run <Field> for a script, Check <Field> for a gate. A field a trait brings counts too, a sub-trait’s and an inherited trait’s included, and is named from its whole path, as its member is: a trait applied as stats gives its onHit script the call RunStatsOnHit, which runs stats.onHit. Each takes the session, the entity — the definition’s own struct, a spawned copy’s included — and one parameter per role the field declares, typed by the struct of the role’s definition:

bool UTaleShardItemScripts::RunOnUse(UTaleShardSession* Session, const FTaleShardItem& Item,
const FTaleShardCombatant& Target, const FTaleShardCombatant& User, FText& OutReason);
bool UTaleShardItemScripts::CheckUsable(UTaleShardSession* Session, const FTaleShardItem& Item,
const FTaleShardCombatant& Target, FText& OutReason);

Each passes its structs’ ids to the session’s Execute Entity Script or Evaluate Entity Condition, so everything Entity scripts and instances says about those holds here. The point is the signature: a role the writer renames or removes changes the call, and your game stops compiling where it passes the old one rather than refusing at play time. A role whose definition has no generated struct takes an FString, an entity’s code or id. The calls are on a library of their own because their parameters are other definitions’ structs: two definitions whose scripts name each other would otherwise include each other’s headers.

Commands

UTaleShardProjectCommands derives the plugin’s TaleShardCommandHandler and adds one event per command:

Declared asGenerated as
ordinaryOn<Command>(typed parameters) — a native event: override On<Command>_Implementation
awaitedOn<Command>Async(typed parameters, Completion) — the story waits until you settle the completion

Derive it in C++, override the events your game answers, and set your class as the Command Handler Class on the TaleShard Player component or the TaleShard Story actor. Its Register Commands is generated: every command is registered already, each through a private thunk that reads the story’s arguments into the typed parameters, so a story calling playsound("alert") reaches OnPlaysound with a string and no wiring from you.

The events are not offered in Blueprint. A Blueprint answers a command with a TaleShard Command event instead — implementing commands — and one placed in a Blueprint derived from this class takes that command over from the generated event.

The parameter list is the command’s fields, in the author’s order, mapped through the same kind table as everything else; an argument the story omitted arrives as its type’s default.

Node field accessors

UTaleShardDialogueFields, one per node type, is the typed read path for C++ code:

Function
Is Dialoguewhether a step is of this type
Get Text, Get Speaker, Get Mood, …one per field, typed: text as FText, a number as double, a boolean as bool, a time of day as FTaleShardTimeOfDay, an entity as the step’s resolved entity, media as a media reference, a select as its enum, a list as an array
Get Where Picture Fieldfor a position or area field: the tag of the picture field it is placed on, so a handler finds the picture without spelling its code
Get Track Length Fieldthe tag of the node-level number field that ends the type’s track; only on a type that names one
Get Replies Blocksone per repeater: the rows as an array of FTaleShardDialogueRepliesBlock structs, each carrying the repeater’s fields
Get Cues Start Field · Get Cues Duration Fieldper repeater that places rows in time: the tags of the block’s start and duration fields; a duration never appears without a start
Get Lines Pause Fieldper repeater drawn as a conversation (bubbles or screenplay): the tag of the number field a conversation waits after each row

Every accessor takes the step and reads the field by code through the plugin’s Find Field, so a field the step does not carry reads as its type’s default. A Blueprint reads the same fields with Get Node Field.

Variables

UTaleShardVariablesLibrary carries Get<Variable>(Session) and Set<Variable>(Session, Value, Reason) per project variable, typed — a number as double, a select as its enum, a list as an array of text. A scoped variable carries its context in the name: GetCombatHp. Every call reads or writes the session, so a save captures it and a gate sees it. A Blueprint uses Get Story Variable and Set Story Variable. Two variables whose codes differ only in case get two accessors, the second numbered: GetGold and GetGold2, each with its own tag (see the data layer). The accessor’s number and the tag’s come from two different naming rules, so they need not agree: in a project holding gold, Gold and Gold2 where Gold2’s id sorts first, Gold2 takes GetGold2 and Gold’s accessor becomes GetGold3, while Gold’s tag is still Gold_2. Do not pair an accessor with a tag by its number.

What a change does to your build

After emission the sync reports what changed — types added, changed and removed, by name — and leaves you in your normal C++ workflow: Live Coding, or a rebuild. It never drives a build.

Two rules worth knowing before the first schema change:

  • A renamed code is a renamed type. The report says FTaleShardGuard -> FTaleShardWarden and prints the CoreRedirects line for DefaultEngine.ini that keeps existing assets loading; it never writes that file itself.
  • A changed enum needs an editor restart. A regenerated UENUM can corrupt Blueprints that reference it until the editor restarts; the report says so when the delta touched one.

Nothing is written unless the whole set validates. Files go to a scratch folder first, the destination is replaced as a whole, and the manifest is written last — so a run that fails part way leaves either the previous generation or nothing, never a mix.

Written for
Unity developers · Advanced integrators
Last updated