Skip to main content

Overview

HayBox allows you to switch between different controller modes on the fly without unplugging your controller. Mode selection is configured through button combinations that you can customize in your config files.

How Mode Selection Works

Mode selection happens in two places:
  1. Default mode on plugin: Configured in config_defaults.hpp
  2. Mode switching: Handled by select_mode() function during runtime
The firmware continuously checks for button combinations that match configured mode activation bindings. When a match is found, it switches to that mode.

Default Mode Configuration

Default modes and their activation bindings are defined in config_defaults.hpp:
HAL/pico/include/config_defaults.hpp
const Config default_config = {
    .game_mode_configs_count = 7,
    .game_mode_configs = {
        GameModeConfig {
            .mode_id = MODE_MELEE,
            .socd_pairs_count = 4,
            .socd_pairs = {
                SocdPair { .button_dir1 = BTN_LF3, .button_dir2 = BTN_LF1, .socd_type = SOCD_2IP_NO_REAC },
                SocdPair { .button_dir1 = BTN_LF2, .button_dir2 = BTN_RF4, .socd_type = SOCD_2IP_NO_REAC },
                SocdPair { .button_dir1 = BTN_RT3, .button_dir2 = BTN_RT5, .socd_type = SOCD_2IP_NO_REAC },
                SocdPair { .button_dir1 = BTN_RT2, .button_dir2 = BTN_RT4, .socd_type = SOCD_2IP_NO_REAC },
            },
            .activation_binding_count = 3,
            .activation_binding = { BTN_LT1, BTN_MB1, BTN_LF4 }, // Mod X + Start + L
        },
        GameModeConfig {
            .mode_id = MODE_ULTIMATE,
            .socd_pairs_count = 4,
            .socd_pairs = {
                SocdPair { .button_dir1 = BTN_LF3, .button_dir2 = BTN_LF1, .socd_type = SOCD_2IP },
                SocdPair { .button_dir1 = BTN_LF2, .button_dir2 = BTN_RF4, .socd_type = SOCD_2IP },
                SocdPair { .button_dir1 = BTN_RT3, .button_dir2 = BTN_RT5, .socd_type = SOCD_2IP },
                SocdPair { .button_dir1 = BTN_RT2, .button_dir2 = BTN_RT4, .socd_type = SOCD_2IP },
            },
            .activation_binding_count = 3,
            .activation_binding = { BTN_LT1, BTN_MB1, BTN_LF2 }, // Mod X + Start + Down
        },
        // ... more modes
    }
};

Default Mode Activation Bindings

The standard button combinations for switching modes are:

Melee Mode

Start + Mod X + LBTN_MB1 + BTN_LT1 + BTN_LF4Default mode with 2IP No Reactivation SOCD

Project M Mode

Start + Mod X + LeftBTN_MB1 + BTN_LT1 + BTN_LF3Optimized for Project M/Project+

Ultimate Mode

Start + Mod X + DownBTN_MB1 + BTN_LT1 + BTN_LF2Uses 2IP with reactivation

FGC Mode

Start + Mod X + BBTN_MB1 + BTN_LT1 + BTN_RF1Hitbox-style fighting game layout

Rivals of Aether

Start + Mod X + BBTN_MB1 + BTN_LT1 + BTN_RF1Configured for Rivals of Aether

Keyboard Mode

Start + Mod Y + LBTN_MB1 + BTN_LT2 + BTN_LF4Only available with DInput backend

The select_mode() Function

The mode selection logic runs continuously in the main loop:
src/core/mode_selection.cpp
void select_mode(CommunicationBackend **backends, size_t backends_count, Config &config) {
    InputState &inputs = backends[0]->GetInputs();

    // Check each configured mode's activation binding
    for (size_t i = 0; i < config.game_mode_configs_count; i++) {
        GameModeConfig &mode_config = config.game_mode_configs[i];
        
        // If all buttons in the activation binding are held
        if (all_buttons_held(inputs.buttons, mode_activation_masks[i]) && i != current_mode_index) {
            current_mode_index = i;
            
            // Set mode for all backends
            for (size_t i = 0; i < backends_count; i++) {
                set_mode(backends[i], mode_config, config);
            }
            return;
        }
    }
}

Adding Custom Mode Bindings

You can add new modes or modify existing bindings by editing the default_config in your platform’s config_defaults.hpp:
1

Open config_defaults.hpp

Navigate to your platform’s config defaults file:
  • Pico: HAL/pico/include/config_defaults.hpp
  • Arduino USB: HAL/avr/avr_usb/include/config_defaults.hpp
  • Arduino No USB: HAL/avr/avr_nousb/include/config_defaults.hpp
