Skip to content

What a step contains

ResolvedField and its typed accessors, choices and why they are blocked, entities, actors, and the five reference value types.

All documentation — Reference
On this page

A PlayStep hands you fields, choices, a cast and some audio. This page is what each of those is.

ResolvedField

TaleShard.Runtime.Engine.ResolvedField — sealed.

One field of a node, or of an entity, resolved against the current state.

What it tells you about itself

MemberTypeNotes
KindFieldKindWhat this field holds. Switch on this, not on RawType.
CodestringThe author-facing code
Labelstring
KeystringThe exact storage key. A host does not normally read one.
PresentableboolIs this a field a player reads?
NonEmptyTextboolDoes it actually carry something to read?
LocalizedboolTrue when the value came from the active language; false when it is a fallback
RawTypestringThe field type exactly as it arrived, so an unknown kind is still nameable
StorageFieldStorageWhere the value lives in the stored document
ReferencesIReadOnlyList<ResolvedRef>Every reference this field holds. Never null.
ExpressionIdstring?Pass it to IScriptRuntime.EvaluateCondition to evaluate this field as a gate
RichRichRender?
LineLineId?
RepeaterEngineRepeaterRef?

The typed accessors

None of these throws. A scalar accessor returns null on the wrong shape; a list accessor returns empty. They do not coerce.

MethodReturnsNotes
Text()string?The rich text if any, else a string value. A number reads as null.
Number()double?A numeric string is not parsed — see below
Bool()bool?Neither "true" nor 1 counts
Vector()Vec3?A three-member all-number list
Point()Point2?A two-member all-number list: a position on a picture
Rect()Rect2?A four-member all-number list: an area on a picture
Date()DateTimeOffset?The instant a date field stores, in UTC; a date with no time is that day’s UTC midnight. Kind-checked.
Time()TimeOfDay?A time field’s "HH:MM". Kind-checked; a value that does not parse is null, never midnight.
Color()Rgba?A color field’s hex as four bytes, by the same rule every colour read uses. Kind-checked.
List()IReadOnlyList<RuntimeValue>A non-list value reads as a one-element list; null reads as empty
Option()string?The stored option value of a select field. Kind-checked.
Options()IReadOnlyList<string>Every stored option of a multi-valued select
HasOption(string value)boolCompares the stored value, ordinally — not a display label
Source()string?The source text of a code or condition field. Kind-checked.
Entity()ResolvedEntity?The first entity referenced. Correct for one and for many.
Entities()IReadOnlyList<ResolvedEntity>
Items()IReadOnlyList<ItemCount>An itemCounts field’s entries: each referenced entity paired with the count at the same position of the value — and with its cell, when the field has a grid (ItemSlots) — in the author’s order, duplicates kept. Kind-checked.
ItemSlotsIReadOnlyList<int?>?An itemCounts field’s cells, paired position for position with its references: the cell of the inventory grid each entry sits in, or null for an entry with none. Null unless some entry has a cell. Items() already pairs it.
Capacityint?An itemCounts field’s grid capacity — how many cells it has, as the field’s declaration states it. Null when the field is unbounded, and on every other kind of field. Every cell in ItemSlots is below it.
Columnsint?How many cells wide an itemCounts field’s grid is, as its declaration states it. Null when the grid has no fixed width, and on every other kind of field. Cells count row by row, so an entry sits at row Slot / Columns, column Slot % Columns.
Media() / MediaList()MediaRef? / list
Tag()TagRef?
Variable()VariableRef?The variable a field NAMES — which spawn point, which flag. Code is the name the runtime is keyed by, ContextCode its context’s or null, and Name the two joined with a dot. Null when the field names none, or names one the project no longer has. A field may also be declared to accept a name the author TYPED, and a typed name arrives as the field’s Text() with no reference at all — so on such a field read field.Variable()?.Name ?? field.Text().
VariableList()list of VariableRefEvery variable a field declared as a LIST of variable links names, in the author’s order; empty when it names none. A variable the project no longer has is left out. A list field never holds a typed name.
NodeTarget()NodeRef?
BoardTarget()BoardRef?
Command()CommandRef?
AsRuntimeValue()RuntimeValueThe raw value, unconverted — the handle for the expression language’s reading, which RuntimeValues.ToNumber (a list reads as its length) and ToBool apply

