flycast/core/ui/settings_controls.cpp

1067 lines
32 KiB
C++

/*
Copyright 2025 flyinghead
This file is part of Flycast.
Flycast is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
Flycast is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Flycast. If not, see <https://www.gnu.org/licenses/>.
*/
#include "settings.h"
#include "gui.h"
#include "input/gamepad_device.h"
#include "hw/maple/maple_devs.h"
#include "vgamepad.h"
#include "oslib/storage.h"
#if defined(USE_SDL)
#include "sdl/dreamlink.h" // For USE_DREAMCASTCONTROLLER
#endif
static float calcComboWidth(const char *biggestLabel) {
return ImGui::CalcTextSize(biggestLabel).x + ImGui::GetStyle().FramePadding.x * 2.0f + ImGui::GetFrameHeight();
}
static const char *maple_device_types[] =
{
"None",
"Sega Controller",
"Light Gun",
"Keyboard",
"Mouse",
"Twin Stick",
"Arcade/Ascii Stick",
"Maracas Controller",
"Fishing Controller",
"Pop'n Music controller",
"Racing Controller",
"Densha de Go! Controller",
"Full Controller",
// "Dreameye",
};
static const char *maple_expansion_device_types[] =
{
"None",
"Sega VMU",
"Vibration Pack",
"Microphone",
};
static const char *maple_device_name(MapleDeviceType type)
{
switch (type)
{
case MDT_SegaController:
return maple_device_types[1];
case MDT_LightGun:
return maple_device_types[2];
case MDT_Keyboard:
return maple_device_types[3];
case MDT_Mouse:
return maple_device_types[4];
case MDT_TwinStick:
return maple_device_types[5];
case MDT_AsciiStick:
return maple_device_types[6];
case MDT_MaracasController:
return maple_device_types[7];
case MDT_FishingController:
return maple_device_types[8];
case MDT_PopnMusicController:
return maple_device_types[9];
case MDT_RacingController:
return maple_device_types[10];
case MDT_DenshaDeGoController:
return maple_device_types[11];
case MDT_SegaControllerXL:
return maple_device_types[12];
case MDT_Dreameye:
// return maple_device_types[13];
case MDT_None:
default:
return maple_device_types[0];
}
}
static MapleDeviceType maple_device_type_from_index(int idx)
{
switch (idx)
{
case 1:
return MDT_SegaController;
case 2:
return MDT_LightGun;
case 3:
return MDT_Keyboard;
case 4:
return MDT_Mouse;
case 5:
return MDT_TwinStick;
case 6:
return MDT_AsciiStick;
case 7:
return MDT_MaracasController;
case 8:
return MDT_FishingController;
case 9:
return MDT_PopnMusicController;
case 10:
return MDT_RacingController;
case 11:
return MDT_DenshaDeGoController;
case 12:
return MDT_SegaControllerXL;
case 13:
return MDT_Dreameye;
case 0:
default:
return MDT_None;
}
}
static const char *maple_expansion_device_name(MapleDeviceType type)
{
switch (type)
{
case MDT_SegaVMU:
return maple_expansion_device_types[1];
case MDT_PurupuruPack:
return maple_expansion_device_types[2];
case MDT_Microphone:
return maple_expansion_device_types[3];
case MDT_None:
default:
return maple_expansion_device_types[0];
}
}
static const char *maple_ports[] = { "None", "A", "B", "C", "D", "All" };
struct Mapping {
DreamcastKey key;
const char *name;
};
static const Mapping dcButtons[] = {
{ EMU_BTN_NONE, "Directions" },
{ DC_DPAD_UP, "Up" },
{ DC_DPAD_DOWN, "Down" },
{ DC_DPAD_LEFT, "Left" },
{ DC_DPAD_RIGHT, "Right" },
{ DC_AXIS_UP, "Thumbstick Up" },
{ DC_AXIS_DOWN, "Thumbstick Down" },
{ DC_AXIS_LEFT, "Thumbstick Left" },
{ DC_AXIS_RIGHT, "Thumbstick Right" },
{ DC_AXIS2_UP, "R.Thumbstick Up" },
{ DC_AXIS2_DOWN, "R.Thumbstick Down" },
{ DC_AXIS2_LEFT, "R.Thumbstick Left" },
{ DC_AXIS2_RIGHT, "R.Thumbstick Right" },
{ DC_AXIS3_UP, "Axis 3 Up" },
{ DC_AXIS3_DOWN, "Axis 3 Down" },
{ DC_AXIS3_LEFT, "Axis 3 Left" },
{ DC_AXIS3_RIGHT, "Axis 3 Right" },
{ DC_DPAD2_UP, "DPad2 Up" },
{ DC_DPAD2_DOWN, "DPad2 Down" },
{ DC_DPAD2_LEFT, "DPad2 Left" },
{ DC_DPAD2_RIGHT, "DPad2 Right" },
{ EMU_BTN_NONE, "Buttons" },
{ DC_BTN_A, "A" },
{ DC_BTN_B, "B" },
{ DC_BTN_X, "X" },
{ DC_BTN_Y, "Y" },
{ DC_BTN_C, "C" },
{ DC_BTN_D, "D" },
{ DC_BTN_Z, "Z" },
{ EMU_BTN_NONE, "Triggers" },
{ DC_AXIS_LT, "Left Trigger" },
{ DC_AXIS_RT, "Right Trigger" },
{ DC_AXIS_LT2, "Left Trigger 2" },
{ DC_AXIS_RT2, "Right Trigger 2" },
{ EMU_BTN_NONE, "System Buttons" },
{ DC_BTN_START, "Start" },
{ DC_BTN_RELOAD, "Reload" },
{ EMU_BTN_NONE, "Emulator" },
{ EMU_BTN_MENU, "Menu" },
{ EMU_BTN_ESCAPE, "Exit" },
{ EMU_BTN_FFORWARD, "Fast-forward" },
{ EMU_BTN_LOADSTATE, "Load State" },
{ EMU_BTN_SAVESTATE, "Save State" },
{ EMU_BTN_BYPASS_KB, "Bypass Emulated Keyboard" },
{ EMU_BTN_SCREENSHOT, "Save Screenshot" },
{ EMU_BTN_NONE, nullptr }
};
static const Mapping arcadeButtons[] = {
{ EMU_BTN_NONE, "Directions" },
{ DC_DPAD_UP, "Up" },
{ DC_DPAD_DOWN, "Down" },
{ DC_DPAD_LEFT, "Left" },
{ DC_DPAD_RIGHT, "Right" },
{ DC_AXIS_UP, "Thumbstick Up" },
{ DC_AXIS_DOWN, "Thumbstick Down" },
{ DC_AXIS_LEFT, "Thumbstick Left" },
{ DC_AXIS_RIGHT, "Thumbstick Right" },
{ DC_AXIS2_UP, "R.Thumbstick Up" },
{ DC_AXIS2_DOWN, "R.Thumbstick Down" },
{ DC_AXIS2_LEFT, "R.Thumbstick Left" },
{ DC_AXIS2_RIGHT, "R.Thumbstick Right" },
{ EMU_BTN_NONE, "Buttons" },
{ DC_BTN_A, "Button 1" },
{ DC_BTN_B, "Button 2" },
{ DC_BTN_C, "Button 3" },
{ DC_BTN_X, "Button 4" },
{ DC_BTN_Y, "Button 5" },
{ DC_BTN_Z, "Button 6" },
{ DC_DPAD2_LEFT, "Button 7" },
{ DC_DPAD2_RIGHT, "Button 8" },
// { DC_DPAD2_RIGHT, "Button 9" }, // TODO
{ EMU_BTN_NONE, "Triggers" },
{ DC_AXIS_LT, "Left Trigger" },
{ DC_AXIS_RT, "Right Trigger" },
{ DC_AXIS_LT2, "Left Trigger 2" },
{ DC_AXIS_RT2, "Right Trigger 2" },
{ EMU_BTN_NONE, "System Buttons" },
{ DC_BTN_START, "Start" },
{ DC_BTN_RELOAD, "Reload" },
{ DC_BTN_D, "Coin" },
{ DC_DPAD2_UP, "Service" },
{ DC_DPAD2_DOWN, "Test" },
{ DC_BTN_INSERT_CARD, "Insert Card" },
{ EMU_BTN_NONE, "Emulator" },
{ EMU_BTN_MENU, "Menu" },
{ EMU_BTN_ESCAPE, "Exit" },
{ EMU_BTN_FFORWARD, "Fast-forward" },
{ EMU_BTN_LOADSTATE, "Load State" },
{ EMU_BTN_SAVESTATE, "Save State" },
{ EMU_BTN_BYPASS_KB, "Bypass Emulated Keyboard" },
{ EMU_BTN_SCREENSHOT, "Save Screenshot" },
{ EMU_BTN_NONE, nullptr }
};
static MapleDeviceType maple_expansion_device_type_from_index(int idx)
{
switch (idx)
{
case 1:
return MDT_SegaVMU;
case 2:
return MDT_PurupuruPack;
case 3:
return MDT_Microphone;
case 0:
default:
return MDT_None;
}
}
static std::shared_ptr<GamepadDevice> mapped_device;
static InputMapping::InputSet mapped_codes; // Stores multiple buttons in the order they were entered
static u64 map_start_time;
static bool arcade_button_mode;
static u32 gamepad_port;
static void unmapControl(const std::shared_ptr<InputMapping>& mapping, u32 gamepad_port, DreamcastKey key)
{
mapping->clear_button(gamepad_port, key);
mapping->clear_axis(gamepad_port, key);
}
static DreamcastKey getOppositeDirectionKey(DreamcastKey key)
{
switch (key)
{
case DC_DPAD_UP:
return DC_DPAD_DOWN;
case DC_DPAD_DOWN:
return DC_DPAD_UP;
case DC_DPAD_LEFT:
return DC_DPAD_RIGHT;
case DC_DPAD_RIGHT:
return DC_DPAD_LEFT;
case DC_DPAD2_UP:
return DC_DPAD2_DOWN;
case DC_DPAD2_DOWN:
return DC_DPAD2_UP;
case DC_DPAD2_LEFT:
return DC_DPAD2_RIGHT;
case DC_DPAD2_RIGHT:
return DC_DPAD2_LEFT;
case DC_AXIS_UP:
return DC_AXIS_DOWN;
case DC_AXIS_DOWN:
return DC_AXIS_UP;
case DC_AXIS_LEFT:
return DC_AXIS_RIGHT;
case DC_AXIS_RIGHT:
return DC_AXIS_LEFT;
case DC_AXIS2_UP:
return DC_AXIS2_DOWN;
case DC_AXIS2_DOWN:
return DC_AXIS2_UP;
case DC_AXIS2_LEFT:
return DC_AXIS2_RIGHT;
case DC_AXIS2_RIGHT:
return DC_AXIS2_LEFT;
case DC_AXIS3_UP:
return DC_AXIS3_DOWN;
case DC_AXIS3_DOWN:
return DC_AXIS3_UP;
case DC_AXIS3_LEFT:
return DC_AXIS3_RIGHT;
case DC_AXIS3_RIGHT:
return DC_AXIS3_LEFT;
default:
return EMU_BTN_NONE;
}
}
static void displayLabelOrCode(const char *label, u32 code, const char *suffix = "")
{
if (label != nullptr)
ImGui::Text("%s%s", label, suffix);
else
ImGui::Text("[%d]%s", code, suffix);
}
static void detect_input_popup(const Mapping *mapping)
{
ImVec2 padding = ScaledVec2(20, 20);
ImguiStyleVar _(ImGuiStyleVar_WindowPadding, padding);
ImguiStyleVar _1(ImGuiStyleVar_ItemSpacing, padding);
if (ImGui::BeginPopupModal("Map Control", NULL, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove))
{
ImGui::Text("Waiting for control '%s'...", mapping->name);
u64 now = getTimeMs();
// Check if we're still in the initial delay period
if (now >= map_start_time)
{
// Check if device is still detecting input (might have been cancelled by button release)
bool still_detecting = mapped_device && mapped_device->is_input_detecting();
// If detection was cancelled by button release, close popup immediately
int remaining = still_detecting ? (int)(5 - (now - map_start_time) / 1000) : 0;
if (remaining < 0)
remaining = 5;
if (still_detecting)
ImGui::Text("Time out in %d s", remaining);
// Display currently detected buttons during the countdown
if (!mapped_codes.empty())
{
ImGui::Text("Current inputs: ");
ImGui::SameLine();
bool first = true;
for (const InputMapping::InputDef& inputDef : mapped_codes)
{
if (!first)
{
ImGui::SameLine();
ImGui::Text("&");
ImGui::SameLine();
}
const char* name = nullptr;
if (inputDef.is_button())
name = mapped_device->get_button_name(inputDef.code);
else
name = mapped_device->get_axis_name(inputDef.code);
displayLabelOrCode(name, inputDef.code);
first = false;
}
// Allow early completion with Confirm button if at least one button is detected
if (ImGui::Button("Confirm"))
remaining = 0;
}
// Wait for the countdown to complete before mapping
if (remaining <= 0)
{
std::shared_ptr<InputMapping> input_mapping = mapped_device->get_input_mapping();
if (input_mapping != NULL && !mapped_codes.empty())
{
unmapControl(input_mapping, gamepad_port, mapping->key);
if (mapped_codes.size() == 1 && mapped_codes.front().is_axis())
{
// Single axis mapping
const InputMapping::InputDef& axisInputDef = mapped_codes.front();
const bool positive = (axisInputDef.type == InputMapping::InputDef::InputType::AXIS_POS);
input_mapping->set_axis(gamepad_port, mapping->key, axisInputDef.code, positive);
DreamcastKey opposite = getOppositeDirectionKey(mapping->key);
// Map the axis opposite direction to the corresponding opposite dc button or axis,
// but only if the opposite direction axis isn't used and the dc button or axis isn't mapped.
if (opposite != EMU_BTN_NONE
&& input_mapping->get_axis_id(gamepad_port, axisInputDef.code, !positive) == EMU_BTN_NONE
&& input_mapping->get_axis_code(gamepad_port, opposite).first == (u32)-1
&& input_mapping->get_button_code(gamepad_port, opposite) == (u32)-1)
input_mapping->set_axis(gamepad_port, opposite, axisInputDef.code, !positive);
}
else
{
input_mapping->set_button(gamepad_port, mapping->key, InputMapping::ButtonCombo{mapped_codes, true});
}
}
// Make sure to cancel input detection to prevent collecting more inputs
if (mapped_device)
mapped_device->cancel_detect_input();
mapped_device = NULL;
mapped_codes.clear();
ImGui::CloseCurrentPopup();
}
}
ImGui::EndPopup();
}
}
static void displayMappedControl(const std::shared_ptr<GamepadDevice>& gamepad, DreamcastKey key)
{
std::shared_ptr<InputMapping> input_mapping = gamepad->get_input_mapping();
InputMapping::ButtonCombo combo = input_mapping->get_button_combo(gamepad_port, key);
if (combo.inputs.empty())
{
// Try axis
std::pair<u32, bool> pair = input_mapping->get_axis_code(gamepad_port, key);
const InputMapping::InputDef inputDef = InputMapping::InputDef::from_axis(pair.first, pair.second);
if (inputDef.is_valid())
displayLabelOrCode(gamepad->get_axis_name(inputDef.code), inputDef.code, inputDef.get_suffix());
}
else
{
// Display button combination in "Button1 & Button2 & ..." format
bool first = true;
for (const InputMapping::InputDef& inputDef : combo.inputs)
{
if (!first)
{
ImGui::SameLine();
ImGui::Text("&");
ImGui::SameLine();
}
const char* name = nullptr;
if (inputDef.is_button())
name = gamepad->get_button_name(inputDef.code);
else if (inputDef.is_axis())
name = gamepad->get_axis_name(inputDef.code);
displayLabelOrCode(name, inputDef.code, inputDef.get_suffix());
first = false;
}
if (combo.inputs.size() > 1)
{
if (ImGui::Checkbox("Sequential", &(combo.sequential)))
// Update mapping with updated combo settings
input_mapping->set_button(gamepad_port, key, combo);
ImGui::SameLine();
ShowHelpMarker(
"When checked, this combo will only activate when all keys are pressed in the given sequence.\n"
"When not checked, the combo will activate when all keys are pressed in any order.");
}
}
}
static void controller_mapping_popup(const std::shared_ptr<GamepadDevice>& gamepad)
{
fullScreenWindow(true);
ImguiStyleVar _(ImGuiStyleVar_WindowRounding, 0);
if (ImGui::BeginPopupModal("Controller Mapping", NULL, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove))
{
const ImGuiStyle& style = ImGui::GetStyle();
const float winWidth = ImGui::GetIO().DisplaySize.x - insetLeft - insetRight - (style.WindowBorderSize + style.WindowPadding.x) * 2;
const float col_width = (winWidth - style.GrabMinSize - style.ItemSpacing.x
- (ImGui::CalcTextSize("Map").x + style.FramePadding.x * 2.0f + style.ItemSpacing.x)
- (ImGui::CalcTextSize("Unmap").x + style.FramePadding.x * 2.0f + style.ItemSpacing.x)) / 2;
static int map_system;
static int item_current_map_idx = 0;
static int last_item_current_map_idx = 2;
std::shared_ptr<InputMapping> input_mapping = gamepad->get_input_mapping();
if (input_mapping == NULL || ImGui::Button("Done", ScaledVec2(100, 30)))
{
ImGui::CloseCurrentPopup();
gamepad->save_mapping(map_system);
last_item_current_map_idx = 2;
ImGui::EndPopup();
return;
}
ImGui::SetItemDefaultFocus();
float portWidth = 0;
if (gamepad->maple_port() == MAPLE_PORTS)
{
ImGui::SameLine();
ImguiStyleVar _(ImGuiStyleVar_FramePadding, ImVec2(ImGui::GetStyle().FramePadding.x, (uiScaled(30) - ImGui::GetFontSize()) / 2));
portWidth = ImGui::CalcTextSize("AA").x + ImGui::GetStyle().ItemSpacing.x * 2.0f + ImGui::GetFontSize();
ImGui::SetNextItemWidth(portWidth);
if (ImGui::BeginCombo("Port", maple_ports[gamepad_port + 1]))
{
for (u32 j = 0; j < MAPLE_PORTS; j++)
{
bool is_selected = gamepad_port == j;
if (ImGui::Selectable(maple_ports[j + 1], &is_selected))
gamepad_port = j;
if (is_selected)
ImGui::SetItemDefaultFocus();
}
ImGui::EndCombo();
}
portWidth += ImGui::CalcTextSize("Port").x + ImGui::GetStyle().ItemSpacing.x + ImGui::GetStyle().FramePadding.x;
}
float comboWidth = ImGui::CalcTextSize("Dreamcast Controls").x + ImGui::GetStyle().ItemSpacing.x + ImGui::GetFontSize() + ImGui::GetStyle().FramePadding.x * 4;
float gameConfigWidth = 0;
if (!settings.content.gameId.empty())
gameConfigWidth = ImGui::CalcTextSize(gamepad->isPerGameMapping() ? "Delete Game Config" : "Make Game Config").x + ImGui::GetStyle().ItemSpacing.x + ImGui::GetStyle().FramePadding.x * 2;
ImGui::SameLine(0, ImGui::GetContentRegionAvail().x - comboWidth - gameConfigWidth - ImGui::GetStyle().ItemSpacing.x - uiScaled(100) * 2 - portWidth);
ImGui::AlignTextToFramePadding();
if (!settings.content.gameId.empty())
{
if (gamepad->isPerGameMapping())
{
if (ImGui::Button("Delete Game Config", ScaledVec2(0, 30)))
{
gamepad->setPerGameMapping(false);
if (!gamepad->find_mapping(map_system))
gamepad->resetMappingToDefault(arcade_button_mode, true);
}
}
else
{
if (ImGui::Button("Make Game Config", ScaledVec2(0, 30)))
gamepad->setPerGameMapping(true);
}
ImGui::SameLine();
}
if (ImGui::Button("Reset...", ScaledVec2(100, 30)))
ImGui::OpenPopup("Confirm Reset");
{
ImguiStyleVar _(ImGuiStyleVar_WindowPadding, ScaledVec2(20, 20));
if (ImGui::BeginPopupModal("Confirm Reset", NULL, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove))
{
ImGui::Text("Are you sure you want to reset the mappings to default?");
static bool hitbox;
if (arcade_button_mode)
{
ImGui::Text("Controller Type:");
if (ImGui::RadioButton("Gamepad", !hitbox))
hitbox = false;
ImGui::SameLine();
if (ImGui::RadioButton("Arcade / Hit Box", hitbox))
hitbox = true;
}
ImGui::NewLine();
{
ImguiStyleVar _(ImGuiStyleVar_ItemSpacing, ImVec2(uiScaled(20), ImGui::GetStyle().ItemSpacing.y));
ImguiStyleVar _1(ImGuiStyleVar_FramePadding, ScaledVec2(10, 10));
if (ImGui::Button("Yes"))
{
gamepad->resetMappingToDefault(arcade_button_mode, !hitbox);
gamepad->save_mapping(map_system);
ImGui::CloseCurrentPopup();
}
ImGui::SameLine();
if (ImGui::Button("No"))
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
}
}
ImGui::SameLine();
const char* items[] = { "Dreamcast Controls", "Arcade Controls" };
if (last_item_current_map_idx == 2 && game_started)
// Select the right mappings for the current game
item_current_map_idx = settings.platform.isArcade() ? 1 : 0;
// Here our selection data is an index.
ImGui::SetNextItemWidth(comboWidth);
// Make the combo height the same as the Done and Reset buttons
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(ImGui::GetStyle().FramePadding.x, (uiScaled(30) - ImGui::GetFontSize()) / 2));
ImGui::Combo("##arcadeMode", &item_current_map_idx, items, IM_ARRAYSIZE(items));
ImGui::PopStyleVar();
if (last_item_current_map_idx != 2 && item_current_map_idx != last_item_current_map_idx)
gamepad->save_mapping(map_system);
const Mapping *systemMapping = dcButtons;
if (item_current_map_idx == 0)
{
arcade_button_mode = false;
map_system = DC_PLATFORM_DREAMCAST;
systemMapping = dcButtons;
}
else if (item_current_map_idx == 1)
{
arcade_button_mode = true;
map_system = DC_PLATFORM_NAOMI;
systemMapping = arcadeButtons;
}
if (item_current_map_idx != last_item_current_map_idx)
{
if (!gamepad->find_mapping(map_system))
if (map_system == DC_PLATFORM_DREAMCAST || !gamepad->find_mapping(DC_PLATFORM_DREAMCAST))
gamepad->resetMappingToDefault(arcade_button_mode, true);
input_mapping = gamepad->get_input_mapping();
last_item_current_map_idx = item_current_map_idx;
}
char key_id[32];
ImGui::BeginChild(ImGui::GetID("buttons"), ImVec2(0, 0), ImGuiChildFlags_FrameStyle, ImGuiWindowFlags_DragScrolling | ImGuiWindowFlags_NavFlattened);
for (; systemMapping->name != nullptr; systemMapping++)
{
if (systemMapping->key == EMU_BTN_NONE)
{
ImGui::Columns(1, nullptr, false);
header(systemMapping->name);
ImGui::Columns(3, "bindings", false);
ImGui::SetColumnWidth(0, col_width);
ImGui::SetColumnWidth(1, col_width);
continue;
}
snprintf(key_id, sizeof(key_id), "key_id%d", systemMapping->key);
ImguiID _(key_id);
const char *game_btn_name = nullptr;
if (arcade_button_mode)
{
game_btn_name = GetCurrentGameButtonName(systemMapping->key);
if (game_btn_name == nullptr)
game_btn_name = GetCurrentGameAxisName(systemMapping->key);
}
if (game_btn_name != nullptr && game_btn_name[0] != '\0')
ImGui::Text("%s - %s", systemMapping->name, game_btn_name);
else
ImGui::Text("%s", systemMapping->name);
ImGui::NextColumn();
displayMappedControl(gamepad, systemMapping->key);
ImGui::NextColumn();
if (ImGui::Button("Map"))
{
// Set a small delay to avoid capturing the button press used to click "Map"
map_start_time = getTimeMs() + 300; // 300ms delay before starting the countdown
ImGui::OpenPopup("Map Control");
mapped_device = gamepad;
mapped_codes.clear(); // Clear previous button codes
// Detect combos only for EMU_BUTTONS
const auto buttonGroup = (systemMapping->key & DC_BTN_GROUP_MASK);
const bool detectCombo = (buttonGroup == EMU_BUTTONS);
// Setup a callback to collect button/axes presses
gamepad->detectInput(true, true, detectCombo, [](u32 code, bool analog, bool positive)
{
if (analog)
mapped_codes.insert_back(InputMapping::InputDef::from_axis(code, positive));
else
mapped_codes.insert_back(InputMapping::InputDef::from_button(code));
});
}
detect_input_popup(systemMapping);
ImGui::SameLine();
if (ImGui::Button("Unmap"))
{
input_mapping = gamepad->get_input_mapping();
unmapControl(input_mapping, gamepad_port, systemMapping->key);
}
ImGui::NextColumn();
}
ImGui::Columns(1, nullptr, false);
scrollWhenDraggingOnVoid();
windowDragScroll();
ImGui::EndChild();
error_popup();
ImGui::EndPopup();
}
}
static void gamepadPngFileSelected(bool cancelled, std::string path)
{
if (!cancelled)
gui_runOnUiThread([path]() {
vgamepad::loadImage(path);
});
}
static void gamepadSettingsPopup(const std::shared_ptr<GamepadDevice>& gamepad)
{
centerNextWindow();
ImGui::SetNextWindowSize(min(ImGui::GetIO().DisplaySize, ScaledVec2(450.f, 300.f)));
ImguiStyleVar _(ImGuiStyleVar_WindowRounding, 0);
if (ImGui::BeginPopupModal("Gamepad Settings", NULL, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_DragScrolling))
{
if (ImGui::Button("Done", ScaledVec2(100, 30)))
{
gamepad->save_mapping();
// Update both console and arcade profile/mapping
int rumblePower = gamepad->get_rumble_power();
float deadzone = gamepad->get_dead_zone();
float saturation = gamepad->get_saturation();
int otherPlatform = settings.platform.isConsole() ? DC_PLATFORM_NAOMI : DC_PLATFORM_DREAMCAST;
if (!gamepad->find_mapping(otherPlatform))
if (otherPlatform == DC_PLATFORM_DREAMCAST || !gamepad->find_mapping(DC_PLATFORM_DREAMCAST))
gamepad->resetMappingToDefault(otherPlatform != DC_PLATFORM_DREAMCAST, true);
std::shared_ptr<InputMapping> mapping = gamepad->get_input_mapping();
if (mapping != nullptr)
{
if (gamepad->is_rumble_enabled() && rumblePower != mapping->rumblePower) {
mapping->rumblePower = rumblePower;
mapping->set_dirty();
}
if (gamepad->has_analog_stick())
{
if (deadzone != mapping->dead_zone) {
mapping->dead_zone = deadzone;
mapping->set_dirty();
}
if (saturation != mapping->saturation) {
mapping->saturation = saturation;
mapping->set_dirty();
}
}
if (mapping->is_dirty())
gamepad->save_mapping(otherPlatform);
}
gamepad->find_mapping();
ImGui::CloseCurrentPopup();
ImGui::EndPopup();
return;
}
ImGui::NewLine();
if (gamepad->is_virtual_gamepad())
{
if (gamepad->is_rumble_enabled()) {
header("Haptic");
OptionSlider("Power", config::VirtualGamepadVibration, 0, 100, "Haptic feedback power", "%d%%");
}
header("View");
OptionSlider("Transparency", config::VirtualGamepadTransparency, 0, 100, "Virtual gamepad buttons transparency", "%d%%");
#if defined(__ANDROID__) || defined(TARGET_IPHONE)
vgamepad::ImguiVGamepadTexture tex;
ImGui::Image(tex.getId(), ScaledVec2(300.f, 150.f), ImVec2(0, 1), ImVec2(1, 0));
#endif
const char *gamepadPngTitle = "Select a PNG file";
if (ImGui::Button("Choose Image...", ScaledVec2(150, 30)))
#ifdef __ANDROID__
{
if (!hostfs::addStorage(false, false, gamepadPngTitle, gamepadPngFileSelected, "image/png"))
ImGui::OpenPopup(gamepadPngTitle);
}
#else
{
ImGui::OpenPopup(gamepadPngTitle);
}
#endif
ImGui::SameLine();
if (ImGui::Button("Use Default", ScaledVec2(150, 30)))
vgamepad::loadImage("");
select_file_popup(gamepadPngTitle, [](bool cancelled, std::string selection)
{
gamepadPngFileSelected(cancelled, selection);
return true;
}, true, "png");
}
else if (gamepad->is_rumble_enabled())
{
header("Rumble");
int power = gamepad->get_rumble_power();
ImGui::SetNextItemWidth(uiScaled(300));
if (ImGui::SliderInt("Power", &power, 0, 100, "%d%%"))
gamepad->set_rumble_power(power);
ImGui::SameLine();
ShowHelpMarker("Rumble power");
}
if (gamepad->has_analog_stick())
{
header("Thumbsticks");
int deadzone = std::round(gamepad->get_dead_zone() * 100.f);
ImGui::SetNextItemWidth(uiScaled(300));
if (ImGui::SliderInt("Dead zone", &deadzone, 0, 100, "%d%%"))
gamepad->set_dead_zone(deadzone / 100.f);
ImGui::SameLine();
ShowHelpMarker("Minimum deflection to register as input");
int saturation = std::round(gamepad->get_saturation() * 100.f);
ImGui::SetNextItemWidth(uiScaled(300));
if (ImGui::SliderInt("Saturation", &saturation, 50, 200, "%d%%"))
gamepad->set_saturation(saturation / 100.f);
ImGui::SameLine();
ShowHelpMarker("Value sent to the game at 100% thumbstick deflection. "
"Values greater than 100% will saturate before full deflection of the thumbstick.");
}
scrollWhenDraggingOnVoid();
windowDragScroll();
ImGui::EndPopup();
}
}
void gui_settings_controls(bool& maple_devices_changed)
{
header("Physical Devices");
{
if (ImGui::BeginTable("physicalDevices", 4, ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_NoSavedSettings))
{
ImGui::TableSetupColumn("System", ImGuiTableColumnFlags_WidthFixed);
ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableSetupColumn("Port", ImGuiTableColumnFlags_WidthFixed);
ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed);
const float portComboWidth = calcComboWidth("None");
const ImVec4 gray{ 0.5f, 0.5f, 0.5f, 1.f };
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
ImGui::TextColored(gray, "System");
ImGui::TableSetColumnIndex(1);
ImGui::TextColored(gray, "Name");
ImGui::TableSetColumnIndex(2);
ImGui::TextColored(gray, "Port");
for (int i = 0; i < GamepadDevice::GetGamepadCount(); i++)
{
std::shared_ptr<GamepadDevice> gamepad = GamepadDevice::GetGamepad(i);
if (!gamepad)
continue;
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
ImGui::Text("%s", gamepad->api_name().c_str());
ImGui::TableSetColumnIndex(1);
ImGui::Text("%s", gamepad->name().c_str());
ImGui::TableSetColumnIndex(2);
char port_name[32];
snprintf(port_name, sizeof(port_name), "##mapleport%d", i);
ImguiID _(port_name);
ImGui::SetNextItemWidth(portComboWidth);
if (ImGui::BeginCombo(port_name, maple_ports[gamepad->maple_port() + 1]))
{
for (int j = -1; j < (int)std::size(maple_ports) - 1; j++)
{
bool is_selected = gamepad->maple_port() == j;
if (ImGui::Selectable(maple_ports[j + 1], &is_selected))
gamepad->set_maple_port(j);
if (is_selected)
ImGui::SetItemDefaultFocus();
}
ImGui::EndCombo();
}
ImGui::TableSetColumnIndex(3);
ImGui::SameLine(0, uiScaled(8));
if (gamepad->remappable() && ImGui::Button("Map"))
{
gamepad_port = 0;
ImGui::OpenPopup("Controller Mapping");
}
controller_mapping_popup(gamepad);
#if defined(__ANDROID__) || defined(TARGET_IPHONE)
if (gamepad->is_virtual_gamepad())
{
if (ImGui::Button("Edit Layout"))
{
vgamepad::startEditing();
gui_setState(GuiState::VJoyEdit);
}
}
#endif
if (gamepad->is_rumble_enabled() || gamepad->has_analog_stick()
|| gamepad->is_virtual_gamepad())
{
ImGui::SameLine(0, uiScaled(16));
if (ImGui::Button("Settings"))
ImGui::OpenPopup("Gamepad Settings");
gamepadSettingsPopup(gamepad);
}
}
ImGui::EndTable();
}
}
ImGui::Spacing();
OptionSlider("Mouse sensitivity", config::MouseSensitivity, 1, 500);
#if defined(_WIN32) && !defined(TARGET_UWP)
OptionCheckbox("Use Raw Input", config::UseRawInput, "Supports multiple pointing devices (mice, light guns) and keyboards");
#endif
#ifdef USE_DREAMCASTCONTROLLER
{
DisabledScope scope(game_started);
OptionCheckbox("Use Physical VMU Memory", config::UsePhysicalVmuMemory,
"Enables direct read/write access to physical VMU memory via DreamPicoPort/DreamConn. "
"This is not compatible with load state events.");
}
#endif
ImGui::Spacing();
header("Dreamcast Devices");
{
bool is_there_any_xhair = false;
if (ImGui::BeginTable("dreamcastDevices", 4, ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_NoSavedSettings,
ImVec2(0, 0), uiScaled(8)))
{
const float mainComboWidth = calcComboWidth(maple_device_types[11]); // densha de go! controller
const float expComboWidth = calcComboWidth(maple_expansion_device_types[2]); // vibration pack
for (int bus = 0; bus < MAPLE_PORTS; bus++)
{
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
ImGui::Text("Port %c", bus + 'A');
ImGui::TableSetColumnIndex(1);
char device_name[32];
snprintf(device_name, sizeof(device_name), "##device%d", bus);
float w = ImGui::CalcItemWidth() / 3;
ImGui::PushItemWidth(w);
ImGui::SetNextItemWidth(mainComboWidth);
if (ImGui::BeginCombo(device_name, maple_device_name(config::MapleMainDevices[bus]), ImGuiComboFlags_None))
{
for (int i = 0; i < IM_ARRAYSIZE(maple_device_types); i++)
{
bool is_selected = config::MapleMainDevices[bus] == maple_device_type_from_index(i);
if (ImGui::Selectable(maple_device_types[i], &is_selected))
{
config::MapleMainDevices[bus] = maple_device_type_from_index(i);
maple_devices_changed = true;
}
if (is_selected)
ImGui::SetItemDefaultFocus();
}
ImGui::EndCombo();
}
int port_count = 0;
switch (config::MapleMainDevices[bus]) {
case MDT_SegaController:
case MDT_SegaControllerXL:
port_count = 2;
break;
case MDT_LightGun:
case MDT_TwinStick:
case MDT_AsciiStick:
case MDT_RacingController:
port_count = 1;
break;
default: break;
}
for (int port = 0; port < port_count; port++)
{
ImGui::TableSetColumnIndex(2 + port);
snprintf(device_name, sizeof(device_name), "##device%d.%d", bus, port + 1);
ImguiID _(device_name);
ImGui::SetNextItemWidth(expComboWidth);
if (ImGui::BeginCombo(device_name, maple_expansion_device_name(config::MapleExpansionDevices[bus][port]), ImGuiComboFlags_None))
{
for (int i = 0; i < IM_ARRAYSIZE(maple_expansion_device_types); i++)
{
bool is_selected = config::MapleExpansionDevices[bus][port] == maple_expansion_device_type_from_index(i);
if (ImGui::Selectable(maple_expansion_device_types[i], &is_selected))
{
config::MapleExpansionDevices[bus][port] = maple_expansion_device_type_from_index(i);
maple_devices_changed = true;
}
if (is_selected)
ImGui::SetItemDefaultFocus();
}
ImGui::EndCombo();
}
}
if (config::MapleMainDevices[bus] == MDT_LightGun)
{
ImGui::TableSetColumnIndex(3);
snprintf(device_name, sizeof(device_name), "##device%d.xhair", bus);
ImguiID _(device_name);
u32 color = config::CrosshairColor[bus];
float xhairColor[4] {
(color & 0xff) / 255.f,
((color >> 8) & 0xff) / 255.f,
((color >> 16) & 0xff) / 255.f,
((color >> 24) & 0xff) / 255.f
};
bool colorChanged = ImGui::ColorEdit4("Crosshair color", xhairColor, ImGuiColorEditFlags_AlphaBar | ImGuiColorEditFlags_AlphaPreviewHalf
| ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_NoTooltip | ImGuiColorEditFlags_NoLabel);
ImGui::SameLine();
bool enabled = color != 0;
if (ImGui::Checkbox("Crosshair", &enabled) || colorChanged)
{
if (enabled)
{
config::CrosshairColor[bus] = (u8)(std::round(xhairColor[0] * 255.f))
| ((u8)(std::round(xhairColor[1] * 255.f)) << 8)
| ((u8)(std::round(xhairColor[2] * 255.f)) << 16)
| ((u8)(std::round(xhairColor[3] * 255.f)) << 24);
if (config::CrosshairColor[bus] == 0)
config::CrosshairColor[bus] = 0xC0FFFFFF;
}
else
{
config::CrosshairColor[bus] = 0;
}
}
is_there_any_xhair |= enabled;
}
ImGui::PopItemWidth();
}
ImGui::EndTable();
}
{
DisabledScope scope(!is_there_any_xhair);
OptionSlider("Crosshair Size", config::CrosshairSize, 10, 100);
}
OptionCheckbox("Per Game VMU A1", config::PerGameVmu, "When enabled, each game has its own VMU on port 1 of controller A.");
}
}