Dialogue
This page uses Transform from Unity. The glossary says what each one is.
The game’s conversations are run by a dialogue system that keeps variables and runs small scripts written in Lua (a scripting language). Dialogue lets your mod read and set those variables, start and stop a conversation, redirect or cancel a conversation when it starts, add answer lines, and add your own functions that conversation scripts can call.
Dialogue variables, conversations and answer lines
Use Start to begin a conversation, SetVariable and the GetVariable calls to work with the Lua variables, and Intercept to run your code before each conversation starts.
public static class Dialogue
{
public static event Action<DialogueArgs> Started;
public static event Action<DialogueArgs> Ended;
public static bool IsAvailable { get; }
public static bool IsActive { get; }
public static string CurrentTitle { get; }
public static bool Exists(string title);
public static bool HasVariable(string name);
public static string GetVariableString(string name, string fallback);
public static float GetVariableNumber(string name, float fallback);
public static bool GetVariableBool(string name, bool fallback);
public static bool SetVariable(string name, object value);
public static string GetActorField(string actor, string field, string fallback);
public static string RegisterFunction(string modId, string name, object target, MethodInfo method);
public static bool Start(string title, Transform actor, Transform conversant);
public static void Stop();
public static bool Narrate(string modId, string text);
public static void Intercept(string modId, ConversationHandler handler);
public static string AddResponse(string modId, string conversationTitle, string anchorText, string playerText, string npcReply, string luaCondition, Action onPicked);
public static string AddResponse(string modId, string conversationTitle, int anchorId, string playerText, string npcReply, string luaCondition, Action onPicked);
public static bool RemoveResponse(string modId, string key);
public static void RemoveAll(string modId);
}
| Property | Type | What it means |
|---|---|---|
IsAvailable | bool | true when the Dialogue API works. |
IsActive | bool | true while a conversation is running. |
CurrentTitle | string | The running conversation’s title. "" when none is running. |
| Method | Returns | What it does |
|---|---|---|
Exists(title) | bool | true when a conversation with this title exists in the game’s dialogue database. |
HasVariable(name) | bool | true when this Lua variable exists. |
GetVariableString(name, fallback) | string | Reads a text variable. |
GetVariableNumber(name, fallback) | float | Reads a number variable. |
GetVariableBool(name, fallback) | bool | Reads a yes-or-no variable. |
SetVariable(name, value) | bool | Writes a variable. true when it was written. |
GetActorField(actor, field, fallback) | string | Reads a text field of a character in the dialogue database. |
RegisterFunction(modId, name, target, method) | string | Adds a Lua function, and gives back its Lua name. |
Start(title, actor, conversant) | bool | Starts a conversation (experimental). |
Stop() | void | Stops the running conversations (experimental). |
Narrate(modId, text) | bool | Shows narration text (experimental, see Dialogue authoring). |
Intercept(modId, handler) | void | Runs your code before every conversation starts. |
AddResponse(modId, conversationTitle, anchorText, ...) | string | Adds an answer line, placed by the text of a line (experimental). |
AddResponse(modId, conversationTitle, anchorId, ...) | string | Adds an answer line, placed by the id of a line (experimental). |
RemoveResponse(modId, key) | bool | Removes an answer line you added (experimental). |
RemoveAll(modId) | void | Removes everything your mod added here. |
| Event | Gives you | When it fires |
|---|---|---|
Started | DialogueArgs | A conversation started. args.Title is its title. |
Ended | DialogueArgs | A conversation ended. args.Title is its title. |
ConversationHandler your code that runs before a conversation starts
This is the delegate you pass to Dialogue.Intercept. The framework calls it before each conversation starts, with the ConversationArgs for that conversation.
public delegate void ConversationHandler(ConversationArgs args);
| Parameter | Type | What it means |
|---|---|---|
args | ConversationArgs | The conversation about to start. |
ConversationArgs a conversation that is about to start
Intercept hands you this before a conversation starts. Set Title to send the player to another conversation. Setting Cancel stops it, and that is experimental.
public sealed class ConversationArgs
{
public string Title { get; set; }
public Transform Actor { get; }
public Transform Conversant { get; }
public bool Cancel { get; set; }
}
| Property | Type | What it means |
|---|---|---|
Title | string | The conversation’s title. Set it to send the player to a different conversation. |
Actor | Transform | The person who starts the conversation. |
Conversant | Transform | The person who is spoken to. |
Cancel | bool | true stops the conversation (experimental). |
Putting it together
Sends one conversation to another, sets a variable, and registers a Lua function that checks a quest step.
using System.Reflection;
using ObenseuerCore;
[ModInfo("example.dialoguetour", "Dialogue Tour", "0.1.0")]
[ModDependency("obenseuer.moddingframework", "0.11.0")]
public sealed class DialogueTour : FrameworkMod
{
protected override void OnLoad()
{
Dialogue.Intercept("mymod", args => { if (args.Title == "mymod/Greeting") args.Title = "mymod/Greeting2"; });
Dialogue.SetVariable("mymod_visits", 3);
string lua = Dialogue.RegisterFunction("mymod", "HasKey", null, typeof(DialogueTour).GetMethod("HasKey"));
Log.Info("Lua name: " + lua);
}
public static bool HasKey(string taskId)
{
return Quests.Reached(taskId, "key");
}
}
| Step | What it does |
|---|---|
Dialogue.Intercept("mymod", ...) | Sends the player from mymod/Greeting to mymod/Greeting2 when the conversation starts. |
Dialogue.SetVariable("mymod_visits", 3) | Writes a Lua variable the conversation scripts can read. |
Dialogue.RegisterFunction("mymod", "HasKey", null, ...) | Adds HasKey as a Lua function, and gives back its Lua name. |
Log.Info("Lua name: " + lua) | Logs the Lua name, so you can check it. |
In a conversation condition you would then write Mod_mymod_HasKey("<taskId>").
Things to know
Variables
HasVariable,GetVariableString,GetVariableNumberandGetVariableBoolread the Lua variables, and give you your fallback if the variable is not there. A typed read returns the fallback when the variable is missing or holds another type, soGetVariableStringreturns the fallback for a number.GetActorFieldreturns a field only when it holds text. Otherwise you get the fallback.SetVariable(name, value)returnstruewhen the variable was written, andfalse(logged once) when the name or the value is not allowed. It returnsfalsewhen:- the
namedoes not match^[A-Za-z_][A-Za-z0-9_]*$(a name must start with a letter or_, and have only letters, digits and_). A newline at the end is refused too; - the
nameis a Lua keyword:and break do else elseif end false for function if in local nil not or repeat return then true until while; - the
nameis one the game writes itself:Actor,ActorIndex,Alert,Conversant,ConversantIndex,Conversation,Converrsant(the game’s own spelling),Currency,Day,PlayerAge,PlayerFirstName,PlayerGender,PlayerLastName,PoliceIdle,Price,SavedLevelName,Speaker,WakeUpReason, or any name that starts withDREAM_; - the
valueis a string that contains a backslash or a control character (such as NUL, a new line, a carriage return or a tab). Double quotes are fine; - the
valueis not a number (NaN), is infinite, or is a double too large for a float; - the
valueis not a string, bool, int, float or double.
- the
- A bad variable name or value can make the whole save fail to load, so the framework refuses them. The game stores the variables in the save as Lua text, and the game cannot read a bad name or value back. (The game saves dialogue variables in
Globals.dialogueas Lua text.) - Numbers are stored as floats, so whole numbers above 16,777,216 lose precision. That is 2^24.
- Start your variable names with your mod’s name, for example
mymod_visits. That keeps them apart from the game’s and from other mods’.
Starting and stopping
Start(title, actor, conversant)(experimental) starts a conversation and returns whether one is running afterwards. It returnsfalsewhen a conversation is already running,titleis empty, or the game is loading or saving. The game can refuse to start a conversation, for example when there is no dialogue screen, so check the result. (It starts the conversation throughInteractableTalk.StartDialoguewhen the conversant has that component, and throughDialogueManager.StartConversationotherwise.)Stop()(experimental) ends every running conversation, and it is never refused. It works even while a save loads or saves. It may leave the person you spoke to in the middle of the conversation, because the game’s cleanup runs only when the game’s own close step happens. (It calls the Dialogue System’sStopAllConversations, also whileSaveGate.Busy. The state thatInteractableTalk.CheckStartDialogueInterruptset when the talk started (PlayerStats.Active, the control locks, the dialogue registration) is cleared only whenDialogueController.OnDialogueClosedruns.)Started(title)andEnded(title)fire for every conversation, including ones the game starts itself. A message can reach the object twice in some setups.CurrentTitleis""wheneverIsActiveisfalse. (They come from the game’sOnConversationStartandOnConversationEndmessages.)
Changing before start
Intercept(modId, handler)runs before every conversation starts, whether the game or this API starts it. Your changes to the game’s dialogue database are in place by the time the conversation begins. Handlers run in the order they were added, and adding the same handler for the same mod again does nothing. (The handler runs in a prefix onDialogueSystemController.StartConversation.)- Setting
args.Titlesends the player to a different conversation. The new title is used only whenExists(title)istrue. Otherwise the original title stays and the refusal is logged once.Titleis last-writer-wins: the last handler to set it counts. It is ignored once the conversation is cancelled. - Setting
args.Cancel = truestops the conversation, and nobody can undo it. AnyCancelwins: once a handler sets it, the later handlers still run and seeCancelastrue, and setting it back tofalsehas no effect. - Experimental: cancelling a conversation can leave the game waiting for it. A cancelled start skips the game’s
StartConversation, so the code that asked for the conversation gets none. Cancel only where you know what started it. Known risks:- a behavior tree step that waits for the conversation to finish can stall. (The stalling task is a BehaviorDesigner task with
waitForConversationCompletion.) - a cutscene starter that waits for a conversation can freeze;
- the talk interaction changes its own state before it starts a conversation, and a later check repairs it. (The talk interaction is the game’s
InteractableTalkclass, and itsDelayCheckrepairs the state.)
- a behavior tree step that waits for the conversation to finish can stall. (The stalling task is a BehaviorDesigner task with
Lua functions
RegisterFunctionadds a function the game’s conversation scripts can call, and gives back its name. The Lua name isMod_<prefix>_<name>. Theprefixis your mod id with each character that is not an ASCII letter or digit replaced by_. When that replacement changes the id, the prefix also gets_and an 8-digit hex hash of the original id, soa.banda_bnever collide, and the name does not depend on which mod loads first. It returnsnullwhen the API is not available. (The hash is FNV-1a.)namemust be a valid Lua name, and the function’s parameters must befloat,stringorbool. Lua numbers arrive asfloat.targetisnullfor a static method, and an object of the method’s class otherwise.- Registering a name again replaces the earlier function.
- The framework adds seven functions of its own, which are not in the
Mod_namespace. It adds them whenDialogueis available, at start and after each scene load.Core_QuestState(taskId)returns"Active","Failed","Finished","Abandoned"or"".Core_ObjectiveState(taskId, objectiveId)returns the state name of the step, or"".Core_CrimeTotal()returnsLaw.Total.Core_CrimeAgainst(npcId)returnsLaw.GetAgainst(npcId).npcIdis a number.Core_ResponsePicked(key)runs theonPickedof the answer line with that key. The framework’s own answer lines call it, so your mod does not.Core_BranchPicked(key)runs theonPickedof the branch with that key. The framework’s own branch lines call it, so your mod does not.Core_LineText(id)returns the text of a line whose text a mod gave as a callback. The framework’s own dialogue lines call it, so your mod does not.