Dialogs
Server-resolved conversation graphs: opaque node/choice payloads, per-choice conditions evaluated SERVER-side and stripped from the wire, choice actions via game SPIs. NPC talk, visual novels, tutorials, quest givers — one engine; Core-only, stateless, empty graph table = inert.
Seams you implement 2
Crossplay calls these; your game supplies them. Each ships an inert or permissive default, so register yours before services.AddCrossplayDialogs(); and it wins.
IDialogAction game SPI
The game's action vocabulary: what picking a choice DOES — accept a quest, open a shop, grant an item, set a flag. The default does nothing; register yours BEFORE AddCrossplayDialogs.
-
void Execute(ISession session, long ownerId, ushort actionId)Executes action actionId for this player.
IDialogCondition game SPI
The game's condition vocabulary: "is condition C true for THIS player right now?" — quest state, stats, items, reputation, anything. Unknown ids should return false (fail closed). The default hides every conditioned choice; register yours BEFORE AddCrossplayDialogs.
-
bool Evaluate(ISession session, long ownerId, ushort conditionId)True when the condition holds for this player.
Providers you can swap 2
Infrastructure seams. Crossplay ships a working implementation of each — replacing one changes where data lives or how it moves, never a game rule.
IDialogPolicy provider
The game's open rule: may THIS player open THIS graph now? Proximity to its NPC, story gates — the game's vocabulary; the default allows every configured graph. Register yours BEFORE AddCrossplayDialogs.
-
bool AllowOpen(ISession session, long ownerId, ushort graphId, out ushort reasonCode)True when opening is allowed. On refusal set reasonCode (0 falls back to DialogNotAllowed).
IDialogSource provider
Where graphs come from. The default serves Graphs; a game can register its own (e.g. graphs authored as Content records, or generated). Register BEFORE AddCrossplayDialogs (a TryAdd seam).
-
bool TryGetGraph(ushort graphId, out DialogGraph graph)The graph for an id, if it exists.
Services you call 1
Crossplay implements these. Resolve them from DI and call them from your own systems.
IDialogService
Server-side Dialogs API — what the wire handlers call, and what game code calls directly (an Interactions verb opening an NPC's dialog is one line of glue).
-
event Action<long, ushort, ushort, ushort> ChoiceMadeRaised after a choice COMMITTED with (owner id, graph id, node id, choice id) — the analytics/quest-trigger hook. The piece never interprets it.
-
bool TryOpen(ISession session, ushort graphId, out ushort reasonCode)Opens a graph for the player: policy gate → entry node (conditions applied) sent. Returns false with a code when refused (the client got DialogClosed).
Configuration 1
Every tunable lives in an options object — there are no magic numbers to hunt for.
DialogOptions
Configurable dialog parameters (defaults here; never inlined in logic).
-
Dictionary<ushort, DialogGraph> Graphs { get; set; }The dialog graphs, keyed by game-defined graph id — the default IDialogSource serves these. Empty (the default) leaves the piece inert: every open answers UnknownDialog. A game can instead register its own source (e.g. serving graphs from the Content piece) and leave this empty.
Wire messages 4
The protocol this piece speaks. Ids are allocated per piece so they can never collide.
DialogOpenRequest Client → server: "open dialog graph X". WHAT triggers it (an NPC click, a terminal, a book, a tutorial beat) is the game's; the piece only knows the graph id.
DialogNode Server → client: one node of the conversation — the current line and what may be said next. A text client prints it, a 3D client lip-syncs a portrait; same bytes.
DialogChooseRequest Client → server: "at node N of graph X, I pick choice C". The server re-validates everything (reachability, the choice's condition) — a forged id changes nothing.
DialogClosed Server → client: the conversation ended (with a reason when it was a refusal).
Unity services you inject 1
Crossplay binds these in the client context. Inject and call them from your own MonoBehaviours and presenters.
IDialogClient
The Dialogs piece, client side: open a server-resolved conversation graph and pick choices. Every node/choice payload is an OPAQUE blob the game renders (text, loc-keys, portraits); choice conditions never reach the client — the server sends only what THIS player may say. A dialog UI binds NodeReceived to its panel and calls Choose.
-
void Open(ushort graphId)Open dialog graph X (answered via NodeReceived or Closed).
-
void Choose(ushort graphId, ushort nodeId, ushort choiceId)Pick choice C at node N of graph X (echo the ids the server last sent).
-
event Action<DialogNode> NodeReceivedA conversation node arrived (the current line + your visible choices).
-
event Action<DialogClosed> ClosedThe conversation ended (ReasonCode 0 = a natural end).