Option() and Source() are kind-checked where most accessors are not, and for a reason worth knowing: an option names a vocabulary, so a free string from a text field could otherwise be compared against a generated enum member and pass on entirely the wrong field. A gate’s source, likewise, must never be readable as a line of dialogue.

Prefer the accessors over References and Value. Those are the raw forms; the accessors exist so you never walk a reference list and switch on its kind yourself.

Which rows of a list reach a step

A field whose Repeater is set belongs to one row of a list. For a read list — one whose rows play as beats rather than as exits — the engine has already chosen which rows the step shows, in this order:

  1. The row’s own condition and its Once tick. A row whose condition failed is dropped whole. A row ticked Once (Repeater.Once) that has already been said is dropped the same way, before its condition is asked.
  2. Taken by. When the node picks its track row by a variable (EngineNode.TakenBy), only the row that rule takes is kept: a row with no start whose condition holds, else the first row whose span holds the variable’s value. No row taken keeps none.
  3. Plays (Repeater.Plays). "first" keeps the holding row with the lowest Index; "random" keeps one drawn by Weight; "leastRecent" keeps the one said longest ago, a row never said before any row that was, the lowest Index among equals — and while any holding row carries a condition (a non-blank Gate), only those rows compete, so an “any time” row waits. Absent keeps every holding row.

What a row having been said means: a step presented it, or a NextLine call returned it. The engine remembers it only for a list that plays "leastRecent" and for a row ticked Once, and the memory is part of the save (SaidRows), so a line said once stays said across a load. A rollback, a load or a redraw shows each moment as it was, with the memory as it stood when that moment was drawn — and stepping back does not unsay a line, just as it does not untake a choice.

A line without a step: NextLine

DialogueEngine.NextLine(string boardId, string nodeId) returns IReadOnlyList<ResolvedField>?, the same fields a step carries; a game reaches the engine as TaleShardPlayer.Engine. It asks a node for its next line without entering the flow — for a line the world triggers rather than the story: a guard spotting the player, a shopkeeper greeting them. No step is presented, the story does not move, no script runs and no visit is counted, and it is legal whether the story stands on this node, on another, or on none.

The node’s first read list answers — the first node-level list in declaration order whose rows are read rather than ported and that has a row. Its rows go through the three rules above, and one row is returned: the list’s Plays pick, or its first holding row when it has no Plays, so a list of Once lines reads out in order, one per call. Under "random" the call consumes the generator’s next number, and only when some row can play. The row is remembered as said, exactly as a presented one is. The return is null for a node that does not exist, a type this engine cannot read, a node with no read list, or no row that holds.

It is also null, with an engine-busy warning, while the engine is drawing. Drawing reports what it could not read on the diagnostic channel, so game code can run in the middle of it, in one of two windows:

  • Re-drawing a past moment — during SetLanguage, Redraw or RestorePlaythrough, each of which draws every moment of the history again against that moment’s own state and said lines. A NextLine asked there would read and record against a past moment and lose its record when the live state is put back.
  • Drawing a live step — from the start of a node’s draw until its step is recorded and its row draws are spent. The draw only looks at the numbers its rows are picked with and spends them once the step is recorded, so a NextLine asked there would take the number the step’s own row was picked with, and a redraw, a rollback or a load of that step could show a different line.

Ask again once the call that draws has returned. A NextLine asked from a step handler (StepPresented) is answered: the step is recorded and spent before it is presented.

ResolvedChoice

One exit offered to the player.

