On this page 14 sections
This page covers two things:
- Hooks: changing game code BLSE has no event or function for, by redirecting it to your own function. This is advanced work: you need to know x86 assembly, the calling conventions of the game's code, and how to find a function's address (with a disassembler such as Ghidra or IDA).
- Crashes: what BLSE does when your plugin crashes, and how to read the crash logs to find the bug.
Come here when no event, filter or Host API function does what you need, or when your plugin crashed and you want to know why.
A minimal hook: find a function by its bytes and run your code before it.
#include "blse_api.h"
typedef int (__fastcall *CountFn)(void *self, void *edx);
static CountFn original_count;
static const BLSE_HostAPI *host;
/* Called instead of the game's function; calls the original to keep the game's behaviour. */
static int __fastcall count_detour(void *self, void *edx) {
int result = original_count(self, edx);
return result; /* change it here */
}
/* Hook once a level runs: game DLLs are not loaded yet when plugins load. */
static void BLSE_CALL on_level(const char *topic, const void *payload, size_t size, void *user) {
static int done;
void *target = NULL;
if (done) return;
done = 1;
/* a made-up pattern: replace it with the start of the function you found */
if (host->find_pattern(host->context, "vampire.dll", "8B 81 ?? ?? ?? ?? 85 C0", &target) != 1) {
host->log("function not found: another game build?");
return;
}
const unsigned char expected[6] = {0x8B, 0x81, 0x8C, 0x08, 0x00, 0x00}; /* read them from the game first */
int r = host->hook_function(host->context, target, expected, sizeof expected,
(void *)count_detour, (void **)&original_count);
if (r != BLSE_HOOK_OK) host->log("hook refused (see blse.log)");
}
BLSE_EXPORT int BLSE_CALL BLSE_PluginLoad(const BLSE_HostAPI *h) {
host = h;
if (h->api_version != BLSE_API_VERSION || !BLSE_HOST_HAS(h, find_pattern)) return 0;
h->subscribe(h->context, BLSE_MSG_LEVEL_INIT, on_level, NULL);
return 1;
}Important, before you start:
- Prefer BLSE's events, filters and functions. A hook ties your plugin to one game build. Functions BLSE hooks itself (
AcceptInput,TakeDamage, ...) are refused: use the matching events and filters. - Main thread only, from
BLSE_PluginLoad, a callback or a task. - Game DLLs are loaded after plugins: hook
vampire.dll,engine.dlland the others atblse.level_initor later, not inBLSE_PluginLoad. - Your detour runs inside the game's code, without BLSE's crash guard. A crash there ends the game. Keep detours short and test them well.
- A detour must have exactly the target's calling convention and arguments (
__thiscallmethods are usually written as__fastcallwith an unusededxargument in C). - Patches stay after a crash disables your plugin (the game may be running inside them), so the detour must not depend on the plugin's other state being sane.
Words used on this page#
| Word | Meaning |
|---|---|
| Hook | A change to game code that makes it call your function |
| Target | The game function (its address) you hook |
| Detour | Your function, called instead of the target |
| Original, trampoline | A small piece of code BLSE builds: the target's first instructions followed by a jump into the rest of the target. Calling original runs the game's unchanged function |
| Patch | Bytes BLSE wrote into game code or data for a hook. BLSE records every patch, its own and every plugin's |
| Call site | One call instruction in game code (E8 and a 4-byte relative address) |
| Slot | A place in memory holding a function pointer: a vtable entry (the methods of a C++ class) or an import table entry (a function another DLL provides) |
| Pattern | Hex bytes to search for, with ?? for bytes that may differ (addresses, offsets): "8B 0D ?? ?? ?? ?? 8B 01" |
| Module | A loaded DLL or the game's .exe: "vampire.dll", "engine.dll" |
| Crash guard | BLSE's protection around calls into your plugin: a crash there disables the plugin instead of ending the game |
| PDB | The debug information file your compiler writes next to the DLL; it turns addresses into source lines |
Hooks#
For game code BLSE does not cover yet, a plugin can hook it through BLSE instead of patching it itself. BLSE checks the code first, keeps a record of every patch (its own and all plugins'), refuses a patch that overlaps another one instead of silently breaking it, and logs each one (plugin mymod: hooked a function at vampire.dll+0x252290: ok).
| Function | |
|---|---|
hook_function(context, target, expected, size, detour, &original) |
target jumps to detour. expected: the first 5-16 bytes of the target, whole instructions without relative jumps or calls. original calls the unhooked code |
hook_call(context, call_site, detour, &original) |
Redirects one call instruction (E8 rel32); original is the function it called |
hook_pointer(context, slot, detour, &original) |
Replaces a function pointer (a vtable or import entry) |
unhook(context, target) |
Undoes the plugin's own patch at that address |
find_pattern(context, module, pattern, &address) |
Finds code by bytes with ?? wildcards in a module's code sections: 1 (unique), 0 (not found), 2 (several: use a longer pattern) |
Results: BLSE_HOOK_OK (1), BLSE_HOOK_BYTES_DIFFER (the code is not what the plugin expects: another build, or another mod's patch), BLSE_HOOK_CONFLICT (overlaps a patch of BLSE or another plugin; the log names it), BLSE_HOOK_INVALID, BLSE_HOOK_FAILED. Main thread only. Hooking the same function twice from one plugin returns the first hook; hooking the same call site or pointer again switches it to the new detour, and original is still the game's function. When a plugin declines to load, its patches are undone; when it is disabled after a crash they stay, because the game may be running inside them. Prefer BLSE's events where they exist: functions BLSE hooks (AcceptInput, TakeDamage, ...) are refused.
typedef void *(__fastcall *OwnerFn)(void *self, void *edx);
static OwnerFn original_owner;
static void *__fastcall owner_detour(void *self, void *edx) { /* ... */ return original_owner(self, edx); }
static const unsigned char expected[6] = {0x8B, 0x81, 0x8C, 0x08, 0x00, 0x00}; /* mov eax,[ecx+88Ch] */
unsigned char *vampire = (unsigned char *)GetModuleHandleW(L"vampire.dll");
if (host->hook_function(host->context, vampire + 0x252290, expected, sizeof expected,
(void *)owner_detour, (void **)&original_owner) != BLSE_HOOK_OK) { /* not this build */ }Game DLLs are loaded after plugins: hook them at blse.level_init or later, not in BLSE_PluginLoad.
Results#
Every hook function returns one of these (b0.5.28):
| Value | Constant | Meaning | What to do |
|---|---|---|---|
| 1 | BLSE_HOOK_OK |
Done; *original is set |
Nothing |
| -1 | BLSE_HOOK_BYTES_DIFFER |
The code is not what you expected: another game build, or another mod patched it (for hook_call: not a call instruction) |
Do not hook; log that this build is not supported |
| -2 | BLSE_HOOK_CONFLICT |
It overlaps a patch of BLSE or another plugin; blse.log names it |
Use BLSE's event or filter for that function, or hook elsewhere |
| -3 | BLSE_HOOK_INVALID |
Bad arguments (a NULL pointer, size outside 5-16, a bad pattern or module), not the main thread, or your plugin is disabled |
Fix the call |
| -4 | BLSE_HOOK_FAILED |
Memory could not be allocated or made writable | Rare; log it |
On every result other than BLSE_HOOK_OK, *original is NULL. Hooks made, and hooks refused because the bytes differ or a patch overlaps, are written to blse.log with your plugin's name, the address as module+offset, and the outcome.
hook_function#
int hook_function(void *context, void *target, const unsigned char *expected, size_t size, void *detour, void **original);Makes the function at target jump to detour. (b0.5.28)
When to use it: change what a whole game function does, for every caller.
target: the function's address (fromfind_pattern, or a module's base address plus an offset you found).expected,size: the firstsizebytes (5 to 16) of the target as your plugin was written for. They must be whole instructions and contain no relative jumps or calls (those are moved into the trampoline, where relative addresses would be wrong). BLSE compares them with the real code first: a difference returnsBLSE_HOOK_BYTES_DIFFER, so a different build is never patched blindly.detour: your function, with the target's exact calling convention and arguments.original: receives a function that runs the unhooked code; call it from your detour to keep the game's behaviour.- Hooking the same target again from your plugin returns
BLSE_HOOK_OKand the first hook'soriginal(it is not hooked twice). After an engine restart has loaded the DLL again with its original bytes, it is hooked anew. - Returns a result.
hook_call#
int hook_call(void *context, void *call_site, void *detour, void **original);Redirects one call instruction to detour. (b0.5.28)
When to use it: change what happens at one place in the game, leaving other callers of the same function alone.
call_site: the address of a 5-bytecall rel32instruction (E8 xx xx xx xx). Anything else returnsBLSE_HOOK_BYTES_DIFFER("not a call instruction" in the log).originalreceives the function the call went to.- Hooking the same call site again switches it to the new detour;
originalis still the game's function (not your earlier detour). - Returns a result.
hook_pointer#
int hook_pointer(void *context, void **slot, void *detour, void **original);Replaces a function pointer: one entry of a vtable or an import table. (b0.5.28)
slot: the address of the pointer (not the pointer's value).originalreceives the old pointer.- Hooking the same slot again switches it to the new detour;
originalis still the game's function. - Returns a result.
unhook#
int unhook(void *context, void *target);Undoes your plugin's own patch at target (the function, call site or slot you hooked). (b0.5.28)
- Returns 1 if a patch was removed, 0 if your plugin has none there.
- If another hook was put over your call or pointer patch since (BLSE chains its own pointer hooks over whatever is in a slot), writing the old bytes back would remove that hook too: the patch is left in place, forgotten, and 0 is returned ("left in place" in the log).
- A trampoline stays allocated after an unhook, so a detour still running inside
originalfinishes safely.
find_pattern#
int find_pattern(void *context, const char *module, const char *pattern, void **address);Finds code by its bytes, so your plugin does not depend on fixed addresses. (b0.5.28)
module: a loaded module's name ("engine.dll","vampire.dll", ...);NULLfor the game's.exe. The module must be loaded already.pattern: hex bytes separated by spaces,??(or?) for a byte that may be anything:"8B 0D ?? ?? ?? ?? 8B 01". Up to 256 bytes, at least one exact byte.- Only the module's executable sections are searched.
- Returns 1 and sets
*addressif the pattern occurs exactly once; 0 if not found; 2 if found more than once (make the pattern longer);BLSE_HOOK_INVALIDfor a bad pattern, a module that is not loaded,addressNULL, or a call off the main thread.*addressisNULLunless 1.
Patches and the plugin's life#
- When a plugin declines to load (returns 0 from
BLSE_PluginLoad), its patches are undone. - When it is disabled after a crash, its patches stay: the game may be running inside them. Its detours keep being called.
- Patches made by BLSE and by every plugin are recorded together; an overlapping patch is refused, never written over another one.
Crash reports name the BLSE function#
When a plugin crashes inside a BLSE function it called (in BLSE itself, or in game or Python code run from there), the log says which: plugin x crashed in its handler for 'blse.game_frame' (inside BLSE's python_exec, which it called): .... If that function was running Python, Python's current frame and recursion depth are put back as they were and a pending Python error is cleared, so the interpreter is not left inside code that no longer runs.
When a plugin crashes#
Since b0.5.6, BLSE runs every call into a plugin under a crash guard: BLSE_PluginLoad, message handlers, console commands, damage filters and Python functions. If the plugin crashes there (access violation, illegal instruction, division by zero, ...), BLSE:
- logs the crash with the plugin's name, what BLSE was calling and where it crashed, for example
plugin mymod crashed in its handler for 'blse.game_frame': access violation (0xc0000005) writing 0x00000000 at mymod.dll+0x1234, and prints the same line in the game console; - disables the plugin: its message handlers, damage filters and Python functions are removed, its console commands answer "not available", and host functions refuse its calls;
- restores its own state (events being delivered, "inside game code" scopes, the running owner), which a crash deep inside a call the plugin made would otherwise leave open;
- lets the game continue. The built-in
blsecommand shows how many plugins were disabled.
The DLL stays loaded, because its code may still be in use elsewhere. A crash in BLSE_PluginLoad returns the plugin as disabled, and BLSE does not try to load it again.
The same guard covers every other callback BLSE makes into a plugin: filters of every kind (input, Discipline, dialogue, animation, clan, file and map filters), tasks added with add_task, and draw and device callbacks. What else goes with a disabled plugin is described with each feature (HUD elements, windows, styles, button sounds, rules); item classes and patches stay.
Limits:
- The crash can leave the plugin's or the game's state half-changed, so the game may misbehave afterwards. The message recommends saving to a new slot and restarting.
- Crashes in the plugin's own threads, in its own hooks of game functions and from stack overflows are not caught, and they still end the game.
- A C++ exception that leaves a callback is treated like a crash and disables the plugin; one from
BLSE_PluginLoaddeclines the load instead. Informational exception codes (debugger output, thread naming) are not treated as crashes.
Use the address in the message (mymod.dll+0x1234) with your PDB to find the crashing line.
Finding the line from an address#
mymod.dll+0x1234 is an offset from the start of your DLL (its RVA). Two ways to turn it into code:
- Link with
/MAPto get a map file. Its list of functions ("Publics by Value") gives each function'sRva+Baseaddress; subtract the base it shows (usually10000000) and look for the function whose start is the closest one at or below0x1234. - Run the game under a debugger, or open a minidump (BLSE writes one for crashes the game does not report), in a debugger with your PDB next to the DLL (Visual Studio, WinDbg): it shows the function and the source line.
Keep the PDB and map file of every build you give out, so you can read the addresses your players report.
Crash log#
When the game itself crashes, it writes crash(<date time>).log in the game folder: the exception, registers, a stack trace (with your plugin's source lines if its PDB is next to it), the game's scope trace and the module list. Since b0.5.8 BLSE adds a [ BLSE ] section to it:
-----[ BLSE ]-----
BLSE b0.5.8, build vtmb-x86-80d73e74-9d00b2c1-c546f4de-2ce854dd
Crashed on the game's main thread.
Map: sm_hub_1
Mod code running: native plugin 'example_plugin'
BLSE events being delivered (outermost first): blse.game_frame
Plugins:
example_plugin
Last BLSE log lines:
...The game writes this log only for crashes on its main thread. A crash on another thread (for example, one of your plugin's own threads) ends the game without a log.
Since b0.5.25 the BLSE section also has the state at the exception, recorded by BLSE when it was raised: the exception and its module, the registers with what each one points to (code module+0x..., object of class CBaseEntity, from the object's RTTI, or text "..."), the return addresses on the stack, and the objects and text found in the first 0x200 bytes of the stack.
How to read the BLSE section: "Mod code running" names the plugin (or Python mod) whose code was running when the game crashed; if it is yours, start there. "BLSE events being delivered" shows which events were in progress, outermost first. The register lines tell you, for example, that ecx held an object of class CBaseEntity, which helps when the crash is in game code your plugin called.
Crashes the game does not report#
The game's crash log only covers its main thread: a crash on any other thread (a thread your plugin started, a sound or loading thread) used to end the game with no log at all. Since b0.5.23 BLSE writes its own report for those, in Bin\loader\blse\crash-<date>_<time>.log: the exception (for an access violation: reading, writing or executing which address), the faulting module and offset, and, if that module is a plugin, its name; the registers; the return addresses found on the crashing thread's stack (module and offset; a scan, so some can be stale); the same BLSE section as above; and the loaded modules. A minidump (.dmp, for a debugger such as Visual Studio or WinDbg) is written next to it. The newest 10 reports are kept. BLSE then passes the crash on to the filter that was installed before it, so Windows error reporting still sees it.