On this page 34 sections
- A minimal plugin
- Read this before you write more
- Words used on the plugin pages
- The plugin contract
- Plugin information and dependencies
- BLSE_PLUGIN_INFO
- plugin_version
- list_plugins
- Host API
- Rules every Host API function follows
- struct_size
- api_version
- log
- context
- runtime_version, build_profile
- add_task
- Index of all Host API fields
- Basics, messaging and plugins (this page)
- Console and the game's Python (python-bridge.md)
- Events, filters, input and game rules (events-and-filters.md)
- Interface (interface.md)
- Drawing (drawing.md)
- The world, saved data and settings (world-and-data.md)
- Conversations (conversations.md)
- Hooks (hooks-and-crashes.md)
- Messaging
- Topics
- Payloads
- Native and Python subscribers
- subscribe
- unsubscribe
- publish
- Work from other threads
- Examples
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):
#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):
cl /nologo /LD /MT /I. myplugin.c /Femyplugin.dllPlugins 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. Useadd_taskto 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 afterlog. 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_PluginLoadruns 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 asblse.level_initorblse.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_profiletells 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:
#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 inconfig/loadorder.txt(-name), or whosemod.inirequirements 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 inloadorder.txtis 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_PluginLoadcounts 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 receiveblse.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)orplugin 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):
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:
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#
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_versionandlist_pluginshere,blse.plugin_version()andblse.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#
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_PluginLoadon. Your dependencies are loaded before you, so you can always see them there; other plugins may load after you, so check for those in ablse.post_loadhandler.
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#
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.
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_versionchanges 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 afterlogthat you use. Do not requirestruct_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 thehostpointer (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_getandjson_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:
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.logunder 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 "".
/* 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#
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#
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:
if (host->api_version != BLSE_API_VERSION) return 0;log#
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.NULLis ignored.- The line is prefixed with your plugin's file name in brackets (
[myplugin] hello) when called fromBLSE_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.
host->log("settings loaded"); /* blse.log: [myplugin] settings loaded */context#
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#
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, useBLSE_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), orNULLwhen 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.
if (!host->build_profile) host->log("unknown game build: only basic features work");add_task#
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. Seeinclude/blse_api.hfor 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_PluginLoadhas 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), butpublishrefuses them. The constantsBLSE_MSG_*inblse_api.hname 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 (
size0) for some events, such asblse.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:
#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#
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.
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#
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 valuesubscribereturned.- 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.
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#
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
publishreturns. - 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').
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:
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.
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
useris memory the task would free, it then stays allocated. - The queue holds at most 4096 tasks across all plugins;
add_taskreturns 0 when it is full. Do not queue a task per small event from a fast thread: collect results and queue one task.
/* 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 andblse.game_loadedworking together; a good starting point for a new mod.
Build a plugin with an x86 MSVC prompt:
cl /nologo /EHsc /LD /MT /Iinclude myplugin.cpp /Femyplugin.dllWhat 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).