Skip to content

en Narrative System

HoCha113 edited this page Jun 21, 2026 · 3 revisions

中文版本

Narrative System

InnoVault provides a declarative narrative framework that separates story content (dialogue, choices, popups, branches) from runtime (playback, progress, scheduling, UI).

Client-only: Narrative UI and NarrativeRunner run on the client only; skipped on Main.dedServ.


What is this? When to use?

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

vs manual UI / DataModules

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

Quick start (5 min)

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();

Architecture

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]
Loading
Component Description
NarrativeScenario Scenario base: Key, style, content, policy
NarrativeComposer Chain builder — recommended 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

Level 1 — Hello World

Minimal scenario + manual Begin() (see 5-minute example). Key points:

  • Scenarios are autoloaded; Key defaults to ModName/TypeName
  • New scenarios queue when a session is already active
  • Localization is consumer responsibility — pass final strings to Say

Inject host services (Mod.Load)

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.


Level 2 — Common patterns

Auto-trigger with Policy

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.

Choices, branches, and jumps

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

Progress and persistence

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

Reward popups

n.Reward(ItemID.Gel, 5, "Quest reward")
 .SayReward("Guide", "Here's your payment.", ItemID.Gel, 5);

Set IRewardGrantService for actual item grant on claim.


Level 3 — Advanced

Custom UI views

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.

Portraits and styles

PortraitRegistry.Register("MyMod", "Guide")
    .Named("Guide")
    .Portrait("default", myTexture);

StyleRegistry.RegisterDialogue("MyMod", "dark", new MyDialogueSkin());

Prefer Speaker("Guide") / Style("dark") for mod-scoped ids.

VN-style skip

session.RequestSkipToNextStop();

Skips ordinary dialogue but stops at choices, popups, commands, branches, waits, timed nodes.

Multiplayer sync (INarrativeSyncService)

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 your INarrativeProgressStore or Command nodes.

Recommended MP model:

  • Dialogue UI: local client only
  • Authoritative progress: server ModPlayer + DataModules or SyncVar
  • INarrativeSyncService: notify server on Completed

API reference

NarrativeComposer

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)

Node types (NodeKind)

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

NarrativeScenario / NarrativeRunner

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)

NarrativePolicy

Property Description
CanTrigger(store, player) Trigger condition (live evaluation)
IsCompleted(store) Completion check; nullCompleted from store
Priority Higher wins
Repeatable Ignore completed when true
Blocked Extra block predicate
OnTriggered / OnCompleted Trigger / completion callbacks

Host services

Interface Description
INarrativeHostService Aggregates progress + rewards
INarrativeProgressStore Progress read/write
IRewardGrantService Grant(RewardPayload, Player)
INarrativeSyncService SyncProgress(scenarioKey, progress) — optional MP

Popups (PopupPayload)

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


Built-in demo

NarrativeDemoScenario has no auto policy. Run /narrativedemo in-game to test dialogue, choices, labels, and popups.


Common mistakes / FAQ

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 By design — UI is local; sync outcomes via Sync
When(...) ignores world changes Each Begin rebuilds graph; ensure conditions are inside When

Related modules


Navigation

Previous Home Next
DataModules System Home Models3D System

Clone this wiki locally