Skip to content

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:

find_package(BML CONFIG REQUIRED)
bml_add_mod(MyMod MyMod.cpp)
bml_install_mod(MyMod)

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, and OnRender;
  • configuration and commands: OnModifyConfig, OnPreCommandExecute, OnPostCommandExecute, and OnCheatEnabled;
  • engine objects: OnLoadObject, OnLoadScript, OnPhysicalize, and OnUnphysicalize;
  • 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;
  • AddTimer and AddTimerLoop scheduling 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::Runtime for runtime state, clock, and scores;
  • BML::Scene for object information, transforms, and named lookup;
  • BML::Gameplay for level, energy, catalog, checkpoint, and reset data;
  • BML::UI for the message board, mod/map menus, and HUD;
  • BML::Speedrun for 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 .imc file contains interface declarations only; field IDs are permanent wire identifiers rather than array positions;
  • bml_target_imc_api generates 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

  • Bui draws Ballance-style ImGui widgets for native overlays.
  • BGui creates in-game UI from Virtools 2D Entities and Behaviors.
  • BML::UI does 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:

  1. the loader calls ImGui::NewFrame before the per-frame mod callbacks;
  2. every mod's OnProcess runs inside that frame;
  3. the loader calls ImGui::Render, which ends the frame;
  4. OnRender runs;
  5. 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.

Further reading