LANCommander.Documentation/Interposer/Plugins/CreatingPlugins.md
github-actions[bot] 40e5877398 Sync docs sources\n\nlancommander@v2.1.0-rc3
interposer@v1.0.5
bf1942@v1.61
callofduty@v1.5b
counterstrikesource@v1.0.0
darkmessiah@unversioned
eliteforce@v1.30
jediacademy@v1.0.0
jedioutcast@v1.4.1
portalknights@unversioned
quake3arena@v1.0.0
rtcw@unversioned
ut2004@v3339
2026-05-28 00:17:13 +00:00

8.7 KiB

sidebar_label sidebar_position
Creating a Plugin 2

Creating a Plugin

A plugin is a standard Windows DLL (.dll) or ASI file (.asi) placed in .interposer\Plugins\. It has no link-time dependency on the Interposer — all API functions are resolved at runtime via GetProcAddress.

Project Setup

Create a new DLL project targeting the same architecture as the game (x86 for 32-bit games, x64 for 64-bit games). No additional libraries or headers are required beyond the Windows SDK.

Plugin Entry Point

The Interposer calls an optional exported function on each plugin immediately after LoadLibrary:

extern "C" __declspec(dllexport) void WINAPI InterposerPluginInit(HMODULE hInterposer);

The hInterposer parameter is the Interposer's own module handle. Use it for all GetProcAddress calls — this works regardless of whether the Interposer was deployed as LANCommander.Interposer.dll, version.dll, dinput8.dll, or an .asi file.

Keep DllMain minimal — heavy initialization belongs in InterposerPluginInit where the full Interposer API is available:

BOOL APIENTRY DllMain(HMODULE /*hModule*/, DWORD /*fdwReason*/, LPVOID /*lpReserved*/)
{
    return TRUE;
}

Resolving the API

Declare function pointer types for the Interposer exports you need and resolve them with GetProcAddress using the hInterposer handle:

using FnInterposerLog                  = void (WINAPI*)(const wchar_t* verb, const wchar_t* message);
using FnInterposerGetConfigString      = BOOL (WINAPI*)(const wchar_t* dotPath, wchar_t* buf, DWORD bufSize);
using FnInterposerRegisterPluginConfig = BOOL (WINAPI*)(const wchar_t* pluginName, const wchar_t* yamlDefaults);

static FnInterposerLog                  pfnLog       = nullptr;
static FnInterposerGetConfigString      pfnGetConfig = nullptr;
static FnInterposerRegisterPluginConfig pfnRegConfig = nullptr;

static bool ResolveAPI(HMODULE hInterposer)
{
    pfnLog       = (FnInterposerLog)                 GetProcAddress(hInterposer, "InterposerLog");
    pfnGetConfig = (FnInterposerGetConfigString)     GetProcAddress(hInterposer, "InterposerGetConfigString");
    pfnRegConfig = (FnInterposerRegisterPluginConfig)GetProcAddress(hInterposer, "InterposerRegisterPluginConfig");

    return pfnLog && pfnGetConfig;
}

API Reference

All exported functions use the WINAPI (__stdcall) calling convention and undecorated extern "C" names.

InterposerRegisterPluginConfig

BOOL InterposerRegisterPluginConfig(const wchar_t* pluginName, const wchar_t* yamlDefaults);

Register default configuration for the plugin. pluginName is the key under Plugins: in Config.yml (e.g. L"MyPlugin"). yamlDefaults is a YAML map body defining default keys and values.

If a Plugins.<pluginName> section already exists in Config.yml, the call is a no-op — user configuration is never overwritten. Otherwise the defaults are merged into the in-memory config (immediately queryable via InterposerGetConfigString) and appended to Config.yml on disk.

if (pfnRegConfig)
{
    pfnRegConfig(L"MyPlugin",
        L"Greeting: 'Hello!'\n"
        L"Count: 42\n"
        L"Enabled: true");
}

After this call, pfnGetConfig(L"Plugins.MyPlugin.Greeting", ...) returns "Hello!" even if the user has never touched Config.yml. On the first run, Config.yml is updated to include:

Plugins:
  MyPlugin:
    Greeting: Hello!
    Count: 42
    Enabled: true

Returns TRUE on success (or if the section already exists). Returns FALSE on error (bad YAML, file write failure).

:::tip Call InterposerRegisterPluginConfig before reading any config values — it ensures defaults are always available. :::


InterposerLog

void InterposerLog(const wchar_t* verb, const wchar_t* message);

Writes a line to the session log regardless of the Logging flags in Config.yml. The log line format matches the rest of the session log:

YYYY-MM-DD HH:MM:SS  [VERB]             <message>

