Create a Settings Plugin

A Settings Plugin is an optional settings.dll loaded by the OpenSR host when the user opens the Settings tab of a plugin inside the Plugin Manager.

Unlike settings.xml, which uses the built-in XML editor, a settings.dll gives full control over rendering, interactions, validation, animations, and workflow.

A Settings Plugin can use any rendering technology:

  • Win32 GDI
  • Direct2D
  • DirectWrite
  • OpenGL
  • Vulkan
  • ImGui
  • Custom software renderer

The example below uses:

  • Direct2D for rendering
  • DirectWrite for text rendering
  • TinyXML2 for settings persistence

The BeamNG Drive example demonstrates:

  • Editable UDP IP/Port fields
  • Boolean toggle switches
  • Clipboard support
  • Caret and text selection handling
  • Validation logic
  • Direct2D custom controls
  • XML settings persistence
  • Parent notification through _PLUGIN_SETTINGS_CHANGED

Although the original example comes from a Game (IN) plugin, the implementation is identical for OUT plugins.


Settings Plugin Lifecycle

Unlike normal plugins, a Settings Plugin only exists while its settings page is visible.

Typical lifecycle:

  1. User opens Plugin Manager
  2. User selects a plugin
  3. User clicks the Settings tab
  4. Host loads settings.dll
  5. Host creates the settings object
  6. Host provides a child HWND
  7. Plugin renders its UI
  8. User edits/saves settings
  9. User closes the page
  10. Host destroys the settings object
  11. DLL can be unloaded

Settings plugins are therefore:

  • Short-lived
  • UI-focused
  • Event-driven
  • Usually single-threaded

Unlike Game or OUT plugins:

  • Lifetime is strictly tied to the visible settings page
  • Settings plugins do NOT run permanent worker threads
  • Settings plugins are temporary UI modules

Folder Structure

Example:

Plugins/
+-- BeamNGDrive/
    +-- BeamNGDrive.osrp
    +-- settings.dll
    +-- settings.xml
    

The presence of settings.dll tells the host to use the custom settings UI instead of the generic XML editor.


Required Interface

A Settings Plugin must implement IPluginSettings.

Example:

class BeamNGDriveSettings : public IPluginSettings
{
public:
    BOOL Initialize(
        HWND hWindow,
        OpenSRContext* context,
        void* buffersOut,
        const wchar_t* pluginFolderPath
    ) override;

    BOOL Shutdown() override;

    BOOL WantsDialogMessages() const override
    {
        return FALSE;
    }
};

Exported Functions

The DLL must export two C functions:

extern "C" __declspec(dllexport)
IPluginSettings* CreatePluginSettings();

extern "C" __declspec(dllexport)
void DestroyPluginSettings(IPluginSettings* pSettings);

Example:

extern "C" __declspec(dllexport)
IPluginSettings* CreatePluginSettings()
{
    return new BeamNGDriveSettings();
}

extern "C" __declspec(dllexport)
void DestroyPluginSettings(IPluginSettings* pSettings)
{
    if (pSettings)
    {
        delete static_cast<BeamNGDriveSettings*>(pSettings);
    }
}

Initialize()

The host provides:

ParameterDescription
hWindowChild window owned by the dashboard
contextShared OpenSR context
buffersOutOptional output buffers
pluginFolderPathAbsolute plugin folder path

Example:

BOOL BeamNGDriveSettings::Initialize(
    HWND hWindow,
    OpenSRContext* context,
    void* buffersOut,
    const wchar_t* pluginFolderPath)
{
    m_hWnd = hWindow;
    m_pContext = context;
    m_pluginFolderPath = pluginFolderPath;

    SetWindowSubclass(
        m_hWnd,
        BeamNGDriveSettings::SubclassWndProc,
        1,
        (DWORD_PTR)this);

    LoadXMLSettings();

    return TRUE;
}

The provided window is owned by the host.

The settings plugin should:

  • Subclass the window
  • Render inside this window only
  • Never destroy the window itself
  • Never modify parent ownership

Shutdown()

Shutdown is called when the settings page closes.

Typical responsibilities:

  • Remove subclass
  • Release Direct2D resources
  • Release DirectWrite resources
  • Destroy temporary objects

Example:

BOOL BeamNGDriveSettings::Shutdown()
{
    if (m_hWnd && IsWindow(m_hWnd))
    {
        RemoveWindowSubclass(
            m_hWnd,
            BeamNGDriveSettings::SubclassWndProc,
            1);
    }

    SafeRelease(&m_pTextFormat);
    SafeRelease(&m_pRenderTarget);
    SafeRelease(&m_pDWriteFactory);
    SafeRelease(&m_pD2DFactory);

    return m_touched;
}

The settings plugin must release:

  • Direct2D resources
  • DirectWrite resources
  • Timers
  • Window subclass hooks
  • Dynamic allocations

The plugin must NOT:

  • Destroy the host window
  • Destroy the parent window
  • Destroy OpenSR-owned resources

Direct2D Rendering

Settings plugins are free to use:

  • Direct2D
  • DirectWrite
  • GDI
  • OpenGL
  • Vulkan
  • Custom software rendering

The BeamNG example uses Direct2D:

  • ID2D1Factory
  • IDWriteFactory
  • ID2D1HwndRenderTarget

Example:

D2D1CreateFactory(
    D2D1_FACTORY_TYPE_SINGLE_THREADED,
    &m_pD2DFactory);

