-
Notifications
You must be signed in to change notification settings - Fork 6
en Narrative System
Declarative narrative: describe story graphs (dialogue, choices, popups, branches) with a C# chain API; the framework handles playback, progress, scheduling, and UI.
Client-only: Narrative UI and NarrativeRunner run on the client only; skipped on Main.dedServ.
Narrative lets you describe story graphs with a C# chain API (NarrativeComposer); the framework handles playback, queuing, progress, and auto-scheduling. Authors inherit NarrativeScenario and implement Build().
Use when:
- Main/side quest dialogue and branching choices
- Story popups and item reward presentation
- Auto-triggered cutscenes via
NarrativePolicy
Don't use when:
- Server-only logic with no UI → plain
ModSystem/ DataModules - Complex custom HUD outside dialogue flow → Basic UI
UIHandle - Full real-time MP dialogue mirroring → default is local playback; implement
INarrativeSyncService
| Narrative | UIHandle | DataModules | |
|---|---|---|---|
| Core | Dialogue graph, choices, scheduling, progress | Generic HUD / panels | Modular save |
| Authoring | Chain Build()
|
Override Draw / LogicUpdate
|
Fields + Store |
| Auto trigger |
NarrativeScheduler + Policy |
Call Open() yourself |
N/A |
| Progress | INarrativeProgressStore |
Roll your own | Built-in persistence |
using InnoVault.Narrative.Composition;
using InnoVault.Narrative.Core;
using InnoVault.Narrative.Runtime;
// 1) Define scenario (autoloaded)
public class IntroScenario : NarrativeScenario
{
protected override void Build(NarrativeComposer n) {
n.Say("Guide", "Welcome!")
.Choice("Guide", "Continue?", c => c
.Option("yes", "Yes", NarrativeTarget.Continue)
.Option("no", "No", NarrativeTarget.End))
.End();
}
}
// 2) Start manually (client)
IntroScenario.Find<IntroScenario>()?.Begin();flowchart TB
Scenario[NarrativeScenario] -->|Build| Graph[NarrativeGraph]
Scheduler[NarrativeScheduler] -->|Tick policies| Runner[NarrativeRunner]
Runner -->|queue / advance| Session[NarrativeSession]
Session --> Views[NarrativeViews]
Progress[INarrativeProgressStore] <-->|read/write| Runner
Services[NarrativeServices] --> Progress
Services --> Reward[IRewardGrantService]
Services --> Sync[INarrativeSyncService]
| Component | Description |
|---|---|
NarrativeScenario |
Scenario base: Key, style, content, policy |
NarrativeComposer |
Chain builder; primary authoring entry |
NarrativeGraph / NarrativeNode
|
Ordered nodes + label index |
NarrativeRunner |
Start, queue, tick, completion |
NarrativeScheduler |
Declarative auto-trigger |
NarrativeSession |
Active playback session |
NarrativeServices |
Progress, rewards, MP sync injection |
NarrativeViews |
UI view registry |
PortraitRegistry / StyleRegistry
|
Portraits and skins |
Minimal scenario + manual Begin() (see 5-minute example). Key points:
- Scenarios are autoloaded;
Keydefaults toModName/TypeName - New scenarios queue when a session is already active
- Localization is your job; pass final translated strings to
Say
using InnoVault.Narrative.Services;
public override void Load() {
NarrativeServices.UseHost(new MyNarrativeHost());
// or:
// NarrativeServices.Progress = myProgressStore;
// NarrativeServices.RewardGrant = myRewardService;
}Default progress is in-memory; rewards display only unless IRewardGrantService is set.
protected override NarrativePolicy ConfigurePolicy() => new() {
Priority = 10,
CanTrigger = (_, player) => player.statLifeMax > 100,
Repeatable = false,
};
NarrativeScheduler.RegisterBlocker(() => Main.LocalPlayer.dead);ConfigurePolicy() runs once at load; delegates like CanTrigger must read live game state.
protected override void Build(NarrativeComposer n) {
n.Say("Guide", "Welcome to my mod!")
.Choice("Guide", "Ready?", c => c
.Option("yes", "Yes", NarrativeTarget.Goto("next"))
.Option("no", "Not yet", NarrativeTarget.End))
.Label("next")
.Say("Guide", "Let's begin.")
.Branch(() => NarrativeServices.Progress.GetFlag(
new NarrativeProgressKey("MyMod", Key, "metNpc")),
NarrativeTarget.Goto("greet"), NarrativeTarget.End)
.End();
}NarrativeTarget: Continue, End, Goto(label), Scenario(key) / Scenario<T>().
var key = new NarrativeProgressKey("MyMod", "MyMod/IntroScenario", "metNpc");
NarrativeServices.Progress.SetFlag(key, true);Two-phase completion: Triggered (scheduler fired) → Completed (fully played), avoiding permanent skips on enqueue.
Persist: Point NarrativeServices.Progress at NarrativeProgressDataModule on OnEnterWorld (see DataModules System).
n.Reward(ItemID.Gel, 5, "Quest reward")
.SayReward("Guide", "Here's your payment.", ItemID.Gel, 5);Set IRewardGrantService for actual item grant on claim.
Before default UI VaultSetup, disable defaults and register custom views:
NarrativeViews.UseDefaultDialogueView = false;
NarrativeViews.UseDefaultChoiceView = false;
NarrativeViews.UseDefaultPopupView = false;
NarrativeViews.Register(myCustomView);| Base | Purpose |
|---|---|
NarrativeDialogueViewBase<TSelf> |
Custom dialogue box |
NarrativeChoiceViewBase<TSelf> |
Custom choice box |
NarrativePopupViewBase<TSelf> |
Custom popup |
Views implement INarrativeView.Sync(NarrativeSession). See Basic UI for HUD patterns.
PortraitRegistry.Register("MyMod", "Guide")
.Named("Guide")
.Portrait("default", myTexture);
StyleRegistry.RegisterDialogue("MyMod", "dark", new MyDialogueSkin());Prefer Speaker("Guide") / Style("dark") for mod-scoped ids.
session.RequestSkipToNextStop();Skips ordinary dialogue but stops at choices, popups, commands, branches, waits, timed nodes.
Default: local playback and progress only. To share completion across players:
public class MyNarrativeSync : INarrativeSyncService
{
public void SyncProgress(string scenarioKey, ScenarioProgress progress) {
// Send net packet; server writes authoritative progress (e.g. ModPlayer + DataModules)
}
}
NarrativeServices.Sync = new MyNarrativeSync();Source behavior: Framework calls Sync?.SyncProgress(key, ScenarioProgress.Completed) only when a scenario fully finishes (NarrativeRunner.FinalizeCompleted). Triggered, choices, and flags are not auto-synced; handle those in INarrativeProgressStore or Command nodes.
Recommended MP model:
- Dialogue UI: local client only
- Authoritative progress: server
ModPlayer+ DataModules or SyncVar -
INarrativeSyncService: notify server onCompleted
| Method | Description |
|---|---|
Label(string) |
Goto label on next node |
Say(...) |
Dialogue line |
SayTimed(...) |
Timed line |
Choice(...) |
Choice prompt |
Popup(payload, blocking) |
Functional popup |
Reward / SayReward
|
Item reward popup |
Command(Action) |
Host command |
Wait / WaitSeconds
|
Wait |
Branch(...) |
Runtime conditional jump |
Goto(label) |
Unconditional goto |
End() |
End scenario |
When(...) |
Build-time conditional (re-evaluated each Build) |
| Kind | Class | Description |
|---|---|---|
Say |
SayNode |
Dialogue + optional TimedSettings
|
Choice |
ChoiceNode |
Choice prompt |
Popup |
PopupNode |
Popup; blocking controls flow |
Command |
CommandNode |
Run Action
|
Branch |
BranchNode |
Conditional / unconditional jump |
Wait |
WaitNode |
Wait fixed ticks |
| Member | Description |
|---|---|
Key |
Unique id, default ModName/TypeName
|
Build(composer) |
abstract — build content |
ConfigurePolicy() |
NarrativePolicy or null (manual only) |
Begin() / Find<T>()
|
Start / lookup |
NarrativeRunner.IsBusy |
Running or queued |
NarrativeRunner.Reset() |
Abort and clear queue (auto on world switch) |
| Property | Description |
|---|---|
CanTrigger(store, player) |
Trigger condition (live evaluation) |
IsCompleted(store) |
Completion check; null → Completed from store |
Priority |
Higher wins |
Repeatable |
Ignore completed when true |
Blocked |
Extra block predicate |
OnTriggered / OnCompleted
|
Trigger / completion callbacks |
| Interface | Description |
|---|---|
INarrativeHostService |
Aggregates progress + rewards |
INarrativeProgressStore |
Progress read/write |
IRewardGrantService |
Grant(RewardPayload, Player) |
INarrativeSyncService |
SyncProgress(scenarioKey, progress) — optional MP |
| Member | Description |
|---|---|
Title |
Title text |
RequireClaim |
Must click to claim |
AutoHoldSeconds |
Auto-dismiss seconds (< 0 = wait forever) |
OnClaimed / OnDismissed
|
Claim / dismiss callbacks |
Factories: Popups.Reward(...), Popups.Message(...).
NarrativeDemoScenario has no auto policy. Run /narrativedemo in-game to test dialogue, choices, labels, and popups.
| Issue | Cause / fix |
|---|---|
| Scenario never auto-triggers |
ConfigurePolicy() null; CanTrigger always false; IsBusy or global blocker |
| Fires once then never again |
Repeatable = false and Completed; check IsCompleted override |
| Policy uses stale player data | Cached state in ConfigurePolicy(); use live delegates |
| Reward popup doesn't grant items | No IRewardGrantService
|
| Progress lost after reload |
NarrativeServices.Progress still default in-memory store |
| Teammates don't see dialogue | Expected: UI is local only; sync outcomes via Sync, not UI |
When(...) ignores world changes |
Each Begin rebuilds graph; ensure conditions are inside When
|
| Previous | Home | Next |
|---|---|---|
| DataModules System | Home | Models3D System |