taisei/src/menu/options.c
Andrei Alexeyev fda8556a39
src: deprecate strdup(), use mem_strdup() explicitly
This fixes a nasty bug that manifests on windows when building without
precompiled headers. util/stringops.h used to silently replace strdup
with a macro that's compatible with mem_free(). This header would
typically be included everywhere due to PCH, but without it the strdup
from libc would sometimes be in scope. On most platforms mem_free() is
equivalent to free(), but not on windows, because we have to use
_aligned_free() there. Attempting to mem_free() the result of a libc
strdup() would segfault in such a configuration.

Avoid the footgun by banning strdup() entirely. Maybe redefining libc
names isn't such a great idea, who knew?
2024-09-10 14:31:55 +02:00

1980 lines
48 KiB
C

/*
* This software is licensed under the terms of the MIT License.
* See COPYING for further information.
* ---
* Copyright (c) 2011-2024, Lukas Weber <laochailan@web.de>.
* Copyright (c) 2012-2024, Andrei Alexeyev <akari@taisei-project.org>.
*/
#include "options.h"
#include "common.h"
#include "config.h"
#include "events.h"
#include "mainmenu.h"
#include "menu.h"
#include "audio/audio.h"
#include "gamepad.h"
#include "renderer/api.h"
#include "resource/font.h"
#include "util/graphics.h"
#include "video.h"
#define OPTIONS_ACTIVE_X_OFFSET 20 /* FIXME hardcoded in draw_menu_list */
#define OPTIONS_ITEM_HEIGHT 20 /* FIXME hardcoded in draw_menu_list */
#define OPTIONS_X_MARGIN 100
#define OPTIONS_Y_MARGIN 100
typedef struct OptionBinding OptionBinding;
typedef int (*BindingGetter)(OptionBinding*);
typedef int (*BindingSetter)(OptionBinding*, int);
typedef bool (*BindingDependence)(void);
typedef enum BindingType {
BT_IntValue,
BT_KeyBinding,
BT_StrValue,
BT_Resolution,
BT_FramebufferResolution,
BT_Scale,
BT_GamepadKeyBinding,
BT_GamepadAxisBinding,
BT_GamepadDevice,
BT_VideoDisplay,
} BindingType;
typedef enum OptionsMenuEntryArgType {
ARGTYPE_BIND = 0xafafaf00,
ARGTYPE_OTHER,
} OptionsMenuEntryArgType;
// Shared header for all structs used as args in menu entries,
// because past me had the brilliant idea of storing the bindings in there
// and now there is a need to tell them apart from other stuff.
// One of these days I'm gonna rewrite this whole file from scratch (copium)
// along with the entire menu system (mega copium), but that day is not today.
typedef struct OptionsMenuArgHeader {
OptionsMenuEntryArgType type;
} OptionsMenuArgHeader;
typedef struct OptionBinding {
OptionsMenuArgHeader header;
union {
char **values;
char *strvalue;
};
bool displaysingle;
int valrange_min;
int valrange_max;
float scale_min;
float scale_max;
float scale_step;
BindingGetter getter;
BindingSetter setter;
BindingDependence dependence;
int selected;
int configentry;
BindingType type;
bool blockinput;
int pad;
} OptionBinding;
// --- Menu entry <-> config option binding stuff --- //
static OptionBinding* bind_new(void) {
return ALLOC(OptionBinding, {
.header.type = ARGTYPE_BIND,
.selected = -1,
.configentry = -1,
});
}
static void bind_free(OptionBinding *bind) {
int i;
if(bind->type == BT_StrValue) {
mem_free(bind->strvalue);
} else if(bind->values) {
assert(bind->valrange_min == 0);
for(i = 0; i <= bind->valrange_max; ++i) {
mem_free(*(bind->values+i));
}
mem_free(bind->values);
}
}
static OptionBinding* bind_get(MenuData *m, int idx) {
MenuEntry *e = dynarray_get_ptr(&m->entries, idx);
OptionsMenuArgHeader *header = e->arg;
if(!header) {
return NULL;
}
if(header->type == ARGTYPE_BIND) {
return CASTPTR_ASSUME_ALIGNED(header, OptionBinding);
}
assume(header->type == ARGTYPE_OTHER);
return NULL;
}
// BT_IntValue: integer and boolean options
// Values are defined with bind_addvalue or bind_setrange
static OptionBinding* bind_option(int cfgentry, BindingGetter getter, BindingSetter setter) {
OptionBinding *bind = bind_new();
bind->type = BT_IntValue;
bind->configentry = cfgentry;
bind->getter = getter;
bind->setter = setter;
return bind;
}
// BT_KeyBinding: keyboard action mapping options
static OptionBinding* bind_keybinding(int cfgentry) {
OptionBinding *bind = bind_new();
bind->configentry = cfgentry;
bind->type = BT_KeyBinding;
return bind;
}
// BT_GamepadKeyBinding: gamepad action mapping options
static OptionBinding* bind_gpbinding(int cfgentry) {
OptionBinding *bind = bind_new();
bind->configentry = cfgentry;
bind->type = BT_GamepadKeyBinding;
return bind;
}
// BT_GamepadAxisBinding: gamepad axis mapping options
attr_unused
static OptionBinding* bind_gpaxisbinding(int cfgentry) {
OptionBinding *bind = bind_new();
bind->configentry = cfgentry;
bind->type = BT_GamepadAxisBinding;
return bind;
}
static int bind_gpdev_get(OptionBinding *b) {
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;
}
return b->selected;
}
#ifndef __SWITCH__
// BT_GamepadDevice: dynamic device list
static OptionBinding* bind_gpdevice(int cfgentry) {
OptionBinding *bind = bind_new();
bind->configentry = cfgentry;
bind->type = BT_GamepadDevice;
bind->getter = bind_gpdev_get;
bind->setter = bind_gpdev_set;
bind->valrange_min = -1;
bind->valrange_max = 0; // updated later
bind->selected = gamepad_device_num_from_guid(config_get_str(bind->configentry));
return bind;
}
// BT_StrValue: with a half-assed "textbox"
static OptionBinding* bind_stroption(ConfigIndex cfgentry) {
OptionBinding *bind = bind_new();
bind->type = BT_StrValue;
bind->configentry = cfgentry;
stralloc(&bind->strvalue, config_get_str(cfgentry));
return bind;
}
#endif
// BT_Resolution: super-special binding type for the resolution setting
static void bind_resolution_update(OptionBinding *bind) {
// FIXME This is brittle. The least we could do is to explicitly store whether the currently selected value represents a fullscreen index or windowed.
bool fullscreen = video_is_fullscreen();
uint nmodes = video_get_num_modes(fullscreen);
VideoMode cur = video_get_current_mode();
log_debug("Fullscreen: %i", fullscreen);
log_debug("Prev selected: %i", bind->selected);
bind->valrange_min = 0;
bind->valrange_max = nmodes - 1;
bind->selected = -1;
log_debug("nmodes = %i", nmodes);
for(int i = 0; i < nmodes; ++i) {
VideoMode m;
attr_unused bool ok = video_get_mode(i, fullscreen, &m);
assert(ok);
log_debug("#%i %ix%i", i, m.width, m.height);
if(m.width == cur.width && m.height == cur.height) {
bind->selected = i;
log_debug("selected #%i", bind->selected);
}
}
}
static OptionBinding* bind_resolution(void) {
OptionBinding *bind = bind_new();
bind->type = BT_Resolution;
bind->selected = -1;
bind_resolution_update(bind);
return bind;
}
// BT_FramebufferResolution: not an actual setting (yet); just display effective resolution in pixels
// This may be different from BT_Resolution in the high-DPI case
// (BT_Resolution is a misnomer; it represents the window size in screen-space units)
static OptionBinding* bind_fb_resolution(void) {
OptionBinding *bind = bind_new();
bind->type = BT_FramebufferResolution;
return bind;
}
// BT_Scale: float values clamped to a range
static OptionBinding* bind_scale(int cfgentry, float smin, float smax, float step) {
OptionBinding *bind = bind_new();
bind->type = BT_Scale;
bind->configentry = cfgentry;
bind->scale_min = smin;
bind->scale_max = smax;
bind->scale_step = step;
return bind;
}
// Returns a pointer to the first found binding that blocks input. If none found, returns NULL.
static OptionBinding* bind_getinputblocking(MenuData *m) {
dynarray_foreach_idx(&m->entries, int i, {
OptionBinding *bind = bind_get(m, i);
if(bind && bind->blockinput) {
return bind;
}
});
return NULL;
}
// Adds a value to a BT_IntValue type binding
static int bind_addvalue(OptionBinding *b, char *val) {
assert(b->valrange_min == 0);
if(b->values == NULL) {
b->values = mem_alloc(sizeof(char*));
b->valrange_min = 0;
b->valrange_max = 0;
} else {
assert(b->valrange_min == 0);
++b->valrange_max;
}
b->values = mem_realloc(b->values, (1 + b->valrange_max) * sizeof(char*));
b->values[b->valrange_max] = mem_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;
}
// Called to select a value of a BT_IntValue type binding by index
static int bind_setvalue(OptionBinding *b, int v) {
if(b->setter)
return b->selected = b->setter(b, v);
else
return b->selected = v;
}
// Called to get the selected value of a BT_IntValue type binding by index
static int bind_getvalue(OptionBinding *b) {
if(b->getter) {
b->selected = b->getter(b);
}
return b->selected;
}
// Selects the next to current value of a BT_IntValue type binding
static int bind_setnext(OptionBinding *b) {
int s = b->selected + 1;
if(s > b->valrange_max) {
s = b->valrange_min;
}
return bind_setvalue(b, s);
}
// Selects the previous to current value of a BT_IntValue type binding
static int bind_setprev(OptionBinding *b) {
int s = b->selected - 1;
if(s < b->valrange_min) {
s = b->valrange_max;
}
return bind_setvalue(b, s);
}
static bool bind_isactive(OptionBinding *b) {
if(!b->dependence)
return true;
return b->dependence();
}
// --- Shared binding callbacks --- //
static int bind_common_onoff_get(OptionBinding *b) {
return !config_get_int(b->configentry);
}
static int bind_common_onoff_set(OptionBinding *b, int v) {
return !config_set_int(b->configentry, !v);
}
static int bind_common_onoff_inverted_get(OptionBinding *b) {
return config_get_int(b->configentry);
}
static int bind_common_onoff_inverted_set(OptionBinding *b, int v) {
return config_set_int(b->configentry, v);
}
static int bind_common_onoffplus_get(OptionBinding *b) {
int v = config_get_int(b->configentry);
if(v > 1)
return v;
return !v;
}
static int bind_common_onoffplus_set(OptionBinding *b, int v) {
if(v > 1)
return config_set_int(b->configentry, v);
return !config_set_int(b->configentry, !v);
}
#define bind_common_int_get bind_common_onoff_inverted_get
#define bind_common_int_set bind_common_onoff_inverted_set
// BT_VideoDisplay: fullscreen display number
static OptionBinding* bind_video_display(int cfgentry) {
OptionBinding *bind = bind_new();
bind->configentry = cfgentry;
bind->type = BT_VideoDisplay;
bind->getter = bind_common_int_get;
bind->setter = bind_common_int_set;
bind->valrange_min = 0;
bind->valrange_max = 0; // updated later
bind->selected = video_current_display();
return bind;
}
static int bind_common_intplus1_get(OptionBinding *b) {
return config_get_int(b->configentry) - 1;
}
static int bind_common_intplus1_set(OptionBinding *b, int v) {
return config_set_int(b->configentry, v + 1) - 1;
}
// --- Binding callbacks for individual options --- //
static bool bind_resizable_dependence(void) {
return video_query_capability(VIDEO_CAP_EXTERNAL_RESIZE) == VIDEO_AVAILABLE;
}
static bool bind_bgquality_dependence(void) {
return !config_get_int(CONFIG_NO_STAGEBG);
}
static bool bind_resolution_dependence(void) {
return video_query_capability(VIDEO_CAP_CHANGE_RESOLUTION) == VIDEO_AVAILABLE;
}
static bool bind_fb_resolution_dependence(void) {
return false;
}
static bool bind_fullscreen_dependence(void) {
return video_query_capability(VIDEO_CAP_FULLSCREEN) == VIDEO_AVAILABLE;
}
static int bind_resolution_set(OptionBinding *b, int v) {
VideoMode m;
if(video_get_mode(v, video_is_fullscreen(), &m)) {
config_set_int(CONFIG_VID_WIDTH, m.width);
config_set_int(CONFIG_VID_HEIGHT, m.height);
}
return v;
}
static int bind_power_set(OptionBinding *b, int v) {
return config_set_int(b->configentry, v * 100) / 100;
}
static int bind_power_get(OptionBinding *b) {
return config_get_int(b->configentry) / 100;
}
static int bind_gamepad_set(OptionBinding *b, int v) {
v = bind_common_onoff_set(b, v);
if(v == 0) {
gamepad_init();
}
return v;
}
// --- Creating, destroying, filling the menu --- //
typedef struct ConfirmDialog {
OptionsMenuArgHeader header;
const char *const text;
void (*const action)(void);
} ConfirmDialog;
static const ConfirmDialog dialog_reset_default = {
.header.type = ARGTYPE_OTHER,
.text = "Reset all settings to defaults?",
.action = config_reset,
};
static const ConfirmDialog dialog_reset_saved = {
.header.type = ARGTYPE_OTHER,
.text = "Reset all settings to the last saved values?",
.action = config_load,
};
typedef struct OptionsMenuContext {
const char *title;
void *data;
MenuData *submenu;
MenuData *submenu_fading;
ConfirmDialog *confirm_dialog;
float submenu_alpha;
void (*draw_overlay)(MenuData *m, struct OptionsMenuContext *ctx);
struct {
bool allowed;
bool active;
} gamepad_testmode;
} OptionsMenuContext;
static void destroy_options_menu(MenuData *m) {
bool change_vidmode = false;
dynarray_foreach_idx(&m->entries, int i, {
OptionBinding *bind = bind_get(m, i);
if(!bind) {
continue;
}
if(bind->type == BT_Resolution && video_query_capability(VIDEO_CAP_CHANGE_RESOLUTION) == VIDEO_AVAILABLE) {
if(bind->selected != -1) {
bool fullscreen = video_is_fullscreen();
VideoMode mode;
if(video_get_mode(bind->selected, fullscreen, &mode)) {
config_set_int(CONFIG_VID_WIDTH, mode.width);
config_set_int(CONFIG_VID_HEIGHT, mode.height);
change_vidmode = true;
}
}
}
bind_free(bind);
mem_free(bind);
});
if(change_vidmode) {
video_set_mode(
config_get_int(CONFIG_VID_DISPLAY),
config_get_int(CONFIG_VID_WIDTH),
config_get_int(CONFIG_VID_HEIGHT),
config_get_int(CONFIG_FULLSCREEN),
config_get_int(CONFIG_VID_RESIZABLE)
);
}
if(m->context) {
OptionsMenuContext *ctx = m->context;
if(ctx->submenu) {
free_menu(ctx->submenu);
}
if(ctx->submenu_fading) {
free_menu(ctx->submenu);
}
mem_free(ctx->data);
mem_free(ctx);
}
}
static void do_nothing(MenuData *menu, void *arg) { }
static void update_options_menu(MenuData *menu);
static void options_menu_input(MenuData*);
static void draw_options_menu(MenuData*);
#define bind_onoff(b) bind_addvalue(b, "on"); bind_addvalue(b, "off")
static bool entry_is_active(MenuData *m, int idx) {
MenuEntry *e = dynarray_get_ptr(&m->entries, idx);
if(!e->action) {
return false;
}
OptionBinding *bind = bind_get(m, idx);
if(bind) {
return bind_isactive(bind);
}
return true;
}
static void begin_options_menu(MenuData *m) {
dynarray_foreach_idx(&m->entries, int i, {
if(entry_is_active(m, i)) {
m->cursor = i;
break;
}
});
}
static MenuData* create_options_menu_base(const char *s) {
MenuData *m = alloc_menu();
m->transition = TransFadeBlack;
m->flags = MF_Abortable;
m->input = options_menu_input;
m->draw = draw_options_menu;
m->logic = update_options_menu;
m->begin = begin_options_menu;
m->end = destroy_options_menu;
m->context = ALLOC(OptionsMenuContext, { .title = s });
return m;
}
static void options_enter_sub(MenuData *parent, MenuData *(*construct)(MenuData*)) {
parent->frames = 0;
enter_menu(construct(parent), NO_CALLCHAIN);
}
#define DECLARE_ENTER_FUNC(enter, construct) \
static void enter(MenuData *parent, void *arg) { \
options_enter_sub(parent, construct); \
}
static void confirm_dialog_draw(MenuData *m) {
OptionsMenuContext *ctx = m->context;
float alpha = ctx->submenu_alpha;
float lineskip = font_get_lineskip(res_font("standard"));
float height = lineskip * 4;
float width = text_width(res_font("standard"), ctx->confirm_dialog->text, 0) + 64;
r_state_push();
alpha *= 0.7;
r_color4(0, 0, 0, alpha);
r_shader_standard_notex();
r_mat_mv_push();
r_mat_mv_translate(SCREEN_W*0.5, SCREEN_H*0.5, 0);
r_mat_mv_push();
r_mat_mv_scale(SCREEN_W, SCREEN_H, 1);
r_draw_quad();
r_mat_mv_pop();
r_mat_mv_scale(width, height, 1);
r_color4(0.1 * alpha, 0.1 * alpha, 0.1 * alpha, alpha);
r_draw_quad();
r_mat_mv_pop();
r_state_pop();
text_draw(ctx->confirm_dialog->text, &(TextParams) {
.align = ALIGN_CENTER,
.color = RGBA_MUL_ALPHA(1, 1, 1, alpha),
.pos = { SCREEN_W*0.5, SCREEN_H*0.5 - lineskip * 0.5 },
.shader = "text_default",
});
dynarray_foreach(&m->entries, int i, MenuEntry *e, {
float x = 0.5f * SCREEN_W - width * 0.5f;
x += (i + 1) * width / (m->entries.num_elements + 1);
float a = e->drawdata / 10;
float ia = 1.0f - a;
text_draw(e->name, &(TextParams) {
.align = ALIGN_CENTER,
.color = RGBA_MUL_ALPHA(
0.9 + ia * 0.1,
0.6 + ia * 0.4,
0.2 + ia * 0.8,
(0.7 + 0.3 * a) * alpha
),
.pos = { x, SCREEN_H*0.5 + lineskip * 0.75f },
.shader = "text_default",
});
});
}
static void confirm_reset(MenuData *m, void *a) {
OptionsMenuContext *ctx = m->context;
ctx->confirm_dialog->action();
menu_action_close(m, a);
}
static void confirm_dialog_logic(MenuData *m) {
animate_menu_list_entries(m);
}
static void confirm_dialog(MenuData *m, void *a) {
MenuData *sub = alloc_menu();
sub->draw = confirm_dialog_draw;
sub->logic = confirm_dialog_logic;
sub->flags = MF_Transient | MF_Abortable;
sub->transition = NULL;
sub->context = m->context;
add_menu_entry(sub, "Yes", confirm_reset, NULL);
add_menu_entry(sub, "No", menu_action_close, NULL);
sub->cursor = 1;
OptionsMenuContext *ctx = m->context;
assert(!ctx->submenu);
ctx->submenu = sub;
ctx->submenu_alpha = 0;
ctx->confirm_dialog = a;
if(ctx->submenu_fading) {
free_menu(ctx->submenu_fading);
ctx->submenu_fading = NULL;
}
}
static MenuData* create_options_menu_controls(MenuData *parent);
DECLARE_ENTER_FUNC(enter_options_menu_controls, create_options_menu_controls)
static MenuData* create_options_menu_gamepad(MenuData *parent);
DECLARE_ENTER_FUNC(enter_options_menu_gamepad, create_options_menu_gamepad)
static MenuData* create_options_menu_gamepad_controls(MenuData *parent);
DECLARE_ENTER_FUNC(enter_options_menu_gamepad_controls, create_options_menu_gamepad_controls)
static MenuData* create_options_menu_video(MenuData *parent);
DECLARE_ENTER_FUNC(enter_options_menu_video, create_options_menu_video)
static MenuData* create_options_menu_video(MenuData *parent) {
MenuData *m = create_options_menu_base("Video Options");
OptionBinding *b;
add_menu_entry(m, "Fullscreen", do_nothing,
b = bind_option(CONFIG_FULLSCREEN, bind_common_onoff_get, bind_common_onoff_set)
); bind_onoff(b);
b->dependence = bind_fullscreen_dependence;
add_menu_entry(m, "Display", do_nothing,
b = bind_video_display(CONFIG_VID_DISPLAY)
);
add_menu_entry(m, "Window size", do_nothing,
b = bind_resolution()
); b->setter = bind_resolution_set;
b->dependence = bind_resolution_dependence;
add_menu_entry(m, "Renderer resolution", do_nothing,
b = bind_fb_resolution()
); b->dependence = bind_fb_resolution_dependence;
b->pad++;
add_menu_entry(m, "Resizable window", do_nothing,
b = bind_option(CONFIG_VID_RESIZABLE, bind_common_onoff_get, bind_common_onoff_set)
); bind_onoff(b);
b->dependence = bind_resizable_dependence;
add_menu_separator(m);
add_menu_entry(m, "Pause the game when not focused", do_nothing,
b = bind_option(CONFIG_FOCUS_LOSS_PAUSE, bind_common_onoff_get, bind_common_onoff_set)
); bind_onoff(b);
add_menu_entry(m, "Vertical synchronization", do_nothing,
b = bind_option(CONFIG_VSYNC, bind_common_onoffplus_get, bind_common_onoffplus_set)
); bind_addvalue(b, "on");
bind_addvalue(b, "off");
if(video_query_capability(VIDEO_CAP_VSYNC_ADAPTIVE) != VIDEO_NEVER_AVAILABLE) {
bind_addvalue(b, "adaptive");
}
add_menu_entry(m, "Skip frames", do_nothing,
b = bind_option(CONFIG_VID_FRAMESKIP, bind_common_intplus1_get, bind_common_intplus1_set)
); bind_addvalue(b, "0");
bind_addvalue(b, "½");
bind_addvalue(b, "");
add_menu_separator(m);
add_menu_entry(m, "Overall rendering quality", do_nothing,
b = bind_scale(CONFIG_FG_QUALITY, 0.1, 1.0, 0.05)
);
add_menu_entry(m, "Draw background", do_nothing,
b = bind_option(CONFIG_NO_STAGEBG, bind_common_onoff_inverted_get, bind_common_onoff_inverted_set)
); bind_onoff(b);
add_menu_entry(m, "Background rendering quality", do_nothing,
b = bind_scale(CONFIG_BG_QUALITY, 0.1, 1.0, 0.05)
); b->dependence = bind_bgquality_dependence;
b->pad++;
add_menu_separator(m);
add_menu_entry(m, "Anti-aliasing", do_nothing,
b = bind_option(CONFIG_FXAA, bind_common_int_get, bind_common_int_set)
); bind_addvalue(b, "none");
bind_addvalue(b, "fxaa");
add_menu_entry(m, "Particle effects", do_nothing,
b = bind_option(CONFIG_PARTICLES, bind_common_int_get, bind_common_int_set)
); bind_addvalue(b, "minimal");
bind_addvalue(b, "full");
add_menu_entry(m, "Postprocessing effects", do_nothing,
b = bind_option(CONFIG_POSTPROCESS, bind_common_int_get, bind_common_int_set)
); bind_addvalue(b, "minimal");
bind_addvalue(b, "fast");
bind_addvalue(b, "full");
add_menu_separator(m);
add_menu_entry(m, "Back", menu_action_close, NULL);
return m;
}
attr_unused
static void bind_setvaluerange_fancy(OptionBinding *b, int ma) {
int i = 0; for(i = 0; i <= ma; ++i) {
char tmp[16];
snprintf(tmp, 16, "%i", i);
bind_addvalue(b, tmp);
}
}
#ifndef __SWITCH__
static bool gamepad_enabled_depencence(void) {
return config_get_int(CONFIG_GAMEPAD_ENABLED) && gamepad_initialized();
}
#endif
static MenuData* create_options_menu_gamepad_controls(MenuData *parent) {
MenuData *m = create_options_menu_base("Gamepad Controls");
add_menu_entry(m, "Move up", do_nothing,
bind_gpbinding(CONFIG_GAMEPAD_KEY_UP)
);
add_menu_entry(m, "Move down", do_nothing,
bind_gpbinding(CONFIG_GAMEPAD_KEY_DOWN)
);
add_menu_entry(m, "Move left", do_nothing,
bind_gpbinding(CONFIG_GAMEPAD_KEY_LEFT)
);
add_menu_entry(m, "Move right", do_nothing,
bind_gpbinding(CONFIG_GAMEPAD_KEY_RIGHT)
);
add_menu_separator(m);
add_menu_entry(m, "Shoot", do_nothing,
bind_gpbinding(CONFIG_GAMEPAD_KEY_SHOT)
);
add_menu_entry(m, "Focus", do_nothing,
bind_gpbinding(CONFIG_GAMEPAD_KEY_FOCUS)
);
add_menu_entry(m, "Use Spell Card", do_nothing,
bind_gpbinding(CONFIG_GAMEPAD_KEY_BOMB)
);
add_menu_entry(m, "Power Surge / Discharge", do_nothing,
bind_gpbinding(CONFIG_GAMEPAD_KEY_SPECIAL)
);
add_menu_separator(m);
add_menu_entry(m, "Skip dialog", do_nothing,
bind_gpbinding(CONFIG_GAMEPAD_KEY_SKIP)
);
add_menu_separator(m);
add_menu_entry(m, "Back", menu_action_close, NULL);
return m;
}
static void destroy_options_menu_gamepad(MenuData *m) {
OptionsMenuContext *ctx = m->context;
if(config_get_int(CONFIG_GAMEPAD_ENABLED)) {
if(strcasecmp(config_get_str(CONFIG_GAMEPAD_DEVICE), ctx->data)) {
gamepad_update_devices();
}
} else {
gamepad_shutdown();
}
destroy_options_menu(m);
}
static inline GamepadButton options_gamepad_testing_button(void) {
return config_get_int(CONFIG_GAMEPAD_KEY_FOCUS);
}
static void draw_gamepad_options_overlay(MenuData *m, OptionsMenuContext *ctx) {
float csize = 86;
int x, y;
gamepad_get_player_analog_input(&x, &y);
cmplx in_processed = CMPLX(
gamepad_normalize_axis_value(x),
gamepad_normalize_axis_value(y));
cmplx in_raw = CMPLX(
gamepad_normalize_axis_value(gamepad_player_axis_value(PLRAXIS_LR)),
gamepad_normalize_axis_value(gamepad_player_axis_value(PLRAXIS_UD)));
double deadzone = gamepad_get_normalized_deadzone();
double maxzone = gamepad_get_normalized_maxzone();
r_mat_mv_push();
r_mat_mv_translate(
SCREEN_W - OPTIONS_X_MARGIN,
OPTIONS_Y_MARGIN + 6.5f*OPTIONS_ITEM_HEIGHT,
0);
r_mat_mv_scale(csize, csize, 0);
r_mat_mv_translate(0.5f, 0.5f, 0);
float snap = clamp(config_get_float(CONFIG_GAMEPAD_AXIS_SNAP), 0, 1);
float diagonal_bias = clamp(config_get_float(CONFIG_GAMEPAD_AXIS_SNAP_DIAG_BIAS), -1, 1);
float w_cardinal = 1.0f - diagonal_bias;
float w_diagonal = 1.0f + diagonal_bias;
float thres_card = (snap * (float)M_PI/8.0f) * w_cardinal;
float thres_diag = (snap * (float)M_PI/8.0f) * w_diagonal;
r_shader("gamepad_circle");
r_uniform_vec4("snap_angles_sincos", sinf(thres_card), cosf(thres_card), sinf(thres_diag), cosf(thres_diag));
r_uniform_vec2("deadzones", deadzone, maxzone);
if(ctx->gamepad_testmode.active) {
r_uniform_vec4("joy_pointers", re(in_processed), im(in_processed), re(in_raw), im(in_raw));
r_color4(1, 1, 1, 1);
} else {
r_uniform_vec4("joy_pointers", 69, 69, 69, 69);
r_color4(0.7, 0.7, 0.7, 0.7);
}
r_draw_quad();
r_mat_mv_pop();
char buf[128];
GamepadButton test_btn = options_gamepad_testing_button();
if(ctx->gamepad_testmode.active) {
snprintf(buf, sizeof(buf),
"Press any button on your gamepad to exit joystick testing mode");
} else {
snprintf(buf, sizeof(buf),
"Press %s on your gamepad to enter joystick testing mode",
gamepad_button_name(test_btn));
}
text_draw(buf, &(TextParams) {
.pos = { SCREEN_W/2.0f, SCREEN_H - OPTIONS_Y_MARGIN },
.align = ALIGN_CENTER,
.shader = "text_default",
.color = RGBA(0.7, 0.7, 0.7, 0.7),
});
}
static MenuData* create_options_menu_gamepad(MenuData *parent) {
MenuData *m = create_options_menu_base("Gamepad Options");
m->end = destroy_options_menu_gamepad;
OptionsMenuContext *ctx = m->context;
ctx->data = mem_strdup(config_get_str(CONFIG_GAMEPAD_DEVICE));
ctx->draw_overlay = draw_gamepad_options_overlay;
ctx->gamepad_testmode.allowed = true;
OptionBinding *b;
#ifndef __SWITCH__
add_menu_entry(m, "Enable Gamepad/Joystick support", do_nothing,
b = bind_option(CONFIG_GAMEPAD_ENABLED, bind_common_onoff_get, bind_gamepad_set)
); bind_onoff(b);
add_menu_entry(m, "Device", do_nothing,
b = bind_gpdevice(CONFIG_GAMEPAD_DEVICE)
); b->dependence = gamepad_enabled_depencence;
#endif
add_menu_separator(m);
add_menu_entry(m, "Customize controls…", enter_options_menu_gamepad_controls, NULL);
add_menu_separator(m);
add_menu_entry(m, "Remap square input into circular", do_nothing,
b = bind_option(CONFIG_GAMEPAD_AXIS_SQUARECIRCLE, bind_common_onoff_get, bind_gamepad_set)
); bind_onoff(b);
add_menu_separator(m);
add_menu_entry(m, "Direction snap factor", do_nothing,
b = bind_scale(CONFIG_GAMEPAD_AXIS_SNAP, 0, 1, 0.05)
);
add_menu_entry(m, "Diagonal bias", do_nothing,
b = bind_scale(CONFIG_GAMEPAD_AXIS_SNAP_DIAG_BIAS, -1, 1, 0.05)
); b->pad++;
add_menu_separator(m);
add_menu_entry(m, "Dead zone", do_nothing,
b = bind_scale(CONFIG_GAMEPAD_AXIS_DEADZONE, 0, 1, 0.01)
);
add_menu_entry(m, "Maximum zone", do_nothing,
b = bind_scale(CONFIG_GAMEPAD_AXIS_MAXZONE, 0, 1, 0.01)
);
add_menu_entry(m, "Sensitivity", do_nothing,
b = bind_scale(CONFIG_GAMEPAD_AXIS_SENS, -2, 2, 0.05)
);
add_menu_separator(m);
add_menu_entry(m, "Back", menu_action_close, NULL);
return m;
}
static MenuData* create_options_menu_controls(MenuData *parent) {
MenuData *m = create_options_menu_base("Controls");
add_menu_entry(m, "Move up", do_nothing,
bind_keybinding(CONFIG_KEY_UP)
);
add_menu_entry(m, "Move down", do_nothing,
bind_keybinding(CONFIG_KEY_DOWN)
);
add_menu_entry(m, "Move left", do_nothing,
bind_keybinding(CONFIG_KEY_LEFT)
);
add_menu_entry(m, "Move right", do_nothing,
bind_keybinding(CONFIG_KEY_RIGHT)
);
add_menu_separator(m);
add_menu_entry(m, "Shoot", do_nothing,
bind_keybinding(CONFIG_KEY_SHOT)
);
add_menu_entry(m, "Focus", do_nothing,
bind_keybinding(CONFIG_KEY_FOCUS)
);
add_menu_entry(m, "Use Spell Card", do_nothing,
bind_keybinding(CONFIG_KEY_BOMB)
);
add_menu_entry(m, "Power Surge / Discharge", do_nothing,
bind_keybinding(CONFIG_KEY_SPECIAL)
);
add_menu_separator(m);
add_menu_entry(m, "Toggle fullscreen", do_nothing,
bind_keybinding(CONFIG_KEY_FULLSCREEN)
);
add_menu_entry(m, "Take a screenshot", do_nothing,
bind_keybinding(CONFIG_KEY_SCREENSHOT)
);
add_menu_entry(m, "Skip dialog", do_nothing,
bind_keybinding(CONFIG_KEY_SKIP)
);
add_menu_separator(m);
add_menu_entry(m, "Toggle audio", do_nothing,
bind_keybinding(CONFIG_KEY_TOGGLE_AUDIO)
);
add_menu_separator(m);
add_menu_entry(m, "Stop the game immediately", do_nothing,
bind_keybinding(CONFIG_KEY_STOP)
);
add_menu_entry(m, "Restart the game immediately", do_nothing,
bind_keybinding(CONFIG_KEY_RESTART)
);
add_menu_entry(m, "Quick save", do_nothing,
bind_keybinding(CONFIG_KEY_QUICKSAVE)
);
add_menu_entry(m, "Quick load", do_nothing,
bind_keybinding(CONFIG_KEY_QUICKLOAD)
);
#ifdef DEBUG
add_menu_separator(m);
add_menu_entry(m, "Toggle God mode", do_nothing,
bind_keybinding(CONFIG_KEY_IDDQD)
);
add_menu_entry(m, "Skip stage", do_nothing,
bind_keybinding(CONFIG_KEY_HAHAIWIN)
);
add_menu_entry(m, "Power up", do_nothing,
bind_keybinding(CONFIG_KEY_POWERUP)
);
add_menu_entry(m, "Power down", do_nothing,
bind_keybinding(CONFIG_KEY_POWERDOWN)
);
add_menu_entry(m, "Disable background rendering (HoM effect)", do_nothing,
bind_keybinding(CONFIG_KEY_NOBACKGROUND)
);
add_menu_entry(m, "Toggle collision areas overlay", do_nothing,
bind_keybinding(CONFIG_KEY_HITAREAS)
);
#endif
add_menu_separator(m);
add_menu_entry(m, "Back", menu_action_close, NULL);
return m;
}
MenuData* create_options_menu(void) {
MenuData *m = create_options_menu_base("Options");
OptionBinding *b;
#ifndef __SWITCH__
add_menu_entry(m, "Player name", do_nothing,
b = bind_stroption(CONFIG_PLAYERNAME)
);
add_menu_separator(m);
#endif
add_menu_entry(m, "Save replays", do_nothing,
b = bind_option(CONFIG_SAVE_RPY, bind_common_onoffplus_get, bind_common_onoffplus_set)
); bind_addvalue(b, "always");
bind_addvalue(b, "never");
bind_addvalue(b, "ask");
add_menu_entry(m, "Auto-restart in Spell Practice", do_nothing,
b = bind_option(CONFIG_SPELLSTAGE_AUTORESTART, bind_common_onoff_get, bind_common_onoff_set)
); bind_onoff(b);
add_menu_entry(m, "Power in Spell and Stage Practice", do_nothing,
b = bind_option(CONFIG_PRACTICE_POWER, bind_power_get, bind_power_set)
); bind_addvalue(b, "0.0");
bind_addvalue(b, "1.0");
bind_addvalue(b, "2.0");
bind_addvalue(b, "3.0");
bind_addvalue(b, "4.0");
add_menu_entry(m, "Shoot by default", do_nothing,
b = bind_option(CONFIG_SHOT_INVERTED, bind_common_onoff_get, bind_common_onoff_set)
); bind_onoff(b);
add_menu_entry(m, "Automatic Power Surge activation", do_nothing,
b = bind_option(CONFIG_AUTO_SURGE, bind_common_onoff_get, bind_common_onoff_set)
); bind_onoff(b);
add_menu_entry(m, "Boss healthbar style", do_nothing,
b = bind_option(CONFIG_HEALTHBAR_STYLE, bind_common_int_get, bind_common_int_set)
); bind_addvalue(b, "classic");
bind_addvalue(b, "modern");
add_menu_entry(m, "Floating score text visibility", do_nothing,
b = bind_scale(CONFIG_SCORETEXT_ALPHA, 0, 1, 0.05)
);
add_menu_separator(m);
add_menu_entry(m, "SFX Volume", do_nothing,
b = bind_scale(CONFIG_SFX_VOLUME, 0, 1, 0.05)
); b->dependence = audio_output_works;
add_menu_entry(m, "BGM Volume", do_nothing,
b = bind_scale(CONFIG_BGM_VOLUME, 0, 1, 0.05)
); b->dependence = audio_output_works;
add_menu_entry(m, "Mute audio", do_nothing,
b = bind_option(CONFIG_MUTE_AUDIO, bind_common_onoff_get, bind_common_onoff_set)
); bind_onoff(b);
add_menu_separator(m);
add_menu_entry(m, "Video options…", enter_options_menu_video, NULL);
add_menu_entry(m, "Customize controls…", enter_options_menu_controls, NULL);
add_menu_entry(m, "Gamepad & Joystick options…", enter_options_menu_gamepad, NULL);
add_menu_separator(m);
auto e = add_menu_entry(
m, "Reload from last saved", confirm_dialog, (void*)&dialog_reset_saved);
e->transition = NULL;
e = add_menu_entry(
m, "Reset to defaults", confirm_dialog, (void*)&dialog_reset_default);
e->transition = NULL;
add_menu_separator(m);
add_menu_entry(m, "Back", menu_action_close, NULL);
return m;
}
// --- Drawing the menu --- //
void draw_options_menu_bg(MenuData* menu) {
draw_main_menu_bg(menu, 0, 0, 0.05, "abstract_brown", "stage1/cirnobg");
r_state_push();
r_mat_mv_push();
r_mat_mv_scale(SCREEN_W, SCREEN_H, 1);
r_shader_standard_notex();
r_mat_mv_translate(0.5, 0.5, 0);
r_color(RGBA(0, 0, 0, 0.5));
r_draw_quad();
r_mat_mv_pop();
r_state_pop();
}
static void update_options_menu(MenuData *menu) {
menu->drawdata[0] += ((SCREEN_W/2 - 100) - menu->drawdata[0])/10.0;
menu->drawdata[1] += ((SCREEN_W - 200) * 1.75 - menu->drawdata[1])/10.0;
menu->drawdata[2] += (OPTIONS_ITEM_HEIGHT*menu->cursor - menu->drawdata[2])/10.0;
animate_menu_list_entries(menu);
OptionsMenuContext *ctx = menu->context;
if(ctx->submenu) {
if(ctx->submenu->state == MS_Dead) {
ctx->submenu_fading = ctx->submenu;
ctx->submenu = NULL;
} else {
fapproach_asymptotic_p(&ctx->submenu_alpha, 1, 0.2, 1e-3);
ctx->submenu->logic(ctx->submenu);
}
}
if(ctx->submenu_fading) {
if(fapproach_asymptotic_p(&ctx->submenu_alpha, 0, 0.2, 1e-3) == 0) {
free_menu(ctx->submenu_fading);
ctx->submenu_fading = NULL;
}
}
}
static void options_draw_item(MenuEntry *e, int i, int cnt, void *ctx) {
MenuData *menu = ctx;
OptionBinding *bind = bind_get(menu, i);
Color clr;
if(!e->name) {
return;
}
r_shader("text_default");
float a = e->drawdata * 0.1;
float alpha = entry_is_active(menu, i) ? 1 : 0.5;
clr = *r_color_current();
if(!entry_is_active(menu, i)) {
color_mul_scalar(&clr, 0.5f);
}
text_draw(e->name, &(TextParams) {
.pos = { (1 + (bind ? bind->pad : 0)) * OPTIONS_ACTIVE_X_OFFSET - e->drawdata, OPTIONS_ITEM_HEIGHT*i },
.color = &clr,
});
if(bind) {
float margin = OPTIONS_X_MARGIN * 2 + OPTIONS_ACTIVE_X_OFFSET;
float origin = SCREEN_W - margin;
switch(bind->type) {
case BT_IntValue: {
int val = bind_getvalue(bind);
if(bind->configentry == CONFIG_PRACTICE_POWER) {
Font *fnt_int = res_font("standard");
Font *fnt_fract = res_font("small");
draw_fraction(
val,
ALIGN_RIGHT,
origin,
OPTIONS_ITEM_HEIGHT*i,
fnt_int,
fnt_fract,
&clr,
&clr,
false
);
} else if(bind->values) {
for(int j = bind->displaysingle? val : bind->valrange_max; (j+1) && (!bind->displaysingle || j == val); --j) {
if(j != bind->valrange_max && !bind->displaysingle) {
origin -= text_width(res_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);
}
text_draw(bind->values[j], &(TextParams) {
.pos = { origin, OPTIONS_ITEM_HEIGHT*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,
});
}
break;
}
case BT_KeyBinding: {
if(bind->blockinput) {
text_draw("Press a key to assign, ESC to cancel", &(TextParams) {
.pos = { origin, OPTIONS_ITEM_HEIGHT*i },
.align = ALIGN_RIGHT,
.color = RGBA(0.5, 1, 0.5, 1),
});
} else {
const char *txt = SDL_GetScancodeName(config_get_int(bind->configentry));
if(!txt || !*txt) {
txt = "Unknown";
}
text_draw(txt, &(TextParams) {
.pos = { origin, OPTIONS_ITEM_HEIGHT*i },
.align = ALIGN_RIGHT,
.color = &clr,
});
}
break;
}
case BT_GamepadDevice: {
if(bind_isactive(bind)) {
// 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 < -1 || bind->selected > bind->valrange_max) {
bind->selected = gamepad_get_active_device();
if(bind->selected < -1) {
bind->selected = -1;
}
}
char *txt;
char buf[64];
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";
}
text_draw(txt, &(TextParams) {
.pos = { origin, OPTIONS_ITEM_HEIGHT*i },
.align = ALIGN_RIGHT,
.color = &clr,
.max_width = (SCREEN_W - margin) / 2,
});
}
break;
}
case BT_VideoDisplay: {
// XXX: see the BT_GamepadDevice case...
bind->valrange_max = video_num_displays() - 1;
if(bind->selected < 0 || bind->selected > bind->valrange_max) {
bind->selected = video_current_display();
if(bind->selected < 0) {
bind->selected = 0;
}
}
char buf[64];
snprintf(buf, sizeof(buf), "#%i: %s", bind->selected + 1, video_display_name(bind->selected));
text_draw(buf, &(TextParams) {
.pos = { origin, OPTIONS_ITEM_HEIGHT*i },
.align = ALIGN_RIGHT,
.color = &clr,
.max_width = (SCREEN_W - margin) / 2,
});
break;
}
case BT_GamepadKeyBinding:
case BT_GamepadAxisBinding: {
bool is_axis = (bind->type == BT_GamepadAxisBinding);
if(bind->blockinput) {
char *text = is_axis ? "Move an axis to assign, Back to cancel"
: "Press a button to assign, Back to cancel";
text_draw(text, &(TextParams) {
.pos = { origin, OPTIONS_ITEM_HEIGHT*i },
.align = ALIGN_RIGHT,
.color = RGBA(0.5, 1, 0.5, 1),
});
} else if(config_get_int(bind->configentry) >= 0) {
int id = config_get_int(bind->configentry);
const char *name = (is_axis ? gamepad_axis_name(id) : gamepad_button_name(id));
text_draw(name, &(TextParams) {
.pos = { origin, OPTIONS_ITEM_HEIGHT*i },
.align = ALIGN_RIGHT,
.color = &clr,
});
} else {
text_draw("Unbound", &(TextParams) {
.pos = { origin, OPTIONS_ITEM_HEIGHT*i },
.align = ALIGN_RIGHT,
.color = &clr,
});
}
break;
}
case BT_StrValue: {
if(bind->blockinput) {
if(*bind->strvalue) {
text_draw(bind->strvalue, &(TextParams) {
.pos = { origin, OPTIONS_ITEM_HEIGHT*i },
.align = ALIGN_RIGHT,
.color = RGBA(0.5, 1, 0.5, 1.0),
});
}
} else {
text_draw(config_get_str(bind->configentry), &(TextParams) {
.pos = { origin, OPTIONS_ITEM_HEIGHT*i },
.align = ALIGN_RIGHT,
.color = &clr,
});
}
break;
}
case BT_Resolution: {
char tmp[32];
int w, h;
VideoMode m;
if(!video_get_mode(bind->selected, video_is_fullscreen(), &m)) {
m = video_get_current_mode();
}
w = m.width;
h = m.height;
snprintf(tmp, sizeof(tmp), "%dx%d", w, h);
text_draw(tmp, &(TextParams) {
.pos = { origin, OPTIONS_ITEM_HEIGHT*i },
.align = ALIGN_RIGHT,
.color = &clr,
});
break;
}
case BT_FramebufferResolution: {
IntExtent fbsize = r_framebuffer_get_size(video_get_screen_framebuffer());
char tmp[32];
snprintf(tmp, sizeof(tmp), "%gx %dx%d", video_get_scaling_factor(), fbsize.w, fbsize.h);
text_draw(tmp, &(TextParams) {
.pos = { origin, OPTIONS_ITEM_HEIGHT*i },
.align = ALIGN_RIGHT,
.color = &clr,
});
break;
}
case BT_Scale: {
int w = 200;
int h = 5;
int cw = 5;
float val = config_get_float(bind->configentry);
float ma = bind->scale_max;
float mi = bind->scale_min;
float pos = (val - mi) / (ma - mi);
char tmp[8];
snprintf(tmp, 8, "%.0f%%", 100 * val);
if(!strcmp(tmp, "-0%"))
strcpy(tmp, "0%");
r_mat_mv_push();
r_mat_mv_translate(origin - (w+cw) * 0.5, OPTIONS_ITEM_HEIGHT * i, 0);
text_draw(tmp, &(TextParams) {
.pos = { -((w+cw) * 0.5 + 10), 0 },
.align = ALIGN_RIGHT,
.color = &clr,
});
r_shader_standard_notex();
r_mat_mv_push();
r_mat_mv_scale(w+cw, h, 1);
r_color(RGBA_MUL_ALPHA(1, 1, 1, (0.1 + 0.2 * a) * alpha));
r_draw_quad();
r_mat_mv_pop();
r_mat_mv_translate(w * (pos - 0.5), 0, 0);
r_mat_mv_scale(cw, h, 0);
r_color(RGBA_MUL_ALPHA(0.9, 0.6, 0.2, alpha));
r_draw_quad();
r_mat_mv_pop();
r_shader("text_default");
break;
}
}
}
}
static void draw_options_menu(MenuData *menu) {
OptionsMenuContext *ctx = menu->context;
r_state_push();
draw_options_menu_bg(menu);
draw_menu_title(menu, ctx->title);
draw_menu_list(menu, OPTIONS_X_MARGIN, OPTIONS_Y_MARGIN, options_draw_item, SCREEN_H * 1.1, menu);
r_state_pop();
if(ctx->draw_overlay) {
r_state_push();
ctx->draw_overlay(menu, ctx);
r_state_pop();
}
if(ctx->submenu_fading) {
r_state_push();
ctx->submenu_fading->draw(ctx->submenu_fading);
r_state_pop();
}
if(ctx->submenu) {
r_state_push();
ctx->submenu->draw(ctx->submenu);
r_state_pop();
}
}
// --- Input/event processing --- //
static bool options_vidmode_change_handler(SDL_Event *event, void *arg) {
MenuData *menu = arg;
dynarray_foreach_idx(&menu->entries, int i, {
OptionBinding *bind = bind_get(menu, i);
if(!bind) {
continue;
}
switch(bind->type) {
case BT_Resolution:
bind_resolution_update(bind);
break;
case BT_IntValue:
if(bind->configentry == CONFIG_FULLSCREEN) {
bind->selected = video_is_fullscreen();
}
break;
default:
break;
}
});
return false;
}
static bool options_rebind_input_handler(SDL_Event *event, void *arg) {
if(!arg) {
return false;
}
OptionBinding *b = arg;
uint32_t t = event->type;
if(b->type == BT_StrValue) {
return false;
}
if(t == SDL_KEYDOWN) {
SDL_Scancode scan = event->key.keysym.scancode;
bool esc = scan == SDL_SCANCODE_ESCAPE;
if(b->type != BT_KeyBinding) {
if(esc) {
b->blockinput = false;
play_sfx_ui("hit");
}
return true;
}
if(!esc) {
for(int i = CONFIG_KEY_FIRST; i <= CONFIG_KEY_LAST; ++i) {
if(config_get_int(i) == scan) {
config_set_int(i, config_get_int(b->configentry));
}
}
config_set_int(b->configentry, scan);
play_sfx_ui("shot_special1");
} else {
play_sfx_ui("hit");
}
b->blockinput = false;
return true;
}
if(t == MAKE_TAISEI_EVENT(TE_MENU_ABORT)) {
play_sfx_ui("hit");
b->blockinput = false;
return true;
}
if(t == MAKE_TAISEI_EVENT(TE_GAMEPAD_BUTTON_DOWN)) {
GamepadButton button = event->user.code;
if(b->type != BT_GamepadKeyBinding) {
/*
if(b->type == BT_GamepadAxisBinding) {
b->blockinput = false;
play_ui_sound("hit");
}
*/
return true;
}
if(button == GAMEPAD_BUTTON_BACK || button == GAMEPAD_BUTTON_START) {
b->blockinput = false;
play_sfx_ui("hit");
return true;
}
for(int i = CONFIG_GAMEPAD_KEY_FIRST; i <= CONFIG_GAMEPAD_KEY_LAST; ++i) {
if(config_get_int(i) == button) {
config_set_int(i, config_get_int(b->configentry));
}
}
config_set_int(b->configentry, button);
b->blockinput = false;
play_sfx_ui("shot_special1");
return true;
}
if(t == MAKE_TAISEI_EVENT(TE_GAMEPAD_AXIS_DIGITAL)) {
GamepadAxis axis = event->user.code;
if(b->type == BT_GamepadAxisBinding) {
if(b->configentry == CONFIG_GAMEPAD_AXIS_UD) {
if(config_get_int(CONFIG_GAMEPAD_AXIS_LR) == axis) {
config_set_int(CONFIG_GAMEPAD_AXIS_LR, config_get_int(CONFIG_GAMEPAD_AXIS_UD));
}
} else if(b->configentry == CONFIG_GAMEPAD_AXIS_LR) {
if(config_get_int(CONFIG_GAMEPAD_AXIS_UD) == axis) {
config_set_int(CONFIG_GAMEPAD_AXIS_UD, config_get_int(CONFIG_GAMEPAD_AXIS_LR));
}
}
config_set_int(b->configentry, axis);
b->blockinput = false;
}
return true;
}
return true;
}
static bool options_text_input_handler(SDL_Event *event, void *arg) {
OptionBinding *b = arg;
if(!b || b->type != BT_StrValue) {
return false;
}
uint32_t t = event->type;
if(t == SDL_TEXTINPUT || t == MAKE_TAISEI_EVENT(TE_CLIPBOARD_PASTE)) {
const size_t max_len = 32;
const char *snd = "generic_shot";
char *text, *text_allocated = NULL;
if(t == SDL_TEXTINPUT) {
text = event->text.text;
} else {
text = text_allocated = SDL_GetClipboardText();
}
assert(text != NULL);
strappend(&b->strvalue, text);
if(strlen(b->strvalue) > max_len) {
/*
* EFFICIENT AS FUCK
*/
uint32_t *u = utf8_to_ucs4_alloc(b->strvalue);
size_t ulen = ucs4len(u);
if(ulen > max_len) {
*(u + max_len) = 0;
mem_free(b->strvalue);
b->strvalue = ucs4_to_utf8_alloc(u);
snd = "hit";
}
mem_free(u);
}
mem_free(text_allocated);
play_sfx_ui(snd);
return true;
}
if(t == SDL_KEYDOWN) {
SDL_Scancode scan = event->key.keysym.scancode;
if(scan == SDL_SCANCODE_ESCAPE) {
play_sfx_ui("hit");
stralloc(&b->strvalue, config_get_str(b->configentry));
b->blockinput = false;
} else if(scan == SDL_SCANCODE_RETURN) {
if(*b->strvalue) {
play_sfx_ui("shot_special1");
} else {
play_sfx_ui("hit");
stralloc(&b->strvalue, "Player");
}
config_set_str(b->configentry, b->strvalue);
b->blockinput = false;
} else if(scan == SDL_SCANCODE_BACKSPACE) {
/*
* MORE EFFICIENT THAN FUCK
*/
uint32_t *u = utf8_to_ucs4_alloc(b->strvalue);
if(*u) {
play_sfx_ui("generic_shot");
*(ucs4chr(u, 0) - 1) = 0;
} else {
play_sfx_ui("hit");
}
mem_free(b->strvalue);
b->strvalue = ucs4_to_utf8_alloc(u);
mem_free(u);
}
return true;
}
if(
t == MAKE_TAISEI_EVENT(TE_MENU_ABORT) &&
(intptr_t)event->user.data1 == INDEV_GAMEPAD
) {
play_sfx_ui("hit");
stralloc(&b->strvalue, config_get_str(b->configentry));
b->blockinput = false;
}
return true;
}
static bool options_input_handler(SDL_Event *event, void *arg) {
MenuData *menu = arg;
OptionsMenuContext *ctx = menu->context;
OptionBinding *bind = bind_get(menu, menu->cursor);
TaiseiEvent type = TAISEI_EVENT(event->type);
switch(type) {
case TE_MENU_CURSOR_UP:
case TE_MENU_CURSOR_DOWN:
case TE_MENU_CURSOR_LEFT:
case TE_MENU_CURSOR_RIGHT:
if((intptr_t)event->user.data1 == INDEV_GAMEPAD && ctx->gamepad_testmode.active) {
return false;
}
default: break;
}
switch(type) {
case TE_MENU_CURSOR_UP:
case TE_MENU_CURSOR_DOWN:
play_sfx_ui("generic_shot");
menu->drawdata[3] = 10;
do {
menu->cursor += (type == TE_MENU_CURSOR_UP ? -1 : 1);
if(menu->cursor >= menu->entries.num_elements) {
menu->cursor = 0;
}
if(menu->cursor < 0) {
menu->cursor = menu->entries.num_elements - 1;
}
} while(!entry_is_active(menu, menu->cursor));
break;
case TE_MENU_CURSOR_LEFT:
case TE_MENU_CURSOR_RIGHT:
play_sfx_ui("generic_shot");
bool next = (type == TE_MENU_CURSOR_RIGHT);
if(bind) {
switch(bind->type) {
case BT_GamepadDevice:
case BT_IntValue:
case BT_Resolution:
case BT_VideoDisplay:
(next ? bind_setnext : bind_setprev)(bind);
break;
case BT_Scale:
config_set_float(bind->configentry,
clamp(
config_get_float(bind->configentry) + bind->scale_step * (next ? 1 : -1),
bind->scale_min, bind->scale_max
)
);
break;
default:
break;
}
}
break;
case TE_MENU_ACCEPT:
play_sfx_ui("shot_special1");
menu->selected = menu->cursor;
if(bind) switch(bind->type) {
case BT_KeyBinding:
case BT_GamepadKeyBinding:
case BT_GamepadAxisBinding:
bind->blockinput = true;
break;
case BT_StrValue:
bind->blockinput = true;
break;
default:
break;
} else close_menu(menu);
break;
case TE_MENU_ABORT:
play_sfx_ui("hit");
menu->selected = -1;
close_menu(menu);
break;
case TE_GAMEPAD_BUTTON_DOWN:
if(ctx->gamepad_testmode.allowed) {
if(ctx->gamepad_testmode.active) {
switch(event->user.code) {
case GAMEPAD_BUTTON_ANALOG_STICK_DOWN:
case GAMEPAD_BUTTON_ANALOG_STICK_LEFT:
case GAMEPAD_BUTTON_ANALOG_STICK_RIGHT:
case GAMEPAD_BUTTON_ANALOG_STICK_UP:
break;
default:
ctx->gamepad_testmode.active = false;
}
} else {
if(event->user.code == options_gamepad_testing_button()) {
ctx->gamepad_testmode.active = true;
}
}
}
break;
default: break;
}
menu->cursor = (menu->cursor % menu->entries.num_elements) + menu->entries.num_elements * (menu->cursor < 0);
return false;
}
#undef SHOULD_SKIP
static bool submenu_input_handler(SDL_Event *event, void *arg) {
if(event->type == MAKE_TAISEI_EVENT(TE_MENU_CURSOR_LEFT)) {
event->type = MAKE_TAISEI_EVENT(TE_MENU_CURSOR_UP);
} else if(event->type == MAKE_TAISEI_EVENT(TE_MENU_CURSOR_RIGHT)) {
event->type = MAKE_TAISEI_EVENT(TE_MENU_CURSOR_DOWN);
}
return false;
}
static void options_menu_input(MenuData *menu) {
OptionBinding *b;
EventFlags flags = EFLAG_MENU;
OptionsMenuContext *ctx = menu->context;
MenuData *sub = ctx->submenu;
if(sub) {
events_poll((EventHandler[]){
{ .proc = submenu_input_handler, .arg = sub },
{ .proc = menu_input_handler, .arg = sub },
{NULL}
}, flags);
return;
}
if((b = bind_getinputblocking(menu)) != NULL) {
if(b->type == BT_StrValue) {
flags |= EFLAG_TEXT;
}
}
events_poll((EventHandler[]){
{
.proc = options_vidmode_change_handler,
.arg = menu,
.priority = EPRIO_SYSTEM,
.event_type = MAKE_TAISEI_EVENT(TE_VIDEO_MODE_CHANGED),
},
{
.proc = options_text_input_handler,
.arg = b,
.priority = EPRIO_CAPTURE,
},
{
.proc = options_rebind_input_handler,
.arg = b,
.priority = EPRIO_CAPTURE,
},
{
.proc = options_input_handler,
.arg = menu,
.priority = EPRIO_NORMAL,
},
{NULL}
}, flags);
}