MemberTypeNotes
ChoiceIdstringKey by this.
PortIdstringWhich handle it leaves by. Not unique within a step.
ContentIReadOnlyList<ResolvedField>
PictureKeystring?The key of the option’s face field — a media or entity reference on the step’s node, read with step.FieldByKey. Set only for an option from a repeater row whose block carries a picture; never content
EnabledboolEvery gate and requirement passed, or there were none
IsTakeableboolEnabled && Target != null
TargetChoiceTarget?Null for a dangling exit — a port with no edge, or an edge to a missing node
Visitedbool
BlockedByIReadOnlyList<ChoiceBlock>Every reason it is drawn but not takeable

Read IsTakeable rather than re-deriving it. The engine uses the same rule to decide whether a node offers a choice at all or merely presents.

ChoiceTarget is NodeId and BoardId.

Why a choice is blocked

ChoiceBlock carries structure only — there is no display copy in it, so your game decides what to show.

MemberTypeNotes
KindBlockKindGate or Requirement
TermsIReadOnlyList<BlockedTerm>What the player needs and what they hold
TagLabelstring?Resolved once, so you need no lookup. Requirements only.
TagIdstring?Requirements only
Reasonstring?The author’s own sentence, when they wrote one — a requirement row’s, or on a Gate the one written on the edge’s condition. An exit’s own gate never carries one
NodeIdstring?Which node carries the requirement this was hoisted from
ExpressionstringFor diagnostics. Never show it to a player.

BlockKind distinguishes the two mechanisms, and a game usually draws them differently: a gate is the author saying not down this path; a requirement is you cannot do this yet — which is the one a player is owed an explanation for.

A BlockedTerm is a Name and its Values:

  • Name is the variable that was checked, never the row’s tag.
  • Values is what the player holds and what was wanted — two values, or none. A bare truth test has no second side.

Together they are what lets you render reputation 2 / 5 without knowing anything about the project. There can be several, because a && b can fail twice.

Entities and actors

ResolvedEntity

An entity as the game sees it: the authored data and the playthrough overlay merged once, so every read path answers the same values.

MemberTypeNotes
Id · Code · Namestring
PictureId · Colorstring?
FieldsIReadOnlyDictionary<string, ResolvedField>Merged authored and overlay values, keyed by field name. Every field the definition has: those it inherits, those an applied trait adds (named with the trait’s prefix, stats.armor) and its own
IntrinsicsIReadOnlyDictionary<string, ResolvedField>$name, $code, $color, $voice, $description, $portrait — see below
OverlaidIReadOnlyList<string>Which field names came from the overlay
DefinitionIdstring
MentionAsEngineMentionLook?How a mention of this entity draws, as its definition declares it: null for its name, else a keycap — the entity is an input action and its Code is what your game maps to its own action. See drawing an action’s glyph

Intrinsics are the fields every entity has, whatever its definition. Bind those when a preset has to work against a project that declares no entity definitions of its own.

$voice is the code of the voice this character is cast to, and it is absent when they are uncast. It is what answers which voice speaks this line with no naming convention: the speaker is already resolved per step, so the voice for a line is the speaker’s — which is why a voice clip on a step carries that code rather than making you look it up.

ResolvedActor

An actor on stage for this step: Entity, ViaFieldKey, and IsSpeaker.

IsSpeaker is true for the actor of the first entity field, which is all the runtime can tell structurally. Your game is free to decide otherwise.

ResolvedAudio

Channel, MediaId, Url, and — on the voice channel only — Line and VoiceCode. Channel is what distinguishes this from an ordinary media reference.

The engine produces two channels:

ChannelOne entry perAlso carries
sfxfield of the node holding a media reference whose file is a sound — its kind in MediaKind is audio. A picture, a video or a file of unknown kind is never here—
voiceline with a voice-over clip in the language you are playing inLine, the same identity a translation and a recording script use; VoiceCode, the voice of the character who says that line — the field its EngineField.SpeakerKey names (in a conversation, that row’s own character, never one on its beat after the pause)

