BLSE0.6

Native plugins

Working with the game's Python

On this page 9 sections

This page is the plugin reference for three bridges between your C code and the game:

  • console commands: a command the player (or a config file, or a key binding) types in the game console runs your C function;
  • the game's own scripting: the game's quests and dialogue are written in Python 2.1; your plugin can run Python code and read Python values, so everything the game's scripts can do, your plugin can do too, on any game build;
  • Python functions in C: a C function your plugin adds can be called from Python mods, game scripts and dialogue conditions.

A minimal example: a console command mymod_humanity that prints the player's Humanity and lowers it by one.

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

static const BLSE_HostAPI *host;

static void BLSE_CALL humanity_cmd(int argc, const char *const *argv, const char *args, void *user) {
    char value[32], line[64];
    if (!host->python_eval(host->context, "FindPlayer().humanity", value, sizeof value, NULL)) {
        host->console_print(host->context, "no game is running\n");
        return;
    }
    snprintf(line, sizeof line, "Humanity was %s\n", value);
    host->console_print(host->context, line);
    host->python_exec(host->context, "FindPlayer().HumanityAdd(-1)");
}

BLSE_EXPORT int BLSE_CALL BLSE_PluginLoad(const BLSE_HostAPI *h) {
    host = h;
    if (h->api_version != BLSE_API_VERSION) return 0;
    if (!h->add_command(h->context, "mymod_humanity", "Shows and lowers the player's Humanity", humanity_cmd, NULL))
        h->log("the command name is taken");
    return 1;
}

Important, before you start:

  • Main thread only. Every function on this page does nothing (or returns 0) from another thread.
  • The game's scripts exist only while a game runs. FindPlayer(), G and the level's entities exist after the main menu has loaded a level; before that, Python calls about them fail.
  • Inside game code, Python cannot run at once. From an event handler delivered inside game code (such as blse.entity_output) python_exec is queued and python_eval fails. See python_exec.
  • The game's Python is version 2.1: no True/False keywords, no with, few modules.

Words used on this page#

Word Meaning
Console The game's command window (the ~ key). Commands typed there, in config files (autoexec.cfg) or bound to keys run console commands
__main__ The Python module the game's own scripts run in. FindPlayer(), Find(name), FindClass(cls) and G live there
G The game's global script variables, saved with every save game. Quests keep their state there
Traceback Python's error report: what went wrong and where. BLSE prints it to the game console
Safe point The end of the server frame, where BLSE runs code that had to wait (queued python_exec code, Python event handlers)
blse_native The Python module BLSE fills with the functions plugins add. Python mods and game scripts call blse_native.<name>(...)
Engine restart The game reloads its engine (for example after some video settings); plugins stay loaded

Console commands#

C
static void BLSE_CALL on_heal(int argc, const char* const* argv, const char* args, void* user) {
    /* argv[0] is the command name; args is the text after it as typed */
    host->console_print(host->context, "healing\n");
}
/* in BLSE_PluginLoad: */
host->add_command(host->context, "mymod_heal", "Heals the player.", on_heal, NULL);
  • Names are 1-63 characters from [A-Za-z0-9_]; prefix them with your mod name. blse_ is reserved.
  • add_command returns 0 if the name is invalid, already used by the game or another mod. Adding the same name again from the same plugin replaces the handler.
  • Commands can be added during BLSE_PluginLoad or later; they become usable when the engine console is ready (shortly after plugins load) and stay registered for the rest of the game, including across engine restarts.
  • Handlers run on the main thread, from the console or from config files/binds. A plugin that returns 0 from BLSE_PluginLoad keeps no working commands.

add_command#

C
typedef void (BLSE_CALL *BLSE_CommandFn)(int argc, const char *const *argv, const char *args, void *user);
int add_command(void *context, const char *name, const char *help, BLSE_CommandFn callback, void *user);

Adds a console command that calls callback. (0.4)

When to use it: a debug command while you develop, a command players bind to a key (bind F6 mymod_toggle), a setting players change in autoexec.cfg.

  • name: 1-63 characters of A-Z a-z 0-9 _. Start it with your mod's name; names starting with blse_ are reserved for BLSE.
  • help: the command's help text, as the game's own commands have; NULL for none.
  • callback: your handler. argc is the number of words, argv[0] the command name and argv[1] ... the words after it, args everything after the name as typed (so you can parse quotes yourself). All strings are borrowed: valid only during the call.
  • user: any pointer, passed back to callback unchanged.
  • Returns 1, or 0 if the name is invalid or taken by the game or another mod, callback is NULL, or it is not called on the main thread. The reason is written to blse.log (console: command 'x' not added: ...).
  • If your plugin is disabled after a crash, its commands stay registered but answer "not available".
C
static void BLSE_CALL speed_cmd(int argc, const char *const *argv, const char *args, void *user) {
    if (argc < 2) { host->console_print(host->context, "usage: mymod_speed <factor>\n"); return; }
    speed_factor = (float)atof(argv[1]);
}

console_print#

C
void console_print(void *context, const char *text);

Prints text to the game console exactly as given: end lines with "\n" yourself. (0.4) Returns nothing; off the main thread, or with text NULL, nothing is printed. For your plugin's log file use log instead.

Using the game's scripting API#

The game's own scripting interface is Python: quests and dialogue use functions such as FindPlayer(), Find(name), FindClass(cls) and the G variables. python_exec and python_eval give native plugins the same access, with no addresses involved, so they work on any game build:

C
char humanity[32]; size_t n = 0;
if (host->python_eval(host->context, "FindPlayer().humanity", humanity, sizeof(humanity), &n)) { /* "7" */ }
host->python_exec(host->context, "FindPlayer().HumanityAdd(-1)");
  • Code runs in __main__, like the game's own scripts. Errors print a traceback to the game console and return 0.
  • Called from inside game code (an entity_output handler), python_exec queues the code and returns 2; it runs at the end of the server frame. python_eval cannot wait and returns 0 there.
  • G and level entities exist only while a game is running (after the main menu loads a level).

