BLSE0.6

Native plugins

Getting started with native plugins

On this page 34 sections

A native plugin is a small program library (a DLL) written in C or C++ that BLSE loads into the game. It can do everything a Python mod can, plus things only compiled code can do: draw with Direct3D, hook game functions, run heavy computations quickly, use threads and Windows APIs.

Come here when you want to write a plugin and need to know how one is built, loaded and talks to BLSE. If you only want to react to game events, change items, add dialogue or show text, a Python mod is easier: no compiler, no crashes that take the game down, and the same features.

This page explains:

  • the plugin contract (what your DLL must export, when it runs, what happens when it returns);
  • the Host API: the table of functions BLSE hands to your plugin, the rules every one of them follows, and an index of all of them with links to their full descriptions;
  • messaging (subscribing to events, publishing your own messages), plugin information and dependencies, and work from other threads.

The other plugin pages are the reference for what plugins can read and change: events, filters and game rules, the world, saved data and settings, interface, drawing with Direct3D 9, conversations, hooks and crash reports and working with the game's Python. Some behaviour is described in full only once, in the matching page of the Python reference; the plugin pages then say what differs.

A minimal plugin#

Save this as myplugin.c next to a copy of include/blse_api.h (the release zip has it under SDK/include):

C
#include "blse_api.h"

static const BLSE_HostAPI *host; /* kept for later calls; valid until the game exits */

/* Called by BLSE every time a level starts. */
static void BLSE_CALL on_level_init(const char *topic, const void *payload, size_t size, void *user) {
    /* The payload starts with the map name, followed by a NUL byte. */
    char line[160] = "entered ";
    size_t used = 8, i;
    for (i = 0; i < size && ((const char *)payload)[i] && used + 1 < sizeof line; ++i)
        line[used++] = ((const char *)payload)[i];
    line[used] = '\0';
    host->log(line);
}

BLSE_EXPORT int BLSE_CALL BLSE_PluginLoad(const BLSE_HostAPI *h) {
    if (h->api_version != BLSE_API_VERSION) return 0;   /* an incompatible BLSE: do not load */
    if (!BLSE_HOST_HAS(h, subscribe)) return 0;          /* a BLSE too old for what we use */
    host = h;
    host->log("hello from myplugin");
    return host->subscribe(host->context, BLSE_MSG_LEVEL_INIT, on_level_init, NULL) != 0;
}

