Skip to content

What to get right first

The ordering rules and one-shot constraints that cost an afternoon each — the media bootstrap, the frozen policy, restart versus start, and settling a command.

All documentation — Start here
On this page

Most of this plugin is forgiving. A handful of things are not, and each of them fails in a way that does not point at its own cause — a blank portrait, a story that never resumes, a gate that reads false after a load.

They are collected here because they are cheap to avoid and expensive to diagnose.

Install the media catalog before anything asks for a picture

MediaCatalog.Install(MediaIndex.KeyById, MediaIndex.KeyByCode, MediaIndex.ExtensionByKey);

A player’s policy is frozen once it is applied

PlayerPolicy declares what your host draws — the node types it never shows, the ones it always shows, the fields it hides. Hand it in at construction.

Every write after that throws and names the property. And a player refuses to attach a policy that declares anything to a story that is already playing.

Same shape, same reason: pass onError to the constructor rather than subscribing afterwards. The policy check runs inside the constructor, so a handler attached after it is already too late to hear the one error you most wanted.

Start is not restart

EngineSession.Start(), DialogueEngine.Start() and TaleShardPlayer.Play() all clear the engine’s own history — and all leave the runtime holding the previous playthrough’s values.

Call IScriptRuntime.Reset() first if you mean restart. Or call TaleShardPlayer.Restart(), which is exactly that pairing.

The symptom if you do not: a second playthrough that begins with the first one’s inventory, and gates that are already true.

Seal your entity data

EntitySet<T> throws on every public member until it is sealed, and Seal() is internal — so the fix is always the generated bootstrap. The exception says so by name:

Call GameBootstrapper.Setup() before reading …

Settle a command exactly once, on your own thread

A CommandCompletion is the token an awaited command settles when it finishes.

Settling twice is refused too; the first answer stands.

This is also why an IEngineDiagnostics implementation must be thread-safe — it is the one thing here that can be written to from wherever a command handler happens to run.

Drain the gate failures

IScriptRuntime.TakeFailures() returns every gate failure since the last call, and clears them.

Read the restore report before you restore again

IScriptRuntime.LastRestoreReport says what the last load had to change — dropped entities, dropped variables, variables that fell back to their authored default, refused values.

Every restore overwrites it. Read it before restoring again.

It matters because a silently defaulted variable makes “why is my gate false after loading?” unanswerable. A variable the save predates takes its authored default; a variable the project no longer declares is dropped. Neither is configurable — two supported load behaviours would mean two sets of saves in the field — so the report is the whole story.

Save through the save writer, not your serializer

Read never throws and returns null for any reason it could not be read. Use TryRead when the reason matters — it checks in a deliberate order: empty, then length before any parse, then the parse, then the format version, then each field.

Release what you load

MediaLibrary is refcounted, and nothing frees anything on your behalf. No LRU, no memory budget, no automatic eviction — which also means nothing is ever freed behind your back.

One load, one Release<T> — including for a load that came back empty, because a failed slot still counts its callers.

The type argument names which load: Release<Sprite>(reference). One media file can be held as more than one kind of asset, and they are counted apart.

ReleaseAll() exists for a scene teardown. Nothing calls it for you.

Smaller ones, worth knowing once

TaleShardPlayer.WaitForEnd()Create it after the story starts. A wait created while nothing is playing is already satisfied.
ContrastPolicy.ReadableOnThrows on an unreadable background — deliberately, unlike an unreadable foreground, which is reported as an outcome
RuntimeValue.OfListThrows on a nested list. The language has none.
UIStrings.SetLanguageRefuses the engine’s internal default key by name. Pass the project’s language code.
ResourcesMediaSource and AddressablesMediaSourceSelf-register at BeforeSceneLoad. Registering your own must either match type, name and priority — it is then dropped as a duplicate — or unregister the built-in first.

Types you cannot new

Several have a private constructor, so a static factory is the only way in. If the compiler refuses your new, this is why — not a missing reference.

TypeThe way in
EngineSessionCreate(…) · FromDirectory(…)
EngineStateIdle · Presenting · AwaitingChoice · Suspended · Finished
ResolvedRefForEntity · ForMedia · ForTag · ForCommand · ForNode · ForBoard
RuntimeValueNull · Of(…) · OfList · OfStrings
StoryTextParse(html)
ContrastPolicyOff · ReadableOn(…)
EntityDisplayOf(ResolvedActor, …) · Of(ResolvedEntity, …)
MediaAddressFrom(…) · Raw(…) · FromCode(…) · Unresolved(…)

Where to go from here

Each rule above has a reference page behind it.

The play loopSession, engine, step — every member
What a step containsFields, choices, entities, values
Saving a playthroughWhat a save carries, and the two load rules
Loading mediaAddresses, the library, and writing a source
The Unity playerThe component most integrations start at
Scripting and commandsThe runtime seam, values, and the completion token
DiagnosticsThe two channels, and every code
Drawing a line of textRich text, escaping, and the formatter
Colour and how an entity looksContrast, the palette, the hex edge
The project’s UI stringsThe one thing this layer localizes
Generated entity dataEntity sets and the seal
Playing a storyThe narrative version of all of this
Written for
Unity developers · Advanced integrators
Last updated