On this page 14 sections
A Python mod is a text file ending in .py. You put it in a folder for your mod, Bin/loader/blse/mods/<your mod>/blse/scripts/, and BLSE runs it when the game starts. In it you tell BLSE what to do when something happens in the game: the player enters a level, kills someone, picks something up, types a console command. No game files are edited, and you need no tools besides a text editor.
You do not need to know Python to start: copy the examples on this page and change them. Mods that only change data (items, merchants, dialogue lines, maps) need no script at all: see Start here.
(Technically: BLSE registers a built-in module named blse in the game's Python 2.1 interpreter before game scripts run, and runs every .py file in the mod folders' blse/scripts/ at game start.)
This page covers writing and installing a mod, then the functions every mod uses (subscribing to events, publishing, console commands). The other pages are the reference for what you can read and change:
| Page | Covers |
|---|---|
| Events | Level changes, frames, saving and loading, entity outputs, combat and gameplay events, entities appearing and disappearing, character creation |
| Dialogue | Reading dialogue, hiding replies, changing texts, new conversations, replies and journal quests |
| Items and inventories | Inventory events, item data, giving, removing, dropping, equipping, new items and weapons |
| Combat, damage, stats and Disciplines | Damage rules, stat changes, Discipline blocking and costs, kills, pushes, the game's Python errors |
| The world | Spawning, teleporting, finding entities, console variables, frame rate, camera, sound |
| NPC AI | Relationships, enemies, schedules, moving, following, senses |
| Models and appearance | Scaling models and bones, faces, skins, colour, materials |
| Animation | Playing animations on NPCs, animation events and rules, new animations for models |
| Companions | NPCs that go with the player through level changes |
| Saved data, settings and translations | Save-game data, values on entities, the shared database, JSON, .ini settings, translated text |
| Interface: text and the HUD overlay | On-screen text and elements drawn over the game |
| Screens and windows | The game's own screens, mods' windows, button sounds |
| Keys and the mouse | Keys, controls and the mouse |
| Mod folders | Mods as folders of game files, merged data files, map patches |
Important, before you start:
- Scripts run before the main menu, when no game is running yet: there is no player, no level and no
G. Do game work inside event handlers (blse.level_init,blse.game_loaded), never at the top of the file. - The game's Python is version 2.1 (from 2001): no
True/False, nowith, nojsonmodule, andprintdoes not reach the console. Useblse.console_printandblse.log. - Every
blsefunction must be called on the game's main thread. Game scripts, event handlers and console commands all run there; a thread you start yourself does not (calls from it raiseRuntimeError). - An error in your script does not crash the game: its traceback (the error message with file and line) goes to the console and
blse.log, and other mods keep running. - Name everything after your mod (
mymod_...,'mymod.something') so mods do not collide.
Words used in these pages#
| Word | Meaning |
|---|---|
| Mod, mod folder | One folder in Bin/loader/blse/mods/ that holds a mod: its scripts, and anything else it needs. See Mod folders |
| Script | A .py file in a mod folder's blse/scripts/. BLSE runs each one once at game start |
| Event, topic | A message BLSE sends when something happens. Its topic is its name, such as 'blse.level_init' |
| Handler, subscriber | Your function that BLSE calls for an event; subscribing connects the two |
Payload, data |
The event's details, one string. Several fields are separated by NUL characters ('\0', the character with code 0): data.split('\0') gives them as a list |
| Callable | Something that can be called like a function: a def function, a lambda, a method |
| Level | One loaded map being played (Santa Monica's hub, a building's inside). Moving through a loading zone changes the level |
| Entity | Anything in a level: the player, NPCs, doors, items lying around. Functions take an entity's index (a slot number; 1 is always the player) or its handle (a number that stays valid only while that entity exists) |
G |
The game's global script variables, saved in every save game. Quests keep their state there |
| Main thread | The game's own thread, where all game code and scripts run |
| Native plugin | A DLL written in C or C++ for BLSE. Python mods can use functions plugins add (see below) |
| Recognized build | A game version BLSE knows exactly (blse.build_id() is not None). Many features need it |
| Traceback | Python's error report: what went wrong, in which file and line |
Your first mod#
Make a folder for your mod, Bin/loader/blse/mods/hello, and inside it a folder blse with a folder scripts in it. Save this as Bin/loader/blse/mods/hello/blse/scripts/hello.py:
import blse
def on_level_init(topic, data):
map, old_map, landmark, loaded = data.split('\0')
blse.log('entered %s' % map)
def hello(name, args):
blse.console_print('Hello from a BLSE mod. You said: %s' % args)
blse.subscribe('blse.level_init', on_level_init)
blse.add_command('hello', hello, 'hello <text>: prints text')Start the game, open the console (~) and type hello world. The mod's lines in Bin/loader/blse/blse.log are marked [python]: walk into another area and the log gets a line entered <map name>.
The folder hello is your mod. BLSE adds it to the load order, Bin/loader/blse/config/loadorder.txt, the first time it sees it. A mod.ini with its name and version is optional (see mod.ini).
What each part does:
| Lines | Mean |
|---|---|
import blse |
Makes BLSE's functions available as blse.<name>. Every mod starts with it |
def on_level_init(topic, data): and the indented lines below it |
A function: a named piece of code that runs only when called. The indented lines (4 spaces) belong to it |
data.split('\0') |
BLSE sends an event's details as one piece of text; this cuts it into its parts: the new map's name, the old map's, ... Each event's parts are listed in Events |
blse.log(...) |
Writes a line to blse.log, the easiest way to see what your mod is doing |
blse.subscribe('blse.level_init', on_level_init) |
"Whenever a level starts, call on_level_init." This is how a mod reacts to the game: pick an event from Events and give the function to run |
blse.add_command('hello', hello, ...) |
Adds the console command hello, which calls the function hello with the text typed after it |
Two mistakes beginners make most: mixing tabs and spaces for indenting (use 4 spaces everywhere), and forgetting the : at the end of a def line. Either one stops the script; the console then shows a traceback, an error message that names the file and the line.
Things to know:
- The interpreter is Python 2.1. Strings are bytes, there is no
jsonorwith, andprintdoes not reach the console: useblse.console_print. - Events are plain messages:
blse.subscribe(topic, function), andfunction(topic, data)is called with the data as a string whose fields are separated by NUL characters (data.split('\0')). - Mods are loaded before the main menu. The game's own objects (
__main__.G, entities) do not exist yet, so do that work in a handler such asblse.level_initorblse.game_loaded. - Keep a mod's state in
G(the game's saved globals) when it should be saved with the game, or use values on entities and the shared database. - Start the names of your topics, entity values, files and console commands with your mod's name so mods do not collide.
- Working examples: the mod folders
examples/mods/killcount,whereami,companion,sprint(withexamples/config/sprint.ini),blse_sampleandguardtalk(the release zip has them underSDK/examples).
How scripts are loaded#
Put a mod's .py files in its mod folder's blse/scripts/ (Bin/loader/blse/mods/<your mod>/blse/scripts/). BLSE runs the scripts at game start, mod by mod in the load order (Bin/loader/blse/config/loadorder.txt) and in alphabetical order inside one mod, after native plugins have loaded and before blse.post_load is published. No changes to the game's own scripts are needed.
- Each file runs once as its own module,
blsemod_<name>insys.modules(sowhereami.pybecomesblsemod_whereami, and other scripts canimport blsemod_whereami). Script names share one namespace across all mods: name yours after your mod. - A mod that is switched off in
loadorder.txt(-name), or whosemod.inirequirements are not met, runs none of its scripts. - File names must be letters, digits and
_, not starting with a digit; other.pyfiles are skipped with a log line. - Scripts run before the main menu: the game's
__main__.Gand level entities do not exist yet. Do that work in event handlers such asblse.level_initorblse.game_loaded. - An error in one script prints its traceback to the game console, is logged in
blse.log, and does not stop the other scripts.SystemExitis ignored. - After an engine restart the game discards all Python state; BLSE runs the scripts again, so they re-subscribe and re-add their commands.
- Files must be plain Python 2.1; Windows or Unix line endings and a UTF-8 BOM are accepted; size limit 4 MiB.
examples/mods/killcount/blse/scripts/killcount.py counts the NPCs the player kills (from OnDeath entity outputs), keeps the counts in G and adds the killcount console command. examples/mods/whereami/blse/scripts/whereami.py is another complete example: it tracks level transitions, keeps a counter in G (saved with the game) and adds the whereami console command.
Game scripts that want to work without BLSE installed can guard the import:
try:
import blse
except ImportError:
blse = NoneAll functions must be called on the game's main thread, which is where game scripts normally run. Calls from other threads raise RuntimeError.
Functions#
| Function | Result |
|---|---|
blse.version() |
Runtime version string, e.g. '0.6' |
blse.build_id() |
ID of the recognized game build, or None for an unknown build |
blse.log(message) |
Writes [python] message to Bin/loader/blse/blse.log |
blse.subscribe(topic, callable) |
Calls callable(topic, data) for every message on topic; returns an integer token |
blse.unsubscribe(token) |
Removes a subscription; returns 1, or 0 if the token was unknown |
blse.publish(topic[, data]) |
Delivers data (a string, may contain NUL bytes; default '') to all subscribers, Python and native, before returning |
blse.add_command(name, callable[, help]) |
Adds a console command; callable(name, args) runs when it is entered (args is the text after the name). Returns 1 |
blse.console_print(text) |
Prints text and a newline to the game console. Python's print does not reach the console in this game |
Topics are case-sensitive strings of 1–128 bytes. Use a prefix for your mod ('mymod.something'). Topics starting with blse. are reserved: subscribing to them is how you receive game events, but publishing to them raises ValueError.
If a subscriber raises an exception, the traceback is printed to the game console and delivery continues with the next subscriber. SystemExit raised from a subscriber is ignored, so it cannot quit the game.
blse.version, blse.build_id#
blse.version() # -> '0.6'
blse.build_id() # -> 'vtmb-x86-...' or Noneversion() returns BLSE's version as a string. build_id() returns the name of the game build BLSE recognized (a long id string), or None for a build BLSE does not know exactly; many features (damage rules, dialogue control, game events) then are not available.
When to use them: tell the player your mod needs a newer BLSE (for that, blse.version_at_least compares versions for you), or skip features on an unknown build.
if blse.build_id() is None:
blse.log('mymod: unknown game build, combat features are off')blse.log#
blse.log(message)Writes the line [python] message to Bin/loader/blse/blse.log (messages longer than 1024 characters are cut). Returns None. The easiest way to see what your mod is doing; start your lines with your mod's name so you find them.
blse.log('mymod: player has %d blood' % blood)blse.subscribe, blse.unsubscribe#
token = blse.subscribe(topic, callable)
blse.unsubscribe(token) # -> 1, or 0subscribe makes BLSE call callable(topic, data) for every message on topic, from now on, until you unsubscribe or the game's Python restarts.
topic: the event's name, 1-128 characters, case-sensitive: one of the game events in Events ('blse.level_init') or another mod's topic ('mymod.found_item').callable: your function; it gets the topic anddata, the payload string (fields separated by'\0'; may be'').- Returns a token (a positive integer) for
unsubscribe. - Errors:
ValueErrorfor an empty or too long topic,TypeErrorifcallablecannot be called. - Subscribe once, at the top of your script. A script that subscribes inside a level event adds one more subscription at every load, and the handler then runs that many times; BLSE warns in the log when Python's subscriptions pass 256, 512, ...
unsubscribe(token) removes the subscription; returns 1, or 0 if the token was unknown (already removed). A handler may unsubscribe itself.
def on_first_level(topic, data):
blse.console_print('Welcome!')
blse.unsubscribe(first_token) # only once
first_token = blse.subscribe('blse.level_init', on_first_level)blse.publish#
blse.publish(topic[, data]) # -> 1, or 0Sends a message of your own to every subscriber, Python mods and native plugins alike, and returns after all of them ran. (Mods use this to talk to each other: one mod publishes 'mymod.door_opened', another subscribes to it.)
topic: 1-128 characters. Topics starting withblse.are reserved for BLSE's game events: publishing one raisesValueError.data: a string (default''); it may contain'\0'to separate fields, like BLSE's own events.- Returns 1 if delivered, 0 if it was refused (for example too many messages inside each other).
- Errors in subscribers are printed and do not stop the delivery.
blse.publish('mymod.found_item', 'item_g_bloodpack\0' + str(count))blse.console_print#
blse.console_print(text)Prints text and a line break to the game console. Returns None. Python's own print does not reach the console in this game.
Console commands#
import blse
def heal(name, args):
amount = 10
if args: amount = int(args.split()[0])
blse.console_print('%s: healing %d' % (name, amount))
blse.add_command('mymod_heal', heal, 'Heals the player. Usage: mymod_heal [amount]')Names are 1-63 characters from letters, digits and _; prefix them with your mod name (blse_ is reserved). Adding a name again from Python replaces its handler. A name the game or a native plugin already uses raises RuntimeError; an invalid name raises ValueError. Exceptions raised by the handler are printed to the console. The built-in command blse lists BLSE's status and all mod commands.
After an engine restart the command stays in the console but answers "not available" until the script calls add_command again, which happens automatically if it runs again on startup.
blse.add_command#
blse.add_command(name, callable[, help]) # -> 1Adds a console command: when the player types name in the console (or a config file or key binding runs it), BLSE calls callable(name, args).
When to use it: a test command while you write your mod, a command players bind to a key (bind F6 mymod_toggle), a way for players to change a setting.
name: 1-63 characters of letters, digits and_. Start it with your mod's name;blse_is reserved.callable: your function.nameis the command as typed (argv[0]),argseverything typed after it, as one string (''if nothing). Split it yourself:args.split().help: a short description, the command's help text (default'').- Returns 1. Errors:
ValueErrorfor an invalid name;RuntimeErrorif the game or a native plugin already uses the name, or the console is not available;TypeErrorifcallablecannot be called. - Calling it again with the same name (any case) replaces your handler.
enabled = 1
def toggle(name, args):
global enabled
enabled = not enabled
blse.console_print('mymod is now %s' % (enabled and 'on' or 'off'))
blse.add_command('mymod_toggle', toggle, 'Turns mymod on or off')(A module variable like enabled is lost when the game closes; keep values that must be saved in __main__.G or with the functions in Saved data.)
Functions from native plugins (blse_native)#
Native plugins can add functions to Python (b0.5.7). They are in the module blse_native, which is also available in __main__ without an import, so game scripts and dialogue conditions can call them directly:
import blse_native
bonus = blse_native.mymod_bonus(FindPlayer().GetName(), 2)
print blse_native.mymod_bonus.__doc__- Arguments and results are
None, ints (longs that fit), floats and strs (NUL bytes allowed). Anything else raisesTypeError. - A function that fails raises
RuntimeErrorwith the plugin's message. dir(blse_native)lists the functions that are loaded. A function whose plugin was disabled after a crash raisesRuntimeError("not available"), even through a reference kept earlier.- The functions run synchronously, in the calling Python code.
Native plugins#
blse.plugin_version(name) returns the version of a loaded native plugin (by its declared or file name, any case; '' if it declares none) or None if it is not loaded; blse.plugins() lists (name, version) for every loaded plugin (b0.5.26). Use them to work together with a plugin only when it is there.
blse.plugin_version, blse.plugins#
blse.plugin_version(name) # -> '1.2', '' or None
blse.plugins() # -> [('sprint', '1.0'), ...](b0.5.26)
plugin_version(name):nameis the plugin's declared name or its file name without.dll, in any case. Returns its version string,''if it declares none, orNoneif no such plugin is loaded.plugins(): a list of(name, version)pairs, one per loaded plugin.
if blse.plugin_version('sprint') is not None:
blse.log('mymod: the sprint plugin is here, using its functions')A plugin a Python mod depends on is usually reached through its blse_native functions or its own topics.