BLSE0.6

Python mods

Dialogue

On this page 12 sections

This page is about the game's conversations: knowing what an NPC says and what the player answers, hiding the player's replies or changing the texts on screen, adding whole new conversations or single replies to the game's, and adding journal quests. Come here for a quest mod, a mod that changes how characters talk, or an NPC of your own that talks.

A minimal working example: log the player's choice in Jack's tutorial conversation, and hide one of the replies.

Python
import blse

def on_choice(topic, data):
    f = data.split('\0')
    if f[0].endswith('jack_tutorial.dlg'):
        blse.log('mymod: the player picked reply line %s' % f[4])

blse.subscribe('blse.dialog_choice', on_choice)
blse.hide_dialog_reply('jack_tutorial.dlg', 25)

Important, before you start:

  • Reading, hiding and changing need a recognized game build (blse.build_id() is not None); on other builds hide_dialog_reply and set_dialog_text raise RuntimeError and the events are never sent. Adding conversations works on every build.
  • Line numbers are the numbers in the .dlg file, not the order on screen. Open the game's file (from its .vpk packs, or with blse.read_file) to find them.
  • Changes apply from the next conversation on: the game reads an NPC's conversation anew each time one starts.
  • Saves keep the conversation's name, not its rows: a game saved with your conversation needs your mod to load it again.

Words used on this page#

Word Meaning
.dlg file The game's file for one conversation, such as dlg/main characters/jack_tutorial.dlg. One row per line of dialogue
Line, line number One row of a .dlg file and its number (column 1). Rows refer to each other by number
NPC line A row the NPC says. Its "next" column is #
Reply A row the player can pick. The replies that follow an NPC line (by number, up to the next NPC line) are its answers; a reply's "next" is the NPC line it leads to, 0 ends the conversation. The game shows at most 4
Node An NPC line with its replies
Condition Python code deciding whether a reply is shown ('G.Guard == 1'), run with pc (the player) and npc set
Action Python code run when a reply is picked, or when an NPC line is said ('pc.SetQuest("MyQuest", 2)')
dialogname The keyvalue naming an NPC's conversation
Virtual conversation A conversation a mod registers under a name of its own ('mymod/guard.dlg'); no file has to exist
Quest An entry in the player's journal with numbered states, set by pc.SetQuest(title, state)

Dialogue#

On recognized game builds, BLSE reports conversations line by line (b0.5.10):

Topic data
blse.dialog_line file, npc_class, npc_name, line, text_male, text_female = data.split('\0'): the NPC is about to say line of the dialogue file
blse.dialog_choice file, npc_class, npc_name, choice, reply_line, next_line, text_male, text_female = data.split('\0'): the player picked reply number choice (1-based, as shown), which is reply_line in the file; the NPC continues with next_line ('0' when the conversation ends)

file is the game's path of the .dlg file, e.g. dlg/main characters/jack_tutorial.dlg; line numbers are the ones in that file. The texts are the file's columns for a male and a female player character (the game shows one of them). Both are delivered at the end of the frame.

Python
import blse
def on_choice(topic, data):
    f = data.split('\0')
    if f[0].endswith('jack_tutorial.dlg') and f[4] == '25':
        blse.log('the player asked Jack what happened')
blse.subscribe('blse.dialog_choice', on_choice)

blse.dialog_line#

The NPC is about to say a line. Fields, in order:

  1. file: the .dlg file's path in the game ('dlg/main characters/jack_tutorial.dlg');
  2. npc_class, 3. npc_name: the NPC's classname and name;
  3. line: the line's number in the file (text: int(line));
  4. text_male, 6. text_female: the line's text as written for a male and a female player (the game shows one of them).

When to use it: track how far a conversation got, react when a character says something, keep statistics.

Python
def on_line(topic, data):
    file, cls, name, line, male, female = data.split('\0')
    if file.endswith('jack_tutorial.dlg') and line == '1':
        blse.log('Jack starts talking')
blse.subscribe('blse.dialog_line', on_line)

blse.dialog_choice#

