mxbmrp3

MXBMRP3 Architecture Guide

This document explains how the MXBMRP3 plugin works, from the ground up. It’s designed to help new contributors understand the codebase quickly.

What Is This Project?

MXBMRP3 is a HUD (Heads-Up Display) plugin for PiBoSo racing simulators (MX Bikes, GP Bikes, WRS, KRP). The plugin displays real-time racing information on screen: lap times, standings, speedometer, track map, and more.

The plugin is a Windows DLL (with .dlo extension) that each game loads at startup. The game calls our exported functions to send us data and request rendering instructions. A multi-game translation layer allows the same core code to work across all supported games.

Project Structure

mxbmrp3/
├── mxbmrp3/                    # Main plugin source code
│   ├── vendor/piboso/          # Game API definitions and exports
│   │   ├── mxb_api.h/.cpp      # MX Bikes API header and DLL exports
│   │   ├── gpb_api.h/.cpp      # GP Bikes API header and DLL exports
│   │   ├── krp_api.h/.cpp      # Kart Racing Pro API header and DLL exports
│   │   └── wrs_api.h           # WRS API header (stubbed)
│   ├── game/                   # Multi-game abstraction layer
│   │   ├── unified_types.h     # Game-agnostic data structures
│   │   ├── game_config.h       # Compile-time game selection
│   │   └── adapters/           # Per-game type converters
│   │       ├── mxbikes_adapter.h
│   │       ├── gpbikes_adapter.h
│   │       └── ...
│   ├── core/                   # Core infrastructure
│   │   ├── plugin_manager.*    # Main coordinator, routes API callbacks
│   │   ├── plugin_data.*       # Central game state cache
│   │   ├── hud_manager.*       # Owns and updates all HUDs
│   │   ├── input_manager.*     # Keyboard and mouse input
│   │   ├── xinput_reader.*     # XInput controller state and rumble
│   │   ├── rumble_profile_manager.* # Per-bike rumble profiles (JSON)
│   │   ├── settings_manager.*  # Save/load configuration (INI file)
│   │   ├── stats_manager.*     # Unified stats, personal bests, odometers (JSON)
│   │   ├── asset_manager.*     # Dynamic asset discovery (fonts, textures, icons)
│   │   ├── font_config.*       # User-configurable font categories
│   │   ├── color_config.*      # User-configurable color palette
│   │   ├── fmx_manager.*       # FMX trick detection and scoring
│   │   ├── fmx_types.h         # FMX data structures and enums
│   │   ├── http_server.*       # Embedded HTTP server with SSE streaming
│   │   ├── event_log_types.h   # Event log entry types and filter flags
│   │   ├── plugin_constants.h  # All named constants
│   │   └── plugin_utils.*      # Shared helper functions
│   ├── handlers/               # Event processors (one per API callback type)
│   │   ├── draw_handler.*      # Frame rendering and FPS tracking
│   │   ├── event_handler.*     # Event lifecycle (init/deinit)
│   │   ├── run_*_handler.*     # Player-only events
│   │   └── race_*_handler.*    # Multiplayer race events
│   ├── hud/                    # Display components
│   │   ├── base_hud.*          # Abstract base class for all HUDs
│   │   ├── *_hud.*             # Full HUDs (complex, configurable)
│   │   ├── *_widget.*          # Simple widgets (focused display)
│   │   └── settings/           # Settings UI components
│   │       ├── settings_hud.*      # Main settings menu
│   │       ├── settings_layout.*   # Layout helper context
│   │       └── settings_tab_*.cpp  # Individual tab renderers
│   └── diagnostics/            # Debugging tools
│       ├── logger.*            # Debug logging to file
│       └── timer.h             # Performance measurement
├── mxbmrp3_data/               # Runtime assets (discovered dynamically)
│   ├── fonts/                  # .fnt files (bitmap fonts)
│   ├── textures/               # .tga files (HUD backgrounds with variants)
│   ├── icons/                  # .tga files (rider icons for map/radar)
│   └── web/                    # Web overlay (HTML/CSS/JS served by HttpServer)
│       └── logos/              # Logo slideshow PNGs (auto-detected by /api/logos)
├── docs/                       # Documentation
├── replay_tool/                # Separate tool for replay analysis
└── mxbmrp3.sln                 # Visual Studio solution

