Skip to content

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
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.

ReadAStep.cpp
#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.

PinWhat it holds
Nodethe 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
Verbwhat kind of moment this is, derived from structure. Choose means options are waiting; Present means a line to read
Arrived Byhow 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 KindExit for the ways out of the node on screen, Board Entry when the path ended and these are the board’s remaining ways in
Awaitingwhat the engine wants from the player right now: nothing, an advance, a choice, or a timer — for which Lapse is the only answer
FlagsIs Latest, Is Terminal, and the same Awaiting
Type Codethe node type’s code, as the editor shows it
ReadWhat it gives
Get Presentedthe fields a player reads, in authored order. Use this to draw
Get Fieldsevery field of the node, presentable or not, in document order
Get Choicesthe exits offered to the player, already gate-filtered. Empty unless the verb is Choose
Get Castthe 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 Audiowhat this step plays rather than draws, per channel. See Speaking the line
Get Variablesa 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 Tracewhy this step happened and what was skipped. For diagnostics
Find Fielda field by its code, as a TaleShard.Field.<Code> tag
Find Field By Keya field by its exact storage key — what an actor’s Via Field Key and a diagnostic carry
Get Blocksthe 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 Withinthe rows of a repeater declared inside another repeater’s block, for one outer row — the row whose Instance Id you pass
Find Block Fielda 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 Textevery 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

PinMeaning
Code, Label, Tagthe author’s field code, its human label, and TaleShard.Field.<Code>
Keythe exact storage key, including a repeater instance’s
Kindthe field type, as a closed enum. Unknown is not an error
Raw Typethe wire string, kept so a type this plugin does not know is still nameable
Storagewhich store the value came from
Presentablewhether a player reads this field
Localizedfalse means this value is a fallback, not the active language
Is Repeated, Repeater Code, Repeater Indexset for a field inside a repeater block
Is Portedtrue 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, Columnsan 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.

ReadAnswers exactly when
Get Textrich 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 Htmlthe field has rendered rich text. Feed it to a presenter; do not draw it raw
Get Numberthe value is a number. A numeric string is not parsed — text-to-number is the one coercion that lies
Get Boolthe value is a bool. Not "true", not 1
Get Vectora three-member all-number list
Get Pointa 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 Recta 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 Timea 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 Listthe members. A scalar reads as a one-element list, absence as empty
Get Option, Get Options, Has Optionthe field is a select. Kind-checked, so a free string from a text field cannot silently match an option
Get Sourcethe field is a code or condition field. Never Get Text — a gate’s source is not a line of dialogue
Get Itemsthe 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 Commandthe 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 Variablethe 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 Listevery 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 Idthe field is a translatable line: board:node:key, never a hash of the text
Is Non Empty Textthe “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.

SpokenLine.cpp
#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.

Next

Choices, and why one is locked.

Written for
Unity developers
Last updated