The player picked a reply. Fields, in order: file, npc_class, npc_name, choice (the reply's position on screen, 1 to 4, counting only visible replies), reply_line (its number in the file), next_line (the NPC line that follows, '0' when the conversation ends), text_male, text_female.

When to use it: give a reward for a choice, remember the player's answer for later (G, values on entities). Compare reply_line, not choice: the position changes when replies are hidden.

Hiding replies and changing texts#

blse.hide_dialog_reply(file, line[, hidden]) (b0.5.22) hides the player's reply line of a dialogue file; hidden=0 shows it again. It works on top of the game's own .dlg conditions: a reply the game would not show stays hidden, the others move up (the reply numbers the player sees, and dialog_choice's choice, count only the visible ones). The last visible reply is never hidden: the game would otherwise show "I do not have a valid reply." and BLSE keeps them all (logged once).

blse.set_dialog_text(file, line, text) shows text instead of an NPC line or a reply (up to 1000 bytes; None or '' removes the replacement). Only the text on screen changes: the voice-over, the line's conditions and actions and where it leads stay the same, and dialog_line / dialog_choice still report the .dlg text. A skill reply keeps its skill dots.

file is matched case-insensitively against the end of the game's path: 'jack_tutorial.dlg', 'main characters/jack_tutorial.dlg' or the full path; '*' matches any file. blse.clear_dialog_rules() removes Python's rules (also cleared on an engine restart).

Python
import blse
blse.hide_dialog_reply('jack_tutorial.dlg', 25)                       # no "What happened in there?"
blse.set_dialog_text('jack_tutorial.dlg', 24, 'Just the basics, Jack.')

blse.hide_dialog_reply#

Python
blse.hide_dialog_reply(file, line[, hidden])   # -> 1
  • file: 1-259 characters, matched against the end of the game's path as above; '*' any file.
  • line: the reply's number in the file, 1 or more.
  • hidden: 1 (default) hides it, 0 removes your rule.
  • Returns 1. Errors: ValueError for a bad file or line; RuntimeError on an unknown game build.
  • Native plugins can decide line by line with a dialogue filter; Python's rules are applied first.

When to use it: replies that make no sense in your mod's story, an option only for some characters (hide it unless a condition holds, and call it again when the condition changes).

blse.set_dialog_text#

Python
blse.set_dialog_text(file, line, text)   # -> 1
  • file, line: as for hide_dialog_reply (an NPC line or a reply).
  • text: the text to show, up to 1000 bytes; None or '' removes the replacement.
  • Returns 1. Errors: ValueError for a bad file or line or a text over 1000 bytes; RuntimeError on an unknown game build.

When to use it: translate lines, fix a typo, show text that depends on your mod's state.

blse.clear_dialog_rules#

Python
blse.clear_dialog_rules()

Removes every rule made with hide_dialog_reply and set_dialog_text from Python. Returns None. An engine restart also clears them.

Conversations from mods#

(b0.5.48) Mods add conversations without replacing any .dlg file, and several mods can add to the same conversation. The game reads an NPC's conversation anew each time a conversation starts, so additions apply from the next conversation on.

Function
blse.add_dialog(name, rows) A whole conversation under a virtual dialogue name ('mymod/guard.dlg': a-z, 0-9, _ . / - and spaces, ending in .dlg; no such file has to exist). An NPC uses it when its dialogname is that name: a spawned one (spawn_entity(..., {'dialogname': name})) or any NPC after blse.set_keyvalue(npc, 'dialogname', name). Line numbers are the mod's own. Calling it again replaces the rows; when another mod registers the same name, the later registration wins (scripts run in load order; logged)
blse.extend_dialog(file, rows) Whole nodes (an NPC line and its replies) added to a conversation: the game's ('jack_tutorial.dlg', matched like hide_dialog_reply's file) or a virtual one. Numbers from 10000 on (the game's files end below 2000). A number another mod already added: the later registration wins (logged); a number the file has keeps the file's row (logged). The same rows again from the same mod are kept once (b0.5.49)
blse.add_dialog_reply(file, npc_line, reply[, first]) A reply offered under the NPC line npc_line of a conversation, reply = (text, next[, condition[, action[, female_text[, malkavian_text]]]]). BLSE numbers it itself (after the file's last line, below 10000). The game shows at most 4 replies: a reply is added only if there is room, unless first=1, which puts it first (a game reply may then drop off the end). Several mods: first replies first, then in the order they were added. The same reply again from the same mod is offered once (b0.5.49)
blse.remove_dialog(name) Removes this script's virtual conversation

rows is a list of tuples (line, text, next[, condition[, action[, female_text[, malkavian_text]]]]), or a string in the .dlg format itself (13 { } columns per row, as the game's files), e.g. a file the mod ships and reads with blse.read_file.

  • next '#' makes the row an NPC line. The replies that follow an NPC line (by number, up to the next NPC line) are the player's answers to it. A reply's next is the NPC line it leads to; 0 ends the conversation.
  • condition (replies) decides whether the reply is shown; action runs when the reply is picked. For an NPC line the condition is ignored and the action runs when the NPC says the line. Both are Python code strings, as in the game's files ('G.Guard == 1', 'pc.SetQuest("MyQuest", 2)', 'IsClan(pc, "Brujah")'), run in the game's __main__ with pc and npc set, or Python callables f(pc, npc) (a condition's result counts as true or false).
  • Limits of the game: a condition or action is cut at 255 bytes, an action is split at every ; and & (each part runs on its own; a ; inside a string breaks it), a condition is split at one | or &. Use a callable for longer code.
  • female_text: shown to a female player (default: text); malkavian_text: a reply's text for a Malkavian (default: text). Braces and line breaks in texts become ( ) and spaces. The game itself removes text between [ and ] from dialogue lines (notes in its files), so such text never shows.
  • Voice: the game looks for sound/character/<name without .dlg>/line<N>_col_<c>.mp3 (or .wav, .vcd), so a mod can voice its lines with loose files: for mymod/guard.dlg line 1 a male player hears sound/character/mymod/guard/line1_col_e.mp3 (compare the column letters with the game's own files in sound/character). Without a file the text is shown.
  • Starting a conversation from a script: the game's own input, blse.fire_input(npc, 'StartPlayerDialog', '0') (the game's Python: npc.StartPlayerDialog(0)).
  • hide_dialog_reply, set_dialog_text, dialog_line and dialog_choice work with added rows like with the game's (a reply added with add_dialog_reply has the number BLSE gave it; dialog_choice reports it).
Python
import blse
blse.add_dialog('mymod/guard.dlg', [
    (1, 'Halt. Who goes there?', '#'),
    (2, 'A friend.', 3),
    (3, 'Pass, friend.', '#', None, 'pc.SetQuest("MyMod_Guard", 2)'),
    (4, 'Thanks.', 0),
])
blse.extend_dialog('jack_tutorial.dlg', [(10001, 'A guard? Talk nice to him.', '#'), (10002, 'Got it.', 21)])
blse.add_dialog_reply('jack_tutorial.dlg', 11, ('Seen the guard?', 10001))

Giving an existing NPC a conversation: blse.set_keyvalue(npc, 'dialogname', name) (world). The name is saved with the NPC: a save made afterwards still names the mod's conversation; without the mod the NPC has nothing to say (the game logs "Could not load dialog file").

A conversation can also be a real .dlg file in a mod folder (<mod>/dlg/mymod/guard.dlg), and nodes can be added to a game file with a .dlg.append merge file; a reply under one of the game's NPC lines always needs add_dialog_reply, because the game lists the replies that follow an NPC line by number.

examples/mods/guardtalk is a complete example mod folder (a spawned NPC with its own conversation, a reply in Jack's tutorial conversation, and a quest).

A row as a tuple#

Position Name Meaning
0 line The row's number (an int). Not given for add_dialog_reply
1 text What is shown (to a male player, and to everyone when female_text is not given)
2 next '#' (or None) for an NPC line; for a reply the NPC line it leads to, 0 to end
3 condition Replies: Python code or a callable f(pc, npc) deciding whether it is shown; None always. Ignored for NPC lines
4 action Python code or a callable f(pc, npc) run when the reply is picked or the NPC line is said; None none
5 female_text Text for a female player (default text)
6 malkavian_text A reply's text for a Malkavian (default text)

A callable is registered once (the same function given again, for example at every level, reuses its entry); conditions call it with the game's pc and npc and count its result as true or false.

Python
def can_bribe(pc, npc):
    return pc.money >= 100
blse.add_dialog_reply('mymod/guard.dlg', 1, ('Here, take this. [$100]', 3, can_bribe, 'pc.MoneyAdd(-100)'))

(The [$100] part is a note: the game removes text in brackets, so the player sees "Here, take this.")

blse.add_dialog#

Python
blse.add_dialog(name, rows)   # -> 1

Registers a whole conversation under a virtual name. When to use it: an NPC your mod spawns or renames, with its own conversation.

  • name: 5-200 characters of a-z 0-9 _ . / - and space, ending in .dlg; no .., not starting with /.
  • rows: a list of tuples (above) or .dlg text. At most 5000 rows; a line number given twice is refused. Rows with an empty text are skipped, as the game does.
  • Returns 1. On bad input it raises ValueError with the reason ('no rows', 'line 3 is given twice', ...).

blse.extend_dialog#

Python
blse.extend_dialog(file, rows)   # -> 1

Adds nodes to an existing conversation, the game's or a virtual one. file: matched like hide_dialog_reply's (1-259 characters, not '*'). rows: numbered from 10000 on. Returns 1; ValueError with the reason on bad input (for example a number below 10000). Lead to your nodes from a reply added with add_dialog_reply.

blse.add_dialog_reply#

Python
blse.add_dialog_reply(file, npc_line, reply[, first])   # -> 1

Offers one reply under an NPC line. reply is a tuple (text, next[, condition[, action[, female_text[, malkavian_text]]]]) (no line number: BLSE numbers it) or one row of .dlg text. npc_line: 1 to 999999. A reply must lead somewhere: next '#' is refused. Returns 1; ValueError with the reason on bad input.

blse.remove_dialog#

Python
blse.remove_dialog(name)   # -> 1 or 0

Removes a virtual conversation your scripts registered with add_dialog. Returns 1 if it was removed, 0 if Python had none of that name.

Quests from mods#

A mod adds journal quests with a merge file in its mod folder, not by replacing the game's quest files: <mod>/blse/merge/vdata/system/quests_<area>.txt (santamonica, downtown, hollywood, chinatown: the journal's tabs) with +Quest { ... } blocks in the game's own format. Several mods can add to the same file. The game's own pc.SetQuest(title, n), pc.GetQuestState(title), dialogue actions and blse.quest_changed then work with the quest as with the game's.

text
QuestTable
{
	+Quest
	{
		"Title"		"MyMod_Guard"
		"DisplayName"	"A Word With The Guard"
		CompletionState { "ID" "1" "Description" "A guard blocks the way." "Type" "incomplete" }
		CompletionState { "ID" "2" "Description" "The guard let you pass." "Type" "success" "AwardMoney" "50" }
	}
}
  • The game finds a quest by the START of its name: a Title that begins with one of the game's titles (Gargoyle_x) finds the game's quest instead. Use a prefix of your mod.
  • Setting a quest that is in a botch state to another state ends the game with an error (the game's own rule): avoid botch states.
  • Saves: the player's quests are saved by title. A save loaded after the mod was removed has no such quest any more; the journal may then show that quest's state on another quest (the game's own behaviour for an unknown title). Players should keep the mod while they play a game started with it.

How the quest block reads: Title is the name scripts use (pc.SetQuest("MyMod_Guard", 2)); DisplayName is shown in the journal; each CompletionState is one state with its ID (the number SetQuest sets), the journal text, and its Type (incomplete, success, ...); other keys such as AwardMoney work as in the game's own quest files. Set a state from a dialogue action or a script, and react with blse.quest_changed.