BLSE0.6

Python mods

Getting started with Python mods

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, no with, no json module, and print does not reach the console. Use blse.console_print and blse.log.
  • Every blse function 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 raise RuntimeError).
  • 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:

Python
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 json or with, and print does not reach the console: use blse.console_print.
  • Events are plain messages: blse.subscribe(topic, function), and function(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 as blse.level_init or blse.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 (with examples/config/sprint.ini), blse_sample and guardtalk (the release zip has them under SDK/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> in sys.modules (so whereami.py becomes blsemod_whereami, and other scripts can import 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 whose mod.ini requirements are not met, runs none of its scripts.
  • File names must be letters, digits and _, not starting with a digit; other .py files are skipped with a log line.
  • Scripts run before the main menu: the game's __main__.G and level entities do not exist yet. Do that work in event handlers such as blse.level_init or blse.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. SystemExit is 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:

Python
try:
    import blse
except ImportError:
    blse = None

All 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#

Python
blse.version()    # -> '0.6'
blse.build_id()   # -> 'vtmb-x86-...' or None

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

Python
if blse.build_id() is None:
    blse.log('mymod: unknown game build, combat features are off')

blse.log#

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

Python
blse.log('mymod: player has %d blood' % blood)

blse.subscribe, blse.unsubscribe#

Python
token = blse.subscribe(topic, callable)
blse.unsubscribe(token)   # -> 1, or 0

subscribe 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 and data, the payload string (fields separated by '\0'; may be '').
  • Returns a token (a positive integer) for unsubscribe.
  • Errors: ValueError for an empty or too long topic, TypeError if callable cannot 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.

Python
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#

Python
blse.publish(topic[, data])   # -> 1, or 0

Sends 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 with blse. are reserved for BLSE's game events: publishing one raises ValueError.
  • 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.
Python
blse.publish('mymod.found_item', 'item_g_bloodpack\0' + str(count))

blse.console_print#

Python
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#

Python
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#

Python
blse.add_command(name, callable[, help])   # -> 1

Adds 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. name is the command as typed (argv[0]), args everything 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: ValueError for an invalid name; RuntimeError if the game or a native plugin already uses the name, or the console is not available; TypeError if callable cannot be called.
  • Calling it again with the same name (any case) replaces your handler.
Python
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:

Python
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 raises TypeError.
  • A function that fails raises RuntimeError with the plugin's message.
  • dir(blse_native) lists the functions that are loaded. A function whose plugin was disabled after a crash raises RuntimeError ("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#

Python
blse.plugin_version(name)   # -> '1.2', '' or None
blse.plugins()              # -> [('sprint', '1.0'), ...]

(b0.5.26)

  • plugin_version(name): name is the plugin's declared name or its file name without .dll, in any case. Returns its version string, '' if it declares none, or None if no such plugin is loaded.
  • plugins(): a list of (name, version) pairs, one per loaded plugin.
Python
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.