BLSE0.6

Native plugins

The world, saved data and settings

On this page 71 sections

This page is the plugin reference for everything that touches the game world and your mod's data:

  • data kept in the save game (save_set, the shared database, values stored on entities);
  • settings files players edit, and JSON files shared by every save;
  • the world: spawning and moving entities, inventories, NPC behaviour, animation, models and their looks, the player's outfits, the camera and sounds;
  • mod folders: giving the game files from memory, changing game files and maps as they load;
  • companions that follow the player from map to map.

Come here when your plugin should remember something, change something in a running level, or change the game's files. Every function behaves like its Python twin; the Python pages (world, data, NPC AI, animation, appearance, mod folders, companions) have more background and examples. This page gives the C side: the exact parameters and return values.

A minimal example: a plugin that counts the player's kills and keeps the count in the save game.

C
#include <string.h>
#include "blse_api.h"

static const BLSE_HostAPI *host;
static int kills;

/* A save was loaded: read our value back (0 if this save has none). */
static void BLSE_CALL on_loaded(const char *topic, const void *payload, size_t size, void *user) {
    size_t n = 0;
    kills = 0;
    if (!host->save_get(host->context, "mymod.kills", &kills, sizeof kills, &n) || n != sizeof kills) kills = 0;
}

/* A new game: start from zero. */
static void BLSE_CALL on_new_game(const char *topic, const void *payload, size_t size, void *user) { kills = 0; }

/* Someone died: if the player killed them, count it and store the new count at once.
   Field 3 of blse.entity_killed is the attacker's classname; payload_field is the
   helper from getting-started.md#payloads. */
static void BLSE_CALL on_killed(const char *topic, const void *payload, size_t size, void *user) {
    char killer[64];
    if (!payload_field(payload, size, 3, killer, sizeof killer) || strcmp(killer, "player")) return;
    ++kills;
    host->save_set(host->context, "mymod.kills", &kills, sizeof kills);
}

BLSE_EXPORT int BLSE_CALL BLSE_PluginLoad(const BLSE_HostAPI *h) {
    host = h;
    if (h->api_version != BLSE_API_VERSION || !BLSE_HOST_HAS(h, save_get)) return 0;
    h->subscribe(h->context, BLSE_MSG_GAME_LOADED, on_loaded, NULL);
    h->subscribe(h->context, BLSE_MSG_NEW_GAME, on_new_game, NULL);
    h->subscribe(h->context, BLSE_MSG_ENTITY_KILLED, on_killed, NULL);
    return 1;
}

Important, before you start:

  • Main thread only, unless an entry says "any thread". Call these functions from BLSE_PluginLoad, a console command, an event handler, a filter or a task added with add_task. From another thread they return 0 (or -1, or BLSE_NO_ENTITY, as each entry says). See Threads.
  • Most world functions work only while a level runs (a map is loaded and being played; not in the main menu, not during loading). Outside a level they return 0.
  • Nothing in the game exists while your plugin loads. There is no level, no player and no save data yet at BLSE_PluginLoad: save_set and save_get return 0 there. Register things that must exist early (file filters, item classes, outfits, model animations) at load; do the rest in event handlers.
  • Entities can disappear at any time (killed, picked up, removed by a script, the level ends). Keep handles, not indexes, between frames, and expect a function to return 0 for an entity that is gone.
  • Several features need a recognized game build (host->build_profile is not NULL): NPC AI, animation, outfits, item classes and others. On other builds they return 0 or -1.
  • Check newer fields before you call them: BLSE_HOST_HAS(host, field) tells you whether the BLSE running your plugin has the function (see Checking that a field exists).

Words used on this page#

Word Meaning
Entity Anything that exists in a level: the player, NPCs, doors, props, items lying around, triggers. Each has a classname (its kind, such as npc_VPedestrian or item_g_bloodpack) and often a targetname (its name in the map, such as guard_1)
Index An entity's slot number on the server, below 8192. 0 is the world itself, 1 is always the player. A slot is reused when an entity is removed, so an index may later name another entity
Handle A number that names one entity for as long as it exists; after the entity is removed the handle names nothing (no reuse). Functions return handles; every entity parameter takes an index or a handle
BLSE_NO_ENTITY 0xFFFFFFFF: "no entity". Returned when there is none, and passed to say "none" (no attacker, clear the enemy, stop following)
Level One loaded map being played. A level runs from blse.level_init to blse.level_shutdown
Keyvalue A setting a map gives an entity, as a key and a text value: targetname = guard_1, model = models/...mdl. Most are read only when the entity spawns
Inventory The items a character (the player, an NPC) or a container (a box, a locker) holds
Stack Several items of one kind kept as one entry with a count (blood packs, ammunition). Weapons and most other items cannot stack: an owner has at most one of each
Schedule A short plan an NPC's AI runs (SCHED_TROIKA_FLEE: run away; SCHED_TROIKA_IDLE_PATROL: walk around). The AI picks a new one when the old one ends or something happens
Sequence One animation stored in a model (walk_all, idle01), numbered in the model's order
Activity What the AI wants to do (ACT_WALK, ACT_IDLE, ACT_RANGE_ATTACK1). The game picks a sequence for the activity from the model
Bone One joint of a model's skeleton (Bip01 Head). Animations move bones
Flex A flex controller: a slider in a face model for one expression or mouth shape (smile, blink, jaw open)
Skin, body group A model's alternative textures (skins), and parts that can be switched (body groups: a hat or none)
Material What a mesh of a model is drawn with: a .vmt file below materials/ naming textures and a shader
Units Distances in the game world. A character is about 72 units tall
Angles Three numbers in degrees: pitch (up/down), yaw (turn left/right), roll (tilt)
Script globals (G) The game's Python variables that it writes into every save. BLSE stores your save data there
Recognized build A game build BLSE knows exactly (host->build_profile is not NULL)

Text that a function copies out (names, lists, JSON) follows the output buffer rules: pass buffer, its capacity in bytes and a size_t that receives the full length; the text is cut to fit and always ends with a NUL. Call once with buffer NULL and capacity 0 to learn the length. Lists end each line with '\n'; fields within a line are separated by a tab ('\t').

Save-game data#

blse.before_save (b0.5.9) is published right before the game writes a save (also autosaves and level transitions). Data written with save_set in its handler is in that save.

save_set/save_get keep small values (up to 1 MiB each) with the save game. BLSE stores them in the game's own script globals (G.blse_data), which the game writes into every save, replaces on load, keeps across level transitions and clears on a new game. So:

  • Write a value whenever it changes, or in a blse.before_save handler.
  • Read your values in blse.game_loaded (sent on the first frame after a save has been restored), not in blse.level_init.
  • Keys follow the topic rules, up to 117 bytes (each key is one line in the save, and the game must still read it when BLSE is removed; quotes, backslashes and bytes outside printable ASCII count as 2 to 4); prefix them with your mod name. blse. keys are reserved.
  • Both functions return 0 before the game's scripts exist (during plugin load, before the main menu).
  • Since b0.5.25, save_set_versioned(context, key, version, data, size) and save_get_versioned(context, key, buffer, capacity, size, &version) store a format version with the record: write your current version, and on load convert older data. Values written with save_set read as version 0. Keys up to 108 bytes.
  • BLSE_MSG_NEW_GAME (blse.new_game, b0.5.25) is published at the first level of a new game, before blse.level_init reaches you: clear your in-memory state. Payload: the map name.

Your data's life in short:

Moment What happens to the values What to do
The game starts, main menu No save data exists yet; save_get/save_set return 0 until the game's scripts exist Nothing
New game (blse.new_game) Empty Reset your in-memory state
Playing Your values live in memory, in G.blse_data save_set whenever a value changes
A save is written (blse.before_save) Whatever is stored now goes into the save Store anything you keep only in your own variables
Level change Kept, like the rest of the game's globals Nothing
A save is loaded (blse.game_loaded) Replaced by the save's values Read your values with save_get
C
static void BLSE_CALL on_loaded(const char *topic, const void *map, size_t size, void *user) {
    int kills = 0; size_t n = 0;
    if (host->save_get(host->context, "mymod.kills", &kills, sizeof(kills), &n) && n == sizeof(kills)) { /* use kills */ }
}
/* whenever it changes: */ host->save_set(host->context, "mymod.kills", &kills, sizeof(kills));

The events are described in Level, frame and save events: blse.before_save, blse.game_loaded, blse.new_game.

save_set#

C
int save_set(void *context, const char *key, const void *data, size_t size);

Stores a copy of size bytes from data under key in the save data. (0.3)

