BML+ native mod API overview¶
This page groups the public headers installed by the BML+ SDK by purpose. The
installed include/BML directory defines the supported native API.
Minimal entry point¶
A native mod is a dynamic library that exports BMLEntry and normally uses the
.bmodp extension:
#include <BML/IMod.h>
class MyMod final : public IMod {
public:
explicit MyMod(IBML *bml) : IMod(bml) {}
const char *GetID() override { return "MyMod"; }
const char *GetVersion() override { return "1.0.0"; }
const char *GetName() override { return "My Mod"; }
const char *GetAuthor() override { return "Author"; }
const char *GetDescription() override { return "Example"; }
DECLARE_BML_VERSION;
};
MOD_EXPORT IMod *BMLEntry(IBML *bml) { return new MyMod(bml); }
MOD_EXPORT void BMLExit(IMod *mod) { delete mod; }
The object returned by BMLEntry is allocated by the Mod DLL. Export
BMLExit and destroy that same object there so allocation and deallocation use
the same C++ runtime. BML calls BMLExit when registration fails after object
creation and when a loaded native Mod is unloaded. For compatibility, BML can
still load an older DLL without BMLExit, but it logs a warning and cannot
destroy that Mod instance safely.
Use the CMake helper installed with the SDK:
bml_add_mod links BML::BML, enables C++20, disables compiler extensions,
and produces MyMod.bmodp. It requires an MSVC-compatible 32-bit target and
makes the linker require the exact C symbols BMLEntry and BMLExit. A missing
or C++-mangled entry point therefore fails the build instead of producing a Mod
that the loader cannot use safely.
bml_install_mod adds the standard install rule. Set CMAKE_INSTALL_PREFIX to
the Ballance ModLoader directory, then use CMake's install target to build
and deploy the Mod under ModLoader/Mods.
Public headers¶
| Header | Purpose |
|---|---|
Version.h, Defines.h |
Version macros, export macros, status codes, and base definitions |
BML.h |
C ABI for version, loader and mod directories, command unregistration, memory, encoding, path, file, and Zip utilities |
BMLAll.h |
Convenience header that includes the complete native SDK surface |
IMod.h, IMessageReceiver.h |
Mod metadata, lifecycle, gameplay, and engine callbacks |
IBML.h |
Loader services, CK managers, lookup, commands, timers, and dependencies |
ICommand.h |
Command execution, completion, and basic argument parsing |
IConfig.h |
Typed configuration properties |
ILogger.h |
Info, Warn, and Error logging |
DataShare.h |
Low-level, named in-process byte sharing |
Types.h, TypeConvert.h |
Object references, vectors, and matrices, plus conversions to and from the Virtools types |
Interface.h |
The versioned interface structs the loader hands out, and how to ask for one |
Runtime.h, Scene.h, Gameplay.h, Speedrun.h, UI.h |
Loader capabilities reached through an interface struct, with inline C++ wrappers |
Imc.h, ImcWire.hpp, ImcCpp.hpp |
IMC C/C++ runtime and wire format |
Bui.h |
Ballance-style ImGui widgets |
Gui.h, Gui/*.h |
BGui wrappers around Virtools entities and behaviours |
InputHook.h |
Keyboard, mouse, controller state, and paired input-block tokens |
ExecuteBB.h |
v0.3 compatibility interface for executing or creating common Building Blocks |
ScriptHelper.h |
Find, connect, insert, and remove behaviour nodes and parameters |
Guids.h, Guids/*.h |
Virtools and Ballance Building Block GUIDs |
Mod lifecycle and events¶
IMod inherits IMessageReceiver. A mod supplies its ID, version, name,
author, description, and required BML version, then overrides only the
callbacks it needs:
- lifecycle:
OnLoad,OnUnload,OnProcess, andOnRender; - configuration and commands:
OnModifyConfig,OnPreCommandExecute,OnPostCommandExecute, andOnCheatEnabled; - engine objects:
OnLoadObject,OnLoadScript,OnPhysicalize, andOnUnphysicalize; - game flow: menu, load, start, reset, pause, exit, next-level, death, finish,
checkpoint, life, and navigation callbacks from
IMessageReceiver.
OnProcess is the only callback that runs inside the active ImGui frame. Draw
every ImGui and Bui control from it, and never from OnRender. See
Three UI surfaces.
OnRender receives one CK_RENDER_FLAGS value. The native API does not expose
separately named before-render and after-render callbacks. Loader notifications
arrive synchronously through the IMod and IMessageReceiver virtuals listed
above. A mod that needs deferred handling should copy the data it needs into a
queue it owns.
IBML services¶
IBML is the main loader service passed to a mod. It provides:
- the CK context and Attribute, Behavior, Collision, Input, Message, Path, Parameter, Render, Sound, and Time managers;
AddTimerandAddTimerLoopscheduling by frames or by milliseconds;- game state, cheat control, in-game messages, and command registration, lookup, and execution;
- named lookup for DataArrays, Groups, Materials, Meshes, 2D/3D Entities, Cameras, Lights, Sounds, Textures, and Behaviors;
- Initial Condition and visibility changes, plus skipping the next render tick;
- ball, floor, module, and transformation type registration and SR/HS scores;
- mod enumeration and lookup, plus dependency registration and queries.
Create timers through IBML. The SDK does not publish a standalone Timer.h;
the loader owns scheduling and callback processing.
AddTimer and AddTimerLoop are each overloaded on CKDWORD and float. The
CKDWORD overloads count frames, the float overloads count milliseconds, and
both units share one name, so an unsuffixed integer literal is ambiguous and
fails to compile. Write the suffix explicitly:
bml->AddTimer(1ul, [] { /* next frame */ });
bml->AddTimer(1000.0f, [] { /* one second later */ });
bml->AddTimerLoop(1.0f, [] { return KeepRunning(); });
The loop callbacks keep running while they return true. Neither overload
returns a handle, so a scheduled timer cannot be cancelled; make the loop
callback return false instead.
Native mod dependencies¶
Register dependencies before BML initializes mods. The constructor is the
usual place because it runs while BMLEntry creates the mod and before any
OnLoad callback:
explicit MyMod(IBML *bml) : IMod(bml) {
AddDependency("RequiredMod", BMLVersion(1, 2, 0));
AddOptionalDependency("OptionalMod", BMLVersion(1, 0, 0));
}
BML orders mods so installed dependencies receive OnLoad before their
dependents. A missing optional dependency is ignored. A missing required
dependency or a dependency cycle prevents the mod initialization phase from
starting; the log identifies the requesting mod, required id and version, or
the mods affected by the cycle. If an installed dependency is older than the
requested version, BML skips the dependent mod's OnLoad and reports both
versions while continuing with other mods.
Configuration, commands, and logging¶
IConfig retrieves an IProperty by category and key. Properties can be
String, Boolean, Integer, Float, or Keyboard Key and support current values,
defaults, comments, and category comments. There is no separate UTF-16
property API; use the explicit conversion functions in BML.h when needed.
ICommand provides the command name, aliases, description, cheat flag,
execution, Tab completion, and basic Integer, Float, and Boolean parsers.
ILogger provides three log levels.
IBML::RegisterCommand takes a raw ICommand * and the loader never deletes
it. Registration returns void and only writes to the log when it fails, which
happens for a null command, an invalid name or alias, and an already registered
name.
IBML has no unregister function, so removing a command goes through
BML_UnregisterCommand in BML.h:
void MyMod::OnUnload() {
if (BML_UnregisterCommand("mycmd") == BML_OK)
delete m_Command; // only now is deleting it safe
}
Only the DLL that registered a command may remove it, which the loader decides
by remembering which module called RegisterCommand. Asking about someone else's
command answers BML_ERROR_ACCESS_DENIED, an unknown name answers
BML_ERROR_NOT_FOUND, and the name is matched the way the console matches it, so
the alias names the command too. Call it from the game thread.
Without that call, a registered command stays in the command table until the
process ends. In particular, do not delete an ICommand in OnUnload while it is
still registered: unloading a mod does not remove its commands by itself, so a
deleted command leaves a dangling entry that the console will still try to run.
ParseFloat clamps to the whole finite float range by default. Earlier releases
defaulted its lower bound to FLT_MIN, the smallest positive normal value, so
negative input was silently clamped to about 1.17e-38. Pass explicit bounds
when a command needs a narrower range.
Loader capabilities¶
Capability added after the legacy C++ interfaces were frozen is published as a
versioned interface struct, fetched by id and major version through
BML_GetInterface. Each has its own header under include/BML, and each header
also declares an inline C++ namespace that folds the lookup and the argument
checks in:
BML::Runtimefor runtime state, clock, and scores;BML::Scenefor object information, transforms, and named lookup;BML::Gameplayfor level, energy, catalog, checkpoint, and reset data;BML::UIfor the message board, mod/map menus, and HUD;BML::Speedrunfor the shared speedrun timer.
Interface.h documents the version rules: a struct grows only by appending a
member and bumping its minor version, and BML_IFACE_HAS asks whether the
running loader has a member added after the header the mod was built against.
The native BML::Gameplay collection reads return complete snapshots in a
caller-owned std::vector. Read the catalog during setup and refresh level
checkpoints or reset points when the level changes; these calls transfer the
complete collection and are not intended for per-frame polling.
The inline C++ operations return a BML status and are marked [[nodiscard]].
Handle the returned status, or use an explicit (void) cast when deliberately
discarding the result of best-effort cleanup.
Inter-mod communication¶
An API a mod publishes for other mods is not an interface struct: only the loader
answers BML_GetInterface. Prefer IMC there:
- a
.imcfile contains interface declarations only; field IDs are permanent wire identifiers rather than array positions; bml_target_imc_apigenerates C++ bindings and adds them to a target;- RPC supports synchronous calls, futures, cancellation, timeouts, and completion callbacks;
- Topic supports bounded subscriber queues, unsubscribe, and drop counts;
- generated types and cached route IDs keep text parsing out of hot paths;
- a consumer discovers at runtime whether a route is there, so provider and consumer can ship separately.
C++ IMC operations that return a BML status are marked [[nodiscard]] as well.
DataShare is suitable for small named byte values when both sides obey its
reference-count and borrowed-pointer lifetime rules. Use IMC when an API has to
evolve on its own schedule, or needs RPC or Topic semantics.
Three UI surfaces¶
Buidraws Ballance-style ImGui widgets for native overlays.BGuicreates in-game UI from Virtools 2D Entities and Behaviors.BML::UIdoes not draw widgets; it controls loader-owned messages, menus, and HUD state. Every one of its calls has to be made from the game thread.
These surfaces solve different problems and are not interchangeable.
Draw ImGui from OnProcess¶
The loader owns the ImGui frame. It opens the frame before mod callbacks run and
ends it immediately after OnProcess returns:
- the loader calls
ImGui::NewFramebefore the per-frame mod callbacks; - every mod's
OnProcessruns inside that frame; - the loader calls
ImGui::Render, which ends the frame; OnRenderruns;- the loader submits the recorded draw data.
So ImGui and Bui calls belong in OnProcess. The same calls made from
OnRender happen after the frame has ended: they draw nothing and may trip an
ImGui assertion. BML::UI message, menu, and HUD calls are not affected,
because they change loader state instead of recording draw commands.
C API ownership¶
BML.h and DataShare.h are callable through the C ABI. Release strings,
wide strings, string arrays, wide-string arrays, and binary buffers allocated
by BML with the matching BML_Free* function. Do not call CRT free across a
DLL boundary.
BML_DataShare_Get returns a borrowed pointer. It becomes invalid when the
same key is set or removed or when the instance is destroyed. Use
BML_DataShare_CopyEx when a stable copy is required.
Where the loader and your mod live¶
BML.h answers both questions a mod has about the file system:
// The loader's own directories. Borrowed, valid for the process, never freed.
const char *loaderDir = BML_GetLoaderPathUtf8(BML_DIR_LOADER);
// Your own installation directory. Allocated, so release it.
char *modRoot = BML_GetModRootUtf8(nullptr);
// ... use modRoot ...
BML_FreeString(modRoot);
BML_GetLoaderPathW and BML_GetLoaderPathUtf8 take a BML_LoaderDirectory:
BML_DIR_WORKING, BML_DIR_TEMP, BML_DIR_GAME, BML_DIR_LOADER, or
BML_DIR_CONFIG. They return a pointer the loader owns, so do not free it. Do
not confuse them with BML_GetDirectoryA/W/Utf8, which parse a path string and
return its directory part.
BML_GetModRootW and BML_GetModRootUtf8 answer with the directory a mod is
installed in. Pass nullptr for your own: it resolves the calling DLL, needs no
registration, and therefore works from your constructor. Pass a mod id to ask
about another mod, which answers with its DLL's directory for a native mod and
with its script root for a script mod. Both allocate, so release the result with
BML_FreeWString or BML_FreeString.
Both return null while the loader is still initializing and when the directory
cannot be resolved. BML_GetModRoot also takes the loader's mod-registry lock,
so call it from your mod rather than from DllMain.