gamepad: support hotplug, any-device option

This commit is contained in:
Andrei Alexeyev 2019-04-19 14:28:32 +03:00
parent 58b964baba
commit b3a0128be6
No known key found for this signature in database
GPG key ID: 363707CD4C7FE8A4
4 changed files with 261 additions and 151 deletions

View file

@ -117,7 +117,7 @@
CONFIGDEF_FLOAT (SCORETEXT_ALPHA, "scoretext_alpha", 1) \
KEYDEFS \
CONFIGDEF_INT (GAMEPAD_ENABLED, "gamepad_enabled", 1) \
CONFIGDEF_STRING (GAMEPAD_DEVICE, "gamepad_device", "default") \
CONFIGDEF_STRING (GAMEPAD_DEVICE, "gamepad_device", "any") \
CONFIGDEF_INT (GAMEPAD_AXIS_UD, "gamepad_axis_ud", 1) \
CONFIGDEF_INT (GAMEPAD_AXIS_LR, "gamepad_axis_lr", 0) \
CONFIGDEF_INT (GAMEPAD_AXIS_FREE, "gamepad_axis_free", 1) \

View file

@ -25,21 +25,25 @@ typedef struct GamepadButtonState {
hrtime_t repeat_time;
} GamepadButtonState;
static struct {
bool initialized;
int current_devnum;
SDL_GameController *device;
SDL_JoystickID instance;
typedef struct GamepadDevice {
SDL_GameController *controller;
SDL_JoystickID joy_instance;
int sdl_id;
} GamepadDevice;
static struct {
GamepadAxisState *axes;
GamepadButtonState *buttons;
struct {
int *id_map;
size_t count;
} devices;
GamepadDevice *devices;
size_t num_devices;
size_t num_devices_allocated;
int active_dev_num;
bool initialized;
bool update_needed;
} gamepad;
#define DEVNUM(dev) (int)(ptrdiff_t)((dev) - gamepad.devices)
static bool gamepad_event_handler(SDL_Event *event, void *arg);
static int gamepad_load_mappings(const char *vpath, int warn_noexist) {
@ -51,8 +55,6 @@ static int gamepad_load_mappings(const char *vpath, int warn_noexist) {
int num_loaded = -1;
LogLevel loglvl = LOG_WARN;
// Yay for gotos!
if(!mappings) {
if(!warn_noexist) {
VFSInfo vinfo = vfs_query(vpath);
@ -92,36 +94,70 @@ static void gamepad_load_all_mappings(void) {
static const char* gamepad_device_name_unmapped(int idx) {
const char *name = SDL_GameControllerNameForIndex(idx);
if(name == NULL) {
return "Unknown device";
}
if(!strcasecmp(name, "Xinput Controller")) {
// HACK: let's try to get a more descriptive name...
const char *prev_name = name;
name = SDL_JoystickNameForIndex(idx);
if(name == NULL) {
name = prev_name;
}
}
return name;
}
const char* gamepad_device_name(int num) {
if(num < 0 || num >= gamepad.devices.count) {
static inline GamepadDevice* gamepad_get_device(int num) {
if(num < 0 || num >= gamepad.num_devices) {
return NULL;
}
return gamepad_device_name_unmapped(gamepad.devices.id_map[num]);
return gamepad.devices + num;
}
const char* gamepad_device_name(int num) {
GamepadDevice *dev = gamepad_get_device(num);
if(dev) {
return gamepad_device_name_unmapped(dev->sdl_id);
}
return NULL;
}
static bool gamepad_update_device_list(void) {
int cnt = SDL_NumJoysticks();
log_info("Updating gamepad devices list");
free(gamepad.devices.id_map);
memset(&gamepad.devices, 0, sizeof(gamepad.devices));
if(gamepad.num_devices > 0) {
assume(gamepad.devices != NULL);
for(uint i = 0; i < gamepad.num_devices; ++i) {
SDL_GameControllerClose(gamepad.devices[i].controller);
}
}
gamepad.num_devices = 0;
if(!cnt) {
free(gamepad.devices);
gamepad.devices = NULL;
gamepad.num_devices_allocated = 0;
log_info("No joysticks attached");
return false;
}
int *idmap_ptr = gamepad.devices.id_map = malloc(sizeof(int) * cnt);
if(gamepad.num_devices_allocated != cnt) {
free(gamepad.devices);
gamepad.devices = calloc(cnt, sizeof(GamepadDevice));
gamepad.num_devices_allocated = cnt;
}
GamepadDevice *dev = gamepad.devices;
for(int i = 0; i < cnt; ++i) {
SDL_JoystickGUID guid = SDL_JoystickGetDeviceGUID(i);
@ -140,15 +176,28 @@ static bool gamepad_update_device_list(void) {
continue;
}
*idmap_ptr = i;
int num = (int)(uintmax_t)(idmap_ptr - gamepad.devices.id_map);
++idmap_ptr;
gamepad.devices.count = (uintptr_t)(idmap_ptr - gamepad.devices.id_map);
dev->sdl_id = i;
dev->controller = SDL_GameControllerOpen(i);
log_info("Found device '%s' (#%i): %s", guid_str, num, gamepad_device_name_unmapped(i));
if(dev->controller == NULL) {
log_sdl_error(LOG_WARN, "SDL_GameControllerOpen");
continue;
}
dev->joy_instance = SDL_JoystickGetDeviceInstanceID(i);
if(dev->joy_instance < 0) {
log_sdl_error(LOG_WARN, "SDL_JoystickGetDeviceInstanceID");
continue;
}
log_info("Found device '%s' (#%i): %s", guid_str, DEVNUM(dev), gamepad_device_name_unmapped(i));
++gamepad.num_devices;
++dev;
}
if(!gamepad.devices.count) {
if(!gamepad.num_devices) {
log_info("No usable devices");
return false;
}
@ -156,46 +205,86 @@ static bool gamepad_update_device_list(void) {
return true;
}
static int gamepad_find_device_by_guid(const char *guid_str, char *guid_out, size_t guid_out_sz, int *out_localdevnum) {
for(int i = 0; i < gamepad.devices.count; ++i) {
static GamepadDevice* gamepad_find_device_by_guid(const char *guid_str, char *guid_out, size_t guid_out_sz, int *out_localdevnum) {
if(gamepad.num_devices < 1) {
*out_localdevnum = GAMEPAD_DEVNUM_INVALID;
return NULL;
}
if(!strcasecmp(guid_str, "any")) {
*out_localdevnum = GAMEPAD_DEVNUM_ANY;
strlcpy(guid_out, "any", guid_out_sz);
return NULL;
}
for(int i = 0; i < gamepad.num_devices; ++i) {
*guid_out = 0;
SDL_JoystickGUID guid = SDL_JoystickGetDeviceGUID(gamepad.devices.id_map[i]);
SDL_JoystickGUID guid = SDL_JoystickGetDeviceGUID(gamepad.devices[i].sdl_id);
SDL_JoystickGetGUIDString(guid, guid_out, guid_out_sz);
if(!strcasecmp(guid_str, guid_out) || !strcasecmp(guid_str, "default")) {
*out_localdevnum = i;
return gamepad.devices.id_map[i];
return gamepad.devices + i;
}
}
*out_localdevnum = -1;
return -1;
*out_localdevnum = GAMEPAD_DEVNUM_INVALID;
return NULL;
}
static int gamepad_find_device(char *guid_out, size_t guid_out_sz, int *out_localdevnum) {
int dev;
static GamepadDevice* gamepad_find_device(char *guid_out, size_t guid_out_sz, int *out_localdevnum) {
GamepadDevice *dev;
const char *want_guid = config_get_str(CONFIG_GAMEPAD_DEVICE);
dev = gamepad_find_device_by_guid(want_guid, guid_out, guid_out_sz, out_localdevnum);
if(dev >= 0) {
if(dev || *out_localdevnum == GAMEPAD_DEVNUM_ANY) {
return dev;
}
if(strcasecmp(want_guid, "default")) {
log_warn("Requested device '%s' is not available, trying first usable", want_guid);
dev = gamepad_find_device_by_guid("default", guid_out, guid_out_sz, out_localdevnum);
if(dev >= 0) {
return dev;
}
log_warn("Requested device '%s' is not available", want_guid);
return gamepad_find_device_by_guid("any", guid_out, guid_out_sz, out_localdevnum);
}
*out_localdevnum = -1;
*out_localdevnum = GAMEPAD_DEVNUM_INVALID;
strcpy(guid_out, want_guid);
return -1;
return NULL;
}
int gamepad_get_active_device(void) {
if(gamepad.initialized) {
return gamepad.active_dev_num;
}
return GAMEPAD_DEVNUM_INVALID;
}
bool gamepad_update_devices(void) {
gamepad.update_needed = false;
if(!gamepad_update_device_list()) {
return false;
}
char guid[33];
GamepadDevice *dev = gamepad_find_device(guid, sizeof(guid), &gamepad.active_dev_num);
if(gamepad.active_dev_num == GAMEPAD_DEVNUM_ANY) {
log_info("Using all available devices (%zu)", gamepad.num_devices);
return true;
}
if(dev == NULL) {
log_info("No devices available");
return false;
}
log_info("Using device '%s' (#%i): %s", guid, gamepad.active_dev_num, gamepad_device_name(gamepad.active_dev_num));
config_set_str(CONFIG_GAMEPAD_DEVICE, guid);
return true;
}
void gamepad_init(void) {
@ -209,44 +298,17 @@ void gamepad_init(void) {
}
gamepad.initialized = true;
gamepad_load_all_mappings();
if(!gamepad_update_device_list()) {
gamepad_shutdown();
return;
}
char guid[33];
int dev = gamepad_find_device(guid, sizeof(guid), &gamepad.current_devnum);
if(dev < 0) {
log_warn("Device '%s' is not available", guid);
gamepad_shutdown();
return;
}
gamepad.device = SDL_GameControllerOpen(dev);
if(!gamepad.device) {
log_warn("Failed to open device '%s' (#%i): %s", guid, dev, SDL_GetError());
gamepad_shutdown();
return;
}
SDL_Joystick *joy = SDL_GameControllerGetJoystick(gamepad.device);
gamepad.instance = SDL_JoystickInstanceID(joy);
gamepad.axes = calloc(GAMEPAD_AXIS_MAX, sizeof(GamepadAxisState));
gamepad.buttons = calloc(GAMEPAD_BUTTON_MAX + GAMEPAD_EMULATED_BUTTON_MAX, sizeof(GamepadButtonState));
log_info("Using device '%s' (#%i): %s", guid, dev, gamepad_device_name(dev));
SDL_GameControllerEventState(SDL_ENABLE);
config_set_str(CONFIG_GAMEPAD_DEVICE, guid);
gamepad.active_dev_num = GAMEPAD_DEVNUM_INVALID;
gamepad_load_all_mappings();
events_register_handler(&(EventHandler){
.proc = gamepad_event_handler,
.priority = EPRIO_TRANSLATION,
});
SDL_GameControllerEventState(SDL_ENABLE);
}
void gamepad_shutdown(void) {
@ -256,13 +318,16 @@ void gamepad_shutdown(void) {
log_info("Disabled the gamepad subsystem");
if(gamepad.device) {
SDL_GameControllerClose(gamepad.device);
}
free(gamepad.axes);
free(gamepad.buttons);
free(gamepad.devices.id_map);
for(uint i = 0; i < gamepad.num_devices; ++i) {
if(gamepad.devices[i].controller) {
SDL_GameControllerClose(gamepad.devices[i].controller);
}
}
free(gamepad.devices);
SDL_GameControllerEventState(SDL_IGNORE);
SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER);
@ -613,12 +678,15 @@ static void gamepad_handle_button_repeat(GamepadButton btn, hrtime_t time) {
}
}
#define INSTANCE_IS_VALID(inst) \
(gamepad.active_dev_num == GAMEPAD_DEVNUM_ANY || (gamepad.active_dev_num >= 0 && (inst) == gamepad.devices[gamepad.active_dev_num].joy_instance))
static bool gamepad_event_handler(SDL_Event *event, void *arg) {
assert(gamepad.initialized);
switch(event->type) {
case SDL_CONTROLLERAXISMOTION:
if(event->caxis.which == gamepad.instance) {
if(INSTANCE_IS_VALID(event->caxis.which)) {
GamepadAxis axis = gamepad_axis_from_sdl_axis(event->caxis.axis);
if(axis != GAMEPAD_AXIS_INVALID) {
@ -629,7 +697,7 @@ static bool gamepad_event_handler(SDL_Event *event, void *arg) {
case SDL_CONTROLLERBUTTONDOWN:
case SDL_CONTROLLERBUTTONUP:
if(event->cbutton.which == gamepad.instance) {
if(INSTANCE_IS_VALID(event->cbutton.which)) {
GamepadButton btn = gamepad_button_from_sdl_button(event->cbutton.button);
if(btn != GAMEPAD_BUTTON_INVALID) {
@ -637,9 +705,19 @@ static bool gamepad_event_handler(SDL_Event *event, void *arg) {
}
}
return true;
case SDL_CONTROLLERDEVICEADDED:
case SDL_CONTROLLERDEVICEREMOVED:
case SDL_CONTROLLERDEVICEREMAPPED:
gamepad.update_needed = true;
return true;
}
if(event->type == MAKE_TAISEI_EVENT(TE_FRAME)) {
if(gamepad.update_needed) {
gamepad_update_devices();
}
hrtime_t time = time_get();
for(GamepadButton btn = 0; btn < GAMEPAD_BUTTON_MAX; ++btn) {
@ -655,20 +733,29 @@ static bool gamepad_event_handler(SDL_Event *event, void *arg) {
}
int gamepad_device_count(void) {
return gamepad.devices.count;
return gamepad.num_devices;
}
void gamepad_device_guid(int num, char *guid_str, size_t guid_str_sz) {
if(num < 0 || num >= gamepad.devices.count) {
if(num == GAMEPAD_DEVNUM_ANY) {
strlcpy(guid_str, "any", guid_str_sz);
return;
}
SDL_JoystickGUID guid = SDL_JoystickGetDeviceGUID(gamepad.devices.id_map[num]);
SDL_JoystickGetGUIDString(guid, guid_str, guid_str_sz);
GamepadDevice *dev = gamepad_get_device(num);
if(dev) {
SDL_JoystickGUID guid = SDL_JoystickGetDeviceGUID(dev->sdl_id);
SDL_JoystickGetGUIDString(guid, guid_str, guid_str_sz);
}
}
int gamepad_device_num_from_guid(const char *guid_str) {
for(int i = 0; i < gamepad.devices.count; ++i) {
if(strcasecmp(guid_str, "any")) {
return GAMEPAD_DEVNUM_ANY;
}
for(uint i = 0; i < gamepad.num_devices; ++i) {
char guid[33] = {0};
gamepad_device_guid(i, guid, sizeof(guid));
@ -677,15 +764,7 @@ int gamepad_device_num_from_guid(const char *guid_str) {
}
}
return -1;
}
int gamepad_current_device_num(void) {
if(gamepad.initialized) {
return gamepad.current_devnum;
}
return -1;
return GAMEPAD_DEVNUM_INVALID;
}
bool gamepad_button_pressed(GamepadButton btn) {

View file

@ -84,6 +84,11 @@ typedef enum GamepadAxis {
GAMEPAD_AXIS_MAX
} GamepadAxis;
enum {
GAMEPAD_DEVNUM_INVALID = -1,
GAMEPAD_DEVNUM_ANY = -2,
};
void gamepad_init(void);
void gamepad_shutdown(void);
void gamepad_restart(void);
@ -93,7 +98,8 @@ int gamepad_device_count(void);
const char* gamepad_device_name(int);
void gamepad_device_guid(int num, char *guid_str, size_t guid_str_sz);
int gamepad_device_num_from_guid(const char *guid_str);
int gamepad_current_device_num(void);
int gamepad_get_active_device(void);
bool gamepad_update_devices(void);
bool gamepad_button_pressed(GamepadButton btn);
bool gamepad_game_key_pressed(KeyIndex key);

View file

@ -39,7 +39,6 @@ typedef struct OptionBinding {
char *strvalue;
};
bool displaysingle;
int valcount;
int valrange_min;
int valrange_max;
float scale_min;
@ -72,9 +71,13 @@ static OptionBinding* bind_new(void) {
static void bind_free(OptionBinding *bind) {
int i;
if(bind->values) {
for(i = 0; i < bind->valcount; ++i)
if(bind->type == BT_StrValue) {
free(bind->strvalue);
} else if(bind->values) {
assert(bind->valrange_min == 0);
for(i = 0; i <= bind->valrange_max; ++i) {
free(*(bind->values+i));
}
free(bind->values);
}
}
@ -128,15 +131,32 @@ static OptionBinding* bind_gpaxisbinding(int cfgentry) {
}
static int bind_gpdev_get(OptionBinding *b) {
return gamepad_device_num_from_guid(config_get_str(b->configentry));
const char *guid = config_get_str(b->configentry);
int val = gamepad_device_num_from_guid(guid);
if(val == GAMEPAD_DEVNUM_ANY) {
val = -1;
}
return val;
}
static int bind_gpdev_set(OptionBinding *b, int v) {
char guid[33] = {0};
if(v == -1) {
v = GAMEPAD_DEVNUM_ANY;
}
gamepad_device_guid(v, guid, sizeof(guid));
if(*guid) {
config_set_str(b->configentry, guid);
if(v == GAMEPAD_DEVNUM_ANY) {
v = -1;
}
b->selected = v;
}
@ -153,7 +173,7 @@ static OptionBinding* bind_gpdevice(int cfgentry) {
bind->getter = bind_gpdev_get;
bind->setter = bind_gpdev_set;
bind->valrange_min = 0;
bind->valrange_min = -1;
bind->valrange_max = 0; // updated later
bind->selected = gamepad_device_num_from_guid(config_get_str(bind->configentry));
@ -173,7 +193,8 @@ static OptionBinding* bind_stroption(ConfigIndex cfgentry) {
// BT_Resolution: super-special binding type for the resolution setting
static void bind_resolution_update(OptionBinding *bind) {
bind->valcount = video.mcount;
bind->valrange_min = 0;
bind->valrange_max = video.mcount - 1;
for(int i = 0; i < video.mcount; ++i) {
VideoMode *m = video.modes + i;
@ -217,14 +238,25 @@ static OptionBinding* bind_getinputblocking(MenuData *m) {
// Adds a value to a BT_IntValue type binding
static int bind_addvalue(OptionBinding *b, char *val) {
b->values = realloc(b->values, ++b->valcount * sizeof(char*));
b->values[b->valcount-1] = malloc(strlen(val) + 1);
strcpy(b->values[b->valcount-1], val);
return b->valcount-1;
assert(b->valrange_min == 0);
if(b->values == NULL) {
b->values = malloc(sizeof(char*));
b->valrange_min = 0;
b->valrange_max = 0;
} else {
assert(b->valrange_min == 0);
++b->valrange_max;
}
b->values = realloc(b->values, (1 + b->valrange_max) * sizeof(char*));
b->values[b->valrange_max] = strdup(val);
return b->valrange_max;
}
attr_unused
static void bind_setvaluerange(OptionBinding *b, int vmin, int vmax) {
assert(b->values == NULL);
b->valrange_min = vmin;
b->valrange_max = vmax;
}
@ -240,14 +272,7 @@ static int bind_setvalue(OptionBinding *b, int v) {
// Called to get the selected value of a BT_IntValue type binding by index
static int bind_getvalue(OptionBinding *b) {
if(b->getter) {
if(b->selected >= b->valcount && b->valcount) {
b->selected = 0;
} else {
b->selected = b->getter(b);
if(b->selected >= b->valcount && b->valcount)
b->selected = 0;
}
b->selected = b->getter(b);
}
return b->selected;
@ -257,11 +282,8 @@ static int bind_getvalue(OptionBinding *b) {
static int bind_setnext(OptionBinding *b) {
int s = b->selected + 1;
if(b->valrange_max) {
if(s > b->valrange_max)
s = b->valrange_min;
} else if(s >= b->valcount) {
s = 0;
if(s > b->valrange_max) {
s = b->valrange_min;
}
return bind_setvalue(b, s);
@ -271,11 +293,8 @@ static int bind_setnext(OptionBinding *b) {
static int bind_setprev(OptionBinding *b) {
int s = b->selected - 1;
if(b->valrange_max) {
if(s < b->valrange_min)
s = b->valrange_max;
} else if(s < 0) {
s = b->valcount - 1;
if(s < b->valrange_min) {
s = b->valrange_max;
}
return bind_setvalue(b, s);
@ -627,7 +646,7 @@ static void destroy_options_menu_gamepad(MenuData *m) {
OptionsMenuContext *ctx = m->context;
if(config_get_int(CONFIG_GAMEPAD_ENABLED) && strcasecmp(config_get_str(CONFIG_GAMEPAD_DEVICE), ctx->data)) {
gamepad_restart();
gamepad_update_devices();
}
destroy_options_menu(m);
@ -924,16 +943,7 @@ static void draw_options_menu(MenuData *menu) {
case BT_IntValue: {
int val = bind_getvalue(bind);
if(bind->valrange_max) {
char tmp[16]; // who'd use a 16-digit number here anyway?
snprintf(tmp, 16, "%d", bind_getvalue(bind));
text_draw(tmp, &(TextParams) {
.pos = { origin, 20*i },
.align = ALIGN_RIGHT,
.color = &clr,
});
} else if(bind->configentry == CONFIG_PRACTICE_POWER) {
if(bind->configentry == CONFIG_PRACTICE_POWER) {
Font *fnt_int = get_font("standard");
Font *fnt_fract = get_font("small");
@ -948,18 +958,29 @@ static void draw_options_menu(MenuData *menu) {
&clr,
false
);
} else for(j = bind->displaysingle? val : bind->valcount-1; (j+1) && (!bind->displaysingle || j == val); --j) {
if(j != bind->valcount-1 && !bind->displaysingle) {
origin -= text_width(get_font("standard"), bind->values[j+1], 0) + 5;
}
} else if(bind->values) {
for(j = bind->displaysingle? val : bind->valrange_max; (j+1) && (!bind->displaysingle || j == val); --j) {
if(j != bind->valrange_max && !bind->displaysingle) {
origin -= text_width(get_font("standard"), bind->values[j+1], 0) + 5;
}
if(val == j) {
clr = *RGBA_MUL_ALPHA(0.9, 0.6, 0.2, alpha);
} else {
clr = *RGBA_MUL_ALPHA(0.5, 0.5, 0.5, 0.7 * alpha);
}
if(val == j) {
clr = *RGBA_MUL_ALPHA(0.9, 0.6, 0.2, alpha);
} else {
clr = *RGBA_MUL_ALPHA(0.5, 0.5, 0.5, 0.7 * alpha);
}
text_draw(bind->values[j], &(TextParams) {
text_draw(bind->values[j], &(TextParams) {
.pos = { origin, 20*i },
.align = ALIGN_RIGHT,
.color = &clr,
});
}
} else {
char tmp[16]; // who'd use a 16-digit number here anyway?
snprintf(tmp, 16, "%d", bind_getvalue(bind));
text_draw(tmp, &(TextParams) {
.pos = { origin, 20*i },
.align = ALIGN_RIGHT,
.color = &clr,
@ -1005,20 +1026,24 @@ static void draw_options_menu(MenuData *menu) {
// XXX: I'm not exactly a huge fan of fixing up state in drawing code, but it seems the way to go for now...
bind->valrange_max = gamepad_device_count() - 1;
if(bind->selected < 0 || bind->selected > bind->valrange_max) {
bind->selected = gamepad_current_device_num();
if(bind->selected < -1 || bind->selected > bind->valrange_max) {
bind->selected = gamepad_get_active_device();
if(bind->selected < 0) {
bind->selected = 0;
if(bind->selected < -1) {
bind->selected = -1;
}
}
char *txt;
char buf[64];
if(bind->valrange_max > 0) {
snprintf(buf, sizeof(buf), "#%i: %s", bind->selected + 1, gamepad_device_name(bind->selected));
txt = buf;
if(bind->valrange_max >= 0) {
if(bind->selected < 0) {
txt = "All devices";
} else {
snprintf(buf, sizeof(buf), "#%i: %s", bind->selected + 1, gamepad_device_name(bind->selected));
txt = buf;
}
} else {
txt = "No devices available";
}