The Big Picture

Here’s how data flows through the plugin:

┌─────────────────────────────────────────────────────────────────────────┐
│                    GAME ENGINE (MX Bikes / GP Bikes / etc.)             │
└─────────────────────────────────────────────────────────────────────────┘
                                    │
                                    ▼
                    ┌───────────────────────────────┐
                    │   mxb_api.cpp / gpb_api.cpp   │
                    │   (Per-Game DLL Exports)      │
                    │                               │
                    │  Startup(), Draw(), RunLap(), │
                    │  RaceEvent(), etc.            │
                    └───────────────────────────────┘
                                    │
                                    ▼
                    ┌───────────────────────────────┐
                    │      Game Adapters            │
                    │   (mxbikes_adapter.h, etc.)   │
                    │                               │
                    │  Convert game structs to      │
                    │  Unified:: types              │
                    └───────────────────────────────┘
                                    │
                                    ▼
                    ┌───────────────────────────────┐
                    │      PluginManager            │
                    │   (Main Coordinator)          │
                    │                               │
                    │  Receives Unified:: types,    │
                    │  routes to handlers           │
                    └───────────────────────────────┘
                                    │
              ┌─────────────────────┼─────────────────────┐
              ▼                     ▼                     ▼
     ┌─────────────────┐   ┌─────────────────┐   ┌─────────────────┐
     │    Handlers     │   │   DrawHandler   │   │  InputManager   │
     │                 │   │                 │   │                 │
     │ Process events, │   │ Triggers HUD    │   │ Tracks mouse,   │
     │ update data     │   │ render cycle    │   │ keyboard state  │
     └─────────────────┘   └─────────────────┘   └─────────────────┘
              │                     │
              ▼                     │
     ┌─────────────────┐            │
     │   PluginData    │◄───────────┘
     │  (State Cache)  │
     │                 │
     │ Stores all game │
     │ state, notifies │
     │ on changes      │
     └─────────────────┘
              │
              │ notifies
              ├──────────────────────────┐
              ▼                          ▼
     ┌─────────────────┐       ┌─────────────────┐
     │   HudManager    │       │   HttpServer    │
     │                 │       │                 │
     │ Owns all HUDs,  │       │ Builds JSON on  │
     │ marks dirty,    │       │ game thread,    │
     │ collects output │       │ streams via SSE │
     └─────────────────┘       └─────────────────┘
              │                          │
              ▼                          ▼
     ┌─────────────────┐       ┌─────────────────┐
     │      HUDs       │       │  Web Overlay    │
     │                 │       │  (Browser/OBS)  │
     │ Build quads &   │       │                 │
     │ strings for     │       │ Standings tower │
     │ rendering       │       │ Event log       │
     └─────────────────┘       │ Focus card      │
              │                └─────────────────┘
              │ returns render data
              ▼
┌─────────────────────────────────────────────────────────────────────────┐
│                    GAME ENGINE (MX Bikes / GP Bikes / etc.)             │
│                          (Renders our output)                           │
└─────────────────────────────────────────────────────────────────────────┘

Core Components

