# How Lighthouse’s Custom Controller Mapping System Works: A Deep Dive into the Mapper Architecture

> Explore Lighthouse's custom controller mapping system, powered by a state-machine driven binding system that wraps the SDL gamepad API and saves mappings in gamecontrollerdbtxt format.

- Repository: [Harbour Masters/Lighthouse](https://github.com/HarbourMasters/Lighthouse)
- Tags: deep-dive
- Published: 2026-08-04

---

**Lighthouse implements a user-editable controller mapping layer through the `Mapper` namespace, which wraps SDL's gamepad API with a state-machine-driven binding system that persists mappings in standard [`gamecontrollerdb.txt`](https://github.com/HarbourMasters/Lighthouse/blob/main/gamecontrollerdb.txt) format.**

The HarbourMasters/Lighthouse project provides flexible controller support for classic game engines by letting players create custom mappings without editing configuration files by hand. This article examines the `Mapper` namespace implementation in [`src/port/Controller/Mapper.h`](https://github.com/HarbourMasters/Lighthouse/blob/main/src/port/Controller/Mapper.h) and [`Mapper.cpp`](https://github.com/HarbourMasters/Lighthouse/blob/main/Mapper.cpp), tracing how raw SDL joystick input becomes persistent, reloadable controller configurations.

## Core Architecture: The Mapper Namespace

The custom controller mapping system centers on three collaborating components:

- **`MappingSession`** – A state machine that guides users through a predefined binding sequence
- **`MapperWindow`** – The ImGui-based UI that displays prompts and visual feedback
- **Persistence layer** – Functions that read and write [`gamecontrollerdb.txt`](https://github.com/HarbourMasters/Lighthouse/blob/main/gamecontrollerdb.txt)-compatible strings

This separation allows the binding logic to remain testable and UI-agnostic while providing an intuitive "click-to-bind" experience for players.

## Detecting Raw SDL Input

The mapping process begins when `MapperWindow` opens an `SDL_Joystick*` handle for the selected device. In `MapperWindow::PollDeviceForSession` ([`Mapper.cpp`](https://github.com/HarbourMasters/Lighthouse/blob/main/Mapper.cpp) lines 151-166), the window polls the joystick each frame and forwards raw events to the active session:

```cpp
void MapperWindow::PollDeviceForSession() {
    if (!mSession.IsActive()) return;
    
    // Poll buttons
    for (int i = 0; i < SDL_JoystickNumButtons(mJoystick); i++) {
        if (SDL_JoystickGetButton(mJoystick, i)) {
            mSession.ProcessButtonDown(i);
        }
    }
    
    // Poll axes and hats similarly...
}

```

This polling approach ensures compatibility with any SDL-supported controller, regardless of whether SDL recognizes it natively.

## The MappingSession State Machine

`MappingSession` manages the binding sequence through an ordered vector of target bindings (`mOrder`) and tracks progress via `mCurrentStep`. As defined in [`Mapper.h`](https://github.com/HarbourMasters/Lighthouse/blob/main/Mapper.h) lines 64-84, the session advances only when the user provides the explicitly requested input type.

### Axis Binding Thresholds

For analog stick bindings, the session implements a two-threshold system to prevent accidental triggers:

| Threshold | Value | Purpose |
|-----------|-------|---------|
| `AXIS_COMMIT_DISTANCE` | 16000 | Binding is locked in when stick moves this far from center |
| `AXIS_RETURN_DISTANCE` | 10000 | Session can advance when stick returns within this range |

These constants (defined in [`Mapper.h`](https://github.com/HarbourMasters/Lighthouse/blob/main/Mapper.h) lines 66-68) ensure the user deliberately moves the stick rather than brushing it accidentally. The state machine requires the stick to exceed the commit distance, then return near center, before marking the binding complete.

A typical axis capture sequence works like this:

```cpp
// Simplified from MappingSession implementation
void ProcessAxisMotion(int axis, Sint16 value) {
    if (mCurrentStepWantsAxis()) {
        if (abs(value) > AXIS_COMMIT_DISTANCE && !mCommitReceived) {
            // Record which direction (positive/negative) and wait for return
            mPendingBinding.axis = axis;
            mPendingBinding.axisPositive = (value > 0);
            mCommitReceived = true;
        }
        else if (mCommitReceived && abs(value) < AXIS_RETURN_DISTANCE) {
            // Stick returned to neutral, binding is complete
            mBindings[mCurrentStep] = mPendingBinding;
            Advance();
        }
    }
}

```

## Generating Standard-Format Mapping Strings

Once all bindings are captured, `GenerateMappingString` (declared in [`Mapper.h`](https://github.com/HarbourMasters/Lighthouse/blob/main/Mapper.h) lines 11-13) produces a [`gamecontrollerdb.txt`](https://github.com/HarbourMasters/Lighthouse/blob/main/gamecontrollerdb.txt)-compatible string. This format—widely used across SDL-based games—ensures portability and tool compatibility:

```

030000005e0400008e02000011010000,Xbox Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,

```

The mapping includes:

- **GUID** – Unique hardware identifier derived from `SDL_JoystickGetGUID`
- **Name** – User-editable display name
- **Button mappings** – Format `logicalButton:physicalInput` (e.g., `a:b0` means "logical A button maps to physical button 0")
- **Axis mappings** – Format `axisName:axisIndex` with sign for direction (e.g., `leftx:a0`)
- **Hat mappings** – Format `dpadDirection:h0.bitmask` (e.g., `dpup:h0.1`)

## Persistence and Runtime Loading

User mappings survive between sessions through two key functions ([`Mapper.h`](https://github.com/HarbourMasters/Lighthouse/blob/main/Mapper.h) lines 19-22):

| Function | Purpose |
|----------|---------|
| `SaveUserMapping` | Appends a new mapping string to [`usergamepadmappings.txt`](https://github.com/HarbourMasters/Lighthouse/blob/main/usergamepadmappings.txt) |
| `LoadUserMappings` | Reads the file at startup and registers each entry via `SDL_GameControllerAddMapping` |

The loading sequence occurs early in initialization, before any game code reads controller state:

```cpp
// Early in main() or equivalent init
void InitializeControllers() {
    SDL_Init(SDL_INIT_GAMECONTROLLER);
    Mapper::LoadUserMappings();  // Registers all user overrides
    
    // SDL now uses custom mappings when opening controllers
    SDL_GameController* pad = SDL_GameControllerOpen(0);
}

```

Because SDL's mapping system is global, the rest of the codebase needs no awareness of custom mappings. Functions like `controller_copyFaceButtons` in [`src/core1/pfsmanager.c`](https://github.com/HarbourMasters/Lighthouse/blob/main/src/core1/pfsmanager.c) (lines 75-82) read standardized SDL controller state that already reflects the user's configuration.

## UI Integration: MapperWindow

`MapperWindow` (defined in [`Mapper.h`](https://github.com/HarbourMasters/Lighthouse/blob/main/Mapper.h) lines 29-71) inherits from `Ship::GuiWindow` to provide the binding interface. Its responsibilities include:

1. **Device selection** – Lists available `SDL_Joystick` devices
2. **Visual controller diagram** – Renders a gamepad with highlighted prompts
3. **Step-by-step guidance** – Displays the current target binding from `mOrder`
4. **Live feedback** – Animates when the session detects valid input

The window maintains loose coupling with `MappingSession` through an explicit interface:

```cpp
// Starting a new mapping session
void MapperWindow::StartMapping() {
    std::vector<int32_t> order = {
        // Analog sticks first (requires commit/return sequence)
        Mapper::AXIS_LEFTX_NEGATIVE, Mapper::AXIS_LEFTX_POSITIVE,
        Mapper::AXIS_LEFTY_NEGATIVE, Mapper::AXIS_LEFTY_POSITIVE,
        Mapper::AXIS_RIGHTX_NEGATIVE, Mapper::AXIS_RIGHTX_POSITIVE,
        Mapper::AXIS_RIGHTY_NEGATIVE, Mapper::AXIS_RIGHTY_POSITIVE,
        // Then face buttons
        SDL_CONTROLLER_BUTTON_A, SDL_CONTROLLER_BUTTON_B,
        SDL_CONTROLLER_BUTTON_X, SDL_CONTROLLER_BUTTON_Y,
        // D-pad, shoulders, triggers, stick clicks, menu buttons...
    };
    
    mSession.Start(mJoystick, order);
    mState = MappingState::InProgress;
}

```

## End-to-End Flow: From Physical Input to Game Action

The complete custom controller mapping lifecycle in Lighthouse follows these stages:

1. **Initialization** – `LoadUserMappings()` parses [`usergamepadmappings.txt`](https://github.com/HarbourMasters/Lighthouse/blob/main/usergamepadmappings.txt) and registers entries with SDL
2. **User activation** – Player opens the controller mapping UI and selects a device
3. **Binding sequence** – `MappingSession` steps through `mOrder`, applying axis thresholds and debouncing
4. **String generation** – `GenerateMappingString()` produces the canonical format
5. **Persistence** – `SaveUserMapping()` appends to [`usergamepadmappings.txt`](https://github.com/HarbourMasters/Lighthouse/blob/main/usergamepadmappings.txt)
6. **Runtime consumption** – Game code reads SDL controller state, unaware of mapping origins

This design isolates mapping complexity in the `Mapper` namespace while ensuring the game receives consistent, logical button names regardless of physical hardware.

## Key Files and Their Roles

| File | Contribution to Custom Controller Mapping |
|------|-------------------------------------------|
| [`src/port/Controller/Mapper.h`](https://github.com/HarbourMasters/Lighthouse/blob/main/src/port/Controller/Mapper.h) | Declares `MappingSession`, binding structures, `GenerateMappingString()`, and `MapperWindow` |
| [`src/port/Controller/Mapper.cpp`](https://github.com/HarbourMasters/Lighthouse/blob/main/src/port/Controller/Mapper.cpp) | Implements state machine logic, file I/O, and UI polling |
| `src/port/Controller/ControlSchemes.h/cpp` | Applies input shaping (e.g., right-stick-to-C-buttons) on top of mapped input |
| [`src/core1/pfsmanager.c`](https://github.com/HarbourMasters/Lighthouse/blob/main/src/core1/pfsmanager.c) | Consumes final SDL controller state via `controller_copyFaceButtons` and related helpers |
| [`usergamepadmappings.txt`](https://github.com/HarbourMasters/Lighthouse/blob/main/usergamepadmappings.txt) (runtime) | Persistent storage in [`gamecontrollerdb.txt`](https://github.com/HarbourMasters/Lighthouse/blob/main/gamecontrollerdb.txt) format |

## Summary

- **The `Mapper` namespace** in [`src/port/Controller/Mapper.h`](https://github.com/HarbourMasters/Lighthouse/blob/main/src/port/Controller/Mapper.h) and [`Mapper.cpp`](https://github.com/HarbourMasters/Lighthouse/blob/main/Mapper.cpp) provides Lighthouse's custom controller mapping capabilities
- **`MappingSession`** implements a state machine with `AXIS_COMMIT_DISTANCE` (16000) and `AXIS_RETURN_DISTANCE` (10000) thresholds for reliable analog capture
- **Standard [`gamecontrollerdb.txt`](https://github.com/HarbourMasters/Lighthouse/blob/main/gamecontrollerdb.txt) format** ensures tool compatibility and portable mapping strings
- **`LoadUserMappings()` and `SaveUserMapping()`** handle persistence without requiring manual file editing
- **SDL's global mapping registry** allows higher-level code to remain agnostic about custom configurations

## Frequently Asked Questions

### How does Lighthouse prevent accidental axis bindings during mapping?

The `MappingSession` state machine requires analog sticks to exceed `AXIS_COMMIT_DISTANCE` (16000) to register a binding, then return within `AXIS_RETURN_DISTANCE` (10000) before advancing. This two-threshold system ensures deliberate stick movement rather than incidental contact.

### Can I edit Lighthouse controller mappings with external tools?

Yes. Because `GenerateMappingString()` produces standard [`gamecontrollerdb.txt`](https://github.com/HarbourMasters/Lighthouse/blob/main/gamecontrollerdb.txt) format, any compatible editor—including Steam's Big Picture controller configurator or standalone tools—can read and modify [`usergamepadmappings.txt`](https://github.com/HarbourMasters/Lighthouse/blob/main/usergamepadmappings.txt). The format uses comma-separated key:value pairs like `a:b0,leftx:a0`.

### Where are custom mappings stored on disk?

User mappings append to [`usergamepadmappings.txt`](https://github.com/HarbourMasters/Lighthouse/blob/main/usergamepadmappings.txt) in the application's configuration directory via `SaveUserMapping()`. The file is parsed at startup by `LoadUserMappings()` and registered with SDL through `SDL_GameControllerAddMapping()`.

### What happens if I map a controller that SDL already recognizes?

Lighthouse user mappings take precedence. When `LoadUserMappings()` calls `SDL_GameControllerAddMapping()`, SDL replaces any existing entry with the same GUID. The game uses the user's custom mapping without requiring code changes in [`pfsmanager.c`](https://github.com/HarbourMasters/Lighthouse/blob/main/pfsmanager.c) or other consumers.