Files
lindbergh-loader_lindbergh-…/src/lindbergh/sdlInput.c
T

2062 lines
79 KiB
C

#include <GL/gl.h>
#include <SDL3/SDL.h>
#include <SDL3/SDL_events.h>
#include <SDL3/SDL_init.h>
#include <SDL3/SDL_joystick.h>
#include <SDL3/SDL_keyboard.h>
#include <SDL3/SDL_mouse.h>
#include <SDL3/SDL_scancode.h>
#include <libgen.h>
#include <math.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include "blitStretching.h"
#include "config.h"
#include "gameControllerMappings.h"
#include "iniParser.h"
#include "sdlInput.h"
#include "jvs.h"
#include "log.h"
#include "touchScreen.h"
// --- GLOBAL STATE AND MAPPINGS ---
ActionState gActionStates[MAX_ENTITIES][NUM_LOGICAL_ACTIONS] = {0};
JVSActionMapping gJvsMap[MAX_ENTITIES][NUM_LOGICAL_ACTIONS];
ChangedAction gChangedActions[NUM_LOGICAL_ACTIONS * MAX_ENTITIES];
LogicalActionProperties gActionProperties[MAX_ENTITIES][NUM_LOGICAL_ACTIONS] = {0};
int gNumChangedActions = 0;
SDLControllers sdlJoysticks = {0};
int gLastGunX[MAX_ENTITIES];
int gLastGunY[MAX_ENTITIES];
int gLastGunXDir[MAX_ENTITIES];
int gLastGunYDir[MAX_ENTITIES];
float gShakeValue[MAX_ENTITIES];
float gShakeIncreaseRate = 10.0f;
float gShakeDecayRate = 0.95f;
// --- NEW GLOBALS FOR GUID MAPPING ---
char gPlayerGUIDs[MAX_PLAYERS + 1][33]; // +1 for SYSTEM, 33 for 32 chars + null terminator
bool gPlayerGUIDsDirty = false; // Flag to check if we need to save the INI
// Direct lookup tables for high-performance event handling
ControlBinding gKeyBindings[SDL_SCANCODE_COUNT] = {0};
ControlBinding gMouseButtonBindings[MAX_MOUSE_BUTTONS] = {0};
ControlBinding gMouseAxisBindings[2] = {0}; // 0=X, 1=Y
BindingPair gJoyAxisBindings[MAX_JOYSTICKS][MAX_JOY_AXES] = {0};
ControlBinding gJoyButtonBindings[MAX_JOYSTICKS][MAX_JOY_BUTTONS] = {0};
BindingPair gControllerAxisBindings[MAX_JOYSTICKS][SDL_GAMEPAD_AXIS_COUNT] = {0};
ControlBinding gControllerButtonBindings[MAX_JOYSTICKS][SDL_GAMEPAD_BUTTON_COUNT] = {0};
HatBinding gJoyHatBindings[MAX_JOYSTICKS][MAX_JOY_HATS] = {0};
extern uint32_t gId;
extern int gGrp;
extern int gWidth;
extern int gHeight;
int widthOri;
int heightOri;
extern SDL_Window *sdlWindow;
extern bool sdlInputInitialized;
extern Dest dest;
extern int phX, phY, phW, phH;
int phIsDragging = 0;
extern int drawableW;
extern int drawableH;
int jvsAnalogueMaxValue;
int jvsAnalogueCenterValue;
GameType gameType;
// Map between string names and LogicalAction enums for INI parsing.
const struct
{
const char *name;
LogicalAction action;
} gActionNameMap[] = {{"Test", LA_Test},
{"Coin", LA_Coin},
{"GearUp", LA_GearUp},
{"GearDown", LA_GearDown},
{"ViewChange", LA_ViewChange},
{"MusicChange", LA_MusicChange},
{"Boost", LA_Boost},
{"BoostRight", LA_BoostRight},
{"Start", LA_Start},
{"Service", LA_Service},
{"Up", LA_Up},
{"Down", LA_Down},
{"Left", LA_Left},
{"Right", LA_Right},
{"Button1", LA_Button1},
{"Button2", LA_Button2},
{"Button3", LA_Button3},
{"Button4", LA_Button4},
{"Button5", LA_Button5},
{"Button6", LA_Button6},
{"Button7", LA_Button7},
{"Button8", LA_Button8},
{"Button9", LA_Button9},
{"Button10", LA_Button10},
{"Trigger", LA_Trigger},
{"OutOfScreen", LA_OutOfScreen},
{"Reload", LA_Reload},
{"GunButton", LA_GunButton},
{"ActionButton", LA_ActionButton},
{"PedalLeft", LA_PedalLeft},
{"PedalRight", LA_PedalRight},
{"Steer", LA_Steer},
{"Gas", LA_Gas},
{"Brake", LA_Brake},
{"GunX", LA_GunX},
{"GunY", LA_GunY},
{"Steer_Left", LA_Steer_Left},
{"Steer_Right", LA_Steer_Right},
{"Gas_Digital", LA_Gas_Digital},
{"Brake_Digital", LA_Brake_Digital},
{"ABC_X", LA_ABC_X},
{"ABC_Left", LA_ABC_Left},
{"ABC_Right", LA_ABC_Right},
{"ABC_Y", LA_ABC_Y},
{"ABC_Up", LA_ABC_Up},
{"ABC_Down", LA_ABC_Down},
{"GunTrigger", LA_GunTrigger},
{"MissileTrigger", LA_MissileTrigger},
{"ClimaxSwitch", LA_ClimaxSwitch},
{"Throttle", LA_Throttle},
{"Throttle_Accelerate", LA_Throttle_Accelerate},
{"Throttle_Slowdown", LA_Throttle_Slowdown},
{"CardInsert", LA_CardInsert},
{"Card1Insert", LA_Card1Insert},
{"Card2Insert", LA_Card2Insert},
{"ButtonA", LA_A},
{"ButtonB", LA_B},
{"ButtonC", LA_C},
{"ButtonD", LA_D},
{"ButtonE", LA_E},
{"ButtonF", LA_F},
{"ButtonG", LA_G},
{"ButtonH", LA_H},
{"ButtonI", LA_I},
{"ButtonJ", LA_J},
{"ButtonK", LA_K},
{"ButtonL", LA_L},
{"ButtonM", LA_M},
{"ButtonN", LA_N},
{"ButtonReach", LA_Reach},
{"ButtonChi", LA_Chi},
{"ButtonPon", LA_Pon},
{"ButtonKan", LA_Kan},
{"ButtonAgari", LA_Agari},
{"ButtonCancel", LA_Cancel}};
const int NUM_ACTION_NAMES = sizeof(gActionNameMap) / sizeof(gActionNameMap[0]);
// --- Default Bindings ---
// These arrays define the built-in control schemes that are used when controls.ini is not found.
// They are also used by createDefaultControlsIni() to generate a fresh INI file.
extern const ControlBinding gDefaultCommonBindings[];
extern const ControlBinding gDefaultDigitalBindings[];
extern const ControlBinding gDefaultDrivingBindings[];
extern const ControlBinding gDefaultAbcBindings[];
extern const ControlBinding gDefaultShootingBindings[];
extern const ControlBinding gDefaultMahjongBindings[];
extern const size_t gDefaultCommonBindingsSize;
extern const size_t gDefaultDigitalBindingsSize;
extern const size_t gDefaultDrivingBindingsSize;
extern const size_t gDefaultAbcBindingsSize;
extern const size_t gDefaultShootingBindingsSize;
extern const size_t gDefaultMahjongBindingsSize;
#ifndef COMPILING_LINDBERGH_ELF
// Forward declaration
void saveGuidsToIni();
/**
* @brief Initializes the entire SDL input system.
* This function initializes SDL subsystems, detects attached controllers,
* loads control schemes from controls.ini, or applies hardcoded defaults.
* It is the main entry point for setting up all input handling.
* @return 0 on success, non-zero on failure.
*/
int initSdlInput(char *controlsPath)
{
if (getConfig()->inputMode == 2)
return 0;
// Initialize GameController subsystem
if (!SDL_Init(SDL_INIT_GAMEPAD))
log_warn("Could not initialize SDL_GameController: %s\n", SDL_GetError());
// Loads the built-in database of controller
loadSDLControllerMappings();
// Load official and community-sourced controller mappings from a database file.
char *envDbPath = getenv("LINDBERGH_CONTROLS_DB_PATH");
if (envDbPath)
{
SDL_AddGamepadMappingsFromFile(envDbPath);
printf("%s loaded as a controller database.\n", basename(envDbPath));
}
gameType = getConfig()->gameType;
jvsAnalogueMaxValue = (1 << getJVSIO()->capabilities.analogueInBits) - 1;
jvsAnalogueCenterValue = jvsAnalogueMaxValue / 2;
memset(gPlayerGUIDs, 0, sizeof(gPlayerGUIDs));
// Set initial analog values for specific games that require it.
if (gGrp == GROUP_HOD4 || gGrp == GROUP_HOD4_TEST)
{
setAnalogue(ANALOGUE_5, jvsAnalogueCenterValue);
setAnalogue(ANALOGUE_6, jvsAnalogueCenterValue);
setAnalogue(ANALOGUE_7, jvsAnalogueCenterValue);
setAnalogue(ANALOGUE_8, jvsAnalogueCenterValue);
}
else if (gId == HARLEY_DAVIDSON)
{
setAnalogue(ANALOGUE_2, jvsAnalogueCenterValue);
}
else if (gameType == DRIVING)
{
setAnalogue(ANALOGUE_1, jvsAnalogueCenterValue);
setAnalogue(ANALOGUE_5, jvsAnalogueCenterValue);
}
// Initialize all mappings and properties.
initJvsMappings();
initActionProperties();
// Load control bindings from specified file on tries to load it from the current folder, otherwise use defaults.
struct stat buffer;
IniConfig *ini;
int isProfileLoaded = 0;
if (stat(controlsPath, &buffer) == 0)
{
log_debug("controls config file loaded from %s\n", controlsPath);
ini = iniLoad(controlsPath);
}
else
{
log_debug("controls config file \"controls.ini\" loaded from current folder\n");
ini = iniLoad("controls.ini");
}
if (ini)
{
printf("Found controls.ini, loading custom configuration...\n");
loadGlobalConfig(ini);
loadProfileFromIni(iniGetSection(ini, "Common"));
if (gameType == DRIVING)
isProfileLoaded = loadProfileFromIni(iniGetSection(ini, "Driving"));
else if (gameType == DIGITAL)
isProfileLoaded = loadProfileFromIni(iniGetSection(ini, "Digital"));
else if (gameType == SHOOTING)
isProfileLoaded = loadProfileFromIni(iniGetSection(ini, "Shooting"));
else if (gameType == ABC)
isProfileLoaded = loadProfileFromIni(iniGetSection(ini, "ABC"));
else if (gameType == MAHJONG)
isProfileLoaded = loadProfileFromIni(iniGetSection(ini, "Mahjong"));
iniFree(ini);
}
if (!isProfileLoaded)
{
setDefaultMappings();
}
// --- NEW GUID-BASED CONTROLLER MAPPING ---
int numJoysticks;
SDL_JoystickID *joystics = SDL_GetJoysticks(&numJoysticks);
int playerToDeviceIndex[MAX_PLAYERS + 1];
for (int i = 0; i <= MAX_PLAYERS; i++)
playerToDeviceIndex[i] = -1; // -1 means unassigned
if (joystics)
{
bool deviceIsClaimed[MAX_JOYSTICKS] = {false};
SDL_JoystickID instanceId;
// First Pass: Match saved GUIDs to available hardware
printf("Attempting to map controllers by saved GUIDs...\n");
for (int player = 1; player <= MAX_PLAYERS; player++)
{
if (strlen(gPlayerGUIDs[player]) > 0 && isProfileLoaded)
{
for (int i = 0; i < numJoysticks; i++)
{
if (deviceIsClaimed[i])
continue;
instanceId = joystics[i];
SDL_GUID guid = SDL_GetJoystickGUIDForID(instanceId);
char device_guid_str[33];
SDL_GUIDToString(guid, device_guid_str, sizeof(device_guid_str));
device_guid_str[4] = '0';
device_guid_str[5] = '0';
device_guid_str[6] = '0';
device_guid_str[7] = '0';
if (strcmp(gPlayerGUIDs[player], device_guid_str) == 0)
{
printf(" - Matched P%d to device %d ('%s') via GUID.\n", player, i, SDL_GetJoystickNameForID(instanceId));
playerToDeviceIndex[player] = instanceId;
deviceIsClaimed[i] = true;
break;
}
}
}
}
// Second Pass: Assign remaining players to unclaimed controllers
printf("Assigning remaining players to available controllers...\n");
for (int player = 1; player <= MAX_PLAYERS; player++)
{
if (playerToDeviceIndex[player] == -1) // If this player is still unassigned
{
for (int i = 0; i < numJoysticks; i++)
{
if (!deviceIsClaimed[i]) // Find the first unclaimed device
{
instanceId = joystics[i];
printf(" - Assigned P%d to first available device %d ('%s').\n", player, i, SDL_GetJoystickNameForID(instanceId));
playerToDeviceIndex[player] = instanceId;
deviceIsClaimed[i] = true;
// Get its GUID and mark for saving.
SDL_GUID guid = SDL_GetJoystickGUIDForID(instanceId);
SDL_GUIDToString(guid, gPlayerGUIDs[player], sizeof(gPlayerGUIDs[player]));
gPlayerGUIDs[player][4] = '0';
gPlayerGUIDs[player][5] = '0';
gPlayerGUIDs[player][6] = '0';
gPlayerGUIDs[player][7] = '0';
gPlayerGUIDsDirty = true;
break;
}
}
}
}
// Now, open devices in player-centric order.
// The index `i` in sdlJoysticks (0..MAX_JOYSTICKS-1) now corresponds to Player (i+1).
// This makes JOY0_, GC0_ always refer to P1, etc.
sdlJoysticks.joysticksCount = 0;
for (int player = 1; player <= MAX_PLAYERS; player++)
{
int deviceIndex = playerToDeviceIndex[player];
int internal_index = player - 1;
if (deviceIndex != -1)
{
if (SDL_IsGamepad(deviceIndex))
{
sdlJoysticks.controllers[internal_index] = SDL_OpenGamepad(deviceIndex);
}
else
{
sdlJoysticks.joysticks[internal_index] = SDL_OpenJoystick(deviceIndex);
}
if (sdlJoysticks.controllers[internal_index] || sdlJoysticks.joysticks[internal_index])
{
sdlJoysticks.joysticksCount++;
}
}
}
}
// Save any new GUID assignments back to the INI file.
if (isProfileLoaded)
saveGuidsToIni();
// After loading all bindings, scan them to identify combined axes.
detectCombinedAxes();
// Apply any final game-specific mapping overrides.
remapPerGame();
sdlInputInitialized = true;
return 0;
}
/**
* @brief Parses a logical action key from the INI file (e.g., "P1_Start").
* @param key The string key from the INI file.
* @param out_player Pointer to store the parsed JVSPlayer.
* @param out_action Pointer to store the parsed LogicalAction.
* @return True if parsing was successful, false otherwise.
*/
bool parseActionKey(const char *key, JVSPlayer *out_player, LogicalAction *out_action)
{
char genericKey[64];
int p_num;
if (sscanf(key, "P%d_%s", &p_num, genericKey) == 2)
{
if (p_num >= 1 && p_num <= MAX_PLAYERS)
*out_player = (JVSPlayer)p_num;
else
return false;
}
else
{
*out_player = PLAYER_1;
strcpy(genericKey, key);
if (strcmp(genericKey, "Test") == 0)
*out_player = SYSTEM;
}
for (int i = 0; i < NUM_ACTION_NAMES; i++)
{
if (strcmp(gActionNameMap[i].name, genericKey) == 0)
{
*out_action = gActionNameMap[i].action;
return true;
}
}
return false;
}
/**
* @brief Initializes the mapping from LogicalActions to JVS inputs.
* This function sets up the default JVS configuration for all players and actions,
* defining whether an action corresponds to a switch, an analog input, or a coin drop.
*/
void initJvsMappings()
{
for (int p = 0; p < MAX_ENTITIES; p++)
{
for (int i = 0; i < NUM_LOGICAL_ACTIONS; i++)
gJvsMap[p][i] = (JVSActionMapping){JVS_CALL_NONE, NONE};
}
gJvsMap[SYSTEM][LA_Test] = (JVSActionMapping){JVS_CALL_SWITCH, BUTTON_TEST};
for (int p = 1; p <= MAX_PLAYERS; p++)
{
gJvsMap[p][LA_Start] = (JVSActionMapping){JVS_CALL_SWITCH, BUTTON_START};
gJvsMap[p][LA_Service] = (JVSActionMapping){JVS_CALL_SWITCH, BUTTON_SERVICE};
gJvsMap[p][LA_Coin] = (JVSActionMapping){JVS_CALL_COIN, COIN};
int shootingPlayerIdx = (p - 1) * 2;
if (gameType == DIGITAL)
{
gJvsMap[p][LA_Up] = (JVSActionMapping){JVS_CALL_SWITCH, BUTTON_UP};
gJvsMap[p][LA_Down] = (JVSActionMapping){JVS_CALL_SWITCH, BUTTON_DOWN};
gJvsMap[p][LA_Left] = (JVSActionMapping){JVS_CALL_SWITCH, BUTTON_LEFT};
gJvsMap[p][LA_Right] = (JVSActionMapping){JVS_CALL_SWITCH, BUTTON_RIGHT};
gJvsMap[p][LA_Button1] = (JVSActionMapping){JVS_CALL_SWITCH, BUTTON_1};
gJvsMap[p][LA_Button2] = (JVSActionMapping){JVS_CALL_SWITCH, BUTTON_2};
gJvsMap[p][LA_Button3] = (JVSActionMapping){JVS_CALL_SWITCH, BUTTON_3};
}
else if (gameType == SHOOTING)
{
gJvsMap[p][LA_GunX] = (JVSActionMapping){JVS_CALL_ANALOGUE, ANALOGUE_1 + shootingPlayerIdx};
gJvsMap[p][LA_GunY] = (JVSActionMapping){JVS_CALL_ANALOGUE, ANALOGUE_2 + shootingPlayerIdx};
gJvsMap[p][LA_Trigger] = (JVSActionMapping){JVS_CALL_SWITCH, BUTTON_1};
gJvsMap[p][LA_Reload] = (JVSActionMapping){JVS_CALL_SWITCH, BUTTON_2};
gJvsMap[p][LA_GunButton] = (JVSActionMapping){JVS_CALL_SWITCH, BUTTON_3};
gJvsMap[p][LA_ActionButton] = (JVSActionMapping){JVS_CALL_SWITCH, BUTTON_4};
gJvsMap[p][LA_ChangeButton] = (JVSActionMapping){JVS_CALL_SWITCH, BUTTON_5};
gJvsMap[p][LA_PedalLeft] = (JVSActionMapping){JVS_CALL_SWITCH, BUTTON_LEFT};
gJvsMap[p][LA_PedalRight] = (JVSActionMapping){JVS_CALL_SWITCH, BUTTON_RIGHT};
}
}
switch ((int)gameType)
{
case DRIVING:
{
gJvsMap[PLAYER_1][LA_ViewChange] = (JVSActionMapping){JVS_CALL_SWITCH, BUTTON_DOWN};
gJvsMap[PLAYER_2][LA_GearUp] = (JVSActionMapping){JVS_CALL_SWITCH, BUTTON_UP};
gJvsMap[PLAYER_2][LA_GearDown] = (JVSActionMapping){JVS_CALL_SWITCH, BUTTON_DOWN};
gJvsMap[PLAYER_2][LA_Boost] = (JVSActionMapping){JVS_CALL_SWITCH, BUTTON_1};
gJvsMap[PLAYER_1][LA_CardInsert] = (JVSActionMapping){JVS_CALL_SWITCH, BUTTON_UP};
gJvsMap[PLAYER_1][LA_Steer] = (JVSActionMapping){JVS_CALL_ANALOGUE, ANALOGUE_1};
gJvsMap[PLAYER_1][LA_Gas] = (JVSActionMapping){JVS_CALL_ANALOGUE, ANALOGUE_2};
gJvsMap[PLAYER_1][LA_Brake] = (JVSActionMapping){JVS_CALL_ANALOGUE, ANALOGUE_3};
gJvsMap[PLAYER_1][LA_Gas_Digital] = (JVSActionMapping){JVS_CALL_ANALOGUE, ANALOGUE_2};
gJvsMap[PLAYER_1][LA_Brake_Digital] = (JVSActionMapping){JVS_CALL_ANALOGUE, ANALOGUE_3};
gJvsMap[PLAYER_1][LA_Steer_Left] = (JVSActionMapping){JVS_CALL_ANALOGUE, ANALOGUE_1};
gJvsMap[PLAYER_1][LA_Steer_Right] = (JVSActionMapping){JVS_CALL_ANALOGUE, ANALOGUE_1};
gJvsMap[PLAYER_1][LA_Up] = (JVSActionMapping){JVS_CALL_SWITCH, BUTTON_UP};
gJvsMap[PLAYER_1][LA_Down] = (JVSActionMapping){JVS_CALL_SWITCH, BUTTON_DOWN};
gJvsMap[PLAYER_1][LA_Left] = (JVSActionMapping){JVS_CALL_SWITCH, BUTTON_LEFT};
gJvsMap[PLAYER_1][LA_Right] = (JVSActionMapping){JVS_CALL_SWITCH, BUTTON_RIGHT};
gJvsMap[PLAYER_2][LA_Steer] = (JVSActionMapping){JVS_CALL_ANALOGUE, ANALOGUE_5};
gJvsMap[PLAYER_2][LA_Gas] = (JVSActionMapping){JVS_CALL_ANALOGUE, ANALOGUE_6};
gJvsMap[PLAYER_2][LA_Brake] = (JVSActionMapping){JVS_CALL_ANALOGUE, ANALOGUE_7};
gJvsMap[PLAYER_2][LA_Gas_Digital] = (JVSActionMapping){JVS_CALL_ANALOGUE, ANALOGUE_6};
gJvsMap[PLAYER_2][LA_Brake_Digital] = (JVSActionMapping){JVS_CALL_ANALOGUE, ANALOGUE_7};
gJvsMap[PLAYER_2][LA_Steer_Left] = (JVSActionMapping){JVS_CALL_ANALOGUE, ANALOGUE_5};
gJvsMap[PLAYER_2][LA_Steer_Right] = (JVSActionMapping){JVS_CALL_ANALOGUE, ANALOGUE_5};
}
break;
case ABC:
{
gJvsMap[PLAYER_1][LA_ABC_X] = (JVSActionMapping){JVS_CALL_ANALOGUE, ANALOGUE_1};
gJvsMap[PLAYER_1][LA_ABC_Left] = (JVSActionMapping){JVS_CALL_ANALOGUE, ANALOGUE_1};
gJvsMap[PLAYER_1][LA_ABC_Right] = (JVSActionMapping){JVS_CALL_ANALOGUE, ANALOGUE_1};
gJvsMap[PLAYER_1][LA_ABC_Y] = (JVSActionMapping){JVS_CALL_ANALOGUE, ANALOGUE_2};
gJvsMap[PLAYER_1][LA_ABC_Up] = (JVSActionMapping){JVS_CALL_ANALOGUE, ANALOGUE_2};
gJvsMap[PLAYER_1][LA_ABC_Down] = (JVSActionMapping){JVS_CALL_ANALOGUE, ANALOGUE_2};
gJvsMap[PLAYER_1][LA_Throttle] = (JVSActionMapping){JVS_CALL_ANALOGUE, ANALOGUE_3};
gJvsMap[PLAYER_1][LA_Throttle_Accelerate] = (JVSActionMapping){JVS_CALL_ANALOGUE, ANALOGUE_3};
gJvsMap[PLAYER_1][LA_Throttle_Slowdown] = (JVSActionMapping){JVS_CALL_ANALOGUE, ANALOGUE_3};
gJvsMap[PLAYER_1][LA_GunTrigger] = (JVSActionMapping){JVS_CALL_SWITCH, BUTTON_1};
gJvsMap[PLAYER_1][LA_MissileTrigger] = (JVSActionMapping){JVS_CALL_SWITCH, BUTTON_2};
gJvsMap[PLAYER_1][LA_ClimaxSwitch] = (JVSActionMapping){JVS_CALL_SWITCH, BUTTON_3};
}
break;
case MAHJONG:
{
gJvsMap[PLAYER_1][LA_A] = (JVSActionMapping){JVS_CALL_SWITCH, BUTTON_RIGHT};
gJvsMap[PLAYER_1][LA_B] = (JVSActionMapping){JVS_CALL_SWITCH, BUTTON_LEFT};
gJvsMap[PLAYER_1][LA_C] = (JVSActionMapping){JVS_CALL_SWITCH, BUTTON_UP};
gJvsMap[PLAYER_1][LA_D] = (JVSActionMapping){JVS_CALL_SWITCH, BUTTON_DOWN};
gJvsMap[PLAYER_1][LA_E] = (JVSActionMapping){JVS_CALL_SWITCH, BUTTON_1};
gJvsMap[PLAYER_1][LA_F] = (JVSActionMapping){JVS_CALL_SWITCH, BUTTON_2};
gJvsMap[PLAYER_1][LA_G] = (JVSActionMapping){JVS_CALL_SWITCH, BUTTON_3};
gJvsMap[PLAYER_1][LA_H] = (JVSActionMapping){JVS_CALL_SWITCH, BUTTON_4};
gJvsMap[PLAYER_1][LA_I] = (JVSActionMapping){JVS_CALL_SWITCH, BUTTON_5};
gJvsMap[PLAYER_1][LA_J] = (JVSActionMapping){JVS_CALL_SWITCH, BUTTON_6};
gJvsMap[PLAYER_1][LA_K] = (JVSActionMapping){JVS_CALL_SWITCH, BUTTON_7};
gJvsMap[PLAYER_2][LA_L] = (JVSActionMapping){JVS_CALL_SWITCH, BUTTON_RIGHT};
gJvsMap[PLAYER_2][LA_M] = (JVSActionMapping){JVS_CALL_SWITCH, BUTTON_LEFT};
gJvsMap[PLAYER_2][LA_N] = (JVSActionMapping){JVS_CALL_SWITCH, BUTTON_UP};
gJvsMap[PLAYER_2][LA_Chi] = (JVSActionMapping){JVS_CALL_SWITCH, BUTTON_DOWN};
gJvsMap[PLAYER_2][LA_Pon] = (JVSActionMapping){JVS_CALL_SWITCH, BUTTON_1};
gJvsMap[PLAYER_2][LA_Kan] = (JVSActionMapping){JVS_CALL_SWITCH, BUTTON_2};
gJvsMap[PLAYER_2][LA_Reach] = (JVSActionMapping){JVS_CALL_SWITCH, BUTTON_3};
gJvsMap[PLAYER_2][LA_Agari] = (JVSActionMapping){JVS_CALL_SWITCH, BUTTON_4};
gJvsMap[PLAYER_2][LA_Cancel] = (JVSActionMapping){JVS_CALL_SWITCH, BUTTON_5};
gJvsMap[PLAYER_2][LA_CardInsert] = (JVSActionMapping){JVS_CALL_SWITCH, BUTTON_7};
}
break;
}
gJvsMap[PLAYER_1][LA_Card1Insert] = (JVSActionMapping){JVS_CALL_SWITCH, BUTTON_4};
gJvsMap[PLAYER_2][LA_Card2Insert] = (JVSActionMapping){JVS_CALL_SWITCH, BUTTON_4};
}
/**
* @brief Applies specific JVS mapping overrides for certain games.
* After the default mappings are set up, this function can patch them with
* game-specific exceptions (e.g., if a game uses an unusual button layout).
*/
void remapPerGame()
{
if (gId == R_TUNED)
{
gJvsMap[PLAYER_1][LA_BoostRight] = (JVSActionMapping){JVS_CALL_SWITCH, BUTTON_RIGHT};
gJvsMap[PLAYER_1][LA_CardInsert] = (JVSActionMapping){JVS_CALL_SWITCH, BUTTON_UP};
}
else if (gGrp == GROUP_HUMMER)
gJvsMap[PLAYER_2][LA_Boost] = (JVSActionMapping){JVS_CALL_SWITCH, BUTTON_DOWN};
else if (gGrp == GROUP_ID4_EXP || gGrp == GROUP_ID4_JAP || gGrp == GROUP_ID5)
gJvsMap[PLAYER_1][LA_ViewChange] = (JVSActionMapping){JVS_CALL_SWITCH, BUTTON_1};
else if (gId == LETS_GO_JUNGLE || gId == LETS_GO_JUNGLE_REVA)
{
gJvsMap[PLAYER_1][LA_GunX] = (JVSActionMapping){JVS_CALL_ANALOGUE, ANALOGUE_2};
gJvsMap[PLAYER_1][LA_GunY] = (JVSActionMapping){JVS_CALL_ANALOGUE, ANALOGUE_1};
gJvsMap[PLAYER_2][LA_GunX] = (JVSActionMapping){JVS_CALL_ANALOGUE, ANALOGUE_2};
gJvsMap[PLAYER_2][LA_GunY] = (JVSActionMapping){JVS_CALL_ANALOGUE, ANALOGUE_1};
}
else if (gId == HARLEY_DAVIDSON)
{
gJvsMap[PLAYER_1][LA_Steer] = (JVSActionMapping){JVS_CALL_ANALOGUE, ANALOGUE_2};
gJvsMap[PLAYER_1][LA_Steer_Left] = (JVSActionMapping){JVS_CALL_ANALOGUE, ANALOGUE_2};
gJvsMap[PLAYER_1][LA_Steer_Right] = (JVSActionMapping){JVS_CALL_ANALOGUE, ANALOGUE_2};
gJvsMap[PLAYER_1][LA_Gas] = (JVSActionMapping){JVS_CALL_ANALOGUE, ANALOGUE_1};
gJvsMap[PLAYER_1][LA_Gas_Digital] = (JVSActionMapping){JVS_CALL_ANALOGUE, ANALOGUE_1};
gJvsMap[PLAYER_1][LA_Brake] = (JVSActionMapping){JVS_CALL_ANALOGUE, ANALOGUE_4};
gJvsMap[PLAYER_1][LA_Brake_Digital] = (JVSActionMapping){JVS_CALL_ANALOGUE, ANALOGUE_4};
gJvsMap[PLAYER_1][LA_MusicChange] = (JVSActionMapping){JVS_CALL_SWITCH, BUTTON_1};
gJvsMap[PLAYER_1][LA_ViewChange] = (JVSActionMapping){JVS_CALL_SWITCH, BUTTON_2};
gJvsMap[PLAYER_1][LA_GearUp] = (JVSActionMapping){JVS_CALL_SWITCH, BUTTON_3};
gJvsMap[PLAYER_1][LA_GearDown] = (JVSActionMapping){JVS_CALL_SWITCH, BUTTON_4};
}
else if (gId == PRIMEVAL_HUNT)
{
gJvsMap[PLAYER_1][LA_Reload] = (JVSActionMapping){JVS_CALL_SWITCH, BUTTON_2};
gJvsMap[PLAYER_1][LA_GunButton] = (JVSActionMapping){JVS_CALL_SWITCH, BUTTON_3};
gJvsMap[PLAYER_2][LA_Reload] = (JVSActionMapping){JVS_CALL_SWITCH, BUTTON_2};
gJvsMap[PLAYER_2][LA_GunButton] = (JVSActionMapping){JVS_CALL_SWITCH, BUTTON_3};
}
}
/**
* @brief Initializes special properties for certain logical actions.
* For example, it defines which analog actions should auto-center (like a joystick)
* and which should not (like a gas pedal).
*/
void initActionProperties()
{
for (int p = 0; p < MAX_ENTITIES; p++)
{
for (int i = 0; i < NUM_LOGICAL_ACTIONS; i++)
{
gActionProperties[p][i].isCentering = false;
gActionProperties[p][i].isCombinedAxis = false;
// Default deadzone for most actions is a sensible 8000
gActionProperties[p][i].deadzone = 8000;
}
// Explicitly define which actions are centering (like sticks)
gActionProperties[p][LA_Steer].isCentering = true;
// Set smaller default deadzones for non-centering actions
gActionProperties[p][LA_Gas].deadzone = 500;
gActionProperties[p][LA_Brake].deadzone = 500;
}
gActionProperties[PLAYER_1][LA_ABC_X].isCentering = true;
gActionProperties[PLAYER_1][LA_ABC_Y].isCentering = true;
gActionProperties[PLAYER_1][LA_Throttle].isCentering = true;
}
/**
* @brief Adds a new control binding to the appropriate lookup table.
* It also checks for and reports any conflicts where a single physical input
* is mapped to two different logical actions. For axes, it allows up to two
* bindings to support split-axis configurations.
* @param binding The ControlBinding to add.
*/
void addBinding(ControlBinding binding)
{
char name1[128], name2[128];
char inputName[64] = "Unknown";
// Handle non-axis types first, as they only allow one binding per input.
if (binding.type != INPUT_TYPE_JOY_AXIS && binding.type != INPUT_TYPE_GAMEPAD_AXIS)
{
ControlBinding *existing = NULL;
switch (binding.type)
{
case INPUT_TYPE_KEY:
if (binding.sdlId < SDL_SCANCODE_COUNT)
{
existing = &gKeyBindings[binding.sdlId];
snprintf(inputName, sizeof(inputName), "KEY_%s", SDL_GetScancodeName(binding.sdlId));
}
break;
case INPUT_TYPE_MOUSE_BUTTON:
if (binding.sdlId < MAX_MOUSE_BUTTONS)
{
existing = &gMouseButtonBindings[binding.sdlId];
snprintf(inputName, sizeof(inputName), "MOUSE_BUTTON_%d", binding.sdlId);
}
break;
case INPUT_TYPE_MOUSE_AXIS:
if (binding.sdlId < 2)
{
existing = &gMouseAxisBindings[binding.sdlId];
snprintf(inputName, sizeof(inputName), "MOUSE_AXIS_%d", binding.sdlId);
}
break;
case INPUT_TYPE_JOY_BUTTON:
if (binding.deviceIndex < MAX_JOYSTICKS && binding.sdlId < MAX_JOY_BUTTONS)
{
existing = &gJoyButtonBindings[binding.deviceIndex][binding.sdlId];
snprintf(inputName, sizeof(inputName), "JOY%d_BUTTON_%d", binding.deviceIndex, binding.sdlId);
}
break;
case INPUT_TYPE_GAMEPAD_BUTTON:
if (binding.deviceIndex < MAX_JOYSTICKS && binding.sdlId < SDL_GAMEPAD_BUTTON_COUNT)
{
existing = &gControllerButtonBindings[binding.deviceIndex][binding.sdlId];
snprintf(inputName, sizeof(inputName), "GC%d_BUTTON_%s", binding.deviceIndex,
SDL_GetGamepadStringForButton(binding.sdlId));
}
break;
default:
break;
}
if (existing)
{
if (existing->type != INPUT_TYPE_NONE && existing->action != binding.action)
{
getLogicalActionString(existing, name1, sizeof(name1), "");
getLogicalActionString(&binding, name2, sizeof(name2), "");
fprintf(stderr, "ERROR: Input assignment conflict for '%s'.\n", inputName);
fprintf(stderr, " It is mapped to both '%s' and '%s'. Please fix controls.ini.\n", name1, name2);
}
else
{
*existing = binding;
}
}
return;
}
if (binding.type == INPUT_TYPE_JOY_HAT)
{
if (binding.deviceIndex < MAX_JOYSTICKS && binding.sdlId < MAX_JOY_HATS)
{
HatBinding *hat = &gJoyHatBindings[binding.deviceIndex][binding.sdlId];
ControlBinding *targetSlot = NULL;
// Determine which directional slot to use
if (binding.axisThreshold == SDL_HAT_UP)
targetSlot = &hat->up;
else if (binding.axisThreshold == SDL_HAT_DOWN)
targetSlot = &hat->down;
else if (binding.axisThreshold == SDL_HAT_LEFT)
targetSlot = &hat->left;
else if (binding.axisThreshold == SDL_HAT_RIGHT)
targetSlot = &hat->right;
if (targetSlot)
{
if (targetSlot->type != INPUT_TYPE_NONE && targetSlot->action != binding.action)
printf("Conflict detection");
else
*targetSlot = binding;
}
}
return;
}
// Special handling for axis types to allow two bindings per physical axis.
BindingPair *pair = NULL;
int devIdx = binding.deviceIndex;
int axisId = binding.sdlId;
if (binding.type == INPUT_TYPE_JOY_AXIS)
{
if (devIdx < MAX_JOYSTICKS && axisId < MAX_JOY_AXES)
{
pair = &gJoyAxisBindings[devIdx][axisId];
snprintf(inputName, sizeof(inputName), "JOY%d_AXIS_%d", devIdx, axisId);
}
}
else if (binding.type == INPUT_TYPE_GAMEPAD_AXIS)
{
if (devIdx < MAX_JOYSTICKS && axisId < SDL_GAMEPAD_AXIS_COUNT)
{
pair = &gControllerAxisBindings[devIdx][axisId];
snprintf(inputName, sizeof(inputName), "GC%d_AXIS_%s", devIdx, SDL_GetGamepadStringForAxis(axisId));
}
}
if (pair)
{
// Find an empty slot in the pair to place the binding.
if (pair->bindings[0].type == INPUT_TYPE_NONE)
{
pair->bindings[0] = binding;
}
else if (pair->bindings[1].type == INPUT_TYPE_NONE)
{
// Check for redundant bindings before adding.
if (pair->bindings[0].action == binding.action)
{
getLogicalActionString(&binding, name1, sizeof(name1), "");
fprintf(stderr, "WARNING: Redundant binding for '%s' on action '%s'. Check controls.ini.\n", inputName, name1);
}
pair->bindings[1] = binding;
}
else
{
fprintf(stderr, "ERROR: Cannot bind more than two actions to axis '%s'. Please fix controls.ini.\n", inputName);
}
}
}
/**
* @brief Applies the default control mappings if controls.ini is not found.
* Iterates through the default binding arrays and adds them to the system.
*/
void setDefaultMappings()
{
log_warn("controls.ini not found. Applying default mappings...\n");
// Add common bindings
for (size_t i = 0; i < gDefaultCommonBindingsSize; i++)
{
addBinding(gDefaultCommonBindings[i]);
}
// Add game-specific bindings
const ControlBinding *game_bindings = NULL;
size_t bindingsCount = 0;
if (gameType == DIGITAL)
{
game_bindings = gDefaultDigitalBindings;
bindingsCount = gDefaultDigitalBindingsSize;
}
else if (gameType == DRIVING)
{
game_bindings = gDefaultDrivingBindings;
bindingsCount = gDefaultDrivingBindingsSize;
}
else if (gameType == ABC)
{
game_bindings = gDefaultAbcBindings;
bindingsCount = gDefaultAbcBindingsSize;
}
else if (gameType == SHOOTING)
{
game_bindings = gDefaultShootingBindings;
bindingsCount = gDefaultShootingBindingsSize;
}
else if (gameType == MAHJONG)
{
game_bindings = gDefaultMahjongBindings;
bindingsCount = gDefaultMahjongBindingsSize;
}
if (game_bindings)
{
for (size_t i = 0; i < bindingsCount; i++)
{
addBinding(game_bindings[i]);
}
}
}
/**
* @brief Parses a single input source string (e.g., "KEY_A", "JOY0_AXIS_0_NEGATIVE")
* and creates a ControlBinding for it.
* @param token The string token representing the physical input.
* @param player The player this binding belongs to.
* @param action The logical action this input should trigger.
*/
void parseSdlSource(const char *token, JVSPlayer player, LogicalAction action)
{
ControlBinding map = {0};
map.action = action;
map.player = player;
map.type = INPUT_TYPE_NONE;
char mutableToken[128];
strncpy(mutableToken, token, 127);
mutableToken[127] = '\0';
char *invertedStr = strstr(mutableToken, "_INVERTED");
if (invertedStr)
{
map.isInverted = true;
*invertedStr = '\0';
}
else
{
map.isInverted = false;
}
if (strncmp(mutableToken, "KEY_", 4) == 0)
{
map.type = INPUT_TYPE_KEY;
if (strcmp(mutableToken + 4, "Comma") == 0 || strcmp(mutableToken + 4, "COMMA") == 0)
map.sdlId = SDL_SCANCODE_COMMA;
else
map.sdlId = SDL_GetScancodeFromName(mutableToken + 4);
}
else if (strncmp(mutableToken, "GC", 2) == 0)
{
int devId;
char typeStr[32], nameStr[64];
if (sscanf(mutableToken, "GC%d_%[^_]_%[^_]", &devId, typeStr, nameStr) == 3)
{
map.deviceIndex = devId;
if (strcmp(typeStr, "BUTTON") == 0)
{
map.type = INPUT_TYPE_GAMEPAD_BUTTON;
map.sdlId = SDL_GetGamepadButtonFromString(nameStr);
}
else if (strcmp(typeStr, "AXIS") == 0)
{
map.type = INPUT_TYPE_GAMEPAD_AXIS;
map.sdlId = SDL_GetGamepadAxisFromString(nameStr);
// Check for axis mode suffixes
if (strstr(mutableToken, "_POSITIVE_HALF"))
{
map.axisMode = AXIS_MODE_POSITIVE_HALF;
}
else if (strstr(mutableToken, "_NEGATIVE_HALF"))
{
map.axisMode = AXIS_MODE_NEGATIVE_HALF;
}
else if (strstr(mutableToken, "_POSITIVE"))
{
map.axisMode = AXIS_MODE_DIGITAL;
map.axisThreshold = 1;
}
else if (strstr(mutableToken, "_NEGATIVE"))
{
map.axisMode = AXIS_MODE_DIGITAL;
map.axisThreshold = -1;
}
else
{
map.axisMode = AXIS_MODE_FULL;
}
}
}
}
else if (strncmp(mutableToken, "JOY", 3) == 0)
{
int devId, compId;
char dirStr[32];
if (sscanf(mutableToken, "JOY%d_BUTTON_%d", &devId, &compId) == 2)
{
map.type = INPUT_TYPE_JOY_BUTTON;
map.deviceIndex = devId;
map.sdlId = compId;
}
else if (sscanf(mutableToken, "JOY%d_AXIS_%d", &devId, &compId) == 2)
{
map.type = INPUT_TYPE_JOY_AXIS;
map.deviceIndex = devId;
map.sdlId = compId;
if (strstr(mutableToken, "_POSITIVE_HALF"))
map.axisMode = AXIS_MODE_POSITIVE_HALF;
else if (strstr(mutableToken, "_NEGATIVE_HALF"))
map.axisMode = AXIS_MODE_NEGATIVE_HALF;
else if (strstr(mutableToken, "_POSITIVE"))
{
map.axisMode = AXIS_MODE_DIGITAL;
map.axisThreshold = 1;
}
else if (strstr(mutableToken, "_NEGATIVE"))
{
map.axisMode = AXIS_MODE_DIGITAL;
map.axisThreshold = -1;
}
else
map.axisMode = AXIS_MODE_FULL;
}
else if (sscanf(mutableToken, "JOY%d_HAT%d_%s", &devId, &compId, dirStr) == 3)
{
map.type = INPUT_TYPE_JOY_HAT;
map.deviceIndex = devId;
map.sdlId = compId;
if (strcmp(dirStr, "UP") == 0)
map.axisThreshold = SDL_HAT_UP;
else if (strcmp(dirStr, "DOWN") == 0)
map.axisThreshold = SDL_HAT_DOWN;
else if (strcmp(dirStr, "LEFT") == 0)
map.axisThreshold = SDL_HAT_LEFT;
else if (strcmp(dirStr, "RIGHT") == 0)
map.axisThreshold = SDL_HAT_RIGHT;
}
}
else if (strncmp(mutableToken, "MOUSE_", 6) == 0)
{
const char *mouseToken = mutableToken + 6;
if (strcmp(mouseToken, "AXIS_X") == 0)
{
map.type = INPUT_TYPE_MOUSE_AXIS;
map.sdlId = 0;
}
else if (strcmp(mouseToken, "AXIS_Y") == 0)
{
map.type = INPUT_TYPE_MOUSE_AXIS;
map.sdlId = 1;
}
else if (strcmp(mouseToken, "LEFT_BUTTON") == 0)
{
map.type = INPUT_TYPE_MOUSE_BUTTON;
map.sdlId = SDL_BUTTON_LEFT;
}
else if (strcmp(mouseToken, "RIGHT_BUTTON") == 0)
{
map.type = INPUT_TYPE_MOUSE_BUTTON;
map.sdlId = SDL_BUTTON_RIGHT;
}
else if (strcmp(mouseToken, "MIDDLE_BUTTON") == 0)
{
map.type = INPUT_TYPE_MOUSE_BUTTON;
map.sdlId = SDL_BUTTON_MIDDLE;
}
}
if (map.type != INPUT_TYPE_NONE)
addBinding(map);
}
/**
* @brief Finds the internal player-centric index for a GameController from an SDL_JoystickID.
* The internal index (0, 1, 2...) corresponds to the player number (P1, P2, P3...).
* @param instance_id The SDL_JoystickID from an SDL_Event.
* @return The internal player-centric index, or -1 if not found.
*/
int getControllerID(SDL_JoystickID instance_id)
{
for (int i = 0; i < MAX_JOYSTICKS; i++)
{
if (sdlJoysticks.controllers[i])
{
SDL_Joystick *joy = SDL_GetGamepadJoystick(sdlJoysticks.controllers[i]);
if (joy && SDL_GetJoystickID(joy) == instance_id)
return i;
}
}
return -1;
}
/**
* @brief Finds the internal player-centric index for a raw Joystick from an SDL_JoystickID.
* @param instance_id The SDL_JoystickID from an SDL_Event.
* @return The internal player-centric index, or -1 if not found.
*/
int getJoystickID(SDL_JoystickID instance_id)
{
for (int i = 0; i < MAX_JOYSTICKS; i++)
{
if (sdlJoysticks.joysticks[i])
{
if (SDL_GetJoystickID(sdlJoysticks.joysticks[i]) == instance_id)
return i;
}
}
return -1;
}
/**
* @brief Applies game-specific logic to determine the correct player for an action.
* Some actions are hardcoded to a specific player (e.g., P2_GearUp).
* @param action The logical action being assigned.
* @param player The player parsed from the INI file.
* @return The corrected JVSPlayer.
*/
JVSPlayer fixPlayerForAction(LogicalAction action, int player)
{
if (action == LA_Boost)
return PLAYER_2;
if ((action == LA_GearUp || action == LA_GearDown) && gId != HARLEY_DAVIDSON)
return PLAYER_2;
return player;
}
/**
* @brief Determines if a Mahjong action belongs to Player 2.
* @param action The logical action to check.
* @return True if it's a Player 2 mahjong action, false otherwise.
*/
bool isMahjongP2(LogicalAction action)
{
switch (action)
{
case LA_L:
case LA_M:
case LA_N:
case LA_Chi:
case LA_Pon:
case LA_Kan:
case LA_Reach:
case LA_Agari:
case LA_Cancel:
case LA_CardInsert:
return true;
default:
return false;
}
}
/**
* @brief Loads and applies all control bindings from a given INI section.
* @param section Pointer to the IniSection to parse.
*/
int loadProfileFromIni(const IniSection *section)
{
if (!section)
return 0;
for (int i = 0; i < section->numPairs; i++)
{
JVSPlayer player;
LogicalAction action;
if (!parseActionKey(section->pairs[i].key, &player, &action))
{
printf("Warning: Unknown action '%s'\n", section->pairs[i].key);
continue;
}
if (player != SYSTEM && (action != LA_Card1Insert || action != LA_Card2Insert))
player = fixPlayerForAction(action, player);
if (action == LA_Card2Insert && gameType == DIGITAL)
player = PLAYER_2;
if (strcmp(section->name, "Mahjong") == 0 && isMahjongP2(action))
player = PLAYER_2;
char *value_copy = strdup(section->pairs[i].value);
char *token = strtok(value_copy, ", ");
while (token != NULL)
{
parseSdlSource(token, player, action);
token = strtok(NULL, ", ");
}
free(value_copy);
}
return 1;
}
/**
* @brief Loads global configuration settings from the [Config] section of the INI.
* This now includes loading saved controller GUIDs for player mapping.
* @param ini Pointer to the loaded IniConfig.
*/
void loadGlobalConfig(const IniConfig *ini)
{
const IniSection *configSection = iniGetSection(ini, "Config");
if (configSection)
{
printf("Loading global settings from [Config] section...\n");
for (int i = 0; i < configSection->numPairs; i++)
{
const char *key = configSection->pairs[i].key;
const char *value = configSection->pairs[i].value;
int p_num = -1;
char actionKeyBuffer[64];
// Check for player-specific prefix like "P1_"
if (sscanf(key, "P%d_%s", &p_num, actionKeyBuffer) == 2)
{
// Player-specific key found (e.g., "P1_Steer_DeadZone")
char *deadzoneSuffix = strstr(actionKeyBuffer, "_DeadZone");
if (deadzoneSuffix)
{
*deadzoneSuffix = '\0'; // Trim to just the action name
for (int j = 0; j < NUM_ACTION_NAMES; j++)
{
if (strcmp(gActionNameMap[j].name, actionKeyBuffer) == 0)
{
LogicalAction action = gActionNameMap[j].action;
if (p_num >= 1 && p_num <= MAX_PLAYERS)
{
gActionProperties[p_num][action].deadzone = atoi(value);
printf(" Set P%d_%s DeadZone to %d\n", p_num, actionKeyBuffer, gActionProperties[p_num][action].deadzone);
}
break;
}
}
}
}
else
{
// Handle generic keys and other settings
char *deadzonePos = strstr(key, "_DeadZone");
if (deadzonePos != NULL)
{
// Generic key found (e.g., "Steer_DeadZone")
// Apply this setting to ALL players as a default
size_t actionNameLen = deadzonePos - key;
strncpy(actionKeyBuffer, key, actionNameLen);
actionKeyBuffer[actionNameLen] = '\0';
for (int j = 0; j < NUM_ACTION_NAMES; j++)
{
if (strcmp(gActionNameMap[j].name, actionKeyBuffer) == 0)
{
LogicalAction action = gActionNameMap[j].action;
int deadzoneVal = atoi(value);
for (int p = 1; p <= MAX_PLAYERS; p++)
{
// Set this only if a player-specific one wasn't already set
// (This requires ordering in the INI, or a second pass - for simplicity, we just set it)
gActionProperties[p][action].deadzone = deadzoneVal;
}
printf(" Set %s DeadZone to %d for all players (default)\n", actionKeyBuffer, deadzoneVal);
break;
}
}
}
else if (strcmp(key, "ShakeIncreaseRate") == 0)
{
gShakeIncreaseRate = atof(value);
printf(" Set ShakeIncreaseRate to %f\n", gShakeIncreaseRate);
}
else if (strcmp(key, "ShakeDecayRate") == 0)
{
gShakeDecayRate = atof(value);
printf(" Set ShakeDecayRate to %f\n", gShakeDecayRate);
}
}
}
}
// Load saved controller GUIDs
const IniSection *guidSection = iniGetSection(ini, "ControllerGUIDs");
if (guidSection)
{
printf("Loading saved controller GUIDs...\n");
for (int i = 0; i < guidSection->numPairs; i++)
{
int p_num;
if (sscanf(guidSection->pairs[i].key, "P%d_GUID", &p_num) == 1)
{
if (p_num >= 1 && p_num <= MAX_PLAYERS)
{
strncpy(gPlayerGUIDs[p_num], guidSection->pairs[i].value, 32);
gPlayerGUIDs[p_num][32] = '\0';
printf(" - Loaded P%d GUID: %s\n", p_num, gPlayerGUIDs[p_num]);
}
}
}
}
}
/**
* @brief Adds an action to a list of "dirty" actions that need processing.
* This prevents redundant processing if an action's state changes multiple
* times between frames.
* @param player The player associated with the action.
* @param action The action that has changed state.
*/
void addActionToDirtyList(JVSPlayer player, LogicalAction action)
{
for (int i = 0; i < gNumChangedActions; i++)
{
if (gChangedActions[i].player == player && gChangedActions[i].action == action)
return;
}
if (gNumChangedActions < (NUM_LOGICAL_ACTIONS * MAX_ENTITIES))
{
gChangedActions[gNumChangedActions++] = (ChangedAction){player, action};
}
}
/**
* @brief Processes a single SDL_Event from the event queue.
* This is the main event handling function. It checks the event type and
* updates the internal state of the corresponding logical action.
* @param e Pointer to the SDL_Event to process.
*/
void processSdlEvent(const SDL_Event *e)
{
if (!sdlInputInitialized)
return;
switch (e->type)
{
case SDL_EVENT_KEY_DOWN:
case SDL_EVENT_KEY_UP:
{
ControlBinding *binding = &gKeyBindings[e->key.scancode];
if (binding->type != INPUT_TYPE_NONE)
{
bool isActive = (e->type == SDL_EVENT_KEY_DOWN);
if (gActionStates[binding->player][binding->action].isActive != isActive)
{
gActionStates[binding->player][binding->action].isActive = isActive;
addActionToDirtyList(binding->player, binding->action);
}
}
}
break;
case SDL_EVENT_JOYSTICK_BUTTON_DOWN:
case SDL_EVENT_JOYSTICK_BUTTON_UP:
{
// printf("Joy Button: %d\n", e->jbutton.button);
int devIdx = getJoystickID(e->jbutton.which);
if (devIdx != -1 && e->jbutton.button < MAX_JOY_BUTTONS)
{
ControlBinding *binding = &gJoyButtonBindings[devIdx][e->jbutton.button];
if (binding->type != INPUT_TYPE_NONE)
{
bool is_active = (e->type == SDL_EVENT_JOYSTICK_BUTTON_DOWN);
if (gActionStates[binding->player][binding->action].isActive != is_active)
{
gActionStates[binding->player][binding->action].isActive = is_active;
addActionToDirtyList(binding->player, binding->action);
}
}
}
}
break;
case SDL_EVENT_GAMEPAD_BUTTON_DOWN:
case SDL_EVENT_GAMEPAD_BUTTON_UP:
{
// printf("GC Button: %s\n", SDL_GameControllerGetStringForButton(e->cbutton.button));
int devIdx = getControllerID(e->gbutton.which);
if (devIdx != -1 && e->gbutton.button < SDL_GAMEPAD_BUTTON_COUNT)
{
ControlBinding *binding = &gControllerButtonBindings[devIdx][e->gbutton.button];
if (binding->type != INPUT_TYPE_NONE)
{
bool is_active = (e->type == SDL_EVENT_GAMEPAD_BUTTON_DOWN);
if (gActionStates[binding->player][binding->action].isActive != is_active)
{
gActionStates[binding->player][binding->action].isActive = is_active;
addActionToDirtyList(binding->player, binding->action);
}
}
}
}
break;
case SDL_EVENT_JOYSTICK_AXIS_MOTION:
{
// printf("Jaxis: %d, Value: %d, Device: %d\n", e->jaxis.axis, e->jaxis.value, e->jaxis.which);
int devIdx = getJoystickID(e->jaxis.which);
if (devIdx != -1 && e->jaxis.axis < MAX_JOY_AXES)
{
BindingPair *pair = &gJoyAxisBindings[devIdx][e->jaxis.axis];
for (int i = 0; i < 2; i++) // Loop over the pair of possible bindings
{
ControlBinding *binding = &pair->bindings[i];
if (binding->type == INPUT_TYPE_NONE)
continue;
// UPDATED: Get deadzone from the action's properties
const int dead_zone = gActionProperties[binding->player][binding->action].deadzone;
ActionState *state = &gActionStates[binding->player][binding->action];
bool state_did_change = false;
float newVal;
if (binding->axisMode == AXIS_MODE_DIGITAL)
{
bool is_active = (abs(e->jaxis.value) > dead_zone)
? ((binding->axisThreshold > 0) ? (e->jaxis.value > 16384) : (e->jaxis.value < -16384))
: false;
if (state->isActive != is_active)
{
state->isActive = is_active;
state_did_change = true;
}
}
else
{
if (gActionProperties[binding->player][binding->action].isCentering)
{
newVal = (abs(e->jaxis.value) < dead_zone) ? 0.5f : (float)(e->jaxis.value + 32768) / 65535.0f;
}
else
{
// For non-centering axes, the deadzone check is different.
// We check if the value has moved sufficiently from its resting point (-32768).
if (abs(e->jaxis.value - (-32768)) < dead_zone)
{
newVal = 0.0f;
}
else
{
newVal = (float)(e->jaxis.value + 32768) / 65535.0f;
}
}
if (binding->isInverted)
newVal = 1.0f - newVal;
if (binding->axisMode == AXIS_MODE_POSITIVE_HALF)
{
if (fabs(state->positiveContribution - newVal) > 0.001f)
{
state->positiveContribution = newVal;
state_did_change = true;
}
}
else if (binding->axisMode == AXIS_MODE_NEGATIVE_HALF)
{
if (fabs(state->negativeContribution - newVal) > 0.001f)
{
state->negativeContribution = newVal;
state_did_change = true;
}
}
else
{ // AXIS_MODE_FULL
if (fabs(state->analogValue - newVal) > 0.001f)
{
state->analogValue = newVal;
state_did_change = true;
}
}
}
if (state_did_change)
addActionToDirtyList(binding->player, binding->action);
}
}
}
break;
case SDL_EVENT_GAMEPAD_AXIS_MOTION:
{
// printf("GC Axis: %s, Value: %d\n", SDL_GameControllerGetStringForAxis(e->caxis.axis), e->caxis.value);
int devIdx = getControllerID(e->gaxis.which);
if (devIdx != -1 && e->gaxis.axis < SDL_GAMEPAD_AXIS_COUNT)
{
BindingPair *pair = &gControllerAxisBindings[devIdx][e->gaxis.axis];
for (int i = 0; i < 2; i++)
{
ControlBinding *binding = &pair->bindings[i];
if (binding->type == INPUT_TYPE_NONE)
continue;
// UPDATED: Get deadzone from the action's properties
const int deadZone = gActionProperties[binding->player][binding->action].deadzone;
ActionState *state = &gActionStates[binding->player][binding->action];
bool stateDidChange = false;
float newVal;
if (binding->axisMode == AXIS_MODE_DIGITAL)
{
bool isActive = (abs(e->gaxis.value) > deadZone)
? ((binding->axisThreshold > 0) ? (e->gaxis.value > 16384) : (e->gaxis.value < -16384))
: false;
if (state->isActive != isActive)
{
state->isActive = isActive;
stateDidChange = true;
}
}
else
{
bool isTrigger =
(e->gaxis.axis == SDL_GAMEPAD_AXIS_LEFT_TRIGGER || e->gaxis.axis == SDL_GAMEPAD_AXIS_RIGHT_TRIGGER);
if (gActionProperties[binding->player][binding->action].isCentering && !isTrigger)
{
newVal = (abs(e->gaxis.value) < deadZone) ? 0.5f : (float)(e->gaxis.value + 32768) / 65535.0f;
}
else // This path is for triggers and other non-centering axes
{
newVal = (abs(e->gaxis.value) < deadZone) ? 0.0f : (float)e->gaxis.value / 32767.0f;
}
if (binding->isInverted)
newVal = 1.0f - newVal;
if (binding->axisMode == AXIS_MODE_POSITIVE_HALF)
{
if (fabs(state->positiveContribution - newVal) > 0.001f)
{
state->positiveContribution = newVal;
stateDidChange = true;
}
}
else if (binding->axisMode == AXIS_MODE_NEGATIVE_HALF)
{
if (fabs(state->negativeContribution - newVal) > 0.001f)
{
state->negativeContribution = newVal;
stateDidChange = true;
}
}
else
{ // AXIS_MODE_FULL
if (fabs(state->analogValue - newVal) > 0.001f)
{
state->analogValue = newVal;
stateDidChange = true;
}
}
}
if (stateDidChange)
addActionToDirtyList(binding->player, binding->action);
}
}
}
break;
case SDL_EVENT_JOYSTICK_HAT_MOTION:
{
int devIdx = getJoystickID(e->jhat.which);
if (devIdx != -1 && e->jhat.hat < MAX_JOY_HATS)
{
HatBinding *hat = &gJoyHatBindings[devIdx][e->jhat.hat];
// An array of the 4 directional bindings for easy iteration
ControlBinding *directions[] = {&hat->up, &hat->down, &hat->left, &hat->right};
for (int i = 0; i < 4; i++)
{
ControlBinding *binding = directions[i];
if (binding->type == INPUT_TYPE_NONE)
continue;
// Check if the current hat state activates this direction
bool is_active = (e->jhat.value & binding->axisThreshold) != 0;
ActionState *state = &gActionStates[binding->player][binding->action];
if (state->isActive != is_active)
{
state->isActive = is_active;
addActionToDirtyList(binding->player, binding->action);
}
}
}
}
break;
case SDL_EVENT_MOUSE_BUTTON_DOWN:
case SDL_EVENT_MOUSE_BUTTON_UP:
{
if (e->button.button < MAX_MOUSE_BUTTONS)
{
ControlBinding *binding = &gMouseButtonBindings[e->button.button];
if (binding->type != INPUT_TYPE_NONE)
{
bool isActive = (e->type == SDL_EVENT_MOUSE_BUTTON_DOWN);
if (gActionStates[binding->player][binding->action].isActive != isActive)
{
int x, y;
gActionStates[binding->player][binding->action].isActive = isActive;
if (gId == PRIMEVAL_HUNT && getConfig()->emulateTouchscreen &&
phIsInsideTouchScreen(e->motion.x, e->motion.y, &x, &y))
{
phTouchClick(x, y, e->type);
phIsDragging = isActive;
}
else
{
if (e->button.button == SDL_BUTTON_RIGHT && (gId == RAMBO || gId == RAMBO_CHINA || gId == TOO_SPICY) &&
isActive)
{
ActionState *state = &gActionStates[binding->player][LA_GunX];
state->analogValue = -1;
addActionToDirtyList(binding->player, LA_GunX);
state = &gActionStates[binding->player][LA_GunY];
state->analogValue = -1;
addActionToDirtyList(binding->player, LA_GunY);
gActionStates[binding->player][LA_Reload].isActive = isActive;
addActionToDirtyList(binding->player, LA_Reload);
}
else
{
addActionToDirtyList(binding->player, binding->action);
}
}
}
}
}
}
break;
case SDL_EVENT_MOUSE_MOTION:
{
float mX = e->motion.x;
float mY = e->motion.y;
float posX = 0.0f, posY = 0.0f;
if (gId == PRIMEVAL_HUNT)
{
int x, y;
if (phIsDragging && phIsInsideTouchScreen(mX, mY, &x, &y))
phTouchClick(x, y, e->type);
int motX = mX, motY = mY;
phTouchScreenCursor(mX, mY, &motX, &motY);
posX = ((float)(motX - phX) / (float)phW);
posY = ((float)(motY - phY) / (float)phH);
}
else
{
if (mX <= dest.X)
posX = 0;
else if (mX >= (dest.W + dest.X - 5))
posX = dest.W;
else
posX = mX - dest.X;
if (mY <= dest.Y)
posY = 0;
else if (mY >= (dest.H + dest.Y - 5))
posY = dest.H;
else
posY = mY - dest.Y;
posX /= dest.W;
posY /= dest.H;
}
ControlBinding *bindingX = &gMouseAxisBindings[0]; // X-Axis
ControlBinding *bindingY = &gMouseAxisBindings[1]; // Y-Axis
if (gameType == SHOOTING && (posX <= 0.01 || posX >= 0.99 || posY <= 0.01 || posY >= 0.99))
{
if (gId == RAMBO || gId == RAMBO_CHINA || gId == TOO_SPICY)
{
if (bindingX->type != INPUT_TYPE_NONE)
{
ActionState *state = &gActionStates[bindingX->player][LA_GunX];
state->analogValue = -1;
addActionToDirtyList(bindingX->player, LA_GunX);
}
if (bindingY->type != INPUT_TYPE_NONE)
{
ActionState *state = &gActionStates[bindingY->player][LA_GunY];
state->analogValue = -1;
addActionToDirtyList(bindingY->player, LA_GunY);
}
}
gActionStates[PLAYER_1][LA_Reload].isActive = true;
addActionToDirtyList(PLAYER_1, LA_Reload);
break;
}
else
{
gActionStates[PLAYER_1][LA_Reload].isActive = false;
addActionToDirtyList(PLAYER_1, LA_Reload);
}
if (bindingX->type != INPUT_TYPE_NONE)
{
ActionState *state = &gActionStates[bindingX->player][bindingX->action];
if (fabs(state->analogValue - posX) > 0.001f)
{
if (bindingX->isInverted)
state->analogValue = 1.0 - posX;
else
state->analogValue = posX;
addActionToDirtyList(bindingX->player, bindingX->action);
}
}
if (bindingY->type != INPUT_TYPE_NONE)
{
ActionState *state = &gActionStates[bindingY->player][bindingY->action];
if (fabs(state->analogValue - posY) > 0.001f)
{
if (bindingY->isInverted)
state->analogValue = 1.0 - posY;
else
state->analogValue = posY;
addActionToDirtyList(bindingY->player, bindingY->action);
}
}
}
break;
case SDL_EVENT_WINDOW_MOUSE_LEAVE:
case SDL_EVENT_WINDOW_MOUSE_ENTER:
{
Uint32 flags = SDL_GetWindowFlags(sdlWindow);
if (!(flags & SDL_WINDOW_FULLSCREEN) && gId == GHOST_SQUAD_EVOLUTION)
{
if (e->type == SDL_EVENT_WINDOW_MOUSE_LEAVE)
{
gActionStates[PLAYER_1][LA_Reload].isActive = true;
addActionToDirtyList(PLAYER_1, LA_Reload);
}
else // This case must be SDL_EVENT_WINDOW_ENTER
{
gActionStates[PLAYER_1][LA_Reload].isActive = false;
addActionToDirtyList(PLAYER_1, LA_Reload);
}
}
}
break;
}
}
/**
* @brief Helper function to scan a pair of axis bindings and update detection flags.
* @param pair The BindingPair to scan.
* @param has_full_axis Array to track actions with full axis bindings.
* @param has_half_axis Array to track actions with half axis bindings.
* @param has_positive Array to track actions with positive half bindings.
* @param has_negative Array to track actions with negative half bindings.
*/
static void scan_axis_bindings(BindingPair *pair, bool hasFullAxis[], bool hasHalfSxis[], bool hasPositive[][NUM_LOGICAL_ACTIONS],
bool hasNegative[][NUM_LOGICAL_ACTIONS])
{
for (int k = 0; k < 2; k++)
{
ControlBinding *binding = &pair->bindings[k];
if (binding->type == INPUT_TYPE_NONE)
continue;
if (binding->axisMode == AXIS_MODE_FULL)
{
hasFullAxis[binding->action] = true;
}
else if (binding->axisMode == AXIS_MODE_POSITIVE_HALF)
{
hasHalfSxis[binding->action] = true;
hasPositive[binding->player][binding->action] = true;
}
else if (binding->axisMode == AXIS_MODE_NEGATIVE_HALF)
{
hasHalfSxis[binding->action] = true;
hasNegative[binding->player][binding->action] = true;
}
}
}
/**
* @brief Scans all loaded bindings to detect combined axes and potential conflicts.
* It checks if any logical action is bound to both a full axis (like a stick)
* and a half-axis (like a trigger), which would cause a conflict. It then flags
* actions that are correctly configured as combined axes.
*/
void detectCombinedAxes()
{
// --- Use local variables for detection ---
bool hasFullAxis[NUM_LOGICAL_ACTIONS] = {false};
bool hasHalfAxis[NUM_LOGICAL_ACTIONS] = {false};
bool hasPositive[MAX_ENTITIES][NUM_LOGICAL_ACTIONS] = {false};
bool hasNegative[MAX_ENTITIES][NUM_LOGICAL_ACTIONS] = {false};
// --- Pass 1: Scan all axis bindings and flag their types ---
for (int i = 0; i < MAX_JOYSTICKS; i++)
{
for (int j = 0; j < MAX_JOY_AXES; j++)
{
scan_axis_bindings(&gJoyAxisBindings[i][j], hasFullAxis, hasHalfAxis, hasPositive, hasNegative);
}
for (int j = 0; j < SDL_GAMEPAD_AXIS_COUNT; j++)
{
scan_axis_bindings(&gControllerAxisBindings[i][j], hasFullAxis, hasHalfAxis, hasPositive, hasNegative);
}
}
// --- Pass 2: Check for conflicts and print warnings ---
for (int i = 0; i < NUM_LOGICAL_ACTIONS; i++)
{
if (hasFullAxis[i] && hasHalfAxis[i])
{
const char *action_name = "Unknown";
for (int j = 0; j < NUM_ACTION_NAMES; j++)
{
if (gActionNameMap[j].action == i)
{
action_name = gActionNameMap[j].name;
break;
}
}
printf("The action '%s' is mapped to both a full axis (e.g., a stick) and half-axes (e.g., triggers).\n", action_name);
printf(" The half-axis bindings will take priority and may override the full axis input.\n");
}
}
// --- Pass 3: Flag actions that are correctly combined ---
for (int p = 0; p < MAX_ENTITIES; p++)
{
for (int i = 0; i < NUM_LOGICAL_ACTIONS; i++)
{
if (hasPositive[p][i] && hasNegative[p][i])
{
gActionProperties[p][i].isCombinedAxis = true;
}
}
}
}
/**
* @brief Updates the analog value for any action flagged as a combined axis.
* This is called every frame to calculate a single analog value from the
* positive and negative contributions of two separate half-axes.
*/
void updateCombinedAxes()
{
for (int p = PLAYER_1; p <= MAX_PLAYERS; p++)
{
for (int i = 0; i < NUM_LOGICAL_ACTIONS; i++)
{
if (gActionProperties[p][i].isCombinedAxis)
{
ActionState *state = &gActionStates[p][i];
// Combine the two halves into a single -1 to 1 value, represented as 0.0 to 1.0
float finalVal = 0.5f + (state->positiveContribution * 0.5f) - (state->negativeContribution * 0.5f);
if (finalVal > 1.0f)
finalVal = 1.0f;
if (finalVal < 0.0f)
finalVal = 0.0f;
if (fabs(state->analogValue - finalVal) > 0.001f)
{
state->analogValue = finalVal;
addActionToDirtyList((JVSPlayer)p, (LogicalAction)i);
}
}
}
}
}
/**
* @brief Processes all actions that have changed state since the last frame.
* It iterates through the "dirty list", reads the current state of each action,
* and sends the appropriate update to the JVS IO board.
*/
void processChangedActions()
{
for (int i = 0; i < gNumChangedActions; i++)
{
JVSPlayer player = gChangedActions[i].player;
LogicalAction actionId = gChangedActions[i].action;
JVSActionMapping *map = &gJvsMap[player][actionId];
ActionState *state = &gActionStates[player][actionId];
switch (map->call_type)
{
case JVS_CALL_SWITCH:
setSwitch(player, map->jvsInput, state->isActive);
break;
case JVS_CALL_ANALOGUE:
// Special handling for digital actions that control an analog JVS input
switch ((int)actionId)
{
case LA_Gas_Digital:
case LA_Brake_Digital:
state->analogValue = state->isActive ? 1.0f : 0.0f;
break;
case LA_Steer_Left:
case LA_ABC_Left:
case LA_ABC_Up:
case LA_Throttle_Slowdown:
state->analogValue = state->isActive ? 0.0f : 0.5f;
break;
case LA_Steer_Right:
case LA_ABC_Right:
case LA_ABC_Down:
case LA_Throttle_Accelerate:
state->analogValue = state->isActive ? 1.0f : 0.5f;
break;
}
if (getConfig()->idSteeringPercentageReduction > 0.0f && map->jvsInput == ANALOGUE_1 &&
(gGrp == GROUP_ID4_EXP || gGrp == GROUP_ID4_JAP || gGrp == GROUP_ID5))
state->analogValue = (state->analogValue - 0.5f) * (getConfig()->idSteeringPercentageReduction / 100.0f) + 0.5f;
setAnalogue(map->jvsInput, (int)(state->analogValue * jvsAnalogueMaxValue));
break;
case JVS_CALL_COIN:
if (state->isActive)
incrementCoin(player, 1);
break;
case JVS_CALL_NONE:
break;
}
}
gNumChangedActions = 0; // Clear the list for the next frame
}
/**
* @brief Calculates and updates the gun shake effect for HOD4.
* This function is called every frame for games that support it.
*/
void updateGunShake()
{
for (int p = PLAYER_1; p <= PLAYER_2; p++)
{
int currentX = jvsAnalogueMaxValue * gActionStates[p][LA_GunX].analogValue;
int currentY = jvsAnalogueMaxValue * gActionStates[p][LA_GunY].analogValue;
int deltaX = currentX - gLastGunX[p];
int deltaY = currentY - gLastGunY[p];
int currentDirX = (deltaX > 2) ? 1 : ((deltaX < -2) ? -1 : 0);
int currentDirY = (deltaY > 2) ? 1 : ((deltaY < -2) ? -1 : 0);
if (currentDirX != 0 && currentDirX == -gLastGunXDir[p])
gShakeValue[p] += abs(deltaX) * gShakeIncreaseRate;
if (currentDirY != 0 && currentDirY == -gLastGunYDir[p])
gShakeValue[p] += abs(deltaY) * gShakeIncreaseRate;
// Apply decay
gShakeValue[p] *= gShakeDecayRate;
if (gShakeValue[p] < 1.0f)
gShakeValue[p] = 0.0f;
// Clamp the shake value to the max internal range (0-512)
if (gShakeValue[p] > 512.0f)
gShakeValue[p] = 512.0f;
// Map the 0-512 shake value to the final 512-1024 JVS output range
int jvsOutputValue = 512 + (int)gShakeValue[p];
// Send the analog values
if (p == PLAYER_1)
{
setAnalogue(ANALOGUE_5, jvsOutputValue);
setAnalogue(ANALOGUE_6, jvsOutputValue);
}
else if (p == PLAYER_2)
{
setAnalogue(ANALOGUE_7, jvsOutputValue);
setAnalogue(ANALOGUE_8, jvsOutputValue);
}
// Update last known positions and directions for the next frame
gLastGunX[p] = currentX;
gLastGunY[p] = currentY;
if (currentDirX != 0)
gLastGunXDir[p] = currentDirX;
if (currentDirY != 0)
gLastGunYDir[p] = currentDirY;
}
}
/**
* @brief Saves the controller GUIDs back to controls.ini if they have changed.
* This is called during initialization if a new controller was assigned to a player.
*/
void saveGuidsToIni()
{
if (!gPlayerGUIDsDirty)
{
return;
}
printf("Saving updated controller GUIDs to controls.ini...\n");
IniConfig *ini = iniLoad("controls.ini");
if (!ini)
{
// If the file doesn't exist, create an empty config in memory to save.
ini = calloc(1, sizeof(IniConfig));
if (!ini)
{
fprintf(stderr, "ERROR: Failed to allocate memory for INI config.\n");
return;
}
}
// Set the GUID value for each player. iniSetValue will create the section/key if needed.
for (int player = 1; player <= MAX_PLAYERS; player++)
{
if (strlen(gPlayerGUIDs[player]) > 0)
{
char key[16];
snprintf(key, sizeof(key), "P%d_GUID", player);
iniSetValue(ini, "ControllerGUIDs", key, gPlayerGUIDs[player]);
}
}
if (iniSave(ini, "controls.ini") == 0)
{
printf("Successfully saved controls.ini with updated GUIDs.\n");
}
else
{
fprintf(stderr, "ERROR: Failed to save controls.ini.\n");
}
iniFree(ini);
gPlayerGUIDsDirty = false;
}
#endif
bool needsPlayer(LogicalAction action, const char *name)
{
switch ((int)action)
{
case LA_Boost:
case LA_BoostRight:
case LA_GearUp:
case LA_GearDown:
case LA_ViewChange:
case LA_MusicChange:
case LA_ABC_X:
case LA_ABC_Y:
case LA_ABC_Left:
case LA_ABC_Right:
case LA_ABC_Up:
case LA_ABC_Down:
case LA_Throttle:
case LA_Throttle_Accelerate:
case LA_Throttle_Slowdown:
case LA_GunTrigger:
case LA_MissileTrigger:
case LA_ClimaxSwitch:
case LA_CardInsert:
return false;
case LA_Up:
case LA_Down:
case LA_Left:
case LA_Right:
if (strcmp(name, "Driving") == 0)
return false;
else
return true;
default:
if (strcmp(name, "Mahjong") == 0)
return false;
else
return true;
}
}
/**
* @brief Helper to get the full "P1_Action" string for a binding.
* Used for creating user-friendly error messages.
* @param binding Pointer to the ControlBinding.
* @param out_str Buffer to store the resulting string.
* @param str_size Size of the output buffer.
*/
void getLogicalActionString(const ControlBinding *binding, char *outStr, size_t strSize, const char *name)
{
const char *actionName = "Unknown";
for (int i = 0; i < NUM_ACTION_NAMES; i++)
{
if (gActionNameMap[i].action == binding->action)
{
actionName = gActionNameMap[i].name;
break;
}
}
if (binding->player == SYSTEM)
{
snprintf(outStr, strSize, "%s", actionName);
}
else
{
if (needsPlayer(binding->action, name))
snprintf(outStr, strSize, "P%d_%s", binding->player, actionName);
else
snprintf(outStr, strSize, "%s", actionName);
}
}
int listSdlControllers(void)
{
if (!SDL_InitSubSystem(SDL_INIT_GAMEPAD))
{
fprintf(stderr, "Could not initialize SDL: %s\n", SDL_GetError());
return 1;
}
int numJoysticks;
SDL_JoystickID *joysticks = SDL_GetJoysticks(&numJoysticks);
printf("Found %d controller(s)\n", numJoysticks);
if (joysticks)
{
for (int i = 0; i < numJoysticks; i++)
{
SDL_JoystickID instanceId = joysticks[i];
const char *name = SDL_GetJoystickNameForID(instanceId);
SDL_GUID guid = SDL_GetJoystickGUIDForID(instanceId);
char guidStr[33];
SDL_GUIDToString(guid, guidStr, sizeof(guidStr));
int count = 0;
int index = 0;
for (int j = 0; j < numJoysticks; j++)
{
instanceId = joysticks[j];
const char *otherName = SDL_GetJoystickNameForID(instanceId);
SDL_GUID other_guid = SDL_GetJoystickGUIDForID(instanceId);
char other_guid_str[33];
SDL_GUIDToString(other_guid, other_guid_str, sizeof(other_guid_str));
if (strcmp(name, otherName) == 0 && strcmp(guidStr, other_guid_str) == 0)
{
if (j < i)
{
index++;
}
count++;
}
}
guidStr[4] = '0';
guidStr[5] = '0';
guidStr[6] = '0';
guidStr[7] = '0';
if (count > 1)
{
printf(" - %s %d: %s", name, index, guidStr);
}
else
{
printf(" - %s: %s", name, guidStr);
}
printf("\n");
}
}
SDL_Quit();
return 0;
}