mxbmrp3

AI Development Context for MXBMRP3

Read This First

This is a racing simulator HUD plugin for PiBoSo racing games (MX Bikes, GP Bikes, WRS, KRP). It’s a DLL plugin written in C++ using each game’s proprietary API, with a shared core that works across all supported games.

For deep technical details: See ARCHITECTURE.md (detailed documentation with mermaid diagrams, component descriptions, dependency graphs, multi-game architecture). This file is a quick-start guide.

Quick Architecture

Game Engine (MX Bikes / GP Bikes / WRS / KRP)
    ↓ (callbacks via plugin API)
mxb_api.cpp / gpb_api.cpp (per-game DLL exports)
    ↓ (converts to unified types via adapters)
PluginManager (receives unified types only)
    ↓
PluginData (singleton - caches all game state)
    ↓ (notifies on data changes)
HudManager (singleton - owns all HUD instances)
    ↓
Individual HUDs (IdealLap, Standings, Map, etc.)
    ↓ (build render primitives)
Game Engine (renders quads/strings)

PluginData ──(notifies on data changes)──→ HttpServer
    ↓ (builds JSON snapshot on game thread)
SSE stream → Web Overlay (browser/OBS)

HudManager ──(2nd frame via collectSurface, if enabled)──→ CompanionWindow
    ↓ (submit quads/strings; own window thread)
hud_sw_renderer → standalone OS window (2nd monitor)

Key Singletons:

Multi-Game Support

The plugin supports multiple PiBoSo games from a single codebase:

Game Config Output Status
MX Bikes MXB-Release mxbmrp3.dlo ✅ Full support
GP Bikes GPB-Release mxbmrp3_gpb.dlo ✅ Core features
Kart Racing Pro KRP-Release mxbmrp3_krp.dlo ✅ Core features (no FMX)
WRS - wrsmrp3.dlo ⏳ Stubbed

Translation Layer:

Build & Test

Full build/test details, prerequisites, and both build tracks live in DEVELOPMENT.md. The essentials for working here:

⚠️ Build Environment:

⚠️ IMPORTANT - Shell Commands:

Testing Discipline

Tests are not optional scaffolding — this project has a real, CI-gated suite that runs on Linux with no game (see DEVELOPMENT.md). When you change behavior, change a test:

Where the test goes (pick the fastest one that can exercise the change):

What you changed Where the test goes
A pure helper (formatting, color, parsing, header-only math) tests/unit/ unit test (doctest) — compiles the real header, ~1s
Standings / gaps / penalties / session logic / anything in the JSON snapshot add/extend a doctest in tests/integration/tests/ using PluginHost + checkStandings (drives real callbacks under Wine, asserts /api/state) — see TESTING.md
A settings / persistence change tests/integration/run_persist_test.sh (load→save round-trip)
A new DLL-boundary callback or array-size/count handling tests/integration/callback_fuzzer.cpp
Config parsing / a new INI or JSON field tests/integration/run_fuzz.sh corpus
A hot-path change (Draw / telemetry / rebuild) confirm tests/integration/run_perf.sh didn’t regress
Installer / packaging (packaging/mxbmrp3.nsi) tests/integration/run_installer_test.sh (makensis + Wine: asserts install/uninstall/registry/data-wipe outcomes)
Web overlay rendering (mxbmrp3_data/web/ — js/overlay-*.js/style.css/index.html) add/extend a Playwright test in tests/web/tests/ driving ?demo (asserts the rendered DOM) — see TESTING.md

These run headless — most via mingw + Wine, the web-overlay tests via Node + Playwright. Manual in-game testing on Windows stays the final check for rendering/input, but it does not excuse skipping an automated test when the logic is testable headless.

Important Patterns & Constraints

Performance Target: 240fps

The plugin must run efficiently at 240fps (4.17ms frame budget). Many competitive players use high refresh rate monitors. Avoid per-frame allocations, unnecessary string operations, and complex calculations in hot paths like Draw() and RunTelemetry().

