Reading a step
The step handle, the resolved field, and the two function libraries that read them — the whole read surface, in one page.
All documentation — Playing a story
Start here
Concepts
Authoring for a build
Playing a story
The generated assets and code
The plugin and sync
On this page
A step is a FTaleShardPlayStep. There is no Text pin on it and no Speaker: a project’s node
types are authored by its own user, so there is no fixed set of keys to pre-extract — the reasoning
is on node meaning is resolved once. A step carries resolved
fields, and your game decides what they mean.
#include "TaleShardFieldLibrary.h"#include "TaleShardStepLibrary.h"#include "TaleShardStepTypes.h"
namespace TaleShardDocSamples{ // Describes a step as text, one line per presented field. TArray<FString> DescribeStep(const FTaleShardPlayStep& Step) { TArray<FString> Lines;
// TypeCode is a dispatch key: it picks a card - a widget, a layout - or the handler that // owns this type's logic. What it must never do is decide what a field MEANS, which is // the decision the resolved payload has already made. Lines.Add(FString::Printf(TEXT("[%s] %s"), *Step.TypeCode, *UEnum::GetValueAsString(Step.Verb)));
for (const FTaleShardResolvedField& Field : UTaleShardStepLibrary::GetPresented(Step)) { // Kind is a closed enum, not a string. switch (Field.Kind) { case ETaleShardResolvedFieldKind::RichText: { // Rendered at sync time. There is no document model downstream. Lines.Add(Field.Label + TEXT(": ") + UTaleShardFieldLibrary::GetText(Field).ToString()); break; } case ETaleShardResolvedFieldKind::Entity: { // Already looked up. False when the reference points at nothing. FTaleShardResolvedEntity Who; if (UTaleShardFieldLibrary::GetEntity(Field, Who)) { Lines.Add(Field.Label + TEXT(": ") + Who.Name); } break; } case ETaleShardResolvedFieldKind::Asset: { // Get Media List is right for one reference and for five. Get Media takes the // first, which is correct for one picture and wrong for five. Url is where the // bytes are, never an asset path; Id is the id. for (const FTaleShardMediaRef& Media : UTaleShardFieldLibrary::GetMediaList(Field)) { Lines.Add(Field.Label + TEXT(": ") + (Media.bHasUrl ? Media.Url : Media.Id)); } break; } default: { // An unknown Kind is not an error: a newer producer can legitimately reach an // older plugin, and RawType still names the type. const FText Text = UTaleShardFieldLibrary::GetText(Field); Lines.Add(FString::Printf(TEXT("%s (%s): %s"), *Field.Label, *Field.RawType, Text.IsEmpty() ? TEXT("-") : *Text.ToString())); break; } } }
return Lines; }}In Blueprint the same reads are nodes: Get Presented on the step, a switch on the field’s Kind, and the field library’s typed reads.
What a step carries
The step is a handle. Its scalars are pins; everything with a shape of its own is read through the TaleShard Step function library, so a Blueprint never copies a list it does not need.
| Pin | What it holds |
|---|---|
| Node | the node’s id, its board, its stored TypeId, its TypeCode, and its TypeTag — TaleShard.NodeType.<Code>. A dispatch key: a card, or this type’s handler. Never what a field means |
| Verb | what kind of moment this is, derived from structure. Choose means options are waiting; Present means a line to read |
| Arrived By | how the player got here, which is a different question from the verb: Start, Jump and Return are arrivals, Walk is not. A transcript that starts a new page at each arrival reads this |
| Choice Kind | Exit for the ways out of the node on screen, Board Entry when the path ended and these are the board’s remaining ways in |
| Awaiting | what the engine wants from the player right now: nothing, an advance, a choice, or a timer — for which Lapse is the only answer |
| Flags | Is Latest, Is Terminal, and the same Awaiting |
| Type Code | the node type’s code, as the editor shows it |
| Read | What it gives |
|---|---|
| Get Presented | the fields a player reads, in authored order. Use this to draw |
| Get Fields | every field of the node, presentable or not, in document order |
| Get Choices | the exits offered to the player, already gate-filtered. Empty unless the verb is Choose |
| Get Cast | the actors on stage, derived from the entity-typed fields. There is no speaker on a step; the first entity field’s actor is marked |
| Get Audio | what this step plays rather than draws, per channel. See Speaking the line |
| Get Variables | a snapshot of every variable as it was when the step was drawn, keyed by qualified code. A copy: a past step keeps its own values |
| Get Trace | why this step happened and what was skipped. For diagnostics |
| Find Field | a field by its code, as a TaleShard.Field.<Code> tag |
| Find Field By Key | a field by its exact storage key — what an actor’s Via Field Key and a diagnostic carry |
| Get Blocks | the fields of a repeater, one block per row in index order — the answer Find Field cannot give. Each block carries its row’s Instance Id |
| Get Blocks Within | the rows of a repeater declared inside another repeater’s block, for one outer row — the row whose Instance Id you pass |
| Find Block Field | a field of one block by its TaleShard.Field.<Code> tag — Find Field for a single row, which is what a generated lane accessor’s tag is for |
| Get Plain Text | every presented field’s plain text, joined by newlines. Marks are lost |
Get Presented already accounts for a node type that declares itself silent, and for a field the author marked as not presentable. That is the “is there anything to show” rule, applied once — do not invent a second one.
What a resolved field carries
| Pin | Meaning |
|---|---|
| Code, Label, Tag | the author’s field code, its human label, and TaleShard.Field.<Code> |
| Key | the exact storage key, including a repeater instance’s |
| Kind | the field type, as a closed enum. Unknown is not an error |
| Raw Type | the wire string, kept so a type this plugin does not know is still nameable |
| Storage | which store the value came from |
| Presentable | whether a player reads this field |
| Localized | false means this value is a fallback, not the active language |
| Is Repeated, Repeater Code, Repeater Index | set for a field inside a repeater block |
| Is Ported | true for a field inside a repeater whose block carries a source handle: its rows are the node’s exits, each row one of the step’s choices — read the row as a choice, not as content |
| Capacity, Columns | an itemCounts field’s inventory grid, as its declaration states it: how many cells it has, and how many cells wide it is. -1 when the field is unbounded or the grid has no fixed width, and on every other kind of field. Cells count row by row, so an entry from Get Items sits at row Slot / Columns, column Slot % Columns |
The field library
A read naming a shape checks only the value’s shape. One naming a vocabulary or a reference checks the kind as well. None of them fails loudly: a read whose question does not apply answers false, and every list is empty rather than absent.
| Read | Answers exactly when |
|---|---|
| Get Text | rich text, else a text value. Not kind-checked, because an author may present a select as a line. A number is not text |
| Get Rich Html | the field has rendered rich text. Feed it to a presenter; do not draw it raw |
| Get Number | the value is a number. A numeric string is not parsed — text-to-number is the one coercion that lies |
| Get Bool | the value is a bool. Not "true", not 1 |
| Get Vector | a three-member all-number list |
| Get Point | a two-member all-number list — a position on a picture, as a 2D vector whose X and Y are each 0..1 of the picture’s box from its top-left corner |
| Get Rect | a four-member all-number list — an area on a picture, as a 2D box whose Min and Max are its corners in the same space |
| Get Time | a string that reads as a time of day, HH:MM — as a Time Of Day struct (Hour, Minute); the time-of-day library answers its Total Minutes and its HH:MM spelling. Midnight, and false, for anything else |
| Get List | the members. A scalar reads as a one-element list, absence as empty |
| Get Option, Get Options, Has Option | the field is a select. Kind-checked, so a free string from a text field cannot silently match an option |
| Get Source | the field is a code or condition field. Never Get Text — a gate’s source is not a line of dialogue |
| Get Items | the field is an itemCounts field — entity items with a count each. Its referenced entities zipped with the counts in its value, position for position, as Item Count entries (Item, Count, and Slot — the cell of the inventory grid the entry sits in, or -1 when it has none) in the author’s order, duplicates kept. Kind-checked; empty, never absent. The grid those cells count over is the field’s own Capacity and Columns pins |
| Get Entity, Get Entities, Get Media, Get Media List, Get Tag, Get Node Target, Get Board Target, Get Command | the matching reference kind is present. Each returns its own narrow struct, so one kind of reference cannot be asked for another kind’s members |
| Get Variable | the variable a field NAMES — which spawn point, which flag. Code is the name the runtime is keyed by, Context Code its context’s (with Has Context beside it), and Name the two joined with a dot, as an author writes it in a condition. False when the field names no variable, or names one the project no longer has. On a field declared as a LIST of variable links it answers only the first; read Get Variable List there. A field’s type may also let the author TYPE a name instead of picking one; a typed name arrives as plain text with NO reference at all, so on such a field read Get Variable → Name, and fall back to Get Text when Get Variable is false |
| Get Variable List | every variable a field declared as a LIST of variable links names — a keep list, a watch list — as Variable Refs in the author’s order. Empty, never absent. A variable the project no longer has is left out, and a list never holds a typed name |
| Get Line Id | the field is a translatable line: board:node:key, never a hash of the text |
| Is Non Empty Text | the “is there anything to show” rule for one field |
Prefer the plural reads. Get Entities, Get Media List and Get Variable List are correct for one reference and for five; the singular ones take the first, which is right for one portrait and wrong for five.
A media reference carries Id, Url with Has Url, and Focal with Has Focal. Focal
is where the picture’s face is — a 2D vector whose X and Y are each a fraction 0..1 of the
picture’s box from its top-left corner, marked once per picture in the editor’s media tab. Read
Has Focal first: when it is false, no face is marked and Focal is zero. A widget that crops the
picture (a portrait frame, a banner) crops toward Focal, and about the centre when Has Focal is
false. The same picture answers the same point wherever it is referenced: a node’s asset field, a
character’s $portrait, an entity’s own asset field.
A command reference carries Code, Tag, Args and Arg Variables. An argument the author bound to a project variable holds that variable’s value as of the moment the step was composed, and Arg Variables names the variable at the same position (empty for every other argument). To fire the call later with the values the variables hold then, see firing a call a field holds.
Naming a field without a magic string
Find Field takes a gameplay tag, and the sync registers one per field code under
TaleShard.Field. In Blueprint that is the tag picker: rename speaker in the editor, sync, and
the tag the node held is gone from the tree — a visible break instead of a silent miss in a shipped
build. In C++ the same tag is requested by name, as the samples on this site do.
A repeater’s fields are read through Get Blocks, one block per row, which is what tells two similarly-named fields apart: a node’s own condition field and a per-row condition field inside a repeater live in different places.
A row can hold a list of its own — a repeater declared inside another repeater’s block, one level. Get Blocks on the inner list’s tag answers every outer row’s inner rows, outer row by outer row; Get Blocks Within, with the outer block’s Instance Id, answers that one row’s own. The generated block struct of the outer list carries them already, as a member named after the inner list. An inner row is content, never a choice.
Speaking the line
Get Audio is what this step plays rather than draws, one entry per channel. The engine fills two:
sfx, one per field of the node holding a media reference whose file is a sound (a picture
or video on the node is never here), and voice, one per line that has a
voice-over recording in the language you are playing in. Switch on Channel, never on the node
type — audio is a channel, not a node type — and give the switch a default, because Channel is a
string so that a later channel breaks no reader.
Play Step Voice does the whole job in one node: it finds the spoken line, resolves it through the media index the sync wrote, plays it, and stops whatever the previous step was saying — so advancing early cuts the line off, which is what a reader expects. Is Speaking is how a game that advances on a timer waits for the line; it does not disable the advance affordance, because a player who wants to move on may. Stop Voice silences it.
The plugin’s dialogue screen does exactly that: it calls Play Step Voice for every step it draws, with no second event and nothing asking what kind of node is playing. The TaleShard Story actor hands it the Media Index the sync wrote — filled in for you when the project has one — and the lines are spoken; with no media index the story plays in silence rather than refusing, and Play Voices on the screen switches it off.
#include "TaleShardStepLibrary.h"#include "TaleShardStepTypes.h"#include "TaleShardVoiceLibrary.h"
namespace TaleShardDocSamples{ // The clip this step speaks, or false when it says nothing aloud - which is every step in a // project with no voice-over. bool FindSpokenLine(const FTaleShardPlayStep& Step, FTaleShardResolvedAudio& OutSpoken) { for (const FTaleShardResolvedAudio& Audio : UTaleShardStepLibrary::GetAudio(Step)) { // Switch on the channel, never on the node type. Audio is a channel: sfx is a sound // a node's fields hold, voice is the recorded line. A channel is a string so a later // one breaks no reader, so anything else is skipped rather than guessed at. if (Audio.Channel != TEXT("voice")) continue;
// The first one. Two voice entries on one step are two lines and belong one after the // other; queueing them is your game's pacing decision. OutSpoken = Audio; return true; } return false; }
// Play it, or say why not. Audio never blocks a story. // // Play Step Voice does the whole job - it resolves the media id through the index, plays the // clip, and stops whatever the previous step was saying. This is what to write when you want // the parts yourself: a mixer of your own, a mute list, a subtitle keyed by line. void SpeakStep( UObject* WorldContextObject, const UTaleShardMediaIndex* MediaIndex, const FTaleShardPlayStep& Step, const TSet<FString>& MutedVoices) { FTaleShardResolvedAudio Spoken; if (!FindSpokenLine(Step, Spoken)) return;
// Voice Code is the speaking character's voice, for a per-character mute. It is absent // when they are uncast, and that is not a reason to withhold the clip: a take exists, so // it plays. if (Spoken.bHasVoiceCode && MutedVoices.Contains(Spoken.VoiceCode)) return;
// Line Id is the same identity a translation and a recording script use, so a caption or // a replay keys off it rather than off the field's position. UTaleShardVoiceLibrary::PlayStepVoice(WorldContextObject, MediaIndex, Step); }}A voice entry carries two things an sfx entry does not. Line Id is the line the clip belongs to
— the same identity Get Line Id gives a field, and the one a translation and a recording script use
— so a caption or a replay keys off it rather than off the field’s position. Voice Code is the
speaking character’s voice, which is what a per-character mute keys off; it is absent when they are
uncast, and that is not a reason to withhold the clip.
The clip is addressed by Media Id and resolves exactly as a portrait does. Step Media Plan already includes it, and Board Media Plan includes a board’s clips when you pass the language you are playing in.
Trace
A step carries a trace — which scripts ran, which gates were evaluated, and how many branches were discarded as unsupported. It is deliberately not documented in detail here: the trace’s gate records do not currently mean the same thing in this runtime as they do in the editor’s play mode, so a page describing them would be describing a divergence rather than a contract. Use those for debugging, not for logic, until that is closed.
The script runs are a contract, and one part of them is read for logic: each run’s Rolls lists
every draw its script made — a roll(20) that came up 14, with its kind, its arguments and what
came up — which is what a game that shows dice reads. A step’s runs include those of every silent
node walked through on the way to it, before its own; each run’s Node Id says which node it
belongs to.
The other part read for logic is Received: what the exits taken on the way to this step handed
over — every entry of every itemCounts field on each fired exit’s row, in walk order, each an
Item Count (the item, resolved like any entity when the step is composed — after the exit’s
scripts and this node’s own have run, like the cast — and its count). A Loot roll’s drawn outcome reaches
the player here, on the step after the roll, and only that outcome’s entries do; the plugin keeps
no inventory and merges nothing, so an item written twice arrives twice. Empty on most steps. A
language change redraws a step with the same entries; a restored save lists none. The items are
never on a step’s Cast: they are the things a row hands over, not who is on stage.