python_exec#

C
int python_exec(void *context, const char *code);

Runs Python 2.1 statements in __main__, like the game's own scripts. (b0.5.2)

When to use it: anything the game's scripts can do: give money (FindPlayer().MoneyAdd(100)), set a quest state (FindPlayer().SetQuest("MyQuest", 2)), set a G variable, call a Python mod's function.

  • code: one or more statements; separate lines with "\n".
  • Returns 1 if the code ran, 2 if it was queued because the call came from inside game code (it then runs at the next safe point, the end of the server frame), 0 on an error (a traceback is printed to the game console), when Python is unavailable, code is NULL, or it is not called on the main thread.
C
host->python_exec(host->context, "G.MyMod_Visited = 1\nFindPlayer().MoneyAdd(50)");

python_eval#

C
int python_eval(void *context, const char *expression, char *buffer, size_t capacity, size_t *size);

Evaluates one Python expression in __main__ and copies str(result) into buffer. (b0.5.2)

When to use it: read a value the game keeps in Python: a stat (FindPlayer().humanity), money (FindPlayer().money), a G variable, whether an entity exists.

  • expression: an expression, not a statement ("G.MyMod_Visited", not "x = 1").
  • The text is copied following the output buffer rules: cut to capacity - 1 bytes and NUL-terminated; *size receives the full length.
  • Returns 1, or 0 on an error (traceback in the console), when Python is unavailable, when called from inside game code (it cannot wait), or off the main thread.
  • Numbers come back as text: convert them with atoi/atof. None comes back as "None".
C
char money[32];
if (host->python_eval(host->context, "FindPlayer().money", money, sizeof money, NULL) && atoi(money) >= 100) { /* can pay */ }

Adding Python functions#

add_python_function(context, name, help, fn, user) (b0.5.7) adds blse_native.<name> for game scripts, Python mods and dialogue conditions. Native code can add functions to the game's Python this way:

C
static int BLSE_CALL bonus(const BLSE_Value* args, int argc, BLSE_Value* result, void* user) {
    if (argc != 1 || args[0].type != BLSE_VALUE_INT) {
        result->type = BLSE_VALUE_STRING; result->s = "expects an int"; result->size = 14;
        return 0;                                   /* raises RuntimeError("mymod_bonus: expects an int") */
    }
    result->type = BLSE_VALUE_INT;
    result->i = args[0].i * 2;
    return 1;
}
host->add_python_function(host->context, "mymod_bonus", "mymod_bonus(n) -> int", bonus, NULL);
  • Values are BLSE_VALUE_NONE, INT (i), FLOAT (f) and STRING (s and size, NUL-terminated, may contain NUL bytes). *result starts as None. A STRING result is copied when the function returns, so a static or stack buffer is fine.
  • Names are Python identifiers of 1-63 characters. Prefix them with your mod name. Adding a name again replaces your earlier function; a name another mod uses is refused (0).
  • The function runs on the main thread inside the calling Python code, so python_exec and python_eval work from it. It may be called from dialogue, so keep it quick.
  • Functions survive engine restarts. A plugin that crashes in one is disabled and its functions removed (see below).

The crash rules are described in When a plugin crashes. After a crash, a call to one of the plugin's functions raises RuntimeError ("not available") in Python, also through a reference a script kept earlier. The Python side is described in Functions from native plugins.

add_python_function#

C
typedef struct BLSE_Value {
    uint32_t type;   /* BLSE_VALUE_NONE, BLSE_VALUE_INT, BLSE_VALUE_FLOAT, BLSE_VALUE_STRING */
    int32_t i;       /* INT */
    double f;        /* FLOAT */
    const char *s;   /* STRING: size bytes, NUL-terminated, may contain NUL bytes */
    size_t size;
} BLSE_Value;
typedef int (BLSE_CALL *BLSE_PythonFunctionFn)(const BLSE_Value *args, int argc, BLSE_Value *result, void *user);
int add_python_function(void *context, const char *name, const char *help, BLSE_PythonFunctionFn fn, void *user);

Makes fn callable from Python as blse_native.<name>(...). (b0.5.7)

When to use it: a dialogue condition that needs C code (blse_native.mymod_can_bribe(npc.GetName())), a calculation a Python mod asks your plugin for, sharing your plugin's state with Python mods.

  • name: a Python identifier: 1-63 characters of A-Z a-z 0-9 _, not starting with a digit or with __. Start it with your mod's name.
  • help: a short description of the function ("mymod_bonus(n) -> int"); NULL for none.
  • fn: your function. It gets the call's arguments as an array of argc values and a result that starts as None. Arguments may be None, ints (longs that fit), floats and strs (NUL bytes allowed), at most 32; anything else raises TypeError in Python before your function is called.
  • Your function returns 1 with *result filled (or left as None), or 0 to raise RuntimeError in Python; a STRING result is then the error message (prefixed with the function's name). A STRING result is copied when your function returns.
  • Strings in args are borrowed: valid only during the call.
  • blse_native is set in __main__ for game scripts and dialogue conditions; Python mods use it too (blse_native.mymod_bonus(2)).
  • Returns 1, or 0 if the name is invalid or taken by another mod, fn is NULL, Python is unavailable, or it is not called on the main thread. The reason is written to blse.log (python: function 'x' not added: ...).

Use from Python:

Python
import blse_native
print blse_native.mymod_bonus(21)     # 42

In a dialogue condition (a .dlg column, see Conversations): blse_native.mymod_bonus(1) > 1.