DO:

DON’T:

Maintenance Invariants (touch X → also update Y)

Regression traps where changing one thing silently rots another. The first five prevent future bugs, not just document past ones. ARCHITECTURE.md has the bug each one prevents.

Design Decisions (Don’t “Fix” These)

Singletons Everywhere Required by plugin API - we get one global entry point, everything branches from there.

Lambdas in settings_hud.cpp rebuildRenderData() Intentional - they capture local layout state. Alternatives were worse (passing 8+ parameters).

Public member variables on HUDs (e.g., m_enabledRows) These are configuration data, not encapsulated state. SettingsHud needs direct access.

HUDs don’t cache raw game data HUDs pull fresh from PluginData on rebuild - they only cache formatted render data (m_displayEntries, m_quads, m_strings). This enforces PluginData as single source of truth and prevents synchronization issues.

Settings reset reuses save/load serialization (don’t add a third list) “Reset to defaults” replays a startup snapshot through the same applier loadSettings() uses, never a hand-maintained list of per-setting resets. Two snapshots back this, and they are intentionally separate:

m_hudDefaults (sparse-save baseline, with base-section edits folded in) is not a clean factory snapshot — don’t “simplify” reset by pointing it at m_hudDefaults or by merging the two caches; that reintroduces stale-default-on-reset bugs (e.g. an upgraded HUD default not taking effect). A new setting gets reset coverage for free as long as it’s wired into save/load. See ARCHITECTURE.md “Settings & Persistence”.

Widget vs HUD Distinction Widgets (TimeWidget, PositionWidget, LapWidget, SpeedWidget, GearWidget, ClockWidget, SpeedoWidget, TachoWidget, BarsWidget, FuelWidget, LeanWidget, GForceWidget, TyreTempWidget, EcuWidget, GamepadWidget, CompassWidget, VersionWidget, SettingsButtonWidget, BenchmarkWidget, DirectorWidget, … — grep _widget.h for the current set) are simplified HUD components with:

Full HUDs (StandingsHud, LapLogHud, PitboardHud, TimingHud, NoticesHud, StatsHud, etc.) have:

Helmet Overlay (HelmetOverlayHud) Full-screen immersion overlay — neither a widget nor a typical HUD:

Companion Window (CompanionWindow + hud_sw_renderer) Standalone OS window that renders the HUD on a second monitor — not a network mirror and unrelated to the web overlay:

Handler-to-API Event Mapping Each handler corresponds to game API callback(s), but receives unified types:

Layered automated tests + manual in-game There is a real, CI-gated test suite, all runnable on Linux with no game engine. TESTING.md is the guide (layers, harness, philosophy, how to add a test); the short version: pure-logic unit tests (tests/unit/, doctest); the integration layer (tests/integration/tests/, doctest) that cross-compiles the whole plugin, loads it under Wine, drives the real callbacks via PluginHost, and asserts the plugin’s computed state — read via snapshot() (built directly, no HTTP server) or typed MXBMRP3_Test_* hooks for internal state, plus real-data golden masters that replay actual in-game callback captures (the in-plugin recorder [Recorder] enabled=1 → tape → replayTape); Playwright web-overlay tests (tests/web/); and specialized runners (persistence, fuzz, perf). They run in CI on every push. Manual in-game testing on Windows remains the final check for rendering, input, and game-specific behavior the headless build can’t exercise — it complements the automated tests, it isn’t replaced by them.

Logger has an internal mutex Logger::log() is called from the game thread and from at least five background threads (HttpServer, UpdateChecker, UpdateDownloader, DiscordManager, RecordsHud). The mutex serializes concurrent writes so log lines don’t interleave. Don’t remove it. The SEH crash filter deliberately doesn’t call Logger to avoid deadlocking on this mutex.

Common Tasks

