On this page 81 sections
- Words used on this page
- Reading events in C
- Level, frame and save events
- blse.post_load
- blse.level_init
- blse.level_shutdown
- blse.level_change
- blse.game_frame
- blse.game_loaded
- blse.new_game
- blse.before_save
- Saving and loading events
- blse.engine_restart
- blse.entity_output
- Console variables, items, input filters
- cvar_get, cvar_set
- cvar_list
- blse.cvar_changed
- item_info
- list_items
- item_files
- read_file
- find_files
- add_input_filter
- combat_npcs
- item_set
- item_reset
- blse.combat_state
- blse.stealth_changed
- blse.npc_combat
- blse.item_loaded
- Gameplay events
- blse.attack
- blse.quest_changed
- blse.entity_input
- Character creation
- Saving, loading and player targets
- blse.game_saved
- blse.before_load
- blse.load_failed
- blse.after_load
- blse.save_deleted
- player_target
- blse.look_target_changed, blse.using_changed, blse.discipline_target_changed
- Damage filters
- add_damage_filter
- blse.damage
- blse.hit
- Stat changes
- blse.stat_change
- Inventory
- list_inventory
- blse.item_added, blse.item_removed, blse.item_equipped, blse.item_unequipped
- Entities appearing and disappearing
- blse.entity_spawned, blse.entity_removed
- Disciplines
- blse.discipline_activated, blse.discipline_ended, blse.discipline_blocked
- Kills, damage, pushes, console commands
- blse.entity_killed
- damage_entity
- entity_velocity
- push_entity
- console_command
- fix_state
- Keys and mouse buttons
- Key names
- key_state
- key_binding
- block_key
- press_key
- control_state
- blse.key_down, blse.key_up
- blse.key_simulated
- blse.mouse_move
- blse.control_down, blse.control_up
- Discipline filters
- add_discipline_filter
- Dialogue
- blse.dialog_line
- blse.dialog_choice
- add_dialog_filter
This page is the plugin reference for reacting to the game and changing its rules:
- events: messages BLSE publishes when something happens (a level starts, an NPC dies, the player picks a dialogue reply, a key is pressed). You subscribe to them;
- filters: your functions that BLSE calls before the game does something (applies a hit, activates a Discipline, shows a dialogue line, passes an input to an entity), so you can change or block it;
- functions for keys, console variables, item data, game files, damage, pushes and console commands.
Come here when your plugin should react to gameplay or change how the game behaves. Every event's meaning is also described, with Python examples, in the Python reference; this page gives the C side.
A minimal example, a plugin that halves the damage the player takes:
#include <string.h>
#include "blse_api.h"
static void BLSE_CALL halve_player_damage(BLSE_Damage *d, void *user) {
if (!strcmp(d->victim_class, "player")) d->damage *= 0.5f;
}
BLSE_EXPORT int BLSE_CALL BLSE_PluginLoad(const BLSE_HostAPI *host) {
if (host->api_version != BLSE_API_VERSION || !BLSE_HOST_HAS(host, add_damage_filter)) return 0;
if (!host->add_damage_filter(host->context, halve_player_damage, NULL))
host->log("damage filters are not available on this game build");
return 1;
}Important, before you start:
- Filters and many event handlers run inside the game's own code (in the middle of applying a hit, showing a dialogue line, changing a stat). Keep them short: read the data, decide, return. Do not change the same thing again from there (no stat changes from a stat handler, no damage from a damage filter), do not call
python_eval(it fails there;python_execis queued), and do not spawn or remove entities. To act, set a flag and do the work later, for example onblse.game_frame. - A crash in a filter or handler disables your plugin for the rest of the session (see When a plugin crashes).
- Most of these features need a recognized game build (
host->build_profileis notNULL). On other builds theadd_*_filterfunctions return 0 and the game events are never published. - Strings in filter structures and payloads are borrowed: valid only until your function returns.
Words used on this page#
| Word | Meaning |
|---|---|
| Filter | A function of yours that BLSE calls before the game acts, with a structure describing what is about to happen. Fields marked writable can be changed to change the outcome |
| Rule | The Python equivalent of a filter, set with a function call (blse.set_damage_multiplier, blse.block_input, ...). Python rules are applied first; filters see their result |
| Classname, targetname | An entity's kind (npc_VVampire, player, prop_door_rotating) and its name in the map (guard_1; often empty) |
| Inflictor | What delivered a hit: the weapon or projectile. The attacker is who caused it |
| Damage type bits | A number whose bits say what kind of damage a hit is (bullet, club, fire, ...), as the game's Source engine defines them: 0 generic, 2 bullet, 128 club |
| Input | A command sent to an entity: from the map's wiring (a button's output opening a door), from a game script (pc.AwardExperience("x") is the input AwardExperience sent to the player) or from the game. Inputs may carry a value |
| Output | The opposite: an event an entity fires (OnDeath, OnTrigger), which the map's wiring turns into inputs on other entities |
| Stat | A character value the game keeps in its stat lists: blood pool, damage taken, Humanity, attributes, abilities, Disciplines |
| Console variable (cvar) | A named setting of the engine, such as fov or sv_cheats, readable and settable in the console |
| Handle | A number that names one entity while it exists. In payloads it is written in decimal. See Words used on the plugin pages |
Reading events in C#
Every BLSE event is a message: subscribe a handler to its topic (BLSE_MSG_* constants in blse_api.h). Most payloads are several text fields separated by NUL bytes, without a NUL at the end. The examples on this page read them with the payload_field helper from Payloads:
static void BLSE_CALL on_quest(const char *topic, const void *payload, size_t size, void *user) {
char quest[128], state[16];
if (!payload_field(payload, size, 0, quest, sizeof quest) ||
!payload_field(payload, size, 1, state, sizeof state)) return;
/* quest = "Tutorial", state = "2" */
}
host->subscribe(host->context, BLSE_MSG_QUEST_CHANGED, on_quest, NULL);Numbers in payloads are decimal text: convert them with atoi, strtoul or atof.
"Delivered inside game code" in an entry means native subscribers are called at once, from inside the game's own function (see the warning above); Python subscribers then get the message at the next safe point (the end of the server frame).
Level, frame and save events#
These events tell you where the game is: a level starting or ending, every frame, saving and loading. Most plugins subscribe to some of them.
blse.post_load#
Topic BLSE_MSG_POST_LOAD. Published once at startup, after every plugin and every Python mod has loaded. No payload.
Use it to talk to other plugins and Python mods: at this point all of them are loaded and subscribed.
blse.level_init#
Topic BLSE_MSG_LEVEL_INIT. Published when a level has started: its entities exist. Payload, four fields:
- the map name (
sm_pawnshop_1); - the previous map (
sm_hub_1), or""; - the landmark the player came through (
haven1), or""; "1"if the level was restored from a save game,"0"otherwise.
A level transition (a door, an area exit) has the previous map and the landmark set and "0". Loading a save or the console's map command has both empty. The main menu's background level reports as sp_genesisdevice_1. Save data may still be the old game's here: read it in blse.game_loaded.
static void BLSE_CALL on_level(const char *topic, const void *payload, size_t size, void *user) {
char map[64], loaded[4];
payload_field(payload, size, 0, map, sizeof map);
payload_field(payload, size, 3, loaded, sizeof loaded);
if (!strcmp(map, "sm_pawnshop_1") && loaded[0] == '0') { /* walked into the pawnshop */ }
}blse.level_shutdown#
Topic BLSE_MSG_LEVEL_SHUTDOWN. Published just before the current level ends (entities still exist), exactly once per blse.level_init. No payload. Forget entity handles you kept here: they are invalid in the next level.
blse.level_change#
Topic BLSE_MSG_LEVEL_CHANGE (b0.5.48). A level change was asked for (a door or exit, a taxi, a script); it happens on a later frame, before blse.level_shutdown. Payload: the map, the landmark ("" if none). Delivered to native subscribers at once, to Python at the next safe point (still before the change). A companion mod uses it to decide who goes along.
blse.game_frame#
Topic BLSE_MSG_GAME_FRAME. Published after every server frame while a level runs. Payload: one byte, '1' if the game simulated this frame, '0' if it is paused.
Use it for work that has to happen continuously (update a HUD element, check a timer), or to act on something a handler inside game code noted. It runs many times a second: keep it cheap, and unsubscribe while you do not need it.
static void BLSE_CALL on_frame(const char *topic, const void *payload, size_t size, void *user) {
if (size < 1 || ((const char *)payload)[0] != '1') return; /* paused */
/* per-frame work */
}blse.game_loaded#
Topic BLSE_MSG_GAME_LOADED. Published once after a save game was loaded and restored, on the first server frame of the loaded level. Payload: the map name. Read your save data here.
blse.new_game#
Topic BLSE_MSG_NEW_GAME (b0.5.25). The first level of a new game (a new character's first map, or the console's map command from the main menu). Save-game values are empty again: reset your in-memory state. Published at that level's start, before blse.level_init reaches other subscribers. Payload: the map name.
blse.before_save#
Topic BLSE_MSG_BEFORE_SAVE (b0.5.9). Published right before the game writes a save: a save game, an autosave, or the current level's state kept for a level transition. Payload: the map name. Values written now with save_set (or G from Python) are in the save. Only when BLSE could verify the game's save method (the analyzed build).
Saving and loading events#
Listed under Saving, loading and player targets: blse.game_saved, blse.before_load, blse.load_failed, blse.after_load, blse.save_deleted.
blse.engine_restart#
Topic BLSE_MSG_ENGINE_RESTART. Published after the game restarted its engine in-process (the console command _restart does this). Payload: a uint32_t restart number (1, 2, ...), 4 bytes in the machine's byte order (not text). Python state from before the restart is gone; plugins keep theirs.
uint32_t number = 0;
if (size == sizeof number) memcpy(&number, payload, sizeof number);blse.entity_output#
Topic BLSE_MSG_ENTITY_OUTPUT (b0.5.1). An entity fired an output: an NPC's OnDeath, a trigger's OnTrigger, a door's OnOpen, an NPC's OnDialogEnd, the player events' OnPlayerTookDamage, about 250 kinds. Payload, five fields:
- the output's name (
""if BLSE could not determine it); - the classname of the entity that fired it;
- its targetname;
- the activator's classname (who caused it;
""if none); - the activator's targetname.
Delivered inside game code, often: copy what you need and act later. Python receives it at the end of the server frame. Recognized builds only. The Python reference lists common outputs.
static void BLSE_CALL on_output(const char *topic, const void *payload, size_t size, void *user) {
char output[64], act[64];
payload_field(payload, size, 0, output, sizeof output);
payload_field(payload, size, 3, act, sizeof act);
if (!strcmp(output, "OnDeath") && !strcmp(act, "player")) ++kills; /* count, act later */
}Console variables, items, input filters#
cvar_get(context, name, buffer, capacity, &size)/cvar_set(context, name, value): read and set console variables (1, or 0 if there is no such variable).item_info(context, classname, &info)fills aBLSE_ItemInfo(setstruct_sizefirst): name, type, model, price, sell price, weight, flags, description.list_itemslists the loaded item classnames, separated by'\n'.- b0.5.35:
cvar_list(context, commands, buffer, capacity, &size)(names,'\n'-separated) and topicBLSE_MSG_CVAR_CHANGED(name, old, new);read_file(context, path, buffer, capacity, &size)(through the game's file system, packs included, at most 4 MB),find_files(context, wildcard, buffer, capacity, &size),item_files(context, buffer, capacity, &size);item_infoalso answers for items not loaded yet (from their file). add_input_filter(context, filter, user):filter(BLSE_Input*, user)sees every input before the entity acts on it; setblock = 1to drop it, or writenew_valueto replace its value (converted to the input's type). Runs under the crash guard; keep it fast.- Topics
BLSE_MSG_COMBAT_STATEandBLSE_MSG_STEALTH_CHANGED; see the Python events. - b0.5.33: topic
BLSE_MSG_NPC_COMBAT(each NPC's fight) andcombat_npcs(context, buffer, capacity, &size)(records separated by'\x1e', six fields by'\x1f': NPC handle, classname, name, target handle, classname, name);item_set(context, classname, field, value)(fields and values as Python'sset_item; 1 changed, 2 remembered for when the game loads the item (also while BLSE has not found the game's items yet, e.g. inBLSE_PluginLoad), 0 invalid) anditem_reset(context, classname). A plugin's item changes are undone when it is disabled or unloaded.
static void BLSE_CALL no_xp(BLSE_Input *in, void *user) {
if (!strcmp(in->input, "AwardExperience")) in->block = 1; /* scripts' pc.AwardExperience(...) */
}
host->add_input_filter(host->context, no_xp, NULL);The entries below describe each of these in full.
cvar_get, cvar_set#
int cvar_get(void *context, const char *name, char *buffer, size_t capacity, size_t *size);
int cvar_set(void *context, const char *name, const char *value);Read and change a console variable, as typing name or name value in the console would. (b0.5.29)
When to use them: a camera mod reads fov; a mod with a "realistic" mode sets engine settings the player could also set by hand; a plugin checks sv_cheats before offering a debug command.
name: the variable's name, such as"fov"or"sv_cheats". Console commands (such asmap) are not variables: both functions return 0 for them.cvar_getcopies the current value as text ("90","1") following the output buffer rules. Returns 1, or 0 if there is no such variable.cvar_set:valueis the new value as text. The variable converts and clamps it itself, as the console does (a number variable given"abc"becomes 0; a variable with limits keeps within them). Returns 1 if the variable exists, 0 otherwise. It does not tell you whether the value was accepted: read it back withcvar_get.- Changes are reported to everyone by
blse.cvar_changed.
char fov[16];
if (host->cvar_get(host->context, "fov", fov, sizeof fov, NULL)) host->log(fov);
host->cvar_set(host->context, "fov", "100");cvar_list#
int cvar_list(void *context, int commands, char *buffer, size_t capacity, size_t *size);Copies the names of all console variables, separated by '\n'. (b0.5.35)
commands: 1 also lists console commands, 0 lists only variables.- Follows the output buffer rules; the list is long (thousands of bytes), so ask for the size first. Returns 1, or 0 if the list is not available (the engine's console is not found yet).
size_t n = 0;
host->cvar_list(host->context, 0, NULL, 0, &n); /* ask for the length */
char *names = (char *)malloc(n + 1);
if (names && host->cvar_list(host->context, 0, names, n + 1, &n)) { /* one name per line */ }
free(names);blse.cvar_changed#
Topic BLSE_MSG_CVAR_CHANGED (b0.5.35). A console variable changed: from the console, the options screen, a config file or a mod. Payload, three fields: the name, the old value, the new value. BLSE compares the values twice a second while anyone listens, so a variable changed and changed back in between is not reported.
item_info#
int item_info(void *context, const char *classname, BLSE_ItemInfo *out);Fills a BLSE_ItemInfo structure with an item's data from the game's item files (vdata/items/<classname>.txt). (b0.5.29; also for items not loaded yet since b0.5.35)
When to use it: a shop or loot mod shows an item's name and price; a weight mod adds up what the player carries.
classname: the item's class, such as"item_w_thirtyeight".out: your structure. Setout->struct_size = sizeof(BLSE_ItemInfo)before the call, or BLSE returns 0.- Returns 1 and fills
out, or 0 for an unknown item.
| Field | Meaning |
|---|---|
name |
The name the game shows (printname); may be a #token that the game translates |
type |
item_type: "armor", "ranged", "melee", ... |
model |
infomodel: the model shown in the inventory |
price |
item_worth: the buying price |
sell_price |
player_sell: what a merchant pays the player |
weight |
The item's weight |
flags |
item_flags, the game's flag bits |
description |
The description text, cut at 2047 bytes |
Text fields are NUL-terminated and cut to fit. Changes made with item_set are seen here when the game has the item loaded.
BLSE_ItemInfo info;
memset(&info, 0, sizeof info);
info.struct_size = sizeof info;
if (host->item_info(host->context, "item_w_thirtyeight", &info)) { /* info.price is 70 */ }list_items#
int list_items(void *context, char *buffer, size_t capacity, size_t *size);Copies the classnames of the items the game has loaded so far, separated by '\n'. (b0.5.29)
The game reads an item's data when it first needs it (a level's items, a merchant's goods), so the list grows as the game goes on. Every item there is a file for is listed by item_files instead. Follows the output buffer rules; returns 1, or 0 from another thread. New items are announced by blse.item_loaded.
item_files#
int item_files(void *context, char *buffer, size_t capacity, size_t *size);Copies the classnames of every item there is a file for in vdata/items, loaded or not, separated by '\n'. Files whose names a class cannot have (copies such as item_a_body_armor - hunter.txt) are left out. (b0.5.35)
Use it to go through every item of the game, for example to change all prices. Follows the output buffer rules; returns 1, or 0 if unavailable.
read_file#
int read_file(void *context, const char *path, char *buffer, size_t capacity, size_t *size);Reads a game file the way the game finds its files: the game's folders and .vpk packs, mod folders first. (b0.5.35)
When to use it: to read the game's own data (a dialogue file, an item file, cfg/config.cfg), or a data file your mod ships in a mod folder.
path: relative to the game's file system, such as"vdata/items/item_w_katana.txt"or"cfg/config.cfg".- The contents are copied following the output buffer rules (a NUL is added after the bytes; binary files may contain NUL bytes, so use
*size). Files over 4 MB are refused. - Returns 1, or 0 if the file does not exist or is too big.
find_files#
int find_files(void *context, const char *wildcard, char *buffer, size_t capacity, size_t *size);Lists the files that match a wildcard, through the game's file system (folders, packs and mod folders). (b0.5.35)
wildcard: a path with*in the file name, such as"vdata/items/*.txt".- Copies the matching file names (not paths), separated by
'\n', following the output buffer rules. - A name found in several places (the game's folder, a pack, a mod) is listed once; at most 8192 names.
- Returns 1 (also when nothing matches: the list is then empty), or 0 for an invalid wildcard or when the game's file system is not available.
add_input_filter#
int add_input_filter(void *context, BLSE_InputFilterFn filter, void *user);
typedef void (BLSE_CALL *BLSE_InputFilterFn)(BLSE_Input *input, void *user);Calls filter for every input an entity receives, before the entity acts on it. The filter may block the input or change its value. (b0.5.29)
When to use it: a mod that stops scripts from giving experience (block AwardExperience), a "locked doors" mod (block Use on doors), a money mod that doubles rewards (change the value of MoneyAdd).
| Parameter | Meaning |
|---|---|
filter |
Your function. It receives a BLSE_Input structure and your user pointer |
user |
Any pointer; passed back to filter |
Returns 1 if the filter was added, 0 if filter is NULL, the call came from another thread or your plugin is disabled.
The structure:
| Field | Meaning |
|---|---|
struct_size |
sizeof(BLSE_Input) |
input |
The input's name: "Use", "Trigger", "AwardExperience", "MoneyAdd", ... |
target_class, target_name |
The entity receiving it |
value |
The value it carries, as text ("" if none) |
activator_class, activator_name |
Who started the chain that led to it (often the player); "" for none |
caller_class, caller_name |
The entity that sent it; "" for none |
block |
Writable. Set to 1 to drop the input: the entity never sees it |
new_value |
Writable. A replacement value (up to 255 bytes). BLSE converts it to the input's own type (a number, a string, three numbers for a vector); if it cannot, the original value is used. Empty: unchanged |
Notes:
- Python rules come first: an input
blse.block_inputblocks never reaches filters, and ablse.change_inputvalue is already innew_value. - Filters run in the order they were added; once one sets
block, the later ones are not called. - An empty value cannot be given: an empty
new_valuemeans "unchanged". - The result is reported by
blse.entity_input(with the replaced value and the blocked flag). - It runs very often (busy maps send many inputs each second): compare the input name first and return.
static void BLSE_CALL double_money(BLSE_Input *in, void *user) {
if (strcmp(in->input, "MoneyAdd") != 0) return;
sprintf_s(in->new_value, sizeof in->new_value, "%d", atoi(in->value) * 2);
}combat_npcs#
int combat_npcs(void *context, char *buffer, size_t capacity, size_t *size);Copies the NPCs that are fighting right now, with their targets. (b0.5.33)
When to use it: a "danger" indicator on the HUD, or a companion mod that checks who attacks the player.
- An NPC fights while it is alive, not idle, and has an enemy it hates (the player or another character); see
blse.npc_combat. - The text holds one record per NPC, records separated by
'\x1e'(byte 30), each with six fields separated by'\x1f'(byte 31): the NPC's handle (decimal), classname, name, then the target's handle, classname and name. A'\x1e'or'\x1f'inside a name is given as a space. Empty when nobody fights. - Follows the output buffer rules. Returns 1, or 0 when no level runs.
item_set#
int item_set(void *context, const char *classname, const char *field, const char *value);Changes a field of an item's data for the rest of the session. (b0.5.33)
When to use it: an economy mod changes prices, a realism mod changes weights, a translation fixes a name.
| Parameter | Meaning |
|---|---|
classname |
The item's class, such as "item_w_thirtyeight" (under 128 bytes) |
field |
"name" (up to 127 bytes), "description" (up to 8191 bytes), "price" (buying), "sell_price", "weight", and for a weapon its ammunition: "ammo_name", "ammo_description", "ammo_price". Case is ignored |
value |
The new value as text. Number fields take whole numbers 0 to 1000000000 |
Returns:
- 1: changed now;
- 2: remembered: the game has not loaded the item yet (or BLSE has not found the game's items yet, for example in
BLSE_PluginLoad); it gets the value when the game loads it; - 0: invalid (unknown field, bad number, text too long, unknown game build, wrong thread).
Notes:
- BLSE changes both of the game's copies: the server's and the client's, which the barter screen takes its prices from (shown prices still include the game's haggling adjustment).
- Changes are not saved. They last until
item_reset, your plugin is disabled or unloaded, or the game exits. Set them again at every game start (inBLSE_PluginLoadis fine: they are remembered until the items load). - When several mods change the same field of the same item, the latest change wins.
- Python:
blse.set_item(see Items).
host->item_set(host->context, "item_w_thirtyeight", "price", "50");
host->item_set(host->context, "item_w_thirtyeight", "name", "Old Thirty-Eight");item_reset#
int item_reset(void *context, const char *classname);Gives back the game's own values for every field your plugin changed on the item. (b0.5.33)
Returns 1 if your plugin had changed the item, 0 if not. Other mods' changes stay.
blse.combat_state#
Topic BLSE_MSG_COMBAT_STATE (b0.5.29). The player's combat state changed. Payload: "combat" (a living NPC has the player as its enemy and hates it; since b0.5.30 bystanders fleeing from the player do not count), "alert" (nobody fights, but the stealth meter shows the player noticed or detected) or "normal". Checked ten times a second; calming down waits 2 seconds. When a level ends during a fight or alert, the event says "normal". Recognized builds only.
Use it for combat music, a HUD warning, or to stop a feature (fast travel, saving) during fights.
blse.stealth_changed#
Topic BLSE_MSG_STEALTH_CHANGED (b0.5.29). The player's stealth level changed. Payload, three decimal fields: the old level, the new level, the meter (0 to 100, as the HUD shows it at that moment). Levels: 0 unseen (green), 1 noticed (yellow), 2 detected (red, "!"). Recognized builds only.
blse.npc_combat#
Topic BLSE_MSG_NPC_COMBAT (b0.5.33). One NPC's fight started, changed target or ended. Payload, seven fields:
- what:
"start","target"(it turned to another enemy) or"stop"(3 seconds after it stopped fighting; at once when it dies or is removed, and for every fight when the level ends); - the NPC's handle (decimal);
- its classname;
- its name;
- its target's handle (for
"stop": the last target); - the target's classname;
- the target's name.
Checked ten times a second. Recognized builds only.
blse.item_loaded#
Topic BLSE_MSG_ITEM_LOADED (b0.5.34). The game loaded an item's data (items are read when first needed: a level, a merchant's goods). Payload: the classname. Checked every game frame. Items loaded before BLSE started watching are listed by list_items instead, so a mod that changes every item calls list_items once and then handles this event.
Gameplay events#
BLSE_MSG_ATTACK, BLSE_MSG_QUEST_CHANGED and BLSE_MSG_ENTITY_INPUT; payloads as in the Python reference.
All three (b0.5.28) are on recognized builds only, delivered to native subscribers right away and to Python at the end of the frame. Experience and Masquerade changes are not separate events: they arrive as blse.stat_change with the stat names experience and masquerade.
blse.attack#
Topic BLSE_MSG_ATTACK. A character attacked. Payload, six fields:
- the kind:
"melee"(every character's swings and punches, once per swing) or"ranged"(the player's guns, and since b0.5.29 NPCs' guns); - the attacker's classname;
- the attacker's name;
- the attacker's handle (decimal);
- the weapon's classname (ranged;
""for melee); - shots: the bullets fired this time (ranged; one event per burst for NPCs),
"1"for melee.
static void BLSE_CALL on_attack(const char *topic, const void *payload, size_t size, void *user) {
char kind[8], cls[64];
payload_field(payload, size, 0, kind, sizeof kind);
payload_field(payload, size, 1, cls, sizeof cls);
if (!strcmp(cls, "player") && !strcmp(kind, "ranged")) ++shots_fired;
}blse.quest_changed#
Topic BLSE_MSG_QUEST_CHANGED. The player's quest was set (the game's SetQuest, from dialogue or a script). Payload: the quest's name ("Tutorial"), the state (decimal). Sent every time it is set, also to the state it already had.
blse.entity_input#
Topic BLSE_MSG_ENTITY_INPUT. An entity received an input: from a game script, from the map's wiring or from the game. Payload, nine fields:
- the input's name;
- the target's classname;
- the target's name;
- the value as text (
""if none; the replacement if a rule or filter changed it); - the activator's classname;
- the activator's name;
- the caller's classname;
- the caller's name;
"1"if a rule or filter blocked it, else"0"(b0.5.29).
Many per second in busy places: compare the input name first. To change or block inputs, use add_input_filter.
Character creation#
BLSE_MSG_CHARGEN_STARTED, _STEP, _ANSWER, _GENDER, _CLAN, _HISTORY, _POINTS, _AUTOLEVEL, _FINISHED and _CANCELLED (b0.5.47); payloads as in the Python reference. Recognized builds only.
These events follow the player through the new-game character sheet and its questionnaire. They are delivered right away, to Python too (the game is paused while the sheet is open). The game applies every choice to the player at once, so at blse.chargen_finished the player already has the clan, stats and name. To steer clan choices, see set_chargen_clan_rule and add_chargen_clan_filter.
| Topic | When | Payload fields |
|---|---|---|
blse.chargen_started (BLSE_MSG_CHARGEN_STARTED) |
The sheet opened to make a character | "1" |
blse.chargen_step (BLSE_MSG_CHARGEN_STEP) |
The questionnaire shows a screen | The screen's name from vdata/system/charcreatewizard.txt in lower case ("help_popup0", "trait_male_vs_female", ...) |
blse.chargen_answer (BLSE_MSG_CHARGEN_ANSWER) |
A button on that screen was clicked, before the game goes on | The screen's name; the button (1-4 as shown) |
blse.chargen_gender (BLSE_MSG_CHARGEN_GENDER) |
The gender was set | "male" or "female" |
blse.chargen_clan (BLSE_MSG_CHARGEN_CLAN) |
The clan was set (after clan rules) | The clan's number in the game's clan table; its name ("Player_Tremere") |
blse.chargen_history (BLSE_MSG_CHARGEN_HISTORY) |
The history was set | Its number ("-1": none) |
blse.chargen_points (BLSE_MSG_CHARGEN_POINTS) |
A point was spent or taken back | "buy" or "sell"; the stat; the count |
blse.chargen_autolevel (BLSE_MSG_CHARGEN_AUTOLEVEL) |
The questionnaire spent the points by a template | The template's arguments |
blse.chargen_finished (BLSE_MSG_CHARGEN_FINISHED) |
Accept: the game starts (the level change to sp_theatre follows) |
Clan number; clan name; gender; the character's name; history; how it was made ("questionnaire", "manual" or "hunter"); "1". Experience was set to 0 just before: give any now |
blse.chargen_cancelled (BLSE_MSG_CHARGEN_CANCELLED) |
Character creation ended without Accept | "cancel" (back to the main menu) or "level_shutdown" (the level ended otherwise) |
static void BLSE_CALL on_finished(const char *topic, const void *payload, size_t size, void *user) {
char clan[64];
payload_field(payload, size, 1, clan, sizeof clan);
if (!strcmp(clan, "Player_Nosferatu")) host->give_item(host->context, 1, "item_g_bluebloodpack");
}
host->subscribe(host->context, BLSE_MSG_CHARGEN_FINISHED, on_finished, NULL);Saving, loading and player targets#
Topics BLSE_MSG_GAME_SAVED, BLSE_MSG_BEFORE_LOAD, BLSE_MSG_LOAD_FAILED, BLSE_MSG_AFTER_LOAD, BLSE_MSG_SAVE_DELETED, BLSE_MSG_LOOK_TARGET_CHANGED, and (b0.5.34) BLSE_MSG_USING_CHANGED, BLSE_MSG_DISCIPLINE_TARGET_CHANGED, BLSE_MSG_ITEM_LOADED; payloads as in the Python reference. player_target(context, kind, buffer, capacity, &size) copies "classname\0targetname\0handle" for kind 0 (under the crosshair), 1 (being used) or 2 (Discipline target); it returns 0 when there is none.
The save and load events (b0.5.27) are delivered on the main thread right away, on recognized builds only. A mod that keeps its own files next to each save uses them to follow the player's saves by name.
blse.game_saved#
Topic BLSE_MSG_GAME_SAVED. A save was written (also quicksaves and autosaves). Payload: the save's name as the game passed it ("quick", "autosave", "Vampire-011.sav"), then "1" if it worked or "0" if it failed.
blse.before_load#
Topic BLSE_MSG_BEFORE_LOAD. A save was asked to be loaded (from the game menu, quickload or the load command), before anything is unloaded. Payload: the save's name.
blse.load_failed#
Topic BLSE_MSG_LOAD_FAILED. The load asked for did not happen. Payload: the save's name, the reason ("the save does not exist", "the save could not be restored"). No blse.after_load follows.
blse.after_load#
Topic BLSE_MSG_AFTER_LOAD. The save was restored, on the first frame of the loaded level, right after blse.game_loaded. Payload: the save's name, the map.
blse.save_deleted#
Topic BLSE_MSG_SAVE_DELETED. The player deleted a save (in the load menu). Payload: its file name without .sav.
player_target#
int player_target(void *context, int kind, char *buffer, size_t capacity, size_t *size);Tells you which entity the player is looking at, using, or would hit with a Discipline. (b0.5.27; kind 3 b0.5.34)
When to use it: an "examine" command that shows details of what the player looks at, a pickpocket mod that checks who is under the crosshair, a debug tool.
kind |
The entity |
|---|---|
| 0 | Under the crosshair: the one the game names on the HUD (an NPC, an item, a door or door knob, a computer) |
| 1 | What the player is in the middle of using: a door knob being tried, a sign being read, a carried prop |
| 2 | The creature a targeted Discipline would hit right now (the game's own pick, redone every frame) |
| 3 | The console's !picker (what ent_* commands act on): the entity straight ahead, else the one nearest the crosshair; any entity, also ones the game does not name |
Copies three NUL-separated fields, classname, targetname and handle (decimal), following the output buffer rules. Use *size (not strlen) and read the fields with payload_field. Returns 1, or 0 if there is no such entity, no level runs or kind is not 0 to 3.
char target[256]; size_t n;
if (host->player_target(host->context, 0, target, sizeof target, &n)) {
char cls[64], handle[16];
payload_field(target, n, 0, cls, sizeof cls);
payload_field(target, n, 2, handle, sizeof handle);
uint32_t entity = (uint32_t)strtoul(handle, NULL, 10); /* usable with entity functions */
}blse.look_target_changed, blse.using_changed, blse.discipline_target_changed#
Topics BLSE_MSG_LOOK_TARGET_CHANGED (b0.5.27), BLSE_MSG_USING_CHANGED and BLSE_MSG_DISCIPLINE_TARGET_CHANGED (b0.5.34). What player_target kind 0, 1 or 2 returns changed. Checked every game frame. Payload, three fields: classname, targetname, handle; all three empty when it is on nothing. The Discipline target is recalculated every frame and can flicker at the edge of range. Recognized builds only.
Damage filters#
add_damage_filter(context, filter, user) (b0.5.3, recognized builds only; returns 0 elsewhere) calls filter(BLSE_Damage*, user) for every hit before the game applies it:
static void BLSE_CALL halve_player_damage(BLSE_Damage* d, void* user) {
if (!strcmp(d->victim_class, "player")) d->damage *= 0.5f;
}
host->add_damage_filter(host->context, halve_player_damage, NULL);damageis writable (negative or invalid values become 0); everything else is information: original damage, damage-type bits, and classname/targetname of victim, attacker and inflictor (the weapon or projectile). The strings are valid during the call only.- Python damage rules (
blse.set_damage_multiplier) are applied first, then filters in the order they were added. - Filters run inside the game's damage code: keep them short.
python_execis queued there,python_evalrefuses. - After the hit,
blse.damagereports the result (seeinclude/blse_api.h).
add_damage_filter#
int add_damage_filter(void *context, BLSE_DamageFilterFn filter, void *user);
typedef void (BLSE_CALL *BLSE_DamageFilterFn)(BLSE_Damage *damage, void *user);When to use it: difficulty mods (the player takes less, enemies take more), immortal NPCs (damage 0 for a quest character), weapon balance (more damage from one weapon class), friendly fire off (no damage between companions).
Returns 1 if the filter was added, 0 if damage hooks are unavailable (unknown game build), filter is NULL, or the call came from another thread.
The structure BLSE passes:
| Field | Meaning |
|---|---|
struct_size |
sizeof(BLSE_Damage) |
damage |
Writable. The damage the game will apply, before its own difficulty and resistance scaling. 0 means no damage. Negative, infinite or NaN values become 0 |
original_damage |
What the game asked for, before Python rules and filters |
damage_type |
The damage type bits |
victim_class, victim_name |
Who is hit ("" if no name) |
attacker_class, attacker_name |
Who caused it ("" if none) |
inflictor_class, inflictor_name |
What delivered it: the weapon or projectile ("" if none) |
Strings are cut at 127 bytes. Filters see the damage after earlier filters changed it.
/* Nobody hurts the quest NPC "ally_beckett". */
static void BLSE_CALL protect(BLSE_Damage *d, void *user) {
if (!strcmp(d->victim_name, "ally_beckett")) d->damage = 0.0f;
}blse.damage#
Topic BLSE_MSG_DAMAGE (b0.5.3). Published after the game applied a hit. Payload, nine fields: victim class, victim name, attacker class, attacker name, inflictor class, inflictor name, the damage applied (after rules and filters, before the game's own scaling), the original damage, the damage type bits. Numbers are decimal text ("12.5"). Delivered inside game code. Recognized builds only.
blse.hit#
Topic BLSE_MSG_HIT (b0.5.48). Every hit, as blse.damage, with entity handles. Payload, eight fields: victim handle, victim class, victim name, attacker handle, attacker class, attacker name, damage, damage type bits (handles decimal, empty for none). Only built while anyone listens. Use it when you need to act on the entities themselves (handles), for example to push the victim.
Stat changes#
blse.stat_change (b0.5.5, recognized builds only) is published after every change to a character stat's base value: eight NUL-separated fields, owner entity index (player = 1), stat index, old, new, method, cause, cause entity and stat name (for example bloodpool, health, humanity). See the Python reference for the meaning of each field. Native subscribers are called inside the game's stat code right after the change: read the payload and return; do not change stats from the handler.
blse.stat_change#
Topic BLSE_MSG_STAT_CHANGE. When to use it: a mod that reacts to feeding (blood pool rises), to the player being hurt (health counts damage taken, so it rises), to Humanity loss, to experience gained.
Payload, eight fields:
- the owner's entity index (the player is
"1"); - the stat's index within its group (compare names, not indexes);
- the old value;
- the new value;
- the game method that changed it:
IncBase,DecBase,AddBase,SubBaseorSetBase; - the cause: the game functions that led to the change, innermost first, separated by
" < "("CBaseCombatCharacter::IncBloodPool < CBaseCombatCharacter::Feed"); - the entity name the innermost cause reported (for kills: the victim), or
""; - the stat's name as the game's stat definitions (
vdata/system/stats.txt) give it, in lower case:bloodpool,health,humanity,experience,masquerade,potence,active_potence, ...
Filling a new character's stats with their defaults is not reported. Delivered inside game code.
static void BLSE_CALL on_stat(const char *topic, const void *payload, size_t size, void *user) {
char owner[8], name[64], new_value[16];
payload_field(payload, size, 0, owner, sizeof owner);
payload_field(payload, size, 7, name, sizeof name);
payload_field(payload, size, 3, new_value, sizeof new_value);
if (!strcmp(owner, "1") && !strcmp(name, "humanity")) humanity_now = atoi(new_value); /* show it later */
}Inventory#
list_inventory(context, name, buffer, capacity, size) (b0.5.12) writes the item classnames carried by the player (name NULL or "") or by the character with that targetname, each followed by a NUL. The topics blse.item_added, blse.item_removed, blse.item_equipped and blse.item_unequipped report changes; see the Python reference.
To give, remove, drop or equip items, see World, inventories, controls and screens.
list_inventory#
int list_inventory(void *context, const char *name, char *buffer, size_t capacity, size_t *size);Copies the item classnames a character carries, in slot order. (b0.5.12)
name: the character's targetname;NULLor""for the player. The first character with that name is used; other entities with the same name (such as a cop's car) are skipped.- The names are copied one after the other, each followed by a NUL, like
"item_w_fists\0item_g_wallet\0". No extra NUL is added, and the copy is cut atcapacitybytes.*sizereceives the full length. - Returns 1, or 0 if there is no such character, no level runs, or the game build is not recognized.
char items[4096]; size_t n = 0, i = 0;
if (host->list_inventory(host->context, NULL, items, sizeof items, &n) && n <= sizeof items) {
while (i < n) { const char *item = items + i; i += strlen(item) + 1; /* use item */ }
}blse.item_added, blse.item_removed, blse.item_equipped, blse.item_unequipped#
Topics BLSE_MSG_ITEM_ADDED, BLSE_MSG_ITEM_REMOVED, BLSE_MSG_ITEM_EQUIPPED, BLSE_MSG_ITEM_UNEQUIPPED (b0.5.12). An item entered or left a character's inventory (picked up, given, bought, sold, used up, removed by a script), or was taken in hand or put away. A weapon switch gives item_unequipped (the old one) then item_equipped (the new one). Payload, four fields: owner classname, owner targetname, item classname, item targetname. For the player (owner classname "player") and NPCs. Nothing is reported while a level loads or shuts down. Delivered inside game code. Recognized builds only.
Entities appearing and disappearing#
blse.entity_spawned and blse.entity_removed (b0.5.13): classname, targetname and handle of entities created or removed while a level runs. Spawns are published at the end of the frame; removals inside the game's removal code, before the entity is destroyed. See the Python reference.
blse.entity_spawned, blse.entity_removed#
Topics BLSE_MSG_ENTITY_SPAWNED and BLSE_MSG_ENTITY_REMOVED. Payload, three fields: classname, targetname, handle (decimal; the same number in both events for one entity).
entity_spawnedcomes at the end of the frame the entity was created in, when its name and keyvalues are set.entity_removedcomes when it is removed, before it is destroyed: the entity still exists while native subscribers run, so you can still read it.- An entity removed in the frame it was created in is reported neither spawned nor removed. Entities created while a level loads or removed while it shuts down are not reported: look at a level's entities at
blse.level_initwithfind_entities. - Recognized builds only.
Use them to track entities you care about (a mod's spawned NPCs, every cop that arrives) without searching the level every frame.
Disciplines#
blse.discipline_activated and blse.discipline_ended (b0.5.14): owner, owner entity index, Discipline name and level, whenever a character's Discipline starts or ends. See the Python reference.
blse.discipline_activated, blse.discipline_ended, blse.discipline_blocked#
Topics BLSE_MSG_DISCIPLINE_ACTIVATED, BLSE_MSG_DISCIPLINE_ENDED (b0.5.14) and BLSE_MSG_DISCIPLINE_BLOCKED (b0.5.20). A character activated a Discipline, its effect ended (ran out or was cancelled), or a rule or filter blocked an activation. Payload, five fields:
- the owner's classname (
""if BLSE cannot confirm the owner); - the owner's targetname;
- the owner's entity index (the player is
"1"); - the Discipline: its
stats.txtname in lower case ("potence","celerity","blood_healing", ...); - its level.
They are worked out from the active_<discipline> stats, so the same changes also appear in blse.stat_change. Delivered inside game code. Recognized builds only.
Kills, damage, pushes, console commands#
BLSE_MSG_ENTITY_KILLED (payload as in Python); damage_entity(context, entity, amount, attacker, type_bits) (attacker BLSE_NO_ENTITY for none); entity_velocity(context, entity, velocity); push_entity(context, entity, velocity, add) (NPCs and physics props too since b0.5.44); console_command(context, line); fix_state always returns -1 since b0.5.49 (BLSE applies no game fixes; the slot stays for the API layout). Main thread. Behaviour as in the Python reference.
blse.entity_killed#
Topic BLSE_MSG_ENTITY_KILLED (b0.5.41). An entity with health (a character, a breakable object) was killed by a hit. Payload, eleven fields: victim classname, name, handle; attacker classname, name, handle; inflictor (the weapon or projectile) classname, name, handle; the damage; the damage type bits. Empty fields where there is none. Delivered right after the hit, inside game code.
static void BLSE_CALL on_killed(const char *topic, const void *payload, size_t size, void *user) {
char killer[64], weapon[64];
payload_field(payload, size, 3, killer, sizeof killer);
payload_field(payload, size, 6, weapon, sizeof weapon);
if (!strcmp(killer, "player")) ++player_kills;
}damage_entity#
int damage_entity(void *context, uint32_t entity, float amount, uint32_t attacker, int type_bits);Hurts an entity through the game's own damage, as a real hit would. (b0.5.41)
When to use it: poison or bleeding over time, traps, a spell that hurts everyone nearby. Because it is the game's damage, the victim reacts to the attacker (flees, fights back), a kill counts as the attacker's (the player loses Humanity, the police come), and damage filters, Python rules and blse.damage all apply.
| Parameter | Meaning |
|---|---|
entity |
The victim: an index or a handle |
amount |
The damage, 0 to 1000000 |
attacker |
Who caused it (an index or handle), or BLSE_NO_ENTITY for nobody |
type_bits |
The damage type bits: 0 generic, 2 bullet, 128 club, ... |
Returns 1 if the damage was dealt, 0 if an entity does not exist, amount is out of range, no level runs or the build is not recognized. Do not call it from a damage filter.
host->damage_entity(host->context, npc, 10.0f, 1 /* the player */, 0);entity_velocity#
int entity_velocity(void *context, uint32_t entity, float velocity[3]);Copies an entity's velocity (x, y, z) in game units a second into velocity. (b0.5.41) Returns 1, or 0 if the entity does not exist or no level runs. A physics prop's velocity here is not its physics velocity (props are moved by the physics engine).
push_entity#
int push_entity(void *context, uint32_t entity, const float velocity[3], int add);Gives an entity a push. (b0.5.41; NPCs and physics props since b0.5.44)
When to use it: knockback from a power, a jump boost, an explosion that throws things around.
| Parameter | Meaning |
|---|---|
entity |
An index or a handle (not the world) |
velocity |
x, y, z in game units a second; each between -10000 and 10000 |
add |
1 adds the velocity to the current one, 0 sets it |
What happens depends on the entity:
- The player: its velocity changes. A push upwards takes it off the ground first.
- An NPC: NPCs set their own velocity every time they think, so BLSE uses the game's flying knockback (what a strong blow does) with the given velocity: the NPC flies (the animation chosen from the direction), lands and gets up. Returns 0 when the game would not knock that NPC back now (in a conversation, dead) or the NPC has no flying animation.
- A physics prop (
prop_physics: cans, bottles, boxes): its physics object's velocity is set or added to.
Returns 1, or 0 for an unknown entity, an out-of-range velocity, no level running or the cases above.
const float up[3] = {0.0f, 0.0f, 400.0f};
host->push_entity(host->context, 1, up, 1); /* the player jumps */console_command#
int console_command(void *context, const char *line);Runs a console command line as if the player typed it. (b0.5.41)
line: 1 to 1023 characters, no line breaks (one command, or several separated by;as in the console).- The command is queued and runs before the next frame, not during this call.
- Returns 1 if it was queued, 0 if the line is empty, too long or has a line break, or the console is not available.
host->console_command(host->context, "give item_w_katana");fix_state#
int fix_state(void *context, const char *name);Always returns -1 since b0.5.49. It reported BLSE's game fixes (b0.5.41 to b0.5.48); BLSE applies no game fixes any more (they belong to a separate fixes mod). The field stays so the structure's layout does not change.
Keys and mouse buttons#
Controls (b0.5.26): BLSE_MSG_CONTROL_DOWN / BLSE_MSG_CONTROL_UP (blse.control_down / blse.control_up) report the command a key press in play ran (+jump, +attack, slot1, ...), whichever key it is bound to; payload: command and key name. control_state(context, command) is 1 while it is held. Text elements also have hud_valign(context, id, 0 top / 1 middle / 2 bottom).
Since b0.5.25: hud_input, hud_focus, hud_input_text, hud_align and hud_size, and BLSE_MSG_HUD_HOVER, BLSE_MSG_HUD_TEXT_CHANGED, BLSE_MSG_HUD_TEXT_SUBMIT, BLSE_MSG_HUD_TEXT_CANCEL; see blse_api.h and the Python reference.
BLSE_MSG_KEY_DOWN / BLSE_MSG_KEY_UP (b0.5.24): key name and context, NUL-separated; delivered from the engine's input handling between frames. key_state(context, key) (1, 0, or -1), key_binding(context, key, buffer, capacity, size), block_key(context, key, blocked) (removed if the plugin is disabled) and press_key(context, key, BLSE_KEY_UP / BLSE_KEY_DOWN / BLSE_KEY_TAP). Key names as in the bind command. b0.5.34: BLSE_MSG_KEY_SIMULATED (a mod's press_key: key, down/up, the mod) and BLSE_MSG_MOUSE_MOVE (dx, dy, x, y, context). See the Python reference.
The HUD functions mentioned above (hud_valign, hud_input, ...) and their events are described on the interface page.
Key names#
Keys are named as in the console's bind command, case ignored: letters and digits ("a", "5"), "F1".."F12", "SPACE", "ENTER", "BACKSPACE", "SHIFT", "CTRL", "ALT", "TAB", "ESCAPE", "CAPSLOCK", the arrows "UPARROW", "DOWNARROW", "LEFTARROW", "RIGHTARROW", "INS", "DEL", "HOME", "END", "PGUP", "PGDN", the keypad "KP_ENTER", "KP_5", "KP_PLUS", ..., mouse buttons "MOUSE1".."MOUSE5", the wheel "MWHEELUP" and "MWHEELDOWN". Keys without a name are "KEY<number>" (the engine's key number), as key events report them.
key_state#
int key_state(void *context, const char *key);Whether a key is held down right now: 1 held, 0 not, -1 unknown key name or input unavailable (unknown build). (b0.5.24)
Use it for "hold a key" features: sprint while Shift is held, show a map while Tab is held. To react to a press, subscribe to blse.key_down instead of checking every frame.
key_binding#
int key_binding(void *context, const char *key, char *buffer, size_t capacity, size_t *size);Copies the console command bound to a key ("+jump", "" if none), following the output buffer rules. Returns 1, or 0 for an unknown key or unavailable input. (b0.5.24)
Use it to show the right key in hints ("press E to use"), or to warn when your mod's key is already bound.
block_key#
int block_key(void *context, const char *key, int blocked);While a key is blocked (by any mod), a press of it during play does not reach the game: its binding does not run. blse.key_down is still published, so your plugin can give the key a new meaning. (b0.5.24)
blocked: 1 blocks, 0 ends your block (other mods' blocks stay).- Returns 1, or 0 for an unknown key or unavailable input.
- Your blocks are removed when your plugin is disabled. Blocking applies to play only, not to the console or menus.
host->block_key(host->context, "f", 1); /* F no longer runs the game's flashlight; we use it */press_key#
int press_key(void *context, const char *key, int mode);Simulates a key as if the player pressed it: its binding runs as for a real press. (b0.5.24)
mode:BLSE_KEY_DOWN(1) presses it and keeps it held,BLSE_KEY_UP(0) releases it,BLSE_KEY_TAP(2) presses it and releases it after one whole game frame.- Returns 1, or 0 for an unknown key, an invalid mode, unavailable input or the wrong thread.
- Simulated presses are not reported as
blse.key_down/blse.key_up; they are reported asblse.key_simulated.
host->press_key(host->context, "SPACE", BLSE_KEY_TAP); /* jump once */control_state#
int control_state(void *context, const char *command);1 while a key whose press ran command ("+jump", "+duck", "+attack", case ignored) is held, else 0; -1 if input is unavailable. (b0.5.26)
Better than key_state for game actions: it works whichever key the player bound to the action.
if (host->control_state(host->context, "+speed") == 1) { /* the player holds the walk/run key */ }blse.key_down, blse.key_up#
Topics BLSE_MSG_KEY_DOWN and BLSE_MSG_KEY_UP (b0.5.24). A key or mouse button was pressed or released. Payload, two fields:
- the key name;
- where it happened:
"game"(playing),"cursor"(a mouse button in cursor mode),"text"(typing into a focused HUD input, b0.5.25),"console"(typing in the console) or"menu"(a game screen or the game menu is open, or no level runs; keys in the game menu since b0.5.25).
Auto-repeat is not reported. Subscribers (native and Python) are called from the engine's input handling, between frames, right away. Recognized builds only.
static void BLSE_CALL on_key(const char *topic, const void *payload, size_t size, void *user) {
char key[32], where[16];
payload_field(payload, size, 0, key, sizeof key);
payload_field(payload, size, 1, where, sizeof where);
if (!_stricmp(key, "F7") && !strcmp(where, "game")) toggle_my_menu();
}
host->subscribe(host->context, BLSE_MSG_KEY_DOWN, on_key, NULL);blse.key_simulated#
Topic BLSE_MSG_KEY_SIMULATED (b0.5.34). A mod simulated a key with press_key. Payload, three fields: the key name, "down" or "up", and the mod ("python" or the plugin's name).
blse.mouse_move#
Topic BLSE_MSG_MOUSE_MOVE (b0.5.34). The mouse moved while the game window is in front; at most once per rendered frame. Payload, five fields:
- dx and
- dy: pixels moved since the last event (while the game looks around with the mouse it puts the cursor back every frame, which is not counted);
- x and
- y: the cursor's position in the window, 0 to 1 (meaningful in menus and cursor mode);
- the context:
"game","menu","console"or"cursor".
blse.control_down, blse.control_up#
Topics BLSE_MSG_CONTROL_DOWN and BLSE_MSG_CONTROL_UP (b0.5.26). A key press in play ran its binding, whichever key it is: "+jump", "+attack", "+use", "slot1", ... control_up follows when that key is released (for every command, not only + ones). Payload, two fields: the command, the key name. Not sent for keys BLSE blocked, keys typed into a HUD input, or keys pressed in the console or menus.
Use it to react to game actions rather than to keys: "the player jumped" works with any key layout.
Discipline filters#
add_discipline_filter(context, filter, user) (b0.5.20) calls filter(BLSE_Discipline*, user) before a character activates a Discipline: owner class, name and entity index, Discipline name and level are information; set allow = 0 to block the activation (no effect, no blood cost) or change cost_factor to scale its blood cost. Python rules are applied first. Filters run inside game code, under the crash guard; a plugin that crashes in one is disabled.
add_discipline_filter#
int add_discipline_filter(void *context, BLSE_DisciplineFilterFn filter, void *user);
typedef void (BLSE_CALL *BLSE_DisciplineFilterFn)(BLSE_Discipline *discipline, void *user);When to use it: a mod that forbids Disciplines in Elysium (block everything while on certain maps), makes one clan's powers cheaper, or stops NPCs from using a Discipline.
Returns 1 if the filter was added, 0 if Discipline control is unavailable (unknown build), filter is NULL or the call came from another thread.
| Field | Meaning |
|---|---|
struct_size |
sizeof(BLSE_Discipline) |
owner_class, owner_name |
The character activating it |
owner_index |
Its entity index; the player is 1 |
discipline |
The stats.txt name in lower case: "potence", "celerity", ... |
level |
The level being activated |
allow |
Writable. 0 blocks the activation: no effect and no blood cost. A Python rule may already have set it to 0 |
cost_factor |
Writable. Multiplies the blood cost: 1 unchanged, 0 free, 2 double. Rounded to whole blood points (halves away from zero). Negative or invalid values become 0. Python rules' factors are already in it |
Filters run in the order they were added, each seeing the previous one's result. A blocked activation is reported by blse.discipline_blocked. The game would show its "Need More Blood" notice for it (a failed activation is all it knows); since b0.5.21 BLSE hides that notice, so show your own explanation (for example with show_text).
static void BLSE_CALL rules(BLSE_Discipline *d, void *user) {
if (d->owner_index == 1 && !strcmp(d->discipline, "potence")) d->allow = 0; /* no Potence */
if (d->owner_index == 1) d->cost_factor *= 0.5f; /* half price */
}Dialogue#
blse.dialog_line and blse.dialog_choice (b0.5.10) report what the NPC says and which reply the player picks, with the dialogue file, the NPC, the line numbers of the .dlg file and the texts. See the Python reference for the fields. Native subscribers are called inside the game's dialogue code.
add_dialog_filter(context, filter, user) (b0.5.22) calls filter(BLSE_DialogLine*, user) for each line the player is about to see: the NPC's line (is_reply = 0) and each reply whose .dlg condition passed (is_reply = 1), with the file, NPC, line number and .dlg texts. Set visible = 0 to hide a reply, or replace_text to show other text (copied when the filter returns, up to 1000 bytes; only the text on screen changes). Python rules (blse.hide_dialog_reply, blse.set_dialog_text) are applied first, and the filter sees their result. The last visible reply is never hidden. Filters run inside game code, under the crash guard.
To add whole conversations, nodes and replies, see Conversations.
blse.dialog_line#
Topic BLSE_MSG_DIALOG_LINE. The NPC is about to say a line. Payload, six fields:
- the dialogue file, as the game names it (
"dlg/main characters/jack_tutorial.dlg"); - the NPC's classname;
- the NPC's targetname;
- the line's number in the
.dlgfile; - the line's text as written for a male player;
- the text for a female player.
The texts are the file's own (a replacement set by a filter is not reported). Delivered inside the game's dialogue code. Recognized builds only.
blse.dialog_choice#
Topic BLSE_MSG_DIALOG_CHOICE. The player picked a reply. Payload, eight fields:
- the dialogue file;
- the NPC's classname;
- the NPC's targetname;
- the choice: 1-based, as shown on screen (hidden replies are not counted);
- the reply's line number in the file;
- the NPC line that follows (
"0"when the conversation ends); - the reply's text for a male player;
- the text for a female player.
static void BLSE_CALL on_choice(const char *topic, const void *payload, size_t size, void *user) {
char file[260], reply[16];
payload_field(payload, size, 0, file, sizeof file);
payload_field(payload, size, 4, reply, sizeof reply);
if (strstr(file, "jack_tutorial.dlg") && !strcmp(reply, "25")) asked_jack = 1;
}add_dialog_filter#
int add_dialog_filter(void *context, BLSE_DialogFilterFn filter, void *user);
typedef void (BLSE_CALL *BLSE_DialogFilterFn)(BLSE_DialogLine *line, void *user);Calls filter for every dialogue line the player is about to see: the NPC's line, and each reply whose .dlg condition passed. The filter may hide a reply or change the text shown. (b0.5.22)
When to use it: hide replies that make no sense in a mod's story, show a different text depending on a mod's state, translate lines on the fly. To decide with Python code instead, see blse.hide_dialog_reply.
Returns 1 if the filter was added, 0 if dialogue control is unavailable (unknown build), filter is NULL or the call came from another thread.
| Field | Meaning |
|---|---|
struct_size |
sizeof(BLSE_DialogLine) |
file |
The dialogue file ("dlg/main characters/jack_tutorial.dlg") |
npc_class, npc_name |
The NPC speaking |
line |
The line's number in the .dlg file |
is_reply |
0 for the NPC's line, 1 for a player reply |
text_male, text_female |
The .dlg texts (the game shows one of them, by the player's gender) |
visible |
Writable, replies only. 0 hides the reply; the remaining replies move up. Python rules may already have set it to 0 |
replace_text |
Writable. A text to show instead (NULL: unchanged). BLSE copies it when the filter returns (up to 1000 bytes), so a local buffer is fine |
Notes:
- Only the text on screen changes: the voice-over, the line's conditions and actions and where it leads stay the same.
- The last visible reply is never hidden: the game would otherwise show "I do not have a valid reply.", so BLSE then keeps them all (logged once).
- Filters run in order, each seeing the previous one's result.
static void BLSE_CALL dialog(BLSE_DialogLine *d, void *user) {
if (d->is_reply && strstr(d->file, "jack_tutorial.dlg") && d->line == 25) d->visible = 0;
if (!d->is_reply && strstr(d->file, "jack_tutorial.dlg") && d->line == 24) d->replace_text = "Just the basics.";
}