verb is normalised automatically: any existing [/] brackets and surrounding whitespace are stripped, the content is truncated to 16 characters, and it is re-wrapped as [verb] right-padded to 18 characters. Pass a plain string such as L"MYPLUGIN" — no manual padding required.


InterposerGetConfigString

BOOL InterposerGetConfigString(const wchar_t* dotPath, wchar_t* buffer, DWORD bufferSize);

Reads a scalar value from Config.yml by dot-separated YAML path. Returns TRUE on success, FALSE if the key does not exist, is not a scalar, or the buffer is too small.

bufferSize is in wchar_t units and must include room for the null terminator.

wchar_t setting[256];
if (pfnGetConfig(L"Plugins.MyPlugin.Setting", setting, ARRAYSIZE(setting)))
{
    // use setting
}

Plugin configuration should live under a Plugins.<PluginName> namespace in Config.yml to avoid collisions:

Plugins:
  MyPlugin:
    Setting: hello
    Count: 42

InterposerGetUsername

BOOL InterposerGetUsername(wchar_t* buffer, DWORD bufferSize);

Returns the effective player username: the value configured in Config.yml under Player.Username or passed via the --username injector flag. Falls back to the real Windows account name (GetUserNameW) if no override is configured.

bufferSize is in wchar_t units including the null terminator. Returns TRUE on success.


InterposerSetRegistryValue

void InterposerSetRegistryValue(const wchar_t* keyPath, const wchar_t* valueName, const wchar_t* value);

Injects a REG_SZ string value into the in-memory virtual registry store. Subsequent RegQueryValueEx calls for keyPath\valueName return value without touching the real registry. The injection is transient — it is not persisted to .interposer\Registry.reg.

keyPath must be a full path beginning with a hive name:

HKEY_LOCAL_MACHINE\SOFTWARE\MyGame\1.0

Set valueName to L"@", L"", or nullptr to target the default (unnamed) registry value — the entry shown as (Default) in Registry Editor.

:::note The target key must already exist in .interposer\Registry.reg for reads to be intercepted. Add an empty key header if no values need to be pre-populated:

[HKEY_LOCAL_MACHINE\SOFTWARE\MyGame\1.0]

:::


InterposerSetRegistryValueBySuffix

DWORD InterposerSetRegistryValueBySuffix(const wchar_t* keySuffix, const wchar_t* valueName, const wchar_t* value);

Like InterposerSetRegistryValue, but matches by suffix rather than exact path. Any key in the virtual store whose path ends with \keySuffix (matched case-insensitively on a backslash component boundary) receives the injected value.

Returns the number of keys updated. A return value of 0 means the suffix matched nothing in the virtual store — check that the target key is present in .interposer\Registry.reg.

This is useful when the full registry path varies between game versions or installations:

// Matches HKEY_LOCAL_MACHINE\...\Electronic Arts\EA Games\Battlefield 1942\ergc
// regardless of any intermediate path components.
pfnSetBySuffix(L"Battlefield 1942\\ergc", L"@", generatedKey);

Minimal Example

#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <string>

using FnInterposerLog                  = void (WINAPI*)(const wchar_t*, const wchar_t*);
using FnInterposerGetConfigString      = BOOL (WINAPI*)(const wchar_t*, wchar_t*, DWORD);
using FnInterposerRegisterPluginConfig = BOOL (WINAPI*)(const wchar_t*, const wchar_t*);

static FnInterposerLog                  pfnLog       = nullptr;
static FnInterposerGetConfigString      pfnGetConfig = nullptr;
static FnInterposerRegisterPluginConfig pfnRegConfig = nullptr;

extern "C" __declspec(dllexport) void WINAPI InterposerPluginInit(HMODULE hInterposer)
{
    pfnLog       = (FnInterposerLog)                 GetProcAddress(hInterposer, "InterposerLog");
    pfnGetConfig = (FnInterposerGetConfigString)     GetProcAddress(hInterposer, "InterposerGetConfigString");
    pfnRegConfig = (FnInterposerRegisterPluginConfig)GetProcAddress(hInterposer, "InterposerRegisterPluginConfig");

    if (!pfnLog || !pfnGetConfig) return;

    // Register defaults — written to Config.yml on first run only.
    if (pfnRegConfig)
        pfnRegConfig(L"MyPlugin", L"Greeting: 'Plugin loaded successfully'");

    // Read config (defaults are immediately available after registration).
    wchar_t greeting[256] = {};
    pfnGetConfig(L"Plugins.MyPlugin.Greeting", greeting, ARRAYSIZE(greeting));

    pfnLog(L"MYPLUGIN", greeting);
}

BOOL APIENTRY DllMain(HMODULE, DWORD, LPVOID)
{
    return TRUE;
}
# After first run, Config.yml automatically contains:
Plugins:
  MyPlugin:
    Greeting: Plugin loaded successfully