Build it as a 32-bit DLL from an "x86 Native Tools Command Prompt for VS" (Visual Studio's 32-bit build prompt):

batch
cl /nologo /LD /MT /I. myplugin.c /Femyplugin.dll

Plugins live in mod folders. In the game folder, make a folder for your mod, Bin/loader/blse/mods/myplugin, and inside it a folder blse with a folder plugins in it. Copy myplugin.dll to Bin/loader/blse/mods/myplugin/blse/plugins/ and start the game. Bin/loader/blse/blse.log then shows plugin myplugin: myplugin.dll: loaded (the mod folder, then the file), the line [myplugin] hello from myplugin, and [myplugin] entered <map> every time a level starts. The console command blse lists the loaded plugins.

Read this before you write more#

  • Main thread only. Call host functions only from the thread that called BLSE_PluginLoad (the game's main thread) or from callbacks BLSE calls. From any other thread they fail and return 0. Use add_task to get back to the main thread.
  • Borrowed strings. Strings and payloads BLSE passes to your callbacks are valid only until the callback returns. Copy what you want to keep.
  • Check before you use a field. Use BLSE_HOST_HAS(host, field) for every Host API field after log. An older BLSE has a shorter table, and calling a field it does not have crashes the game. See Checking that a field exists.
  • A crash disables your plugin. If your code crashes inside a callback BLSE called, BLSE stops calling your plugin for the rest of the session, and the game may be unstable. Crashes in your own threads and your own hooks end the game. See When a plugin crashes.
  • No game yet at load time. BLSE_PluginLoad runs before the main menu. No level runs, the game's Python globals (G) do not exist, and functions that need them (save data, entities, inventories) return 0. Do that work in event handlers, such as blse.level_init or blse.game_loaded.
  • Most features need a recognized game build. Functions that work with the game's internals (damage, dialogue, inventories, entities) are only available on the analyzed game build (the Unofficial Patch's executables); on others they return 0. build_profile tells you which build runs.

Words used on the plugin pages#

Word Meaning
Host API The structure BLSE_HostAPI (in blse_api.h) that BLSE passes to BLSE_PluginLoad. Its fields are the functions and values your plugin uses. "BLSE" is the host, your DLL the plugin
Field One entry of that structure, such as host->log or host->subscribe
context A pointer that identifies your plugin to BLSE. You pass host->context as the first argument of almost every function. See context
Main thread The thread on which the game runs its frames, scripts and interface. BLSE_PluginLoad and all BLSE callbacks run on it
Callback A function of yours that BLSE calls later: a message handler, a console command, a filter, a task
Topic The name of a message, such as "blse.level_init". See Messaging
Payload The data that comes with a message: size bytes, often several text fields separated by NUL bytes
NUL, NUL-separated NUL is the byte with the value 0 ('\0' in C). "NUL-separated" means several text fields put one after the other with a 0 byte between them. blse.level_init's payload, for example, is sm_pawnshop_1, NUL, sm_hub_1, NUL, haven1, NUL, 0
NUL-terminated A C string: text followed by a 0 byte that marks its end
Level One loaded map with its entities. "While a level runs" means between blse.level_init and blse.level_shutdown; the main menu shows no level
Entity Any object in a level: the player, NPCs, doors, items lying around, triggers, sounds. Entities have a classname (their kind, such as npc_VVampire) and often a targetname (their name in the map, such as guard_1)
Entity index, handle Two ways to name an entity. The index is its slot number in the server's entity list (the player is 1, all are below 8192); a slot is reused when an entity goes. The handle is a number that names one entity for as long as it exists (its index plus a serial number), so an old handle never points to a new entity. Functions that take an entity accept either. BLSE_NO_ENTITY (0xFFFFFFFF) means "no entity"
Recognized build A game executable and DLLs that BLSE has analyzed. host->build_profile is non-NULL on one
Safe point A moment when BLSE delivers queued messages to Python: the end of a server frame, and just before blse.level_init and blse.level_shutdown
Output buffer A char array of yours that a function writes text into. See Output buffers

The plugin contract#

A plugin is a 32-bit (x86) DLL in a mod folder's blse/plugins/ (Bin/loader/blse/mods/<your mod>/blse/plugins/) that exports this function:

C
#include "blse_api.h"

BLSE_EXPORT int BLSE_CALL BLSE_PluginLoad(const BLSE_HostAPI* host);

In C++, declare it extern "C" so the exported name is exactly BLSE_PluginLoad. A DLL without that export is skipped (the log says no BLSE_PluginLoad export; skipped).

Return nonzero to stay loaded, or zero to be unloaded. Plugins are loaded once, at game startup, on the main thread: the plugins of all loaded mod folders as one set, dependencies first, otherwise in alphabetical order of their file names. Loaded plugins stay loaded until the game exits. The full contract is in the comments of include/blse_api.h; this page summarizes it.

In more detail:

  • When. BLSE loads all plugins once, at game startup, before the main menu and before any Python mod runs. Plugins with declared dependencies are loaded after the plugins they need; the rest load in alphabetical order of their file names (case is ignored). When every plugin and Python mod has loaded, BLSE publishes blse.post_load.
  • Which plugins. BLSE reads the blse/plugins/ folder of every loaded mod folder and loads all their DLLs as one set: which mod a plugin comes from does not change its place in the order. A mod switched off in config/loadorder.txt (-name), or whose mod.ini requirements are not met, loads none of its plugins. If two mods ship a DLL with the same file name, the one from the mod later in loadorder.txt is loaded and the other is logged as not loaded.
  • Return value. Return nonzero (normally 1) to stay loaded. Return 0 to decline: BLSE then removes everything the plugin set up (subscriptions, console commands, filters, Python functions, HUD elements, queued tasks, outfits, item classes, conversations), undoes its game patches, and unloads the DLL. A declining plugin must stop any threads it started and leave nothing else behind.
  • Exceptions and crashes. A C++ exception that leaves BLSE_PluginLoad counts as declining (return 0). A crash (an access violation or another hardware fault) disables the plugin instead: the DLL stays in memory, BLSE never calls it again and does not try to load it again. See When a plugin crashes.
  • No unload call. There is no "unload" function: a loaded plugin stays loaded until the game process exits. The game's in-process engine restart (the console command _restart) does not reload plugins; they keep their subscriptions and receive blse.engine_restart.
  • Log lines. For each DLL the log says what happened, with the mod folder first, for example plugin myplugin: myplugin.dll: loaded, plugin myplugin: myplugin.dll: declined to load (BLSE_PluginLoad returned 0) or plugin myplugin: myplugin.dll: could not be opened (missing dependency or not a 32-bit DLL?).

Plugin information and dependencies#

Since b0.5.26 a plugin can declare who it is and what it needs, with one line (blse_api.h):

C
BLSE_PLUGIN_INFO("mymod", "1.2.0", "Your name", "What it does", "b0.5.26", "otherplugin>=1.0, thirdplugin");

The fields are the plugin's name (its id for dependencies and queries; NULL uses the file name), its version (dotted numbers), author, description, the oldest BLSE version it works with (NULL: any) and the plugins it needs (NULL: none), each optionally with >=version. BLSE reads this before running any plugin code (the DLL is mapped without running it, then released), then:

  • loads dependencies before the plugins that need them (otherwise plugins load by file name);
  • refuses a plugin, with the reason in the log, when BLSE is too old, a dependency is not installed, too old, refused itself or part of a cycle, or another plugin already has the name:
text
plugin meta_missing.dll: not loaded: needs the plugin 'nothere', which is not installed
plugin meta_tooold.dll: not loaded: needs 'metabase' 2.0 or newer (installed: 1.2.0)
plugin meta_base.dll: loaded (metabase 1.2.0 by BLSE tests)
plugin aaa_dependent.dll: loaded (dependent 0.1 by BLSE tests)

From BLSE_PluginLoad on, plugin_version(context, name, buffer, capacity, size) tells whether another plugin is loaded (by its declared or file name, any case) and copies its version; list_plugins gives all of them as name version lines. Python: blse.plugin_version(name) (a string, '' without a version, None if not loaded) and blse.plugins() (a list of (name, version)). The console command blse lists the plugins with versions and authors. Plugins without BLSE_PLUGIN_INFO load as before, named by their file name.

BLSE_PLUGIN_INFO#

C
BLSE_PLUGIN_INFO(name, version, author, description, requires_blse, dependencies);

Declares your plugin's identity and needs. Put the line once, at file level (outside any function), in one of your plugin's source files. It exports a constant structure named BLSE_Plugin that BLSE reads without running your code. It is optional: a plugin without it loads as before, with its file name as its name and no version.

When to use it: always, if other plugins or Python mods may want to work with yours, or if your plugin needs a newer BLSE or another plugin. A plugin that adds features to another (say, an add-on for a big gameplay mod) declares that mod as a dependency, so it loads after it, and players get a clear log line instead of a crash when it is missing.

Parameter Meaning
name Your plugin's id: what other plugins write in their dependencies and pass to plugin_version. 1 to 63 characters from A-Z a-z 0-9 _ . -. NULL uses the DLL's file name without .dll
version Your plugin's version: numbers separated by dots, such as "1.2.0", at most 31 characters, optionally after a short letter prefix ("b0.5.26"). NULL or "": no version
author, description Free text shown by the blse console command and in the log (up to 127 and 511 bytes). NULL for none
requires_blse The oldest BLSE version your plugin works with, such as "b0.5.26" or "0.6". Letter prefixes are ignored when versions are compared, so "b0.5.26" means 0.5.26. NULL: any version. Note that the version field you check needs BLSE_HOST_HAS in any case
dependencies The plugins yours needs, separated by commas (or semicolons), each a plugin name optionally followed by >=version: "otherplugin>=1.0, thirdplugin". NULL or "": none

What BLSE does with it:

  • Dependencies are loaded first. A plugin is refused (not loaded, with the reason in the log) when BLSE is older than requires_blse, a dependency is missing or older than asked, a dependency was refused itself, dependencies form a cycle, or another plugin (earlier by file name) already uses the same name.
  • Invalid information (a bad name, a bad version, an unreadable string) also refuses the plugin, with the reason in the log.
  • The name is how other code finds your plugin: plugin_version and list_plugins here, blse.plugin_version() and blse.plugins() in Python.

Log lines still show the file name: a plugin mymod_core.dll declaring the name mymod writes [mymod_core] ... from host->log.

plugin_version#

C
int plugin_version(void *context, const char *name, char *buffer, size_t capacity, size_t *size);

Tells whether another plugin is loaded and gives its version. (b0.5.26)

When to use it: to work together with another plugin only when it is there. A HUD plugin can, for example, show a compass only when a map plugin is installed, or check that the other plugin is new enough for a message format.

  • name: the plugin's declared name or its file name without .dll; case is ignored.
  • buffer, capacity, size: receive the version text, "" if that plugin declares none. They follow the output buffer rules.
  • Returns 1 if the plugin is loaded and not disabled after a crash, 0 if it is not loaded, was disabled, or the call came from another thread.
  • Works from BLSE_PluginLoad on. Your dependencies are loaded before you, so you can always see them there; other plugins may load after you, so check for those in a blse.post_load handler.
C
char version[32]; size_t n;
if (BLSE_HOST_HAS(host, plugin_version) &&
    host->plugin_version(host->context, "mapmod", version, sizeof version, &n)) {
    /* mapmod is loaded; version holds e.g. "2.1.0" */
}

Python: blse.plugin_version(name) returns the version ('' without one) or None.

list_plugins#

C
int list_plugins(void *context, char *buffer, size_t capacity, size_t *size);

Copies the names and versions of all loaded plugins (not disabled ones). (b0.5.26)

When to use it: for a diagnostics command or an "about" window that lists what is installed.

  • The text has one line per plugin, name version (a space between; the version is empty when the plugin declares none), lines separated by '\n' (no newline after the last one). It follows the output buffer rules.
  • Returns 1, or 0 when called from another thread.
C
char all[2048]; size_t n;
if (host->list_plugins(host->context, all, sizeof all, &n)) host->console_print(host->context, all);

Python: blse.plugins() returns a list of (name, version) tuples.

Host API#

BLSE_PluginLoad receives a pointer to a BLSE_HostAPI structure. Its fields are the functions your plugin calls (and a few values). You call a function through the pointer: host->log("text"), host->subscribe(host->context, ...).

The table below is the original quick list of the oldest fields. The index of all fields further down lists every field, grouped by subject, with a link to its full description.

Field Since Purpose
struct_size, api_version 0.1 Compatibility checks: the size of the structure BLSE gives you, and the version of its layout. See struct_size and api_version
log(message) 0.1 Writes a line to blse.log, prefixed with the plugin's file name (e.g. [savenotes_plugin] ...) when called from the plugin's load function, message callbacks or console commands; [plugin] otherwise
context 0.1 Pass back to the messaging functions (and to every other function that takes it): it tells BLSE which plugin calls
subscribe(context, topic, callback, user) 0.1 Returns a nonzero token, or 0 on failure
unsubscribe(context, token) 0.1 Removes one of the calling plugin's subscriptions
publish(context, topic, payload, size) 0.1 Delivers synchronously to all subscribers, native and Python
runtime_version 0.2 For example "0.6"
build_profile 0.2 ID of the recognized game build, or NULL
save_set(context, key, data, size) 0.3 Stores bytes in the save game; NULL/0 removes the key (details)
save_get(context, key, buffer, capacity, &size) 0.3 Reads a stored value (pass NULL/0 to query its size)
add_command(context, name, help, callback, user) 0.4 Adds a console command (details)
console_print(context, text) 0.4 Prints text to the game console as is
python_exec(context, code) b0.5.2 Runs Python statements in the game's __main__ (1 ran, 2 queued, 0 error) (details)
python_eval(context, expression, buffer, capacity, &size) b0.5.2 Evaluates a Python expression and returns str(result)
add_damage_filter(context, filter, user) b0.5.3 Sees and may change every hit before the game applies it (details)
... Later fields: see the index below and blse_api.h
add_task(context, task, user) b0.5.27 Runs task(user) on the main thread; callable from any thread
player_target(context, kind, buffer, capacity, &size) b0.5.27 The entity under the crosshair, being used, or the Discipline target; kind 3 (b0.5.34): the console's !picker
hook_function, hook_call, hook_pointer, unhook, find_pattern b0.5.28 Hooks through BLSE, checked and recorded (see Hooks)
cvar_get, cvar_set, item_info, list_items, add_input_filter b0.5.29 Console variables, item data, input filters
open_menus, menu_panels, menu_set_text, menu_set_visible, menu_move, camera_get, camera_set, play_sound, stop_sound b0.5.30 What game screens show; camera and field of view; sounds

Compatibility rules:

  • api_version changes only on incompatible changes. New fields are only ever appended. Since 0.6 this is a promise: later versions never change or remove a field, a function's arguments or a message's payload, so a plugin built for one version keeps working with later ones.
  • Check BLSE_HOST_HAS(host, field) for every field after log that you use. Do not require struct_size == sizeof(BLSE_HostAPI): that would reject both older and newer hosts.
  • The structure (and context) stay valid for the whole game session, so you may keep the host pointer (since b0.5.6).

Rules every Host API function follows#

These rules hold for every function in the table unless its own entry says otherwise. They are not repeated in every entry.

Threads#

Call host functions on the game's main thread: in BLSE_PluginLoad, or in a callback BLSE called (a message handler, a console command, a filter, a task, a draw callback). From any other thread they do nothing and return their failure value (0, or -1 or BLSE_NO_ENTITY where that is the failure value).

Exceptions, which work from any thread:

  • add_task, to get work back to the main thread;
  • log (lines from other threads are prefixed [plugin] instead of your plugin's name);
  • translate, translation_language, translate_text, frame_stats, json_get and json_set.

Checking that a field exists#

The Host API grows: every BLSE version may add fields at the end of the structure. A player may run your plugin with an older BLSE whose structure is shorter. If you read a field that BLSE's structure does not have, you read whatever memory follows it, and calling that "function" crashes the game.

struct_size says how long BLSE's structure is. The macro BLSE_HOST_HAS(host, field) compares it with the position of a field and is true when BLSE has that field:

C
if (BLSE_HOST_HAS(host, hud_box)) {
    host->hud_box(host->context, "panel", 0.1f, 0.1f, 0.3f, 0.2f, 0x00000080u, 0);
} else {
    host->log("this BLSE is too old for the panel (needs b0.5.24)");
}

Check every field after log that you use, once, at load time (store the result in a variable if you check often). If your plugin cannot work without a field, return 0 from BLSE_PluginLoad with a log line that names the BLSE version it needs, or declare it with requires_blse in BLSE_PLUGIN_INFO.

A newer BLSE has a longer structure; that is fine. Never compare struct_size with sizeof(BLSE_HostAPI) for equality: that rejects both older and newer BLSE versions.

Return values#

  • Most functions return int: 1 for success, 0 for failure (bad arguments, wrong thread, nothing found, no level running, not available on this game build). Each entry lists the reasons.
  • Some functions return -1 for "not available on this game build" or "unknown", where 0 is a real answer (for example key_state, item_count, lookup_sequence).
  • Functions that return entities return a handle, or BLSE_NO_ENTITY (0xFFFFFFFF) for none.
  • Most functions do not log why they failed. Functions that do (dialogue content, outfits, item classes) write the reason to blse.log under your plugin's name.

Strings you pass in#

Text is UTF-8 and NUL-terminated. BLSE copies strings you pass before the function returns (unless an entry says otherwise), so you may free or reuse your buffers afterwards. Pointers you pass as user are kept as they are and handed back to your callback: they must stay valid as long as the callback is registered.

Strings and payloads you receive#

Strings BLSE passes to your callbacks (topics, payloads, command arguments, the strings inside filter structures) belong to BLSE and are valid only until your callback returns. Copy anything you want to keep. Never write into them, except into fields documented as writable.

runtime_version and build_profile are the exception: they stay valid until the game exits.

Output buffers (buffer, capacity, size)#

Functions that give you text take three parameters at the end:

Parameter Meaning
char *buffer Your array that receives the text. May be NULL to ask only for the length
size_t capacity The size of your array in bytes (use sizeof buffer for an array). 0 when buffer is NULL
size_t *size Receives the full length of the text in bytes, without the terminating NUL. May be NULL if you do not need it

The text is always NUL-terminated and cut to capacity - 1 bytes if it is longer. So if *size >= capacity, the text was cut: allocate *size + 1 bytes and call again. On failure, most functions set *size to 0 and buffer to "".

C
/* Read a value of any length. */
size_t n = 0;
char small[64];
if (host->cvar_get(host->context, "fov", small, sizeof small, &n)) {
    if (n >= sizeof small) {                 /* cut: ask again with enough room */
        char *big = (char *)malloc(n + 1);
        if (big && host->cvar_get(host->context, "fov", big, n + 1, &n)) { /* use big */ }
        free(big);
    } else { /* use small */ }
}

Exceptions: save_get and save_get_versioned copy raw bytes (no NUL is added, buffer is not cleared on failure), and list_inventory copies names that are each followed by a NUL, without adding another one. Their entries say so.

Lists come in a few shapes, and each entry says which: lines separated by '\n', lines each followed by '\n', tab-separated columns ('\t') in lines, or records separated by '\x1e' with fields separated by '\x1f' (for text that may itself contain line breaks).

struct_size#

C
uint32_t struct_size;

The size in bytes of the BLSE_HostAPI structure that this BLSE version filled in. (0.1)

You rarely read it yourself: the macro BLSE_HOST_HAS(host, field) uses it to tell whether a field exists. See Checking that a field exists. Several structures you pass to BLSE (BLSE_ItemInfo, BLSE_FrameStats, BLSE_Animation) have a struct_size field of their own: there you set it to sizeof the structure before the call, so BLSE knows which version of the structure you were compiled with.

api_version#

C
uint32_t api_version;

The version of the Host API's layout; BLSE_API_VERSION (currently 1) in the header you compile with. (0.1)

It changes only if BLSE ever makes an incompatible change (removes or changes a field). Adding fields does not change it. Check it once in BLSE_PluginLoad and decline to load if it differs from the version you were built for:

C
if (host->api_version != BLSE_API_VERSION) return 0;

log#

C
void log(const char *message);

Writes one line to Bin/loader/blse/blse.log. (0.1)

When to use it: to record what your plugin does and why something failed. It is the first place players and you look when a mod misbehaves. Unlike most functions it takes no context.

  • message: the text, without a trailing newline. Lines longer than 1100 bytes are cut. NULL is ignored.
  • The line is prefixed with your plugin's file name in brackets ([myplugin] hello) when called from BLSE_PluginLoad, a message handler, a console command or another callback BLSE called. From other threads, or code BLSE did not call, the prefix is [plugin].
  • May be called from any thread.
C
host->log("settings loaded");                   /* blse.log: [myplugin] settings loaded */

context#

C
void *context;

A pointer that identifies your plugin to BLSE. Pass host->context as the first argument of every host function that takes one. (0.1)

BLSE uses it to know who calls: which plugin owns a subscription, a console command, a HUD element, a hook or a rule; which name to put in log lines; and what to remove when your plugin declines to load or is disabled after a crash. Do not look inside it, and never pass another plugin's context. It stays valid until the game exits, so you may keep it in a static variable (since b0.5.6).

runtime_version, build_profile#

C
const char *runtime_version;
const char *build_profile;

Two strings that describe the running BLSE and game. (0.2)

  • runtime_version: BLSE's version, for example "0.6". Use it in log lines and bug reports. To check whether a feature exists, use BLSE_HOST_HAS, not this string.
  • build_profile: the id of the game build BLSE recognized, such as "vtmb-x86-80d73e74-9d00b2c1-c546f4de-2ce854dd" (the Unofficial Patch with its executable loader), or NULL when BLSE does not know this build. On an unknown build, features that need the game's internals (damage, dialogue, inventories, entity events and most entity functions) are unavailable: their functions return 0 and their events are never published.

Both stay valid until the game exits.

C
if (!host->build_profile) host->log("unknown game build: only basic features work");

add_task#

C
int add_task(void *context, BLSE_TaskFn task, void *user);
typedef void (BLSE_CALL *BLSE_TaskFn)(void *user);

Runs task(user) later on the game's main thread. This is the one host function you may call from any thread. (b0.5.27)

Details and an example are in Work from other threads.

Index of all Host API fields#

Every field, grouped by subject, with the page where it is described in full. "Since" is the BLSE version that added it.

Basics, messaging and plugins (this page)#

Field Since What it does
struct_size 0.1 Size of the structure, for BLSE_HOST_HAS
api_version 0.1 Layout version; decline if it is not BLSE_API_VERSION
log 0.1 Writes a line to blse.log
context 0.1 Your plugin's identity; pass it to every function
subscribe 0.1 Calls your function for every message on a topic
unsubscribe 0.1 Stops one subscription
publish 0.1 Sends a message to all subscribers, native and Python
runtime_version, build_profile 0.2 BLSE's version; the recognized game build or NULL
plugin_version, list_plugins b0.5.26 Whether another plugin is loaded, its version; all plugins
add_task b0.5.27 Runs a function on the main thread; callable from any thread

Console and the game's Python (python-bridge.md)#

Field Since What it does
add_command, console_print 0.4 Adds a console command; prints text to the console
python_exec, python_eval b0.5.2 Runs Python statements; evaluates a Python expression
add_python_function b0.5.7 Makes a C function callable from Python as blse_native.<name>

Events, filters, input and game rules (events-and-filters.md)#

Field Since What it does
add_damage_filter b0.5.3 Sees and changes every hit before the game applies it
list_inventory b0.5.12 The items a character carries
add_discipline_filter b0.5.20 Blocks Disciplines or changes their blood cost
add_dialog_filter b0.5.22 Hides dialogue replies or changes the text shown
key_state, key_binding, block_key, press_key b0.5.24 Keys: held or not, what they are bound to, blocking, simulated presses
control_state b0.5.26 Whether a control (+jump, +attack, ...) is held
player_target b0.5.27 The entity under the crosshair, being used, or targeted
cvar_get, cvar_set b0.5.29 Reads and sets console variables
item_info, list_items b0.5.29 An item's data; the items the game has loaded
add_input_filter b0.5.29 Sees, blocks or changes every input an entity receives
combat_npcs b0.5.33 The NPCs fighting now and their targets
item_set, item_reset b0.5.33 Changes an item's name, prices, weight; undoes the changes
cvar_list, read_file, find_files, item_files b0.5.35 All console variables; reads a game file; finds files; all item files
damage_entity, entity_velocity, push_entity b0.5.41 Hurts an entity; reads and changes its velocity
fix_state b0.5.41 Always -1 since b0.5.49; kept for the layout
console_command b0.5.41 Runs a console command line

Interface (interface.md)#

Field Since What it does
show_text b0.5.16 Shows text in the middle, the top-left corner or the console
hud_set, hud_remove b0.5.18 HUD text elements drawn over the game
hud_box, hud_image, hud_clickable, hud_cursor b0.5.24 Boxes, images, clickable elements, the mouse pointer
hud_input, hud_focus, hud_input_text, hud_align, hud_size b0.5.25 Text entry fields; alignment; element sizes
hud_valign b0.5.26 Vertical alignment of text
open_menus, menu_panels, menu_set_text, menu_set_visible, menu_move b0.5.30 What the game's screens show; changing their panels
menu_set_image, menu_press, menu_command, menu_add_label, menu_remove b0.5.31 More changes to screens; labels from mods
menu_add_button, menu_add_image b0.5.32 Buttons and images from mods
window_create, window_show, window_remove, menu_add_text_entry, hud_multiline, hud_hint b0.5.36 Mods' own windows; text fields; multi-line HUD inputs
menu_set_colors, menu_set_font, menu_reset_style, menu_set_hint, menu_set_border b0.5.37 Restyling panels
translate_text b0.5.43 Translates a $KEY text
menu_add_checkbox, menu_add_slider, menu_add_dropdown, menu_add_progress, menu_add_list, menu_list_add, menu_list_clear, menu_value, menu_set_value, menu_set_items, menu_block, hud_in_menus b0.5.46 Controls on screens; blocking screens; HUD over menus
hud_line, hud_polygon, hud_rotate, hud_font, screen_size b0.5.49 Lines, shapes, rotation, fonts; the screen size
set_chargen_clan_rule, add_chargen_clan_filter b0.5.49 Clans while a character is made
set_button_sound, clear_button_sounds, play_ui_sound b0.5.49 Sounds for buttons; interface sounds

Drawing (drawing.md)#

Field Since What it does
add_draw_callback, remove_draw_callback, d3d9_device, add_device_callback b0.5.49 Drawing with the game's Direct3D 9 device

The world, saved data and settings (world-and-data.md)#

Field Since What it does
save_set, save_get 0.3 Data saved with the save game
save_set_versioned, save_get_versioned b0.5.25 The same, with a format version
camera_get, camera_set, play_sound, stop_sound b0.5.30 First or third person and field of view; sounds
camera_view, play_sound_at, play_sound_on, stop_sound_on, sound_length b0.5.31 Where the view is; positional sounds; a sound's length
camera_override b0.5.32 Renders the game from another place
ini_get, ini_set b0.5.37 Settings files in config/
bone_names, bone_position, model_scale, set_model_scale b0.5.38 A model's bones; scaling a model
translate, translation_language, frame_stats b0.5.38 Translated strings; the language; frame rate
entity_data_set, entity_data_get, entity_data_keys, entity_data_find b0.5.40 Values stored on entities, saved with the game
entity_position, find_entities b0.5.40 Where an entity is; entities near a point
json_get, json_set b0.5.40 JSON files in data/
db_get, db_set b0.5.42 The database shared with Python mods, saved with the game
fire_input, set_relationship, relationship, set_enemy, npc_enemy, start_schedule, npc_schedule, npc_state, move_npc, follow_npc, npc_senses, set_npc_sense, npc_leader b0.5.45 NPC AI
spawn_entity, teleport_entity, give_item, remove_item, item_count, equip_item, drop_item, move_item b0.5.46 Spawning, teleporting, inventories
bone_transform, set_bone_transform, reset_bones, modified_bones, flex_names, flex_value, set_flex, release_flex, entity_skin, set_entity_skin, body_groups, set_body_group, render_color, set_render_color, set_render_mode, set_entity_material, model_materials, entity_materials b0.5.47 Appearance: bones, faces, skins, colour, materials
provide_file, add_file_filter, add_map_filter, list_mods, mod_file_owner, mod_info b0.5.48 Mod folders and file filters
set_companion, list_companions, block_companion_map b0.5.48 Companions
npc_conditions, npc_weapon b0.5.48 Why an NPC does not fire
list_sequences, lookup_sequence, play_animation, stop_animation, animation_state, set_animation_rule, add_animation_filter, activity_id, activity_name b0.5.49 Animation
add_model_animations, model_animations b0.5.49 New animations for models
add_item_class, item_classes b0.5.49 New item classes
add_outfit, wear_outfit, worn_outfit, list_outfits, set_player_model, player_model 0.6 Player outfits

Conversations (conversations.md)#

Field Since What it does
add_dialog, extend_dialog, add_dialog_reply, set_keyvalue b0.5.48 Conversations from mods; an entity's keyvalues

Hooks (hooks-and-crashes.md)#

Field Since What it does
hook_function, hook_call, hook_pointer, unhook, find_pattern b0.5.28 Patching game code through BLSE

Messaging#

BLSE and mods talk through messages. A message has a topic (its name, such as "blse.level_init") and a payload (bytes of data). You subscribe a function to a topic, and BLSE calls it for every message published on that topic. BLSE publishes the game's events this way (levels starting, saves, damage, dialogue, ...), and plugins and Python mods can publish their own topics to talk to each other.

  • Callbacks receive (topic, payload, size, user). Topic and payload are borrowed for the duration of the call; copy anything you need later.
  • All host calls must be made on the main thread; calls from other threads fail and return 0. The one exception is add_task (below).
  • Delivery is synchronous. Subscribers added during a dispatch do not receive that dispatch, and a subscriber removed before its turn is skipped. Nested publishing is limited to 16 levels. Messages are not queued or replayed.
  • Topics starting with blse. are reserved for BLSE: plugins can subscribe to them but not publish them. See include/blse_api.h for the event topics and their payloads; they are the same as in the Python events.
  • A plugin that returns 0 or throws a C++ exception from BLSE_PluginLoad has its subscriptions removed before it is unloaded. It must not leave threads or hooks behind.

A crash in a callback (an access violation or another native fault) or a C++ exception that leaves it disables the plugin (see When a plugin crashes); a C++ exception thrown from BLSE_PluginLoad declines the load. Catch your own exceptions.

"Synchronous" means publish calls every subscriber, one after the other, before it returns. "Messages are not queued or replayed" means a message reaches only the functions subscribed at that moment: subscribe early (in BLSE_PluginLoad) so you do not miss events.

Topics#

  • A topic is a case-sensitive string of 1 to 128 bytes.
  • Use your mod's name as a prefix for your own topics: "mymod.ready", "mymod.score_changed". Then they never collide with other mods' topics.
  • Topics starting with blse. are BLSE's. You may subscribe to them (this is how you receive game events), but publish refuses them. The constants BLSE_MSG_* in blse_api.h name them. Each event, when it fires and what its payload holds, is described on the page for its subject: events and filters and the Python events.

Payloads#

A payload is size bytes at payload. Your own topics may carry any bytes (a struct, a number, text). BLSE's events carry:

  • nothing (size 0) for some events, such as blse.level_shutdown;
  • one text, such as a map name, for others;
  • several text fields separated by NUL bytes for most of them, such as blse.level_init: map, previous map, landmark, "1"/"0".

A payload is not NUL-terminated: always use size. This helper copies one field of a NUL-separated payload into a C string; the examples on the other plugin pages use it:

C
#include <string.h>

/* Copies field number `index` (0 = the first) of a NUL-separated payload into `out`
   (NUL-terminated, cut to `capacity - 1` bytes). Returns 0 if the payload has fewer fields. */
static int payload_field(const void *payload, size_t size, int index, char *out, size_t capacity) {
    const char *p = (const char *)payload, *end = p + size;
    size_t n = 0;
    if (capacity == 0) return 0;
    while (index > 0 && p < end) { if (*p++ == '\0') --index; }
    if (index > 0) return 0;
    while (p < end && *p && n + 1 < capacity) out[n++] = *p++;
    out[n] = '\0';
    return 1;
}

In Python the same is data.split('\0').

Native and Python subscribers#

Native and Python subscribers share the same topics: a plugin receives what Python mods publish and the other way round. Native subscribers are always called at once. Python subscribers are also called at once, except for messages published while the game is inside its own code (from a handler for blse.entity_output or a damage event, for example) and for blse.entity_output itself: Python receives those at the next safe point, the end of the server frame. BLSE events that are "delivered inside game code" call native subscribers from deep inside the game: do as little as possible there (copy the data, set a flag) and act later, for example on blse.game_frame.

subscribe#

C
uint64_t subscribe(void *context, const char *topic, BLSE_MessageFn callback, void *user);
typedef void (BLSE_CALL *BLSE_MessageFn)(const char *topic, const void *payload, size_t size, void *user);

Calls callback for every message published on topic from now on. (0.1)

When to use it: for every game event your plugin reacts to. A plugin that keeps notes in save games subscribes to blse.game_loaded to read them; a HUD plugin subscribes to blse.game_frame to update its display.

Parameter Meaning
context host->context
topic The topic to listen to: a BLSE_MSG_* constant or another mod's topic. 1 to 128 bytes, case-sensitive. Topics nobody publishes (yet) are allowed
callback Your function. It receives the topic, the payload and its size, and your user pointer
user Any pointer you like (or NULL); passed back to callback unchanged. Lets one function serve several subscriptions

Returns a nonzero token that names this subscription (keep it if you want to unsubscribe later), or 0 on failure: wrong thread, callback NULL, an empty or too long topic, or your plugin was disabled.

  • The same function may be subscribed several times (to one or more topics); it is then called once per subscription.
  • Subscribing while a message is being delivered does not deliver that message to the new subscriber.
  • Subscriptions last until you unsubscribe, your plugin declines to load, or it is disabled after a crash. They survive engine restarts.
C
static void BLSE_CALL on_loaded(const char *topic, const void *payload, size_t size, void *user) {
    char map[64];
    if (payload_field(payload, size, 0, map, sizeof map)) host->log(map);
}
/* in BLSE_PluginLoad: */
if (!host->subscribe(host->context, BLSE_MSG_GAME_LOADED, on_loaded, NULL)) return 0;

unsubscribe#

C
int unsubscribe(void *context, uint64_t token);

Removes one of your own subscriptions. (0.1)

When to use it: for events you need only for a while. A plugin that waits for one blse.game_frame after a level starts, or that follows a fight only while it lasts, subscribes when it starts and unsubscribes when it is done, so it costs nothing the rest of the time.

  • token: the value subscribe returned.
  • Returns 1 if the subscription was removed, 0 if the token is unknown, belongs to another plugin, was already removed, or the call came from another thread.
  • A callback may unsubscribe itself. A subscription removed while a message is being delivered, before its turn, is skipped.
C
static uint64_t frame_token;
static void BLSE_CALL once(const char *topic, const void *payload, size_t size, void *user) {
    host->log("first frame of the level");
    host->unsubscribe(host->context, frame_token);
}
/* when a level starts: */
frame_token = host->subscribe(host->context, BLSE_MSG_GAME_FRAME, once, NULL);

publish#

C
int publish(void *context, const char *topic, const void *payload, size_t size);

Sends a message to every current subscriber of topic, native and Python, before it returns. (0.1)

When to use it: to let other plugins and Python mods know something happened in yours ("the player opened my shop", "a new score"), or to give a Python part of your mod data from the C part.

Parameter Meaning
topic Your topic, 1 to 128 bytes, with your mod's prefix. Topics starting with blse. are refused
payload The data, any bytes. May be NULL when size is 0
size The number of bytes in payload

Returns 1 if the message was accepted (also when nobody subscribes), 0 on wrong thread, an invalid or reserved topic, payload NULL with size above 0, or when messages are nested more than 16 levels deep (a subscriber that publishes, whose subscriber publishes, ...).

  • BLSE copies the payload first, so subscribers cannot see later changes to your buffer, and you may reuse it as soon as publish returns.
  • Subscribers run one after the other, in the order they subscribed. A subscriber that crashes is disabled; the others still run.
  • Python subscribers receive the payload as a string. For text with several fields, separate them with NUL bytes so Python can use data.split('\0').
C
static const char msg[] = "gold\0" "250";   /* two fields: item, amount (no NUL at the end) */
host->publish(host->context, "mymod.reward", msg, sizeof msg - 1);

A Python mod receives it with:

Python
def on_reward(topic, data):
    item, amount = data.split('\0')
blse.subscribe('mymod.reward', on_reward)

Work from other threads#

add_task(context, task, user) may be called from any thread. It queues task(user) to run on the game's main thread at the next game frame, or the next paint while no level runs (main menu, loading), in the order queued. Inside the task every host function is available. Returns 1 if queued, 0 if the plugin was disabled, the arguments are invalid or 4096 tasks are already waiting. A task that crashes disables its plugin like any other callback; its remaining tasks are dropped. Keep user alive until the task has run.

C
static void BLSE_CALL show(void *user) { host->console_print(host->context, (const char *)user); }
/* on a worker thread: */ host->add_task(host->context, show, (void *)"download finished\n");

When to use it: a plugin that does slow work on its own thread (reads a large file, talks to a web service, computes a path) uses add_task to hand the result back to the game, because every other host function works only on the main thread.

Parameter Meaning
context host->context
task Your function, void BLSE_CALL task(void *user)
user Passed to task. It must stay valid until the task has run: use static data, or memory the task frees itself

Notes:

  • Tasks run after BLSE's other per-frame work (on the game frame while a level runs, otherwise when the interface is painted), in the order they were added. A task added while tasks run (also by a task) waits for the next frame.
  • Tasks of a plugin that declines to load or is disabled before they run are dropped without being called. If user is memory the task would free, it then stays allocated.
  • The queue holds at most 4096 tasks across all plugins; add_task returns 0 when it is full. Do not queue a task per small event from a fast thread: collect results and queue one task.
C
/* A worker thread hands a result string to the game. */
static void BLSE_CALL show_result(void *user) {
    host->console_print(host->context, (char *)user);
    free(user);                                   /* allocated by the worker */
}
static DWORD WINAPI worker(LPVOID unused) {
    char *text = _strdup("lookup finished\n");
    if (text && !host->add_task(host->context, show_result, text)) free(text); /* not queued */
    return 0;
}

Examples#

  • examples/hello_plugin.cpp: logs one line.
  • examples/messaging_plugin.cpp: subscribes to BLSE events and exchanges messages.
  • examples/sprint_plugin.cpp: the Sprint mod as a plugin (settings file, key and control state, the per-frame event, HUD boxes, console commands).
  • examples/savenotes_plugin.cpp: a complete small mod. Console commands (savenotes_add, savenotes_list, savenotes_remove, savenotes_clear) keep notes in the current save game, and loading a save reports how many notes it holds. Shows commands, save data and blse.game_loaded working together; a good starting point for a new mod.

Build a plugin with an x86 MSVC prompt:

batch
cl /nologo /EHsc /LD /MT /Iinclude myplugin.cpp /Femyplugin.dll

What the options mean: /LD builds a DLL, /MT links the C runtime into it (players need no extra runtime installed), /EHsc enables C++ exceptions, /Iinclude is the folder that holds blse_api.h, /Fe names the output. The prompt must be the x86 (32-bit) one: the game is a 32-bit program and cannot load a 64-bit DLL. Add /Zi to get a .pdb file; keep it next to the DLL so crash reports show your source lines (see crash reports).