Adding a New HUD

  1. Create class inheriting from BaseHud (.h and .cpp files in mxbmrp3/hud/)
  2. Add files to Visual Studio project:
    • mxbmrp3/mxbmrp3.vcxproj - Add <ClInclude> for .h and <ClCompile> for .cpp
    • mxbmrp3/mxbmrp3.vcxproj.filters - Add filter entries to place files in Header Files\hud and Source Files\hud
    • Without these entries, the build will fail with linker errors (LNK2019 unresolved externals)
  3. Implement rebuildRenderData() - builds vectors of quads/strings
  4. Register in HudManager constructor (add pointer, getter, initialize in initialize(), null in clear())
  5. Add tab in SettingsHud for configuration: a Tab enum value (settings_hud.h), a renderTab<Name>/optional handleClickTab<Name> in a new settings/settings_tab_*.cpp, and one row in the per-tab descriptor registry s_tabRegistry (settings_hud.cpp) — the row drives display order, name, tooltip id, the tab-list checkbox, game gating, render/click routing, and the per-tab reset (no switches to edit). Plain numeric steppers (“value = applyAccelerated*; mark dirty”) should use ctx.addSteppedControl + a SteppedControl descriptor instead of new ClickRegion::Type enum pairs.
  6. Add save/load via the per-HUD serializer registry (settings_hud_registry.{h,cpp}): write a cap_<Name> and app_<Name> (private static SettingsManager members — declared in settings_hud_registry_decls.inc, defined in settings_hud_registry.cpp) and add one row { "<Name>", &SettingsManager::cap_<Name>, &SettingsManager::app_<Name> } to hudSectionRegistry(). That single row registers the HUD for capture, apply, and on-disk serialization at once — captureToCache, applyProfile, and serializeSettings all iterate the registry, so there is no longer a separate hudOrder list to forget (the FriendsHud “third hardcoded list” trap is gone by construction). Reset stays automatic via the factory snapshots. For a global single-value setting, use writeGlobalSettings()/applyGlobalLine() in settings_manager_global.cpp instead. Game-gated HUDs wrap their fn decls (in the .inc), their definitions, and their registry row in the same #if GAME_HAS_*settings_manager.h includes game_config.h before the .inc so the guards resolve.
    • The functions are SettingsManager members so they inherit its friend-ship with the HUD classes (the bodies read/write private HUD members); hudSectionRegistry() is a friend so it can take their addresses.
    • tests/integration/tests/settings_sections_test.cpp remains a belt-and-suspenders CI check that every section captureToCache() produces is actually serialized (via MXBMRP3_Test_CapturedSections).

Debugging Rendering Issues

Working with Game API Events

When implementing event handlers or debugging timing/lap data:

Working with the Web Overlay

The embedded HTTP server (core/http_server.cpp) streams race data to browser-based overlays via Server-Sent Events (SSE):