music and ambience are part of the model and have no producer. Channel is a string rather than an enum so a later one breaks no reader — give any switch on it a default.

Two voice entries on one step are two lines and belong one after the other, not on top of each other. VoiceCode is absent when the speaker is uncast, and that is not a reason to withhold the clip: a take exists, so it plays.

The value types

Five narrow structs rather than one general reference, so that a host cannot ask a reference of one kind for another kind’s members and read a meaningless null.

TypeHolds
MediaRefId; Url — where the bytes are, as the payload gave it, not an asset key; and Focal — where the picture’s face is, a Point2 in 0..1 of the picture’s box, or null when no face is marked. Crop toward it; centre when null
TagRefId and Label. The label is the author’s own; the id is never a fallback for it.
CommandRefCode and Args — positional, one per parameter the command declares, as a dispatcher expects. An unfilled parameter is null; one the author bound to a variable holds that variable’s value as of the moment the step was composed. ArgVariables, aligned with Args, names each bound argument’s variable by its context-qualified name (empty for a literal, and an empty list when nothing is bound); ArgVariableName(i) answers one or null. To fire a held call later with its bound arguments read now, pass it to TaleShardPlayer.RereadCommandArguments (or EngineSession.RereadCommandArguments, or call.Reread(runtime)), which replaces each bound argument with the variable’s current value and copies the rest. On an entity’s commandCall field too
NodeRefNodeId, and BoardId which may be absent for a target on the same board
BoardRefBoardId

Vec3 is three doubles, X/Y/Z. Deliberately not an engine vector type: you convert at your own edge, which keeps this assembly free of engine references.

Point2 is two doubles, X/Y, and Rect2 four, X/Y/W/H — a position and an area on a picture, each number a fraction 0..1 of the picture’s box from its top-left corner. Multiply by the drawn picture’s size at your own edge; the same reasoning as Vec3 keeps them out of UnityEngine’s types.

ItemCount is an Item — a ResolvedEntity, resolved like any entity reference when the step is composed, so its values are the playthrough’s at that moment: for an entry an exit handed over, after the exit’s scripts and the next node’s own scripts have run, exactly as the step’s cast is read — an int Count of one or more, and an int? Slot: the cell of the inventory grid the entry sits in, a whole number from zero, or null when it has none. It is one entry of an itemCounts field (Items()), and one entry of what an exit handed over on the way to a step (step.Trace.Received). Entries are the author’s: in order, duplicates kept, never merged. The cell is the author’s too — the one the field’s card shows the item in when the field has a capacity, the one she placed it in when it has not — so a game with a grid inventory seats the item there, and a game without one ignores the number. The grid the cells count over is the field’s own Capacity and Columns; a hand-over row has no grid, so a received entry’s Slot is read against the grid of wherever the game puts it.

FieldKind

Switch on this rather than on RawType, where a typo resolves to nothing.

Text · RichText · Number · Boolean · Date · Time · Asset · Entity · Select · Color · Vector3 · Code · NodeRef · BoardRef · DocumentRef · VariableRef · CommandCall · TagRef · Condition · Position · Area · ItemCounts

Two companions in the same place:

  • FieldStorage — Unknown · Values · Fragment · CodeText
  • RefKind — Unknown · Entity · Media · Tag · Command · Node · Board · Variable.

Each has a Parse and a ToWire on a static helper — FieldKinds, FieldStorages, RefKinds — which is how a wire name becomes a kind and back.

ResolvedRef

The raw form behind References. Its constructor is private; six factories build one — ForEntity, ForMedia, ForTag, ForCommand, ForNode, ForBoard.

Its ten nullable properties are only meaningful after switching on Kind, which is exactly what the typed accessors on ResolvedField already did. Reach for it only when you are writing something generic over every reference kind.

Args is typed as RuntimeValue because no type reachable from PlayStep’s public surface may come from a JSON library — a constraint worth knowing if you are extending this surface yourself.

Written for
Unity developers · Advanced integrators
Last updated