1. The Plugin API (vendor/piboso/*_api.*)

Each PiBoSo game defines a C API that plugins must implement. The APIs are nearly identical, with game-specific struct variations. Each game has its own API file:

Key exported functions (same across all games):

Function When Called Purpose
Startup() Game starts Initialize plugin, return telemetry rate
Shutdown() Game closes Clean up resources
EventInit() Track loaded Receive track/vehicle info
RunInit() Player goes on track Session begins
RunTelemetry() Every physics tick Receive vehicle telemetry (100Hz)
RunLap() Lap completed Receive lap time
Draw() Every frame Return quads/strings to render
RaceEvent() Online race starts Receive race info
RaceClassification() Continuously Receive standings updates

The API uses C structs to pass data. Each game’s structs have different field names and contents:

The adapter layer (game/adapters/*.h) converts these game-specific structs to unified types (Unified::TelemetryData, Unified::VehicleEventData, etc.) that the core plugin uses.

Exception barrier (vendor/piboso/api_guard.h): Every DLL export wraps its body in API_GUARD_CATCH("ExportName"). The host game doesn’t support C++ exceptions across the DLL boundary, so any uncaught throw from PluginManager downward would terminate the host process. The macro catches std::exception and ... at the boundary, logs via DEBUG_WARN_F, and returns a sensible fallback value. When adding a new export, follow the same pattern.

2. PluginManager (core/plugin_manager.*)

The central coordinator. It:

Note: PluginManager is game-agnostic - it never sees raw game API structs, only Unified::* types.

// Example: mxb_api.cpp converts and forwards to PluginManager:
// In mxb_api.cpp:
void RunLap(void* _pData, int _iDataSize) {
    auto* gameData = static_cast<SPluginsBikeLap_t*>(_pData);
    auto unified = Adapter::toPlayerLap(gameData);  // Convert to unified type
    PluginManager::getInstance().handleRunLap(&unified);
}

// PluginManager receives unified type:
void PluginManager::handleRunLap(Unified::PlayerLapData* psLapData) {
    RunLapHandler::getInstance().handleRunLap(psLapData);
}

3. PluginData (core/plugin_data.*)

The single source of truth for all game state. This singleton:

Key data structures:

// Example: Handler stores data, HUD reads it
// In handler:
PluginData::getInstance().updateSpeedometer(speed, gear, rpm, fuel);

// In HUD:
const BikeTelemetryData& data = PluginData::getInstance().getBikeTelemetry();
int speedMph = data.speedometer * MS_TO_MPH;

4. HudManager (core/hud_manager.*)

Owns and orchestrates all HUD instances. It:

5. Handlers (handlers/*)

Each handler processes a specific category of game events. They’re all singletons.

Run Handlers (player-only, single-player or your own bike):

Race Handlers (all riders in online races):

Other Handlers:

6. ProfileManager (core/profile_manager.*)

Manages HUD layout profiles for different game contexts:

Features:

7. RumbleProfileManager (core/rumble_profile_manager.*)

Manages per-bike rumble profiles stored in a JSON file:

Features:

8. StatsManager (core/stats_manager.*)

Unified stats system that tracks per-track/bike stats, global race stats, personal bests, and odometer data in a single JSON file ({save_path}/mxbmrp3/mxbmrp3_stats.json).

Per track+bike stats (TrackBikeStats):

Personal bests (StatsPersonalBestData):

Global stats (GlobalStats):

Per-bike odometers:

Session transients (not persisted):

Features:

9. FmxManager (core/fmx_manager.*)

Manages FMX (Freestyle Motocross) trick detection and scoring:

Data types are defined in fmx_types.h:

Display settings are split between global and per-profile:

10. HttpServer (core/http_server.*)

Embedded HTTP server that streams race data to browser-based overlays (OBS browser source):

Threading model:

SSE streaming (/api/events):

JSON data contract (raw data, no filtering — web UI filters client-side):

Static file serving:

Feature gating:

11. Event Log System (core/event_log_types.h, hud/event_log_hud.*)

Timestamped feed of race events, used by both the in-game HUD and web overlay:

Event types (defined in event_log_types.h):

Filter flags:

Storage:

12. CrashHandler (core/crash_handler.*)

Top-level Structured Exception Handling (SEH) filter for unhandled hardware faults: access violations, stack overflows, divide-by-zero, illegal instructions. These faults live below the C++ exception system: catch (...) doesn’t intercept them, so they would otherwise crash the host without leaving any diagnostic context behind. The CrashHandler complements the C++ exception barrier at the DLL boundary by handling the failure modes the C++ machinery can’t reach.

What it does:

Minidump contents:

Design constraints inside the filter:

What it does NOT do:

The HUD System

BaseHud (hud/base_hud.*)

Abstract base class that all HUDs inherit from. Provides:

Rendering Infrastructure:

Dirty Flag System (for performance):

Positioning & Scaling:

Visibility & Interaction:

Two Types of Display Components

Full HUDs (complex, highly configurable):

Overlays (full-screen, telemetry-driven):

Widgets (simple, focused):

HUD Lifecycle

  1. Creation: HudManager creates all HUDs in initialize()
  2. Configuration: SettingsManager loads saved positions/settings
  3. Data Update: PluginData changes -> HudManager notifies -> HUD marked dirty
  4. Render Cycle: Every frame:
    • update() called -> if dirty, calls rebuildRenderData()
    • getQuads() and getStrings() return render data
  5. Shutdown: Settings saved, HUDs destroyed

Creating a New HUD

Here’s the pattern for adding a new HUD:

// 1. Create header: hud/my_hud.h
class MyHud : public BaseHud {
public:
    MyHud();
    void update() override;
    bool handlesDataType(DataChangeType type) const override;

private:
    void rebuildRenderData() override;
    void rebuildLayout() override;
};

// 2. Implement: hud/my_hud.cpp
MyHud::MyHud() {
    setDraggable(true);
    setPosition(0.1f, 0.1f);  // Top-left area
    m_quads.reserve(1);       // Background
    m_strings.reserve(5);     // Text lines
    rebuildRenderData();
}

bool MyHud::handlesDataType(DataChangeType type) const {
    return type == DataChangeType::SessionData;  // What triggers updates?
}

void MyHud::update() {
    if (isDataDirty()) {
        rebuildRenderData();
        clearDataDirty();
    } else if (isLayoutDirty()) {
        rebuildLayout();
        clearLayoutDirty();
    }
}

void MyHud::rebuildRenderData() {
    m_quads.clear();
    m_strings.clear();

    auto dim = getScaledDimensions();

    // Add background
    addBackgroundQuad(START_X, START_Y, width, height);

    // Add text
    addString("Hello", x, y, Justify::LEFT, Fonts::ROBOTO_MONO,
              ColorConfig::getInstance().getPrimary(), dim.fontSize);

    setBounds(START_X, START_Y, START_X + width, START_Y + height);
}

// 3. Register in HudManager::initialize()
auto myHudPtr = std::make_unique<MyHud>();
m_pMyHud = myHudPtr.get();
registerHud(std::move(myHudPtr));

// 4. Add settings tab in SettingsHud (optional)
// 5. Add save/load in SettingsManager (optional)

Rendering System

The game engine handles actual rendering. We just provide instructions.

Quads (SPluginQuad_t)

Rectangles with 4 corners. Used for:

struct SPluginQuad_t {
    float m_aafPos[4][2];    // 4 corners, each with (x, y)
    int m_iSprite;           // 0 = solid color, 1+ = sprite index
    unsigned long m_ulColor; // ABGR format
};

Strings (SPluginString_t)

Text to render:

struct SPluginString_t {
    char m_szString[100];    // Text content
    float m_afPos[2];        // Position (x, y)
    int m_iFont;             // Font index (1-based)
    float m_fSize;           // Font size
    int m_iJustify;          // 0=left, 1=center, 2=right
    unsigned long m_ulColor; // ABGR format
};

Coordinate System

Color Format

Colors use ABGR (Alpha-Blue-Green-Red) format:

// Helper in plugin_utils.h
constexpr unsigned long makeColor(uint8_t r, uint8_t g, uint8_t b, uint8_t a = 255) {
    return (static_cast<unsigned long>(a) << 24) |
           (static_cast<unsigned long>(b) << 16) |
           (static_cast<unsigned long>(g) << 8) |
           static_cast<unsigned long>(r);
}

Settings & Persistence

SettingsManager (core/settings_manager.*)

Saves/loads HUD configuration to INI file format:

[StandingsHud]
visible=1
showTitle=1
backgroundOpacity=0.8
scale=1.0
offsetX=0.05
offsetY=0.1
displayRowCount=20

[SpeedWidget]
visible=1
scale=1.0
offsetX=0.4125
offsetY=0.6882

Settings are saved:

Per-profile vs global sections

Settings fall into two kinds, persisted differently:

One serialization, three consumers (save / load / reset)

Both save and load route global sections through a single pair of functions, so they can’t drift as settings are added:

Reset = replay the factory snapshot through the same applier. At startup (before the user’s INI is parsed, while every singleton holds its constructor defaults), captureFactoryDefaults() captures two snapshots:

Why two HUD caches? m_hudDefaults is the sparse-save baseline and has the user’s hand-edited base [HudName] keys folded in at load (so they round-trip). That makes it the wrong source for “reset to defaults” — it would restore the file’s baseline (or, after a plugin upgrade, an old version’s default) instead of this build’s. m_hudFactoryDefaults is captured before any folding, so reset always means this build’s defaults. Don’t collapse the two. (Migration note: legacy keys are read from their old section as a fallback and migrate to the new section on next save — e.g. update keys [General]/[Advanced][Updates], units [General][Display].)

SettingsHud (hud/settings_hud.*)

In-game settings menu (toggle with ~ key). Allows users to:

Settings Layout System

The settings UI uses a helper class (SettingsLayoutContext) for consistent layout across all tabs:

mxbmrp3/hud/settings/
├── settings_hud.h/.cpp          # Main SettingsHud class
├── settings_layout.h/.cpp       # SettingsLayoutContext helper
├── settings_tab_general.cpp     # General preferences & profiles
├── settings_tab_appearance.cpp  # Fonts & colors
├── settings_tab_standings.cpp   # Standings HUD options
├── settings_tab_map.cpp         # Track map options
├── settings_tab_radar.cpp       # Radar options
├── settings_tab_*.cpp           # Other tab implementations
└── ...

SettingsLayoutContext provides standardized control rendering:

Method Purpose
addSectionHeader(title) Section divider with label
addToggleControl(label, value, ...) On/Off toggle with < value > arrows
addCycleControl(label, value, ...) Multi-value cycle control
addStandardHudControls(hud) Common controls (Visible, Title, Texture, Opacity, Scale)
addWidgetRow(name, hud, ...) Table row for Widgets tab
addSpacing(factor) Vertical spacing

Control Width Standardization: All controls use VALUE_WIDTH = 10 to ensure vertical alignment - users can toggle settings by moving the mouse vertically without horizontal adjustment.

Tooltip System

Tooltips provide contextual help when hovering over controls. Strings are compiled into the plugin (no external file).

TooltipManager (core/tooltip_manager.h) is a header-only singleton that:

Tooltips are rendered when hovering over:

The row-wide tooltip regions are created by passing a tooltipId parameter to control helpers like addToggleControl() and addCycleControl().

Asset Management

The plugin uses a dynamic asset discovery system that scans subdirectories at startup.

AssetManager (core/asset_manager.*)

Discovers and registers assets from plugins/mxbmrp3_data/ subdirectories:

Directory File Type Purpose
fonts/ .fnt Bitmap fonts (game engine format)
textures/ .tga HUD background textures with variants (e.g., standings_hud_1.tga)
icons/ .tga Rider icons for map/radar display

Texture Variants: Textures can have numbered variants (e.g., standings_hud_1.tga, standings_hud_2.tga). Users can cycle through variants in settings.

Icon Discovery: Icons are discovered alphabetically. Use AssetManager::getIconSpriteIndex(filename) to get the sprite index for a specific icon by filename. Settings store icon filenames for persistence.

User Asset Overrides: Users can override bundled assets by placing custom files in the save directory:

FontConfig (core/font_config.*)

Maps semantic font categories to user-selected fonts:

Category Default Font Usage
TITLE EnterSansman-Italic HUD titles
NORMAL RobotoMono-Regular Standard text
STRONG RobotoMono-Bold Emphasized text
MARKER FuzzyBubbles-Regular Handwritten style
SMALL Tiny5-Regular Map/radar labels

Access via PluginConstants::Fonts::getTitle(), getNormal(), etc.

ColorConfig (core/color_config.*)

User-configurable color palette with semantic slots:

Input Handling

InputManager (core/input_manager.*)

Polls Windows for input state each frame:

Drag-and-Drop

HUDs can be dragged with right-click:

  1. handleMouseInput() detects click within bounds
  2. Saves initial position as drag origin
  3. Updates offset while button held
  4. validatePosition() keeps HUD on screen

Auto-Update System

The plugin includes an optional auto-update system that checks for new versions on GitHub.

UpdateChecker (core/update_checker.*)

Checks GitHub releases API for newer versions:

UpdateDownloader (core/update_downloader.*)

Downloads and installs plugin updates:

Update Flow:

  1. UpdateChecker detects new version → status = UPDATE_AVAILABLE
  2. User clicks “Install” in settings → UpdateDownloader starts
  3. Download → Verify → Backup existing → Extract → Install
  4. Status = READY → Restart required

Vendor Dependency: Uses vendor/miniz/ for ZIP extraction (public domain, single-file library).

Key Design Patterns

Singletons

Most core components are singletons:

class PluginData {
public:
    static PluginData& getInstance() {
        static PluginData instance;
        return instance;
    }
private:
    PluginData() = default;
};

Why? The plugin API gives us one entry point. The game calls our exported functions - we don’t create multiple instances.

Dirty Flag Pattern

Instead of rebuilding every frame:

  1. Data changes -> mark dirty
  2. Next render -> check dirty flag
  3. If dirty -> rebuild, clear flag
  4. If clean -> reuse cached data

This is crucial for performance since Draw() is called every frame.

Standard Pattern (Most HUDs)

Use processDirtyFlags() for HUDs that rely on DataChangeType notifications:

void MyHud::update() {
    processDirtyFlags();  // Handles isDataDirty/isLayoutDirty automatically
}

Self-Detection Pattern (Polling Widgets)

Some widgets display values that don’t trigger DataChangeType notifications (e.g., session time updates continuously but doesn’t fire SessionData). These widgets must poll PluginData and detect changes themselves:

void TimeWidget::update() {
    // 1. Poll fresh data
    int currentTime = pluginData.getSessionTime();

    // 2. Compare to cached "last rendered" value
    if (currentSeconds != m_cachedSeconds) {
        setDataDirty();  // Self-mark dirty
    }

    // 3. Process dirty flags
    if (isDataDirty()) {
        rebuildRenderData();
        m_cachedSeconds = currentSeconds;  // Update cache AFTER rebuild
        clearDataDirty();
        clearLayoutDirty();
    }
    else if (isLayoutDirty()) {
        rebuildLayout();
        clearLayoutDirty();
    }
}

Why can’t these use processDirtyFlags()? The cache update must happen after rebuildRenderData() using local variables calculated before the dirty check. The onAfterDataRebuild() hook exists for simpler cases, but these widgets use values computed at the top of update().

Hybrid Pattern (Change Detection Before, Standard After)

Some HUDs do change detection but don’t need post-rebuild caching:

void NoticesHud::update() {
    // Change detection - updates member state and marks dirty
    if (wrongWay != m_bIsWrongWay) {
        m_bIsWrongWay = wrongWay;  // State updated BEFORE dirty check
        setDataDirty();
    }

    processDirtyFlags();  // Can use standard helper
}

When to Use Which Pattern

Pattern Use When Examples
processDirtyFlags() HUD relies on DataChangeType notifications StandingsHud, IdealLapHud, MapHud
Hybrid Polls data but caches state BEFORE dirty check NoticesHud, GapBarHud
Self-Detection Needs to cache “last rendered value” AFTER rebuild TimeWidget, PositionWidget, LapWidget

Handler Singleton Macro

All handlers use this pattern:

// In header
class MyHandler {
public:
    static MyHandler& getInstance();
    void handleSomething(Data* data);
};

// In .cpp
DEFINE_HANDLER_SINGLETON(MyHandler)

void MyHandler::handleSomething(Data* data) {
    HANDLER_NULL_CHECK(data);
    // Process data...
}

Data Change Notifications

// PluginData notifies HudManager directly (no observer pattern overhead)
void PluginData::notifyHudManager(DataChangeType changeType) {
    HudManager::getInstance().onDataChanged(changeType);
}

// HudManager marks relevant HUDs as dirty
void HudManager::onDataChanged(DataChangeType changeType) {
    for (auto& hud : m_huds) {
        if (hud->handlesDataType(changeType)) {
            hud->setDataDirty();
        }
    }
}

Constants & Configuration

All magic numbers live in plugin_constants.h:

namespace PluginConstants {
    namespace FontSizes {
        constexpr float NORMAL = 0.0200f;
        constexpr float LARGE = 0.0300f;
    }

    // Colors are configurable via ColorConfig singleton
    // ColorConfig::getInstance().getPrimary(), getSecondary(), etc.

    namespace Session {
        constexpr int RACE_1 = 6;
        constexpr int RACE_2 = 7;
    }
}

Debugging

Debug Logging

DEBUG_INFO("Plugin initialized");
DEBUG_INFO_F("Received %d riders", count);
DEBUG_WARN("Something unexpected");

Logs go to {save_path}/mxbmrp3/mxbmrp3.log

Performance Timing

SCOPED_TIMER_THRESHOLD("MyFunction", 100);  // Logs if > 100us

Build Configurations

Common Gotchas

  1. Don’t cache game data in HUDs for rendering - Always read fresh from PluginData when building render data. HUDs only cache formatted render data (m_quads, m_strings). Exception: Widgets that poll continuously-changing values (like session time) may cache “last rendered value” for change detection - see “Self-Detection Pattern” in Dirty Flag Pattern section.

  2. 0-based vs 1-based indexing - API uses 0-based lap numbers, UI shows 1-based. Check the API header comments.

  3. C++ exceptions must not cross the DLL boundary - The host game terminates if a C++ exception escapes a DLL export. Every export in vendor/piboso/*_api.cpp wraps its body in API_GUARD_CATCH (see vendor/piboso/api_guard.h). When adding a new export, follow the same pattern. Similarly, every std::thread body (HttpServer, UpdateChecker, UpdateDownloader, DiscordManager, RecordsHud::performFetch) wraps itself in a top-level try/catch, since an uncaught throw in a std::thread calls std::terminate(). For hardware faults that don’t go through the C++ exception system (null deref, OOB, divide-by-zero), the SEH filter in core/crash_handler.* writes a minidump for diagnosis but doesn’t prevent the crash.

  4. Game thread vs background threads - All PiBoSo API callbacks (Draw, RunTelemetry, etc.) run on the game thread. PluginData, HudManager, SettingsManager, and the various other managers are game-thread-only and not thread-safe. Background threads exist for I/O (HttpServer, DiscordManager, UpdateChecker, UpdateDownloader, RecordsHud’s fetch thread) and must NOT touch those singletons directly. They consume snapshots built on the game thread instead (see HttpServer::buildJsonSnapshot, DiscordManager::updateSnapshot). The Logger has its own internal mutex and is safe to call from any thread.

  5. Sprite indices are 1-based - Index 0 means “solid color fill”, not “first sprite”.

  6. Font indices are 1-based - Font index 0 is invalid.

  7. Icon ordering is alphabetical - Icons in mxbmrp3_data/icons/ are discovered alphabetically. Use filename-based lookups via AssetManager for persistence; icon additions/removals won’t break saved settings.

Multi-Game Support

The plugin supports multiple PiBoSo racing games from a single codebase using compile-time game selection.

Supported Games

Game Mod ID Vehicle Type Splits Unique Features
MX Bikes mxbikes Bike (2 wheels) 2 Straight Rhythm
GP Bikes gpbikes Bike (2 wheels) 3 ECU/TC/AW, Tread temps
WRS wrs Car (4-6 wheels) 2 Rolling start, Turbo, Handbrake
KRP krp Kart (4 wheels) 2 Session series, Qualify heats

Build Configurations

Each game produces its own DLL:

Configuration Output Install Location
MXB-Release mxbmrp3.dlo MX Bikes plugins/
GPB-Release mxbmrp3_gpb.dlo GP Bikes plugins/
KRP-Release mxbmrp3_krp.dlo Kart Racing Pro plugins/
(future) mxbmrp3_wrs.dlo WRS plugins/

The Visual Studio project uses conditional compilation to include only the relevant API file:

<!-- MX Bikes API - excluded from GP Bikes builds -->
<ClCompile Include="vendor\piboso\mxb_api.cpp">
  <ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='GPB-Debug|x64'">true</ExcludedFromBuild>
  <ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='GPB-Release|x64'">true</ExcludedFromBuild>
</ClCompile>

Feature Flags

Compile-Time (game/game_config.h):

#if GAME_HAS_RACE_SPEED
void handleRaceSpeed(const Unified::RaceSpeedData* data);
#endif

Runtime (adapter constants):

if constexpr (Game::Adapter::HAS_RACE_SPEED) {
    // Show speed trap data
}

Key feature flags:

Variable Split Count

Games have different numbers of timing splits. Unified types use a dynamic count:

struct RaceLapData {
    int splits[MAX_SPLITS];  // MAX_SPLITS = 3
    int splitCount;          // Actual count (2 for MXB, 3 for GPB)
};

Updating Vendor APIs

When PiBoSo releases a new API version:

  1. Update the vendor header (mxb_api.h, gpb_api.h, etc.)
  2. Update the adapter to handle new/changed fields
  3. Update the API cpp if new callbacks are added
  4. Update unified types if new data needs to be shared

The adapter layer isolates changes - core HUDs don’t need modification for most API updates.

API Differences

Identical across all games:

Per-game variations:

Quick Reference: File Locations

What Where
API entry points (MX Bikes) vendor/piboso/mxb_api.cpp
API entry points (GP Bikes) vendor/piboso/gpb_api.cpp
Game adapters game/adapters/*_adapter.h
Unified types game/unified_types.h
Game config game/game_config.h
Central state core/plugin_data.cpp
HUD base class hud/base_hud.cpp
All constants core/plugin_constants.h
Asset manager core/asset_manager.cpp
Font configuration core/font_config.cpp
Color configuration core/color_config.cpp
Update checker core/update_checker.cpp
Update downloader core/update_downloader.cpp
XInput / Rumble core/xinput_reader.cpp
Stats manager core/stats_manager.cpp
Rumble profiles manager core/rumble_profile_manager.cpp
FMX trick detection core/fmx_manager.cpp
FMX types core/fmx_types.h
HTTP server core/http_server.cpp
Event log types/flags core/event_log_types.h
Event log HUD hud/event_log_hud.cpp
Web overlay (HTML/CSS/JS) mxbmrp3_data/web/
Settings UI hud/settings/settings_hud.cpp
Settings layout helpers hud/settings/settings_layout.cpp
Settings tabs hud/settings/settings_tab_*.cpp
Tooltip definitions core/tooltip_manager.h (embedded)
Tooltip manager core/tooltip_manager.h
Settings file {save_path}/mxbmrp3/mxbmrp3_settings.ini
Stats file {save_path}/mxbmrp3/mxbmrp3_stats.json
Rumble profiles file {save_path}/mxbmrp3/rumble_profiles.json
Log file {save_path}/mxbmrp3/mxbmrp3.log
Build output (MX Bikes) build/MXB-Release/mxbmrp3.dlo
Build output (GP Bikes) build/GPB-Release/gpbmrp3.dlo
Runtime assets {game_path}/plugins/mxbmrp3_data/{fonts,textures,icons}/
User asset overrides {save_path}/mxbmrp3/{fonts,textures,icons}/

Quick Reference: Adding Features

Task Steps
Add new HUD Create class, inherit BaseHud, register in HudManager
Add new data type Add struct to PluginData, add DataChangeType enum
Add new per-HUD setting Add field + capture/apply in SettingsManager’s per-HUD cache; reset is automatic (snapshot)
Add new global setting Add to writeGlobalSettings() and applyGlobalLine() (one emit + one apply); reset is automatic
Add settings tab Create settings_tab_*.cpp, add tab enum, register in SettingsHud
Add tooltip Add entry to the maps in core/tooltip_manager.h, pass tooltipId to control helper
Add keyboard shortcut Handle in HudManager::processKeyboardInput()
Add new handler Create handler class, route from PluginManager
Add new font Place .fnt file in mxbmrp3_data/fonts/ (auto-discovered)
Add new texture Place .tga file in mxbmrp3_data/textures/ (auto-discovered)
Add new icon Place .tga file in mxbmrp3_data/icons/ (auto-discovered, alphabetical order)
Add new event log type Add enum to event_log_types.h, add flag, update eventLogTypeToFlag(), add to handlers
Add field to web overlay Add to buildJsonSnapshot() in http_server.cpp, consume in app.js
Add game-specific feature Add to unified_types.h, update adapters, add feature flag to game_config.h
Support new game Create adapter in game/adapters/, add API file in vendor/piboso/, update game_config.h