2

Add a new GameModeConfig

Insert a new mode configuration in the game_mode_configs array:
GameModeConfig {
    .mode_id = MODE_CUSTOM,  // or MODE_RIVALS_OF_AETHER, etc.
    .socd_pairs_count = 4,
    .socd_pairs = {
        // Define your SOCD pairs (see SOCD documentation)
        SocdPair { .button_dir1 = BTN_LF3, .button_dir2 = BTN_LF1, .socd_type = SOCD_2IP },
        SocdPair { .button_dir1 = BTN_LF2, .button_dir2 = BTN_RF4, .socd_type = SOCD_2IP },
        SocdPair { .button_dir1 = BTN_RT3, .button_dir2 = BTN_RT5, .socd_type = SOCD_2IP },
        SocdPair { .button_dir1 = BTN_RT2, .button_dir2 = BTN_RT4, .socd_type = SOCD_2IP },
    },
    .button_remapping_count = 0,
    .activation_binding_count = 3,
    .activation_binding = { BTN_LT1, BTN_MB1, BTN_RF8 },  // Custom binding
},
3

Update the mode count

Increment game_mode_configs_count to match the new total number of modes:
.game_mode_configs_count = 8,  // Increased from 7
4

Rebuild firmware

Compile and flash the updated firmware to your controller.

Available Button Identifiers

When creating activation bindings, use these button identifiers:
BTN_LF1  // Right
BTN_LF2  // Down  
BTN_LF3  // Left
BTN_LF4  // L (shield)
BTN_LF5  // (if present)

BTN_LT1  // Mod X
BTN_LT2  // Mod Y

Setting Modes Programmatically

You can also set modes programmatically using the set_mode() functions:
// Set a specific mode by ID
set_mode(backend, MODE_MELEE, config);

// Set a mode using a GameModeConfig
GameModeConfig &mode_config = config.game_mode_configs[0];
set_mode(backend, mode_config, config);

// Set a controller mode directly
Melee20Button melee_mode;
melee_mode.SetConfig(mode_config, config.melee_options);
set_mode(backend, &melee_mode);

// Set a keyboard mode (DInput only)
CustomKeyboardMode keyboard_mode;
keyboard_mode.SetConfig(mode_config, config.keyboard_modes[0]);
set_mode(backend, &keyboard_mode);

Mode-Specific Options

Some modes support additional configuration options:

Melee Mode Options

.melee_options = {
    .crouch_walk_os = false,              // Enable crouch walk option select
    .disable_ledgedash_socd_override = false,  // Disable ledgedash coordinate override
}

Project M Mode Options

.project_m_options = {
    .true_z_press = true,                 // Send actual Z instead of L+A macro
    .disable_ledgedash_socd_override = false,  // Disable ledgedash coordinate override
}

Understanding Mode Activation Masks

The firmware uses bitmasks for efficient button combination checking:
void setup_mode_activation_bindings(const GameModeConfig *mode_configs, size_t mode_configs_count) {
    // Build bit masks for each mode's activation binding
    for (size_t i = 0; i < mode_configs_count; i++) {
        mode_activation_masks[i] = make_button_mask(
            mode_configs[i].activation_binding,
            mode_configs[i].activation_binding_count
        );
    }
}
This pre-computation makes the runtime check very fast:
if (all_buttons_held(inputs.buttons, mode_activation_masks[i])) {
    // Switch to mode i
}

SOCD Configuration in Modes

Each mode can specify its own SOCD (Simultaneous Opposite Cardinal Direction) resolution. The SOCD type is passed when creating the mode config:
GameModeConfig {
    .mode_id = MODE_MELEE,
    .socd_pairs = {
        SocdPair { .button_dir1 = BTN_LF3, .button_dir2 = BTN_LF1, .socd_type = SOCD_2IP_NO_REAC },
        // Left vs Right with 2IP No Reactivation
    },
}
See the SOCD documentation for detailed information about SOCD types and configuration.

Best Practices

Avoid Conflicting Bindings

Make sure your activation bindings don’t overlap. For example, if one mode uses Start + Mod X + L and another uses Start + Mod X, the latter will always activate first.

Test on Plugin

Remember that modes can also be selected on plugin by holding the activation binding. Test this to ensure your bindings work correctly.

Use Consistent SOCD

Keep SOCD settings consistent across similar modes to avoid confusion when switching.

See Also

Build docs developers (and LLMs) love