When to use it: anything your mod must remember when the player saves and loads: a counter, a quest stage of your own, which reward was given.

  • key: a name for the value, following the topic rules, at most 117 bytes counted as written in the save (quotes, backslashes and bytes outside printable ASCII count 2 to 4 each). Start it with your mod's name ("mymod.kills"); keys starting with blse. are reserved for BLSE.
  • data, size: the bytes to store, any binary content (a number, a struct, text), at most 1 MiB. BLSE copies them; your buffer can be freed afterwards. data NULL with size 0 removes the key.
  • Returns 1, or 0 if the key is invalid or too long, the value is over 1 MiB, or no game session exists yet (the game's script globals do not exist before the main menu, so it fails at plugin load).
  • Store a struct only if its layout never changes, or use save_set_versioned so you can convert old data.

save_get#

C
int save_get(void *context, const char *key, void *buffer, size_t capacity, size_t *size);

Reads a value stored with save_set (or save_set_versioned). (0.3)

  • buffer, capacity: where to copy the bytes and how many fit. BLSE copies the first min(capacity, length) bytes and adds nothing (no NUL: this is binary data). buffer may be NULL to ask only for the length.
  • size: receives the value's full length in bytes (may be NULL). It is set to 0 when the call fails.
  • Returns 1 if the key exists, 0 if it does not or the save data is not available yet.
C
size_t n = 0;
if (host->save_get(host->context, "mymod.names", NULL, 0, &n)) {   /* ask for the length */
    char *text = (char *)malloc(n + 1);
    if (text && host->save_get(host->context, "mymod.names", text, n, &n)) text[n] = 0;   /* add the NUL yourself */
    free(text);
}

save_set_versioned, save_get_versioned#

C
int save_set_versioned(void *context, const char *key, uint32_t version, const void *data, size_t size);
int save_get_versioned(void *context, const char *key, void *buffer, size_t capacity, size_t *size, uint32_t *version);

Like save_set and save_get, with a format version number stored next to the value. (b0.5.25)

When to use them: your saved data is a struct that may change in a later version of your mod. Write your current format's version; when you load, look at the version and convert older data.

  • version: any number you choose (1, 2, ...). save_get_versioned stores the version found in *version (0 for values written with plain save_set, so older saves stay readable). version may be NULL.
  • Keys up to 108 bytes (the version is kept under a key of its own).
  • save_set_versioned with data NULL and size 0 removes the record and its version.
  • Returns like save_set / save_get.
C
typedef struct { int kills; int deaths; } StatsV2;   /* version 1 had only kills */
StatsV2 s = {0};
uint32_t version = 0; size_t n = 0;
if (host->save_get_versioned(host->context, "mymod.stats", &s, sizeof s, &n, &version)) {
    if (version < 2) s.deaths = 0;   /* converts version 0/1 data: only the first int was stored */
}
host->save_set_versioned(host->context, "mymod.stats", 2, &s, sizeof s);

Settings files#

ini_get(context, mod, section, key, buffer, capacity, &size) copies a value from Bin\loader\blse\config\<mod>.ini (like key_binding; 0 if the file or key is missing); ini_set(context, mod, section, key, value) writes it (NULL removes the key) and keeps the rest of the file. Format and rules as in the Python reference. The example plugin examples/sprint_plugin.cpp reads its settings this way.

A settings file is a plain text file players can edit:

ini
; a comment (also # at the start of a line)
[Sprint]
key = ALT
speed_factor = 1.6

ini_get, ini_set#

C
int ini_get(void *context, const char *mod, const char *section, const char *key, char *buffer, size_t capacity, size_t *size);
int ini_set(void *context, const char *mod, const char *section, const char *key, const char *value);

Read and write one setting in your mod's settings file. (b0.5.37)

When to use them: options players change by hand (a key, a speed, a colour) without recompiling your plugin.

  • mod: the file's name without .ini, 1-63 characters of A-Z a-z 0-9 _ - .. The file is Bin\loader\blse\config\<mod>.ini.
  • section, key: matched without regard to case. Spaces around keys and values are ignored; a value in double quotes keeps its spaces. Nothing after a value is a comment (color = #FF0000 is a colour).
  • ini_get copies the value as text (always text: convert numbers with atoi/atof). Returns 1, or 0 if the file or the key is missing.
  • ini_set writes value, keeping the rest of the file as it was (comments too), and makes the folder and file if needed. value NULL removes the key (every copy of it in the section). Returns 1 or 0.
  • Files are UTF-8, up to 1 MB. Main thread.
  • Ship the file with comments explaining each setting, and write missing keys with their defaults so players can find them.
C
char text[32];
float speed = 1.6f;
if (host->ini_get(host->context, "sprint", "Sprint", "speed_factor", text, sizeof text, NULL)) speed = (float)atof(text);
else host->ini_set(host->context, "sprint", "Sprint", "speed_factor", "1.6");   /* write the default */

World, inventories, controls and screens#

spawn_entity(context, classname, origin, yaw, pairs, pair_count) (pairs: key, value, key, value, ...) returns the handle or BLSE_NO_ENTITY; teleport_entity(context, entity, origin, angles) (NULL keeps each); give_item, remove_item(..., count), item_count, equip_item, drop_item, move_item(context, from, to, classname). Controls: menu_add_checkbox, menu_add_slider, menu_add_dropdown(..., items, item_count, selected, ...), menu_add_progress, menu_add_list, menu_list_add, menu_list_clear, menu_value(context, screen, path, buffer, capacity, &size), menu_set_value, menu_set_items; menu_block(context, screen, blocked); hud_in_menus(context, id, on). Messages BLSE_MSG_MENU_CHANGED and BLSE_MSG_MENU_BLOCKED. Main thread. Behaviour as in the Python reference and controls.

The controls, menu_block, hud_in_menus and their events are explained entry by entry on the interface page: Controls, lists and blocked screens. Spawning, teleporting and inventories (b0.5.46) follow here. All of them: main thread, while a level runs. owner, from, to and entity are an index or a handle (1 is the player).

spawn_entity#

C
uint32_t spawn_entity(void *context, const char *classname, const float origin[3], float yaw,
                      const char *const *pairs, size_t pair_count);

Creates an entity the way a map does, and spawns it into the level. (b0.5.46)

When to use it: an NPC that appears when the player enters a room, an item lying on a table as a reward, a prop.

  • classname: the kind of entity, 1-63 characters of A-Z a-z 0-9 _ ("npc_VPedestrian", "item_g_bloodpack", "prop_physics"). player and worldspawn are refused.
  • origin: where, in world units; each coordinate between -65536 and 65536.
  • yaw: which way it faces, in degrees.
  • pairs, pair_count: the map's keyvalues as an array of strings, key, value, key, value, ...; pair_count is the number of pairs (not strings), at most 256. NULL and 0 for none. Keys: 1-63 characters of A-Z a-z 0-9 _; values up to 255 characters. classname and origin may not appear (they are the parameters). Keys the entity does not know are ignored, as in maps.
  • Returns the new entity's handle, or BLSE_NO_ENTITY if anything is refused: bad arguments, no level, a class the game does not know, an entity that failed to spawn or removed itself while spawning. Unlike Python's blse.spawn_entity, the reason is not logged for plugins.
  • Spawned entities are saved with the game like the map's own.
C
const char *kv[] = {
    "model", "models/character/npc/common/citizen/male/male_citizen_1.mdl",
    "targetname", "mymod_guard",
};
float at[3];
if (host->entity_position(host->context, 1, at)) {    /* next to the player */
    at[0] += 64;
    uint32_t guard = host->spawn_entity(host->context, "npc_VPedestrian", at, 180.0f, kv, 2);
    if (guard == BLSE_NO_ENTITY) host->log("the guard could not be spawned");
}

teleport_entity#

C
int teleport_entity(void *context, uint32_t entity, const float *origin, const float *angles);

Moves an entity somewhere at once, and/or turns it. (b0.5.46)

  • origin: the new position (three floats, each between -65536 and 65536), or NULL to keep the position.
  • angles: pitch, yaw, roll in degrees, or NULL to keep them. At least one of the two must be given.
  • Works on the player, NPCs and props; not on the world (index 0).
  • Returns 1, or 0 if the entity does not exist or a value is invalid.
C
const float spot[3] = {120.0f, -340.0f, 16.0f};
host->teleport_entity(host->context, 1, spot, NULL);   /* the player, facing as before */

give_item#

C
int give_item(void *context, uint32_t owner, const char *classname);

Gives one item to the player, an NPC or a container. (b0.5.46)

When to use it: a reward, a shop of your own (read the price with item_info, take the money through the game's Python pc.MoneyAdd, then give the item).

  • classname: the item's class, such as "item_g_bloodpack" or "item_w_thirtyeight" (1-63 characters of A-Z a-z 0-9 _).
  • The player's item goes through the game's own GiveItem. If the owner already has one: a stack (blood packs, ammunition) grows by one; a second unstackable item is refused, as the game allows one of each.
  • Returns 1, or 0 if refused (no such owner, no inventory, an unknown class, already owned and not stackable).

remove_item#

C
int remove_item(void *context, uint32_t owner, const char *classname, int count);

Removes up to count items of a class, the way the game's RemoveItem does (from a stack first, then the item itself). (b0.5.46)

  • count: how many, at least 1 (values above 100000 count as 100000).
  • Returns how many were removed: 0 if the owner had none or the arguments are invalid, fewer than count if it had fewer.

item_count#

C
int item_count(void *context, uint32_t owner, const char *classname);

How many items of a class the owner has, counting stacks (a stack of 5 blood packs is 5). (b0.5.46)

  • Returns the number (0 if it has none), or -1 if the owner does not exist or has no inventory, or the arguments are invalid.
C
if (host->item_count(host->context, 1, "item_g_bloodpack") >= 3) host->remove_item(host->context, 1, "item_g_bloodpack", 3);

equip_item#

C
int equip_item(void *context, uint32_t owner, const char *classname);

The character takes an item it owns in its hands (a weapon). (b0.5.46) Returns 1, or 0 if it does not own one or cannot hold it.

drop_item#

C
int drop_item(void *context, uint32_t owner, const char *classname);

Drops one item of the class into the world, as the inventory screen does. (b0.5.46) Returns 1, or 0 if the owner has none or the item may not be dropped (quest items).

move_item#

C
int move_item(void *context, uint32_t from, uint32_t to, const char *classname);

Moves an item (a whole stack) from one inventory to another: the player, NPCs, containers. (b0.5.46)

When to use it: a stash box of your own, an NPC handing the player its weapon, taking a companion's items.

  • The same item entity moves, with its ammunition and state.
  • Not the item the character holds in its hands: switch away first (equip_item with another item).
  • Returns 1, or 0 if from has no such item, from and to are the same, the item is in hands, or to refuses it (a full inventory; the item then stays with from).

NPC AI#

fire_input(context, entity, input, value, activator); set_relationship(context, npc, target, disposition, priority) with BLSE_DISPOSITION_HATE/FEAR/LIKE/NEUTRAL, relationship(context, npc, target, &disposition, &priority); set_enemy(context, npc, target) (BLSE_NO_ENTITY clears and calms), npc_enemy(context, npc); start_schedule(context, npc, name), npc_schedule(context, npc, buffer, capacity, &size), npc_state(context, npc) (BLSE_NPC_STATE_*, -1 if not an NPC); move_npc(context, npc, goal, position, run) (goal BLSE_NO_ENTITY: to position); follow_npc(context, npc, leader), npc_leader(context, npc); npc_senses(context, npc, values) (sight, field of view in degrees, vision and hearing multipliers, stealth level), set_npc_sense(context, npc, sense, value); (b0.5.48) why an NPC does not fire: npc_conditions(context, npc, ids, capacity, &count) (the AI condition ids set now: 64 no ammunition, 79 can fire its ranged weapon, 95/96 too close/too far, 97 not facing its enemy, 99 a character it does not hate is in the line of fire, 71 lost its enemy) and npc_weapon(context, npc, classname, capacity, &size, &clip) (the weapon in its hands and the rounds in its clip). Messages BLSE_MSG_NPC_SCHEDULE and BLSE_MSG_NPC_STATE. Main thread. Behaviour as in the Python reference.

Everything goes through the game's own code (the functions its maps and its aiscripted_schedule entity use). (b0.5.45 unless noted.) Main thread, while a level runs; the functions return 1 or 0 unless an entry says otherwise. npc is an NPC's index or handle; other entities likewise.

Things to know first:

  • Who fights. Hate makes an NPC fight only if its kind can: civilians (pedestrians, bluebloods, prostitutes, patients) have no attack and take cover instead. Thugs, gangsters, guards and cops attack.
  • Enemies stay while hated. The game drops an enemy the NPC does not hate. To start a fight: set_relationship(npc, target, BLSE_DISPOSITION_HATE, 99), then set_enemy(npc, target). To end one: neutral again, then set_enemy(npc, BLSE_NO_ENTITY).
C
/* An NPC defends the player: whoever hurts the player becomes its enemy. */
static void make_bodyguard(uint32_t npc, uint32_t attacker) {
    host->set_relationship(host->context, npc, attacker, BLSE_DISPOSITION_HATE, 99);
    host->set_enemy(host->context, npc, attacker);
}

fire_input#

C
int fire_input(void *context, uint32_t entity, const char *input, const char *value, uint32_t activator);

Sends an input to any entity (also one without a name) as the map's wiring does. (b0.5.45)

When to use it: open a door ("Open"), kill an NPC ("Kill"), start a map's logic from your plugin.

  • input: the input's name ("Open", "Kill", "SetAnimation").
  • value: the input's value as text, up to 1023 characters; NULL or "" for none.
  • activator: the entity that "caused" it (maps pass it on), or BLSE_NO_ENTITY for the player.
  • Input filters and blse.entity_input see it like any input.
  • Returns 1 if the entity handled the input, 0 otherwise.
  • BLSE keeps every different input text for the rest of the game (an entity may keep a pointer to it), at most 20000.
C
host->fire_input(host->context, door, "Open", NULL, BLSE_NO_ENTITY);

set_relationship, relationship#

C
int set_relationship(void *context, uint32_t npc, uint32_t target, int disposition, int priority);
int relationship(void *context, uint32_t npc, uint32_t target, int *disposition, int *priority);

How an NPC feels about one entity. (b0.5.45)

  • disposition: BLSE_DISPOSITION_HATE (1, attacks or takes cover), BLSE_DISPOSITION_FEAR (2, runs away), BLSE_DISPOSITION_LIKE (3), BLSE_DISPOSITION_NEUTRAL (4).
  • priority: decides between several enemies, higher first (0 is normal).
  • set_relationship is saved with the game.
  • relationship reads it into *disposition and *priority (either may be NULL), also the game's own relationships (by class) when no mod set one.
  • Return 1 or 0.

set_enemy, npc_enemy#

C
int set_enemy(void *context, uint32_t npc, uint32_t target);
uint32_t npc_enemy(void *context, uint32_t npc);

set_enemy makes target (a character: the player or an NPC) the NPC's enemy and tells it where the target is. BLSE_NO_ENTITY clears the enemy and calms the NPC: back to idle, its current schedule (taking cover, chasing) ended, so it talks again. An NPC keeps an enemy only while it hates it. Returns 1 or 0. (b0.5.45)

npc_enemy returns the handle of the NPC's enemy, or BLSE_NO_ENTITY (none, not an NPC, or called off the main thread).

start_schedule, npc_schedule#

C
int start_schedule(void *context, uint32_t npc, const char *schedule);
int npc_schedule(void *context, uint32_t npc, char *buffer, size_t capacity, size_t *size);

start_schedule runs one of the NPC's schedules once, then its own AI carries on. (b0.5.45)

  • schedule: the game's name, with or without SCHED_ ("SCHED_TROIKA_FLEE" or "TROIKA_FLEE"). Each kind of NPC knows its own set (SCHED_TROIKA_COWER, SCHED_VCOP_WANDER_PATROL, ...); npc_schedule and the blse.npc_schedule event show which ones an NPC uses.
  • It starts when the NPC's running schedule ends, as the map's ChangeSchedule input does: a walking NPC first finishes its step of the path (up to about 1.5 seconds).
  • Returns 1, or 0 if its kind has no such schedule, or it is dead, in a scripted scene or a conversation.
  • SCHED_TROIKA_FORCED_RANGE_ATTACK1 makes an NPC face its enemy and fire regardless, until the enemy dies.

npc_schedule copies the name of the schedule the NPC runs now ("SCHED_TROIKA_IDLE_PATROL"; "" if none). Returns 1, or 0 if it is not an NPC.

npc_state#

C
int npc_state(void *context, uint32_t npc);

The NPC's state, one of the BLSE_NPC_STATE_* numbers: NONE 0, IDLE 1, COMBAT 2, ALERT 3, SCRIPT 4, PLAYDEAD 5, PRONE 6, DEAD 7, FLEEING 8, RETREATING 9, COWERING 10, HUNTING 11, DIALOG 12, OBLIVIOUS 13, CRIMINAL_SUSPICION 14. Returns -1 if it is not an NPC. (b0.5.45)

move_npc#

C
int move_npc(void *context, uint32_t npc, uint32_t goal, const float position[3], int run);

Sends the NPC walking (or running) somewhere. (b0.5.45)

  • goal: an entity to go to (where it is now). BLSE_NO_ENTITY: go to position instead.
  • position: a point in the world, used only when goal is BLSE_NO_ENTITY (then it must not be NULL).
  • run: 1 runs, 0 walks.
  • Moves go to a marker BLSE places (an info_target named blse_goal, reused, never removed), because the game reads its goal again while walking and would crash on a goal entity removed meanwhile.
  • A move ends a follow_npc. Returns 1 or 0.

follow_npc, npc_leader#

C
int follow_npc(void *context, uint32_t npc, uint32_t leader);
uint32_t npc_leader(void *context, uint32_t npc);

follow_npc makes the NPC follow leader (the player, another NPC, any entity): when it is more than 160 units away it is sent after it, running beyond 420. (b0.5.45)

  • leader BLSE_NO_ENTITY stops following. It also stops when the NPC dies, the level ends, or it is moved with move_npc; fights, scripted scenes and conversations pause it.
  • Not kept in saves: store it yourself (for example with entity_data_set) and call it again after a load and at blse.level_init. For an NPC that goes along through level changes, see Companions.
  • The game's own follower system is not used (it ends the game for NPCs in a squad, and pedestrians ignore it).
  • Returns 1 or 0. npc_leader returns the handle of the entity it follows, or BLSE_NO_ENTITY.

npc_senses, set_npc_sense#

C
int npc_senses(void *context, uint32_t npc, float values[5]);
int set_npc_sense(void *context, uint32_t npc, const char *sense, float value);

How well an NPC sees and hears. (b0.5.45)

npc_senses fills values (an array of 5 floats, must not be NULL):

Index Meaning
0 Sight distance, in units
1 Field of view, in degrees
2 The stealth system's vision multiplier in effect (how quickly it notices a sneaking player by sight)
3 The hearing multiplier in effect
4 The NPC's stealth level, 1-10

set_npc_sense changes one: sense is "sight" (units), "fov" (degrees, 1-360), "vision" or "hearing" (multipliers). A negative vision or hearing gives the game's own value back (from the NPC's stealth level; a value the map set is not restored: read it first). Kept in saves. Both return 1 or 0.

C
host->set_npc_sense(host->context, npc, "sight", 4096.0f);

npc_conditions, npc_weapon#

C
int npc_conditions(void *context, uint32_t npc, int *ids, size_t capacity, size_t *count);
int npc_weapon(void *context, uint32_t npc, char *classname, size_t capacity, size_t *size, int *clip);

Why an NPC does or does not shoot. (b0.5.48)

npc_conditions fills ids with the AI condition numbers set on the NPC now (0-255), at most capacity of them, and stores how many there are in *count (this may be more than capacity). The ones that matter for shooting: it fires only while it has 79 (can fire its ranged weapon). Others it can have instead: 64 no ammunition in the clip, 8 or 95 too close (under 100 units, or under the weapon's minimum), 96 too far, 97 not facing its enemy, 99 a character it does not hate is in the line of fire (46 when that lasts over 2.5 seconds), 102 sight blocked, 71 it lost its enemy.

npc_weapon copies the class of the weapon in its hands (output buffer rules) and stores the rounds in its clip in *clip (may be NULL).

Both return 1, or 0 if it is not an NPC (npc_weapon also when it holds no weapon).

C
int ids[64]; size_t n = 0;
if (host->npc_conditions(host->context, npc, ids, 64, &n))
    for (size_t i = 0; i < n && i < 64; ++i) if (ids[i] == 64) host->log("out of ammunition");

blse.npc_schedule, blse.npc_state#

Topics BLSE_MSG_NPC_SCHEDULE and BLSE_MSG_NPC_STATE (b0.5.45). Recognized builds only. Delivered to native subscribers at once, to Python at the end of the frame.

  • blse.npc_schedule: an NPC started a schedule (every change, also the game's own; frequent: subscribe only while you need it). Payload, five fields: the NPC's handle (decimal), classname, name, the old schedule's name ("" if none), the new one's ("SCHED_TROIKA_FLEE").
  • blse.npc_state: an NPC's state changed. The same first three fields, then the old and the new state as words: none, idle, combat, alert, script, playdead, prone, dead, fleeing, retreating, cowering, hunting, dialog, oblivious, criminal_suspicion. Checked ten times a second while anyone listens.
C
static void BLSE_CALL on_state(const char *topic, const void *payload, size_t size, void *user) {
    char handle[16], now[32];
    if (payload_field(payload, size, 0, handle, sizeof handle) && payload_field(payload, size, 4, now, sizeof now)
        && !strcmp(now, "fleeing")) { /* (uint32_t)strtoul(handle, NULL, 10) is running away */ }
}

(payload_field is the helper from Payloads.)

Animation#

(b0.5.49; check BLSE_HOST_HAS(host, activity_name).) list_sequences(context, entity, buffer, capacity, &size) copies index<TAB>name<TAB>activity<TAB>loops<TAB>seconds lines, one per sequence; lookup_sequence(context, entity, name) returns the number or -1 (a decimal number names a sequence by its number); play_animation(context, entity, sequence, rate, flags) with BLSE_ANIM_LOOP and BLSE_ANIM_STILL; stop_animation(context, entity); animation_state(context, entity, &state) fills a BLSE_Animation (set struct_size first); set_animation_rule(context, who, activity, sequences) (NULL sequences remove it; a plugin's rules end when it declines to load or is disabled after a crash); activity_id(context, name), activity_name(context, number, buffer, capacity, &size).

add_animation_filter(context, filter, user): int filter(void *user, uint32_t entity, int activity, int translated, int sequence) is called on the main thread inside the NPC's think each time its AI picks a sequence for an activity (after the rules), with the activity asked for, the one after the game's weapon translation and the sequence chosen so far; it returns the sequence to use (an invalid number keeps the choice). Keep it fast and do not start animations, spawn or remove entities in it. A crash in it disables the plugin as usual (its DLL stays loaded).

Messages BLSE_MSG_ANIM_STARTED, BLSE_MSG_ANIM_ENDED, BLSE_MSG_ANIM_CHOSEN (delivered at once, inside the game's own code: the same restrictions). Behaviour as in the Python reference.

All animation functions: main thread, while a level runs; entities with a model (characters, props).

list_sequences, lookup_sequence#

C
int list_sequences(void *context, uint32_t entity, char *buffer, size_t capacity, size_t *size);
int lookup_sequence(void *context, uint32_t entity, const char *sequence);

list_sequences copies every sequence the entity's model has (its own and those of the animation models it includes), in the game's numbering, one line each: index<TAB>name<TAB>activity<TAB>loops<TAB>seconds. activity is "" if the sequence has none, loops is 1 or 0, seconds is its length (three decimals). Returns 1 or 0. (b0.5.49)

lookup_sequence returns a sequence's number by name (case ignored), or by its number written in decimal ("12"); -1 if the model has no such sequence.

play_animation, stop_animation#

C
int play_animation(void *context, uint32_t entity, const char *sequence, float rate, unsigned flags);
int stop_animation(void *context, uint32_t entity);

Plays a sequence on an entity. (b0.5.49)

When to use it: an NPC waves, sits down, dances; a prop opens.

  • sequence: a name or a decimal number, as for lookup_sequence.
  • rate: playback speed, 0 to 16 (1 normal).
  • flags: 0, or a combination of BLSE_ANIM_LOOP (repeat until stop_animation) and BLSE_ANIM_STILL (the NPC stops at once, through the game's WAIT_FOR_SCRIPT schedule started with the animation, and walks on afterwards). Other bits: the call returns 0.
  • An NPC keeps the animation against its own AI until it ends (once, or with LOOP until stopped) or the game changes its sequence itself (death, a flinch, a conversation, a scripted scene, a task that sets an activity). Other entities just get the sequence.
  • stop_animation ends a mod's animation; 0 if none plays.
C
/* a sequence name taken from list_sequences for this model */
if (host->lookup_sequence(host->context, npc, "my_wave") >= 0)
    host->play_animation(host->context, npc, "my_wave", 1.0f, BLSE_ANIM_STILL);

animation_state#

C
int animation_state(void *context, uint32_t entity, BLSE_Animation *out);

Fills *out with the entity's current animation. Set out->struct_size = sizeof(BLSE_Animation) first (a smaller value: 0 is returned). Returns 1 or 0. (b0.5.49)

Field Meaning
sequence The sequence number playing
activity That sequence's activity number, -1 none
npc_activity Characters: the activity it plays now, else -1
ideal_activity Characters: the activity its AI wants, else -1
cycle How far through the sequence, 0 to 1
rate Playback rate, 1 normal
loops, finished 1 or 0
held 1 while a mod's animation plays (play_animation)
name, activity_name The sequence's and the activity's names ("" if none)

set_animation_rule#

C
int set_animation_rule(void *context, const char *who, const char *activity, const char *sequences);

A rule: when an NPC's AI picks a sequence for activity, use one of sequences instead. (b0.5.49)

When to use it: new idle animations for one NPC, a different walk for every cop.

  • who: "*" (every NPC), "#<handle>" (one entity), "model:<part of the model path>", a classname or a targetname.
  • activity: a name such as "ACT_WALK", or its number.
  • sequences: sequence names separated by commas or spaces; one is chosen at random each time (only those the model has). NULL or "" removes the rule.
  • A plugin's rules end when it declines to load or is disabled after a crash. Returns 1 or 0.
C
/* every NPC named guard_1 idles with one of two sequences the mod added (add_model_animations) */
host->set_animation_rule(host->context, "guard_1", "ACT_IDLE", "my_idle1, my_idle2");

add_animation_filter#

C
typedef int (BLSE_CALL *BLSE_AnimationFilterFn)(void *user, uint32_t entity, int activity, int translated, int sequence);
int add_animation_filter(void *context, BLSE_AnimationFilterFn filter, void *user);

Your function sees every pick after the rules and decides the sequence. (b0.5.49) Called with the entity's handle, the activity asked for, the activity after the game's weapon translation, and the sequence chosen so far; it returns the sequence number to use (return sequence to keep the choice; an invalid number also keeps it). It runs for every NPC's activity change: keep it fast, and do not start animations, spawn or remove entities in it. A crash disables the plugin. Returns 1, or 0 if filter is NULL.

activity_id, activity_name#

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

Convert between an activity's name and number. activity_id returns the number ("ACT_WALK"), or -1 if unknown. activity_name copies the name; returns 1, or 0 if there is no activity with that number. (b0.5.49)

blse.anim_started, blse.anim_ended, blse.anim_chosen#

Topics BLSE_MSG_ANIM_STARTED, BLSE_MSG_ANIM_ENDED, BLSE_MSG_ANIM_CHOSEN (b0.5.49). Delivered to native subscribers at once, inside the game's own code (the same restrictions as filters), to Python at the next safe point. BLSE installs the hooks behind them when the first subscriber appears. Every entity with a model.

  • blse.anim_started: an entity's sequence was (re)started. Payload, seven fields: handle (decimal), classname, name, sequence number, sequence name, the sequence's activity name ("" none), the previous sequence's name ("" none).
  • blse.anim_ended: a sequence ended. The same first six fields, then finished (it reached its end) or interrupted (another sequence replaced it first). A looping sequence never finishes: it ends when another one replaces it.
  • blse.anim_chosen: an NPC's AI picked a sequence for an activity. Payload: handle, classname, name, the activity asked for, the activity after the game's translation, the sequence's name, and 1 if a rule or filter changed the game's choice (else 0). Frequent: subscribe only while you need it.

add_model_animations, model_animations#

New animations for models (b0.5.49; check BLSE_HOST_HAS(host, model_animations)): add_model_animations(context, target_mdl, anim_mdl, flags) adds an animation-only .mdl to a model (or, for a shared animation model such as models/character/shared/male/npc_allsequences.mdl, to every model including it directly) when the engine next loads it; flags BLSE_ANIMS_REPLACE also hides the originals with the same names (renamed <name>@orig). Returns 1, 0 for invalid paths, -1 if not available on this game build. Call it from BLSE_PluginLoad (before models load); a plugin's registrations end when it declines to load or is disabled after a crash (models already loaded keep them until their next load). model_animations(context, filter, buffer, capacity, &size) copies registered<TAB>target<TAB>file<TAB>add|replace<TAB>who lines, then model<TAB>file per file added to a loaded model and model<TAB>-name per hidden sequence. Behaviour, targets and the blse/animations.txt data file as in the Python reference.

C
int add_model_animations(void *context, const char *target_mdl, const char *anim_mdl, uint32_t flags);
int model_animations(void *context, const char *filter, char *buffer, size_t capacity, size_t *size);

When to use it: your mod ships new idles or dances for the game's characters without editing their .mdl files.

  • target_mdl: the model to add to (a path such as "models/character/.../name.mdl"), or a shared animation model that other models include, meaning every model that includes it directly.
  • anim_mdl: an animation-only .mdl compiled for the target's skeleton, found through the game's search paths, mod folders included ("models/mymod/male_idles.mdl"). Its sequences come after the model's own, so the original numbers stay.
  • Paths: case and / or \ do not matter.
  • flags: 0 adds; BLSE_ANIMS_REPLACE also hides the target's sequences with the same names, so the new ones play instead (for a shared target in every file it includes, for an exact model only in its own). Two replacers of one name: the later registration wins.
  • model_animations: only lines whose paths contain filter (NULL or "": all). Returns 1 or 0.
  • Mods without code can use a blse/animations.txt in their folder instead.

add_item_class, item_classes#

New items and weapons (b0.5.49; check BLSE_HOST_HAS(host, item_classes)): add_item_class(context, classname, template_class) makes classname (item_<name>) an item class the game can create everywhere it creates items, as template_class's C++ class (its behavior) with its own data from vdata/items/<classname>.txt, which the mod ships. Returns 1, 0 if refused (the reason goes to the plugin's log), -1 if not available on this game build. Register in BLSE_PluginLoad, before levels and saves load: a save holding the item needs the class. A plugin's classes end when it declines to load; they stay when it is disabled after a crash (they run no plugin code, and a save holding its items would crash the game on load without them). item_classes(context, buffer, capacity, &size) copies classname<TAB>template<TAB>who<TAB>created lines. Templates, files and limits as in the Python reference. New behavior for a mod's weapon (a new projectile, a fire mode) is the plugin's own hook on the template's functions (hook_function), checking the entity's classname; blse.attack, damage and hit events already name the new class.

C
int add_item_class(void *context, const char *classname, const char *template_class);
int item_classes(void *context, char *buffer, size_t capacity, size_t *size);
  • classname: "item_<name>", 6-63 characters of a-z 0-9 _ ("item_w_mymod_sabre"). The game creates it everywhere it creates items: maps, give/GiveItem, merchants' stock, spawning, loading saves.
  • template_class: one of the game's item classes whose behaviour it copies ("item_w_katana": a melee weapon; a gun, a blood pack, a key, ...). 184 of the game's 202 item classes can be templates; the Python page lists the others (containers, item_g_keyring, item_w_remington_m_700, item_w_unarmed, ...).
  • Your mod folder ships vdata/items/<classname>.txt: names, models, pictures, prices, damage, ammunition, sounds.
  • The same class again: the later registration wins.
  • Returns 1; 0 if refused, with the reason in your plugin's log (add_item_class refused: ...); -1 if not available on this game build.
  • item_classes: created is how many entities of the class were made this session. Mods without code can use a blse/items.txt instead.
C
BLSE_EXPORT int BLSE_CALL BLSE_PluginLoad(const BLSE_HostAPI *h) {
    if (BLSE_HOST_HAS(h, item_classes)) h->add_item_class(h->context, "item_w_mymod_sabre", "item_w_katana");
    return 1;
}

The shared database#

db_get(context, path, buffer, capacity, &size) copies a value of the database shared with Python mods (blse.db_get) as JSON text; db_set(context, path, json) sets one from JSON text (NULL removes it). Main thread. Entity values set from Python as lists or dicts come to entity_data_get as JSON text.

db_get, db_set#

C
int db_get(void *context, const char *path, char *buffer, size_t capacity, size_t *size);
int db_set(void *context, const char *path, const char *json);

One JSON document saved with the game and shared by every mod, Python and plugins alike. (b0.5.42)

When to use it: structured data (lists, nested values) that must be in the save, or data other mods read (a Python mod's G is not readable from C; the database is).

  • path: where in the document. "" is the whole database; keys separated by ., list indexes in [ ]: "mymod.settings.speed", "mymod.list[2]". Start with your mod's name. NULL is refused.
  • db_get copies the value as compact JSON text ({"kills":3}, "text", 12). Returns 1, or 0 if there is no value there.
  • db_set parses json and sets the value, making the objects on the way; json NULL removes it. Values may be nested at most 64 deep. Returns 1, or 0 for invalid JSON or an invalid path.
  • Saved, loaded and cleared on a new game with the game's script globals; up to 1 MB. Main thread.
C
host->db_set(host->context, "mymod.stats", "{\"kills\": 0, \"names\": []}");
char kills[32];
if (host->db_get(host->context, "mymod.stats.kills", kills, sizeof kills, NULL)) { int k = atoi(kills); }

Values on entities, finding entities, JSON files#

  • entity_data_set(context, entity, key, value) (text; NULL removes), entity_data_get(context, entity, key, buffer, capacity, &size), entity_data_keys(...) (each followed by \n) and entity_data_find(context, key, handles, capacity, &count): the values Python mods see with blse.entity_get (numbers set from Python come as digits). Main thread.
  • entity_position(context, entity, origin) and find_entities(context, center, radius, classname, flags, handles, capacity, &count), nearest first; flags BLSE_FIND_CHARACTERS | BLSE_FIND_ALIVE | BLSE_FIND_NO_PLAYER. Main thread.
  • json_get(context, file, path, buffer, capacity, &size) copies a value as JSON text; json_set(context, file, path, json) sets one from JSON text (NULL removes it). Any thread.

Behaviour as in the Python reference.

entity_data_set, entity_data_get, entity_data_keys, entity_data_find#

C
int entity_data_set(void *context, uint32_t entity, const char *key, const char *value);
int entity_data_get(void *context, uint32_t entity, const char *key, char *buffer, size_t capacity, size_t *size);
int entity_data_keys(void *context, uint32_t entity, char *buffer, size_t capacity, size_t *size);
int entity_data_find(void *context, const char *key, uint32_t *handles, size_t capacity, size_t *count);

Named values stored on an entity, saved with the game and shared with every mod: one mod marks an NPC, another reads the mark. (b0.5.40)

When to use it: "this NPC was already fed on", "this guard was bribed", a counter per character.

  • key: 1-63 characters of A-Z a-z 0-9 _ - ., compared without regard to case. Start it with your mod's name ("mymod.fed").
  • value: text, up to 4096 characters; NULL removes the key. At most 256 keys per entity.
  • entity_data_get copies the value as text. Values set from Python come as text too: numbers as digits, lists and dicts as JSON text. Returns 1, or 0 if the entity or key does not exist.
  • entity_data_keys copies the entity's keys, each followed by '\n'.
  • entity_data_find fills handles with this level's entities (and the player) that have key, at most capacity; *count receives how many there are (this may be more than capacity).
  • Values follow the entity across saves and level changes and go with it when it is removed (killed and cleaned up, picked up, deleted). Main thread, while a level runs. All return 1 or 0.
C
char text[16];
int fed = 0;
if (host->entity_data_get(host->context, npc, "mymod.fed", text, sizeof text, NULL)) fed = atoi(text);
snprintf(text, sizeof text, "%d", fed + 1);
host->entity_data_set(host->context, npc, "mymod.fed", text);

entity_position#

C
int entity_position(void *context, uint32_t entity, float origin[3]);

Copies where an entity is (x, y, z in world units) into origin. Returns 1, or 0 if there is no such entity. Main thread. (b0.5.40)

find_entities#

C
int find_entities(void *context, const float center[3], float radius, const char *classname, unsigned flags,
                  uint32_t *handles, size_t capacity, size_t *count);

The entities within radius units of center, nearest first. (b0.5.40)

When to use it: the NPCs near the player, every item in a room, the nearest door.

  • classname: "" (or NULL) any class, "npc_*" a prefix, or one class; case is ignored.
  • flags: 0 or any of BLSE_FIND_CHARACTERS (only the player and NPCs), BLSE_FIND_ALIVE (characters that are not dead), BLSE_FIND_NO_PLAYER (leave out the player).
  • handles: filled with up to capacity handles; *count receives how many were found (at most 512).
  • Returns 1, or 0 if no level runs. Main thread.
C
float me[3];
uint32_t near[32]; size_t n = 0;
if (host->entity_position(host->context, 1, me) &&
    host->find_entities(host->context, me, 400.0f, "npc_*", BLSE_FIND_ALIVE | BLSE_FIND_NO_PLAYER, near, 32, &n)) {
    for (size_t i = 0; i < n && i < 32; ++i) { /* near[i] */ }
}

json_get, json_set#

C
int json_get(void *context, const char *file, const char *path, char *buffer, size_t capacity, size_t *size);
int json_set(void *context, const char *file, const char *path, const char *json);

Structured data in Bin\loader\blse\data\<file>.json, shared by every save (statistics across playthroughs, unlocks, settings with lists). (b0.5.40) Any thread.

  • file: a name without .json, folders separated by / ("mymod/stats"), of A-Z a-z 0-9 _ - . (not starting or ending with ., not a Windows device name such as con, nul, aux, com1).
  • path: keys separated by ., list indexes in [ ] ("stats.kills", "list[2].name", "" the whole file). Setting the index one past the end of a list appends.
  • json_get copies the value as compact JSON text. Returns 1, or 0 if the file or value is missing or the file is broken.
  • json_set sets the value from JSON text and keeps the rest of the file, making the objects on the way (and the file if needed); json NULL removes the value. Values may be nested 64 deep. Returns 1, or 0 for invalid JSON, an invalid name or path, or a broken file.

Models, translated strings, frame rate#

  • bone_names(context, entity, buffer, capacity, &size): the bone names, each followed by \n (like list_plugins); bone_position(context, entity, bone, origin, angles).
  • model_scale(context, entity, &scale, &parts) and set_model_scale(context, entity, scale, parts): the scale (0.05-8) and the parts scaled with it, BLSE_SCALE_VIEW | BLSE_SCALE_HITBOXES | BLSE_SCALE_COLLISION (0: the picture only), and how long (b0.5.39): BLSE_SCALE_KEEP (applied again after loads and level changes); without it: until the level ends.
  • translate(context, mod, key, buffer, capacity, &size) and translation_language(context, buffer, capacity, &size).
  • frame_stats(context, &stats) with stats.struct_size = sizeof(BLSE_FrameStats).

Entities are indexes (below 8192) or handles. Models: main thread while a level runs; translations and frame figures: any thread. Behaviour and limits as in the Python reference.

bone_names, bone_position#

C
int bone_names(void *context, uint32_t entity, char *buffer, size_t capacity, size_t *size);
int bone_position(void *context, uint32_t entity, const char *bone, float origin[3], float angles[3]);

bone_names copies the model's bone names in the model's order, each followed by '\n' ("Bip01\nBip01 Pelvis\n..."). bone_position copies where one bone is in the world (origin) and how it is turned (angles: pitch, yaw, roll); bone is matched without regard to case; neither array may be NULL. Both return 1, or 0 for an entity without a model or an unknown bone. Main thread, while a level runs; entities with a model (characters, props, items lying in the world). (b0.5.38)

When to use them: put an effect on an NPC's head, aim at a hand.

model_scale, set_model_scale#

C
int model_scale(void *context, uint32_t entity, float *scale, unsigned *parts);
int set_model_scale(void *context, uint32_t entity, float scale, unsigned parts);

Draw a model larger or smaller. (b0.5.38; BLSE_SCALE_KEEP b0.5.39)

  • scale: 0.05 to 8 (1 normal). model_scale stores the current scale (1 normal, 0 never set, drawn as 1); scale must not be NULL, parts may be.
  • parts: what is scaled besides the picture, any of:
Bit Characters The player
BLSE_SCALE_VIEW The eye height (where they see and aim from) The eye height, standing and crouched
BLSE_SCALE_HITBOXES The bones hits, attachments and bone_position use The same
BLSE_SCALE_COLLISION The box others bump into The box and the hulls the player moves with

0 scales only the picture.

  • How long: without BLSE_SCALE_KEEP until the level ends (saves hold the entity at the size it had before, and the parts end). With BLSE_SCALE_KEEP the scale and its parts are kept in the save and applied again after a load, when the level is entered again and (the player) after every level change. Calling it again without KEEP ends a kept scale.
  • NPCs still find their paths as normal-sized characters: a scaled-up NPC with COLLISION can get stuck in doorways. Scale its look, view and hit boxes without COLLISION when it has to walk around.
  • Return 1 or 0. Main thread, while a level runs.
C
host->set_model_scale(host->context, giant, 1.8f, BLSE_SCALE_VIEW | BLSE_SCALE_HITBOXES | BLSE_SCALE_COLLISION | BLSE_SCALE_KEEP);

translate, translation_language#

C
int translate(void *context, const char *mod, const char *key, char *buffer, size_t capacity, size_t *size);
int translation_language(void *context, char *buffer, size_t capacity, size_t *size);

Text in the player's language. (b0.5.38) Any thread.

  • translate copies the text for key (with or without a leading $) from Bin\loader\blse\translations\<mod>_<language>.txt, else <mod>_english.txt, else the key itself (UTF-8). Returns 1, or 0 only for an invalid mod name or key.
  • translation_language copies the language translations use ("english", "german", ...).
  • For text you hand to BLSE's interface functions you do not need these (they translate $KEY text themselves); see translate_text in Translated interface text. File format: Translated strings.

frame_stats#

C
int frame_stats(void *context, BLSE_FrameStats *out);

Frame-rate figures over the last second, measured at the end of every frame the game draws. (b0.5.38) Any thread. Set out->struct_size = sizeof(BLSE_FrameStats) first. Returns 1, or 0 until two frames were drawn (or with a smaller struct_size).

Field Meaning
fps Frames a second over the last second
last_ms The last frame's time, in milliseconds
average_ms The average frame time over the last second
worst_ms The longest frame over the last second
frames Frames drawn in the last second
C
BLSE_FrameStats st = { sizeof(BLSE_FrameStats) };
if (host->frame_stats(host->context, &st) && st.worst_ms > 50.0) host->log("a slow frame");

Appearance#

b0.5.47; behaviour and limits as in the Python reference. Main thread while a level runs; each returns 1 or 0.

  • Bones: bone_transform(context, entity, bone, &t) and set_bone_transform(context, entity, bone, &t) with a BLSE_BoneTransform (scale[3], offset[3], angles[3], flags: BLSE_BONE_HITBOXES | BLSE_BONE_ALONE | BLSE_BONE_KEEP); an identity transform removes the modifier. reset_bones(context, entity, bone) (NULL: all); modified_bones(context, entity, buffer, capacity, &size).
  • Faces: flex_names(...) (\n after each), flex_value(context, entity, name, &value, &minimum, &maximum, &held), set_flex(context, entity, name, value, flags) (BLSE_FLEX_KEEP), release_flex(context, entity, name) (NULL: all).
  • Skins, body groups, colour: entity_skin(context, entity, &skin, &count), set_entity_skin(context, entity, skin, flags) (BLSE_LOOK_FADE | BLSE_LOOK_KEEP), body_groups(...) ("name\tcount\tvalue\n" per group), set_body_group(context, entity, group, value, flags), render_color(context, entity, rgba, &mode, &fx), set_render_color(context, entity, r, g, b, a, flags) and set_render_mode(context, entity, mode, fx, flags) (-1 keeps a value).
  • Materials: set_entity_material(context, entity, original, replacement, flags) ("" hides, NULL removes; both NULL: all of the entity's), model_materials(...) and entity_materials(...) ("original\treplacement\n").

Each entry below returns 0 also on builds where the feature is unavailable. entity: an index (1 = the player) or a handle.

How long changes last. By default a change lasts until the entity is gone or the level ends. The *_KEEP flags (BLSE_BONE_KEEP, BLSE_FLEX_KEEP, BLSE_LOOK_KEEP, all the value 8) keep it in the save and apply it again after loads and level changes (the player's on every level). Skins, body groups and colour are also saved by the game itself; KEEP applies them again for example after a script gave the character another model.

bone_transform, set_bone_transform, reset_bones, modified_bones#

C
typedef struct BLSE_BoneTransform {
    float scale[3];   /* 0.05 to 8 along the bone's axes; 1 1 1 unchanged */
    float offset[3];  /* model units in the bone's axes, -256 to 256 */
    float angles[3];  /* pitch, yaw, roll in degrees, -360 to 360 */
    unsigned flags;   /* BLSE_BONE_* */
} BLSE_BoneTransform;
int bone_transform(void *context, uint32_t entity, const char *bone, BLSE_BoneTransform *transform);
int set_bone_transform(void *context, uint32_t entity, const char *bone, const BLSE_BoneTransform *transform);
int reset_bones(void *context, uint32_t entity, const char *bone);
int modified_bones(void *context, uint32_t entity, char *buffer, size_t capacity, size_t *size);

A modifier per bone, applied in the bone's own space after the game built the skeleton: make a head bigger, bend an arm, move a hand. (b0.5.47)

  • bone: the bone's name (see bone_names); must not be NULL for the first two.
  • flags: BLSE_BONE_HITBOXES also moves the bones hits, attachments and bone_position use; BLSE_BONE_ALONE moves only this bone (its children normally follow); BLSE_BONE_KEEP kept in the save.
  • set_bone_transform sets the bone's whole modifier; scale 1 1 1 with offset and angles 0 removes it.
  • bone_transform reads it (an identity transform if none).
  • reset_bones removes one bone's modifier, or all of the entity's with bone NULL.
  • modified_bones lists the bones with modifiers, each followed by '\n'.
C
BLSE_BoneTransform t = { {1.5f, 1.5f, 1.5f}, {0, 0, 0}, {0, 0, 0}, BLSE_BONE_HITBOXES };
host->set_bone_transform(host->context, npc, "Bip01 Head", &t);

flex_names, flex_value, set_flex, release_flex#

C
int flex_names(void *context, uint32_t entity, char *buffer, size_t capacity, size_t *size);
int flex_value(void *context, uint32_t entity, const char *name, float *value, float *minimum, float *maximum, int *held);
int set_flex(void *context, uint32_t entity, const char *name, float value, unsigned flags);
int release_flex(void *context, uint32_t entity, const char *name);

Facial expressions. (b0.5.47)

  • flex_names lists the model's flex controllers, each followed by '\n'.
  • flex_value reads one in its own units, with its range and whether BLSE holds it (*held 1). Any out pointer may be NULL.
  • set_flex holds a controller at value (clamped to its range; sent to the client in 64 steps) against the game's own expressions until release_flex, the entity is gone or the level ends. BLSE_FLEX_KEEP: kept in the save. Blinking and lip sync still play on top.
  • release_flex gives one back to the game, or all with name NULL.

entity_skin, set_entity_skin#

C
int entity_skin(void *context, uint32_t entity, int *skin, int *count);
int set_entity_skin(void *context, uint32_t entity, int skin, unsigned flags);

The model's skin (its set of textures). entity_skin stores the current skin in *skin (must not be NULL) and how many the model has in *count (may be NULL). set_entity_skin sets one, 0 to count - 1; flags: BLSE_LOOK_FADE uses the game's cross-fade instead of switching at once, BLSE_LOOK_KEEP keeps it. (b0.5.47)

body_groups, set_body_group#

C
int body_groups(void *context, uint32_t entity, char *buffer, size_t capacity, size_t *size);
int set_body_group(void *context, uint32_t entity, const char *group, int value, unsigned flags);

body_groups copies one line per group: name<TAB>count<TAB>value (how many sub-models it has and the one shown). set_body_group shows sub-model value (0 to count - 1) of a group, by name; flags BLSE_LOOK_KEEP. Bloodlines' own character models have few body groups. (b0.5.47)

render_color, set_render_color, set_render_mode#

C
int render_color(void *context, uint32_t entity, int rgba[4], int *mode, int *fx);
int set_render_color(void *context, uint32_t entity, int r, int g, int b, int a, unsigned flags);
int set_render_mode(void *context, uint32_t entity, int mode, int fx, unsigned flags);

Tint and fade a model. (b0.5.47)

  • render_color copies red, green, blue, alpha (0-255) into rgba (must not be NULL) and the render mode and effect into *mode and *fx (either may be NULL).
  • set_render_color: 255 255 255 is no tint; -1 keeps a value. An alpha below 255 fades the entity: BLSE draws it see-through until it is gone or the level ends (with BLSE_LOOK_KEEP also after loads). Without KEEP the alpha value itself stays on the entity (the game saves it) while the entity is drawn solid again after a load: set it back to 255 when the fade should end.
  • set_render_mode: the game's render mode and render effect numbers, 0-255; -1 keeps a value.
  • Colour and mode work on any entity, also ones without a model.
C
host->set_render_color(host->context, npc, 255, 64, 64, 128, 0);   /* red and half see-through */

set_entity_material, model_materials, entity_materials#

C
int set_entity_material(void *context, uint32_t entity, const char *original, const char *replacement, unsigned flags);
int model_materials(void *context, uint32_t entity, char *buffer, size_t capacity, size_t *size);
int entity_materials(void *context, uint32_t entity, char *buffer, size_t capacity, size_t *size);

One entity drawn with other materials, without changing other entities with the same model (drawing only). (b0.5.47)

  • original: a material of the model, by its path (from model_materials) or just its name in the model.
  • replacement: a material path without materials/ and .vmt (a .vmt your mod ships, not a bare texture); "" hides those meshes; NULL removes the replacement (with original NULL too: all of the entity's).
  • flags: BLSE_LOOK_KEEP keeps it in the save.
  • model_materials lists the model's material paths, each followed by '\n'. entity_materials copies original<TAB>replacement lines for each replacement BLSE draws.

Player outfits#

Outfits are clothes the player wears whatever armor is equipped; the armor keeps its protection. The whole idea, the rules for model files and the exact behaviour are in the Python reference: read its "Read this first" list before you start. Plugins have the same functions as Python, as six BLSE_HostAPI fields (added in 0.6 after item_classes; check host->struct_size with BLSE_HOST_HAS as for any newer field: api_version does not change when fields are added). Mods without code can use a blse/outfits.txt file instead.

Why BLSE is needed for this: the game picks the player's model from clan, gender and armor every time a weapon or armor is taken out or put away (also when first and third person change), so a plain model change does not last. BLSE keeps one player model slot, filled by an outfit or a single model, and puts that model back whenever the game picks the clan model. Scripts' and cutscenes' own models and the Protean beast form pass unchanged.

All six return 1 on success, 0 if refused (the reason is written to blse.log, attributed to your plugin) and -1 where noted if outfits are not available on this game version. Call all six on the game's main thread (BLSE_PluginLoad, a command, an event handler or add_task); from another thread they return 0. Text results use the usual buffer, capacity, &size pattern: size receives the full length, so a call with a too-small buffer tells you how much to allocate.

add_outfit(context, id, fields): registers an outfit, the same as an [id] section of outfits.txt.

  • id: the outfit's name for later calls, 1 to 63 of a-z 0-9 _ (upper case is lowered).
  • fields: the outfit's keys, one key=value per line (\n between lines): name, male, female, male.<clan>, female.<clan>, skin, body.<group>, material.<original>. At least one model.
  • Returns 1, 0 (bad id, key or value, or no model), -1 (not available). The same id again replaces the outfit (logged); the later one wins.
  • Register at plugin load, so outfits worn in a save exist when it loads. Your outfits are removed when your plugin declines to load; a worn one is then taken off. They stay when it is disabled after a crash (they run no plugin code, and the player keeps the clothes on).

wear_outfit(context, id): puts the outfit on the player; id NULL or "" takes off whatever BLSE put on (the game's clan and armor model comes back).

  • Returns 1 (worn, or will be as soon as the level or the beast form allows), 0 (unknown id, no model for the player's gender or clan, a missing file, a model of the other gender), -1 (not available). Kept in the save.

worn_outfit(context, buffer, capacity, &size): the worn outfit's id, "" if none.

list_outfits(context, buffer, capacity, &size): every registered outfit, one line each: id\twho\tname\tmale\tfemale\n (who: mod <folder>, python or the plugin's name; empty fields for what was not given).

set_player_model(context, model, options): one model on the player without an outfit (a disguise); it survives weapon and armor changes like an outfit.

  • model: "models/....mdl"; NULL or "" removes it.
  • options: 0 lasts until the level ends; BLSE_LOOK_KEEP keeps it in the save until removed.
  • Returns 1, 0, -1 as wear_outfit. Wearing an outfit replaces it and the other way round.

player_model(context, buffer, capacity, &size): the model the player has right now (the game's, an outfit's, a cutscene's); "" while no level runs.

The event BLSE_MSG_OUTFIT_CHANGED ("blse.outfit_changed"): sent when what the player wears from BLSE changes. Payload, four NUL-separated texts: the outfit now ("" none, "*" a set_player_model model), the one before, the model BLSE put on ("" the game's own), and why: wear, remove, load, missing, level, unregistered (meanings in the Python reference).

The reasons in short: wear an outfit or model was put on; remove it was taken off; load worn again from a save; missing a save's or the worn outfit's model or registration is gone, so the game's model is back; level a model set without BLSE_LOOK_KEEP ended with the level; unregistered the plugin that registered the outfit went away.

The C signatures:

C
int add_outfit(void *context, const char *id, const char *fields);
int wear_outfit(void *context, const char *id);
int worn_outfit(void *context, char *buffer, size_t capacity, size_t *size);
int list_outfits(void *context, char *buffer, size_t capacity, size_t *size);
int set_player_model(void *context, const char *model, unsigned options);
int player_model(void *context, char *buffer, size_t capacity, size_t *size);

worn_outfit, list_outfits and player_model return 1 (the text may be empty), or 0 if called off the main thread.

C
/* At plugin load: one outfit, and a console command that puts it on. */
static const BLSE_HostAPI *host;

static void BLSE_CALL wear_cmd(int argc, const char *const *argv, const char *args, void *user) {
    if (host->wear_outfit(host->context, "my_suit") != 1)
        host->console_print(host->context, "the suit cannot be worn now (see blse.log)\n");
}

BLSE_EXPORT int BLSE_CALL BLSE_PluginLoad(const BLSE_HostAPI *h) {   /* in a .cpp file: extern "C" BLSE_EXPORT ... */
    host = h;
    if (h->struct_size < offsetof(BLSE_HostAPI, player_model) + sizeof(h->player_model)) return 0; /* BLSE too old */
    h->add_outfit(h->context, "my_suit",
                  "name=Black suit\n"
                  "male=models/my_mod/suit_m.mdl\n"
                  "female=models/my_mod/suit_f.mdl\n");
    h->add_command(h->context, "wear_suit", "Puts on the black suit", wear_cmd, NULL);
    return 1;
}

(BLSE_HOST_HAS(h, player_model) is the short form of that struct_size check.)

Camera and sounds#

  • camera_get(context, &third_person, &fov): 1 or 0 (-1 if unknown) and the field of view in degrees (0 is the game's default, -1 outside a level).
  • camera_set(context, third_person, fov): switches first/third person through the game's toggle and sets the field of view (0 restores the default); -1 keeps either. 1 if everything asked was done.
  • play_sound(context, file, volume, pitch) and stop_sound(context, file): a sound below sound/, heard by the player without position.
  • play_ui_sound(context, file, volume, pitch) (b0.5.49): an interface sound, played the way the game's main menu plays its own (also with no level loaded). Button sounds: see Interface.
  • b0.5.31: camera_view(context, origin, angles) (float[3] each); b0.5.32: camera_override(context, origin, angles) (either may be NULL to keep the game's; both NULL: the game's view again; it also ends with the level and when the plugin is disabled or unloaded); play_sound_at(context, file, origin, volume, pitch, attenuation), play_sound_on(context, file, entity, volume, pitch, attenuation) (an entity handle or index), stop_sound_on(context, file, entity); sound_length(context, file, &seconds).

See the Python reference.

All camera and sound functions: main thread.

camera_get, camera_set#

C
int camera_get(void *context, int *third_person, int *fov);
int camera_set(void *context, int third_person, int fov);

First or third person, and the field of view. (b0.5.30)

  • camera_get stores *third_person (1 third person, 0 first person, -1 unknown) and *fov (degrees; 0 means the game's default; -1 outside a level). Either pointer may be NULL. Returns 1 if either value is known, else 0.
  • camera_set: third_person 0 or 1 switches through the game's own toggle, -1 keeps it; fov 1 to 179 sets the field of view, 0 restores the default, -1 keeps it. Returns 1 if everything asked was done.
C
host->camera_set(host->context, 1, -1);   /* third person, field of view unchanged */

camera_view, camera_override#

C
int camera_view(void *context, float origin[3], float angles[3]);
int camera_override(void *context, const float *origin, const float *angles);

camera_view (b0.5.31) copies the position and angles (pitch, yaw, roll) the last frame was drawn from; both arrays must be given. Returns 0 outside a level.

camera_override (b0.5.32) draws the game from another place (a cutscene camera, a security monitor) while the player still moves and aims as before.

  • origin, angles: the view to use; NULL keeps the game's for that one.
  • Both NULL ends your plugin's override (another plugin's stays). It also ends when the level ends or your plugin is disabled or unloaded.
  • Returns 1 or 0.
C
const float eye[3] = {0.0f, 0.0f, 512.0f}, down[3] = {90.0f, 0.0f, 0.0f};
host->camera_override(host->context, eye, down);   /* look straight down from above */
/* later */ host->camera_override(host->context, NULL, NULL);

play_sound, stop_sound, play_ui_sound#

C
int play_sound(void *context, const char *file, float volume, int pitch);
int stop_sound(void *context, const char *file);
int play_ui_sound(void *context, const char *file, float volume, int pitch);

Sounds the player hears without a position (b0.5.30; play_ui_sound b0.5.49).

  • file: a path below sound/ ("player/pl_wade2.wav").
  • volume: 0 to 1. pitch: 100 normal (higher is faster and higher).
  • play_ui_sound plays an interface sound the way the game's main menu plays its own: also on the main menu with no level loaded and over the paused Esc menu. Sounds for buttons: Interface.
  • Return 1 if the engine took the request.

play_sound_at, play_sound_on, stop_sound_on, sound_length#

C
int play_sound_at(void *context, const char *file, const float origin[3], float volume, int pitch, float attenuation);
int play_sound_on(void *context, const char *file, uint32_t entity, float volume, int pitch, float attenuation);
int stop_sound_on(void *context, const char *file, uint32_t entity);
int sound_length(void *context, const char *file, float *seconds);

Sounds with a place in the world. (b0.5.31)

  • play_sound_at plays from a point; play_sound_on from an entity (index or handle), following it as it moves; stop_sound_on stops one on an entity.
  • attenuation: how quickly it gets quieter with distance; 0.8 is normal, 0 is heard everywhere.
  • sound_length stores the file's length in seconds, read from its WAV or MP3 header. Returns 1, or 0 (and 0 seconds) if unknown.
C
float secs = 0;
host->play_sound_on(host->context, "player/pl_wade2.wav", npc, 1.0f, 100, 0.8f);
if (host->sound_length(host->context, "player/pl_wade2.wav", &secs)) { /* secs */ }

Mod folders and file filters#

(b0.5.48) Behaviour and limits as in the Python reference. Main thread; each returns 1 or 0.

  • provide_file(context, path, data, size): the game reads these bytes as the game file path ("vdata/items/x.txt") from now on, over the game's and the mods' files (merge files still apply); data NULL removes it.
  • add_file_filter(context, path, filter, user): filter(user, path, data, size, set, set_context) is called when a game file matching path (* and ? allowed) is built, on the main thread (a matching file the game first opens on another thread is built without filters; logged). Keep path narrow: each matching file is copied into blse/cache/files. To change the file it calls set(set_context, new_data, new_size) before returning; not calling it keeps the file as it is.
  • add_map_filter(context, filter, user): the same for a map's entity text, called with the map's name when the server parses it (a new game, the map command, a map's first visit; not a loaded save or a revisited map).
  • list_mods(context, buffer, capacity, &size): "name\tfolder\n" for each enabled mod folder, in load order (the last one wins).
  • mod_file_owner(context, path, buffer, capacity, &size): the mod providing a game file, "(merged)" or "(provided)"; 0 if none.
  • (b0.5.48) mod_info(ctx, folder, buffer, capacity, size): one mod folder, loaded or not, as key\tvalue lines: folder, name, version, author, description, requires, requires_blse (its mod.ini), position (0 if not loaded), loaded, reason. Conflicts follow one rule: the mod later in loadorder.txt wins (the Python page).

Filters run inside BLSE's crash guard like dialogue filters: a filter that crashes disables its plugin. The game keeps many files after reading them once, so register filters in BLSE_PluginLoad.

A mod folder is a folder of game files (models, sounds, vdata item files, maps) that BLSE puts over the game's own; see A mod folder. A merge file adds to a data file instead of replacing it. The game keeps many files it read once: a change reaches only files it opens later.

C
static void BLSE_CALL add_sound_entry(void *user, const char *path, const char *data, size_t size, BLSE_SetBytesFn set, void *set_context) {
    /* e.g. append a line to a sound script */
    static const char extra[] = "\r\n\"my.sound\" { \"wave\" \"my_mod/hit.wav\" }\r\n";
    char *out = (char *)malloc(size + sizeof(extra));
    if (!out) return;
    memcpy(out, data, size);
    memcpy(out + size, extra, sizeof(extra) - 1);
    set(set_context, out, size + sizeof(extra) - 1); /* BLSE copies the bytes */
    free(out);
}
/* in BLSE_PluginLoad: */
if (BLSE_HOST_HAS(host, add_file_filter)) host->add_file_filter(host->context, "scripts/game_sounds_*.txt", add_sound_entry, NULL);

provide_file#

C
int provide_file(void *context, const char *path, const void *data, size_t size);

The game reads size bytes from data as the game file path from now on, over the game's and the mods' own files (merge files still apply to it). BLSE copies the bytes. data NULL removes your file again. (b0.5.48)

When to use it: data you build at run time (an item file from your settings, a generated sound script).

  • path: a game path with / ("vdata/items/item_w_mymod_sabre.txt").
  • Returns 1 or 0. mod_file_owner then reports "(provided)" for the path.

add_file_filter, add_map_filter#

C
typedef void (BLSE_CALL *BLSE_SetBytesFn)(void *set_context, const char *data, size_t size);
typedef void (BLSE_CALL *BLSE_TextFilterFn)(void *user, const char *name, const char *data, size_t size,
                                             BLSE_SetBytesFn set, void *set_context);
int add_file_filter(void *context, const char *path, BLSE_TextFilterFn filter, void *user);
int add_map_filter(void *context, BLSE_TextFilterFn filter, void *user);

Change a game file, or a map's entities, while the game loads it. (b0.5.48)

When to use them: add a line to a sound script, change a value in an item file, add an NPC to a map, remove a locked door.

  • path (add_file_filter): the files to filter, * (any characters) and ? (one) allowed. Keep it narrow: each matching file is copied into blse/cache/files.
  • Your filter is called on the main thread with your user value, name (the file's path, or for a map filter the map's name), and the current bytes data, size (after the mods and merges; borrowed, not NUL-terminated).
  • To change the content, build the new bytes and call set(set_context, new_data, new_size) before you return; BLSE copies them. Not calling set keeps the content as it is. set with NULL and 0 makes the file empty.
  • File filters run when a matching file is built; a matching file the game first opens on another thread is built without filters (this is logged).
  • Map filters get a map's entity text when the server parses it: a new game, the map command, the first visit of a map. Not a loaded save or a revisited map (those come from the save).
  • Filters run inside BLSE's crash guard: a crash disables your plugin. Register them in BLSE_PluginLoad.
  • Return 1, or 0 if an argument is missing or invalid.

list_mods, mod_file_owner, mod_info#

C
int list_mods(void *context, char *buffer, size_t capacity, size_t *size);
int mod_file_owner(void *context, const char *path, char *buffer, size_t capacity, size_t *size);
int mod_info(void *context, const char *folder, char *buffer, size_t capacity, size_t *size);

Information about the installed mod folders. (b0.5.48)

  • list_mods copies name<TAB>folder lines for each enabled mod folder, in load order: the last one wins conflicts.
  • mod_file_owner copies the name of the mod providing the game file path, or "(merged)" (built from merge files) or "(provided)" (from provide_file). Returns 0 if no mod provides it (the game's own file).
  • mod_info copies one mod folder's details, loaded or not, as key<TAB>value lines: folder, name, version, author, description, requires, requires_blse (from its mod.ini), position (1 = lowest priority, 0 if not loaded), loaded (1 or 0), reason (why it is not loaded). Returns 0 if there is no such folder.

Companions#

Behaviour as in the Python reference. Main thread while a level runs; 1 or 0.

  • set_companion(context, npc, on, options): on 1 makes the NPC a companion (it goes with the player through level changes, with its whole state), 0 ends it. options: NULL or words: "follow", "stay", "range=<units>". Kept in the save.
  • list_companions(context, all_maps, buffer, capacity, &size): "handle\tmap\tclass\tname\toptions\n" per companion of this level; all_maps 1 adds those left on other maps (empty handle).
  • block_companion_map(context, map, block): no companion goes to that map while blocked; not saved.
  • Events: BLSE_MSG_COMPANION (what, handle, class, name, map; what is arrived, stayed or lost) and BLSE_MSG_HIT (every hit with the victim's and attacker's handles).

A companion is an NPC that goes with the player through level changes with its whole saved state: the game carries it as it carries the player's items, with its inventory, BLSE entity values and kept looks. (b0.5.48)

set_companion#

C
int set_companion(void *context, uint32_t npc, int on, const char *options);
  • npc: an index or handle. on 1 makes it a companion, 0 ends it.
  • options: NULL or "" for none, else words separated by spaces: "follow" (BLSE calls follow_npc(npc, player) again on every level and after loads), "stay" (registered, but does not go along), "range=<units>" (goes along only when within that distance of the player).
  • Kept in the save. Returns 1 or 0.
C
host->set_companion(host->context, npc, 1, "follow range=1500");

list_companions#

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

Copies handle<TAB>map<TAB>class<TAB>name<TAB>options lines for each companion of this level. all_maps 1 also lists those left on other maps (their handle field is empty). Returns 1 or 0.

block_companion_map#

C
int block_companion_map(void *context, const char *map, int block);

block 1: no companion goes to that map (a cutscene map, a place it must not be); 0 ends the block. Not saved. Returns 1 or 0.

blse.companion#

Topic BLSE_MSG_COMPANION. Payload, five fields: what happened, the NPC's handle (decimal; empty when it is not on this level), classname, name, a map. What happened:

  • arrived: it came along from the map given;
  • stayed: it stayed on the map given (stay, a blocked map, out of range, or dead);
  • lost: it died or was removed; it is no longer a companion.

Delivered to native subscribers at once, to Python at the next safe point. For hits between companions and enemies see blse.hit.