DWriteCreateFactory(
    DWRITE_FACTORY_TYPE_SHARED,
    __uuidof(IDWriteFactory),
    reinterpret_cast<IUnknown**>(&m_pDWriteFactory));

Rendering is performed inside WM_PAINT.


Window Subclassing

The example subclasses the provided host child window:

SetWindowSubclass(
    m_hWnd,
    BeamNGDriveSettings::SubclassWndProc,
    1,
    (DWORD_PTR)this);

This allows the settings page to process:

  • Mouse input
  • Keyboard input
  • Painting
  • Resize events
  • Timers

Typical messages:

WM_PAINT
WM_LBUTTONDOWN
WM_MOUSEMOVE
WM_KEYDOWN
WM_CHAR
WM_SIZE
WM_TIMER

Example dispatcher:

LRESULT CALLBACK BeamNGDriveSettings::SubclassWndProc(
    HWND hWnd,
    UINT uMsg,
    WPARAM wParam,
    LPARAM lParam,
    UINT_PTR uIdSubclass,
    DWORD_PTR dwRefData)
{
    BeamNGDriveSettings* pThis =
        (BeamNGDriveSettings*)dwRefData;

    switch (uMsg)
    {
        case WM_PAINT:
            pThis->OnPaint();
            return 0;
    }

    return DefSubclassProc(
        hWnd,
        uMsg,
        wParam,
        lParam);
}

Managing Settings

The BeamNG example manages six settings:

<settings type="unique">
    <option name="outgauge ip" type="text">127.0.0.1</option>
    <option name="outgauge port" type="number">4441</option>

    <option name="motionsim ip" type="text">127.0.0.1</option>
    <option name="motionsim port" type="number">4444</option>

    <option name="outgauge allowed" type="bool">false</option>
    <option name="motionsim allowed" type="bool">true</option>
</settings>

The implementation therefore manages:

  • Two UDP IP addresses
  • Two UDP ports
  • Two boolean enable/disable toggles

The XML file is loaded during Initialize().


Loading XML Settings

Example:

tinyxml2::XMLDocument doc;

doc.LoadFile(pathObj.c_str());

tinyxml2::XMLElement* root =
    doc.FirstChildElement("settings");

Each <option> is parsed and copied into internal UI fields.


Saving XML Settings

The plugin validates all fields before saving.

Example:

for (auto& field : m_inputs)
{
    field.Validate();

    if (!field.isValid)
        allValid = false;
}

Then values are written back to XML:

opt->SetText(valStr.c_str());

Finally:

doc.SaveFile(pathObj.c_str());

Input Validation

The example contains:

  • IP validation
  • Partial IP validation during typing
  • Numeric clamping
  • Port range validation

Example:

TYPE_NUMBER, 1024, 65535

The settings UI visually highlights invalid fields using a red border.


Clipboard Support

The example implements:

  • CTRL+C
  • CTRL+V
  • CTRL+X
  • CTRL+A

Using standard Win32 clipboard APIs:

OpenClipboard()
GetClipboardData()
SetClipboardData()

Caret and Text Selection

The example uses DirectWrite hit testing:

pLayout->HitTestPoint(...)

This enables:

  • Mouse caret placement
  • Drag selection
  • Keyboard navigation
  • Accurate text measurement

The settings page therefore behaves similarly to a native text editor.


Toggle Controls

The example also implements custom Direct2D toggle switches.

Example managed values:

bool m_outgaugeAllowed = true;
bool m_motionSimAllowed = true;

Rendering is performed manually using:

  • Rounded rectangles
  • Ellipses
  • Custom colors

Toast Notifications

The example contains a lightweight toast system.

Example:

ShowToast(L"Settings Saved Successfully");

Used for:

  • Save confirmation
  • Validation errors
  • User feedback

Notifying the Parent Plugin

After saving settings, it is strongly recommended to notify the parent plugin window.

Example:

HWND hParent = GetParent(m_hWnd);

if (hParent && IsWindow(hParent))
{
    PostMessage(
        hParent,
        _PLUGIN_SETTINGS_CHANGED,
        0,
        0);
}

This allows the parent plugin to:

  • Reload configuration
  • Reopen sockets
  • Refresh internal state
  • Rebuild outputs
  • Apply runtime changes immediately

This mechanism is especially important for:

  • UDP plugins
  • Serial devices
  • HID devices
  • Motion systems
  • Network outputs

Important Notes

Settings plugins should:

  • Avoid heavy background processing
  • Avoid long blocking operations
  • Release all Direct2D resources correctly
  • Never assume permanent existence
  • Treat the provided HWND as host-owned

Recommended behavior:

  • Load settings during Initialize()
  • Save only on explicit user action
  • Notify parent after successful save
  • Keep UI responsive

Recommended Architecture

A good Settings Plugin architecture usually separates:

  • Rendering
  • Input handling
  • XML persistence
  • Validation
  • UI widgets

The BeamNG example demonstrates a clean custom-control approach using:

  • InputField
  • Toggle controls
  • Subclassed message handling
  • Manual Direct2D rendering

This architecture scales well for:

  • Complex plugin settings
  • Device calibration tools
  • Motion tuning panels
  • Complex network configuration pages
  • Advanced configuration panels
  • Device setup pages (shifter, pedals set calibration)
  • Real-time network configuration
  • Visual editors
  • Direct2D/DirectWrite custom interfaces
  • Hardware-specific utilities (Firmware update)