Adding Support for a New Game Feature

  1. Add field to appropriate Unified:: struct in game/unified_types.h
  2. Add conversion in each adapter (game/adapters/*_adapter.h)
  3. Add feature flag to game/game_config.h if game-specific
  4. Update handlers/HUDs to use the new field

Disabling a Feature Per-Game

When an entire feature (HUD, manager, integration) doesn’t apply to one or more games — e.g. FMX freestyle tricks on karts, Discord Rich Presence on non-MXB, the records provider on non-MXB:

  1. Add a GAME_HAS_X flag to game/game_config.h. Examples already in the file: GAME_HAS_DISCORD, GAME_HAS_HTTP_SERVER, GAME_HAS_FMX, GAME_HAS_RECORDS_PROVIDER. Pattern:
    #if defined(GAME_MXBIKES) || defined(GAME_GPBIKES)
        #define GAME_HAS_FMX 1
    #else
        #define GAME_HAS_FMX 0
    #endif
    
  2. Gate the HUD registration in HudManager::initialize(). Leave the member pointer as nullptr; existing null-checks downstream (if (m_pFmxHud)) will fall through silently.
  3. Gate the settings tab in SettingsHud — set gameGated = true on the tab’s row in s_tabRegistry (settings_hud.cpp). isTabAvailable() then skips the tab whenever its hud getter returns the nullptr you set up in step 2 — no #if block needed (runtime null-check pattern, like TAB_RECORDS/TAB_FMX/TAB_FRIENDS).
  4. Gate the hotkey row in settings_tab_hotkeys.cpp. The hotkey action itself can stay in the enum (the handler in HudManager::processHotkeys is already null-safe), but the row should be hidden so users don’t see a binding that does nothing.
  5. Gate handler entry points that feed the disabled manager (run_telemetry_handler.cpp, race_session_handler.cpp, etc.). Skip the singleton calls entirely so the binary doesn’t pull them in.
  6. Gate SettingsManager save/load if the disabled HUD has its own profile section. Crucial when HudManager::getXxxHud() returns a Hud& with assert(m_pXxxHud) — calling it with a null member crashes in debug and null-derefs in release.
  7. Gate the installer (packaging/mxbmrp3.nsi) if the feature has supporting data files (e.g. web/ for HTTP server) so they don’t ship to a build that can’t use them.

If a .cpp file’s GAME_HAS_X reference is in a file that doesn’t transitively include game_config.h, add #include "../../game/game_config.h" (path from the file). The handlers’ plugin_data.h already pulls it in; hud_manager.h pulls it in; isolated tab files like settings_tab_hotkeys.cpp may need the explicit include.

Reference implementations to copy from: FMX (commit deba67f), Discord (GAME_HAS_DISCORD), Records provider (GAME_HAS_RECORDS_PROVIDER).

Files You’ll Likely Need

Core:

Multi-Game Layer:

HUD Base:

Example HUDs:

Web Overlay (mxbmrp3_data/web/ — organized into subfolders: js/ overlay scripts, fonts/ the .ttf web fonts, icons/ the .svg chip/gear icons, logos/ the slideshow PNGs. index.html, sw.js, style.css, and custom.css/custom-sample.css stay at the root — the service worker scope must be /, index.html is served at /, and custom.css has a dedicated no-cache handler. When adding/renaming/moving a served asset, update its path in index.html/style.css and PRECACHE_URLS in sw.js, the installer’s per-folder File blocks in packaging/mxbmrp3.nsi (three game sections), and AssetManager::syncUserAssets if it’s a new subfolder):

Settings:

Testing (see TESTING.md — the canonical guide):

Callback-tape recorder (in-plugin, replaces the old standalone mxbmrp3_record.dlo):

Dev tools (all namespaced mxbmrp3_*; the first two are projects in mxbmrp3.sln):

Regenerating a shipped font

The shipped bitmap fonts are generated from the source .ttf (in mxbmrp3_data/web/fonts/) with mxbmrp3_fontgen, normalized so every font renders numbers at a consistent size/width/position (normalize = 1: cell 135, digit-advance 0.489, centered). The cell height is the atlas resolution, not the on-screen size (the renderer scales by size × screenH / cellH), so the 135px cell keeps text crisp when a HUD draws it larger than the cell (high-DPI, or scaled-up widgets like the speedo); the atlas auto-grows to 2048² to hold it. RobotoMono-Regular.fnt is the reference, regenerated at 135px via test.sh’s cfg. To rebuild them all: tools/mxbmrp3_fontgen/regen_shipped.sh. To add/replace one: drop a .ttf in mxbmrp3_data/web/fonts/, run tools/mxbmrp3_fontgen/mxbmrp3_fontgen <font>.ttf mxbmrp3_data/fonts/<font>.fnt, commit the .fnt.


Git & Development Workflow

Commit Message Conventions

Branch Naming

Version Management

Peer Reviews

Development Style