Files
fs2open.github.com/code/controlconfig/controlsconfig.cpp
T
Goober5000andClaude Opus 4.8 bed228d264 avoid undefined behavior from UNREACHABLE on reachable code paths (#7555)
* avoid undefined behavior from UNREACHABLE on reachable code paths

UNREACHABLE expands to __assume(false) / __builtin_unreachable() in
release builds, which is undefined behavior if the code is ever actually
reached. Many UNREACHABLE sites only guard against unexpected-but-
possible runtime states (values from tables, mission/save files, network
packets, scripting, or user input), so reaching them should degrade
gracefully rather than invoke UB.

Replace UNREACHABLE with Assertion(false, ...) at all such sites. It
still pops an error in debug builds, but in release it is a harmless
no-op that simply falls through to the following code. Sites that are
genuinely unreachable by local invariants (e.g. a switch over a bounded
random number) are left as UNREACHABLE.

A few converted sites had no safe fall-through -- an uninitialized read,
null dereference, or null call would have followed -- so they also get a
minimal recovery: initialize the value, guard the dereference, return
early, or skip the offending item.

Where the converted site was a simple guard of the form
    if (x) { Assertion(false, ...); return; }
hoist the check onto the invariant instead:
    Assertion(!x, ...);
    if (x) { return; }
This states the expectation directly and shows the offending value in
the debug popup, while keeping the same graceful runtime degradation.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* addendum

After consultation with Lafiel, use UNREACHABLE in situations that are effectively impossible even if not provably impossible.  UNREACHABLE = "this shouldn't ever happen"; Assertion = "if this ever happens, it's a coding error".  Also fix a few other sites and change a couple to Warnings.

---------

Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
2026-07-09 11:20:25 +09:00

3174 lines
89 KiB
C++

/*
* Copyright (C) Volition, Inc. 1999. All rights reserved.
*
* All source code herein is the property of Volition, Inc. You may not sell
* or otherwise commercially exploit the source or things you created based on the
* source.
*
*/
#include "camera/photomode.h"
#include "cfile/cfile.h"
#include "cmdline/cmdline.h"
#include "controlconfig/controlsconfig.h"
#include "controlconfig/presets.h"
#include "debugconsole/console.h"
#include "../freespace2/freespace.h"
#include "gamehelp/contexthelp.h"
#include "gamesequence/gamesequence.h"
#include "gamesnd/gamesnd.h"
#include "globalincs/alphacolors.h"
#include "globalincs/undosys.h"
#include "graphics/font.h"
#include "hud/hudsquadmsg.h"
#include "io/joy.h"
#include "io/key.h"
#include "io/timer.h"
#include "missionui/missionscreencommon.h"
#include "network/multi_pmsg.h"
#include "network/multiutil.h"
#include "scripting/scripting.h"
#include "scripting/global_hooks.h"
#include "playerman/player.h"
#include "pilotfile/pilotfile.h"
#include "popup/popup.h"
#include "ui/ui.h"
#include "ui/uidefs.h"
#ifndef NDEBUG
#include "hud/hud.h"
#endif
#define NUM_BUTTONS 19
#define NUM_TABS 4
// coordinate indicies
#define CONTROL_X_COORD 0
#define CONTROL_Y_COORD 1
#define CONTROL_W_COORD 2
#define CONTROL_H_COORD 3
const char* Conflict_background_bitmap_fname[GR_NUM_RESOLUTIONS] = {
"ControlConfig", // GR_640
"2_ControlConfig" // GR_1024
};
const char* Conflict_background_bitmap_mask_fname[GR_NUM_RESOLUTIONS] = {
"ControlConfig-m", // GR_640
"2_ControlConfig-m" // GR_1024
};
// control list area
int Control_list_coords[GR_NUM_RESOLUTIONS][4] = {
{
20, 58, 596, 259 // GR_640
},
{
32, 94, 956, 424 // GR_1024
}
};
// width of the control name section of the list
int Control_list_ctrl_w[GR_NUM_RESOLUTIONS] = {
350, // GR_640
600 // GR_1024
};
// x start position of the primary binding area column
int Control_list_first_x[GR_NUM_RESOLUTIONS] = {
350, // GR_640
640 // GR_1024 (712)
};
// width of the both binding area columns
int Control_list_first_w[GR_NUM_RESOLUTIONS] = {
116, // GR_640 (198)
150 // GR_1024 (230)
};
// x start position of the secondary binding area column
int Control_list_second_x[GR_NUM_RESOLUTIONS] = {
480, // GR_640
806, // GR_1024
};
// display the "more..." text under the control list
int Control_more_coords[GR_NUM_RESOLUTIONS][2] = {
{
320, 326 // GR_640
},
{
500, 542 // GR_1024
}
};
// area to display "conflicts with..." text
int Conflict_wnd_coords[GR_NUM_RESOLUTIONS][4] = {
{
32, 313, 250, 32 // GR_640
},
{
48, 508, 354, 46 // GR_1024
}
};
// conflict warning anim coords
int Conflict_warning_coords[GR_NUM_RESOLUTIONS][2] = {
{
320, 420 // GR_640
},
{
512, 669 // GR_1024
}
};
// for flashing the conflict text
#define CONFLICT_FLASH_TIME 250
UI_TIMESTAMP Conflict_stamp;
int Conflict_bright = 0;
#define LIST_BUTTONS_MAX 42
static int Num_cc_lines; // Number of Cc_lines to display on the current page. Is, at worse, CCFG_MAX + NUM_JOY_AXIS_ACTIONS
SCP_vector<cc_line> Cc_lines;
// Backups for use when user closes the config menu without saving
SCP_vector<CCI> Control_config_backup;
// The current preset that the user is working on and will likely want to save to
static SCP_string Current_preset_name;
// Undo system
Undo_system Undo_controls;
// all this stuff is localized/externalized
#define NUM_INVERT_TEXT 2
char *Axis_text[NUM_AXIS_TEXT];
char *Mouse_button_text[NUM_MOUSE_TEXT];
char *Invert_text[NUM_INVERT_TEXT];
int Control_check_count = 0;
static int Tab; // which tab we are currently in
static int Binding_mode = 0; // are we waiting for a key to bind it?
static UI_TIMESTAMP Bind_time = UI_TIMESTAMP::invalid();
static int Search_mode = 0; // are we waiting for a key to bind it?
static int Last_key = -1;
static int Selected_line = 0; // line that is currently selected for binding
static selItem Selected_item = selItem::None;
static int Scroll_offset;
static CC_bind Axis_override;
static int Background_bitmap;
static int Conflicts_tabs[NUM_TABS];
static UI_BUTTON List_buttons[LIST_BUTTONS_MAX]; // buttons for each line of text in list
static UI_WINDOW Ui_window;
static unsigned int Defaults_cycle_pos = 0; // the controls preset that was last selected
static UI_TIMESTAMP bound_timestamp;
static char bound_string[40];
int Control_config_overlay_id;
SCP_vector<conflict> Conflicts;
#define TARGET_TAB 0
#define SHIP_TAB 1
#define WEAPON_TAB 2
#define COMPUTER_TAB 3
#define SCROLL_UP_BUTTON 4
#define SCROLL_DOWN_BUTTON 5
#define ALT_TOGGLE 6
#define SHIFT_TOGGLE 7
#define INVERT_AXIS 8
#define CANCEL_BUTTON 9
#define UNDO_BUTTON 10
#define RESET_BUTTON 11
#define SEARCH_MODE 12
#define BIND_BUTTON 13
#define HELP_BUTTON 14
#define ACCEPT_BUTTON 15
#define CLEAR_OTHER_BUTTON 16
#define CLEAR_ALL_BUTTON 17
#define CLEAR_BUTTON 18
ui_button_info CC_Buttons[GR_NUM_RESOLUTIONS][NUM_BUTTONS] = {
{ // GR_640
ui_button_info("CCB_00", 32, 348, 17, 384, 0), // target tab
ui_button_info("CCB_01", 101, 348, 103, 384, 1), // ship tab
ui_button_info("CCB_02", 173, 352, 154, 384, 2), // weapon tab
ui_button_info("CCB_03", 242, 347, 244, 384, 3), // computer/misc tab
ui_button_info("CCB_04", 614, 73, -1, -1, 4), // scroll up
ui_button_info("CCB_05", 614, 296, -1, -1, 5), // scroll down
ui_button_info("CCB_06", 17, 452, 12, 440, 6), // alt toggle
ui_button_info("CCB_07", 56, 452, 50, 440, 7), // shift toggle
ui_button_info("CCB_09", 162, 452, 155, 440, 9), // invert
ui_button_info("CCB_10", 404, 1, 397, 45, 10), // cancel
ui_button_info("CCB_11", 582, 347, 586, 386, 11), // undo
ui_button_info("CCB_12", 576, 1, 578, 45, 12), // default
ui_button_info("CCB_13", 457, 4, 453, 45, 13), // search
ui_button_info("CCB_14", 516, 4, 519, 45, 14), // bind
ui_button_info("CCB_15", 540, 428, 500, 440, 15), // help
ui_button_info("CCB_16", 574, 432, 571, 412, 16), // accept
ui_button_info("CCB_18", 420, 346, 417, 386, 18), // clear other
ui_button_info("CCB_19", 476, 346, 474, 386, 19), // clear all
ui_button_info("CCB_20", 524, 346, 529, 386, 20), // clear button
},
{ // GR_1024
ui_button_info("2_CCB_00", 51, 557, 27, 615, 0), // target tab
ui_button_info("2_CCB_01", 162, 557, 166, 615, 1), // ship tab
ui_button_info("2_CCB_02", 277, 563, 246, 615, 2), // weapon tab
ui_button_info("2_CCB_03", 388, 555, 391, 615, 3), // computer/misc tab
ui_button_info("2_CCB_04", 982, 117, -1, -1, 4), // scroll up
ui_button_info("2_CCB_05", 982, 474, -1, -1, 5), // scroll down
ui_button_info("2_CCB_06", 28, 723, 24, 704, 6), // alt toggle
ui_button_info("2_CCB_07", 89, 723, 80, 704, 7), // shift toggle
ui_button_info("2_CCB_09", 260, 723, 249, 704, 9), // invert
ui_button_info("2_CCB_10", 646, 2, 635, 71, 10), // cancel
ui_button_info("2_CCB_11", 932, 555, 938, 619, 11), // undo
ui_button_info("2_CCB_12", 921, 1, 923, 71, 12), // default
ui_button_info("2_CCB_13", 732, 6, 726, 71, 13), // search
ui_button_info("2_CCB_14", 825, 6, 831, 71, 14), // bind
ui_button_info("2_CCB_15", 864, 685, 800, 704, 15), // help
ui_button_info("2_CCB_16", 919, 692, 914, 660, 16), // accept
ui_button_info("2_CCB_18", 672, 553, 668, 619, 18), // clear other
ui_button_info("2_CCB_19", 761, 553, 749, 619, 19), // clear all
ui_button_info("2_CCB_20", 838, 553, 846, 619, 20), // clear button
}
};
// strings
#define CC_NUM_TEXT 20
UI_XSTR CC_text[GR_NUM_RESOLUTIONS][CC_NUM_TEXT] = {
{ // GR_640
{ "Targeting", 1340, 17, 384, UI_XSTR_COLOR_GREEN, -1, &CC_Buttons[0][TARGET_TAB].button },
{ "Ship", 1341, 103, 384, UI_XSTR_COLOR_GREEN, -1, &CC_Buttons[0][SHIP_TAB].button },
{ "Weapons", 1065, 154, 384, UI_XSTR_COLOR_GREEN, -1, &CC_Buttons[0][WEAPON_TAB].button },
{ "Misc", 1411, 244, 384, UI_XSTR_COLOR_GREEN, -1, &CC_Buttons[0][COMPUTER_TAB].button },
{ "Alt", 1510, 12, 440, UI_XSTR_COLOR_GREEN, -1, &CC_Buttons[0][ALT_TOGGLE].button },
{ "Shift", 1511, 50, 440, UI_XSTR_COLOR_GREEN, -1, &CC_Buttons[0][SHIFT_TOGGLE].button },
{ "Invert", 1342, 155, 440, UI_XSTR_COLOR_GREEN, -1, &CC_Buttons[0][INVERT_AXIS].button },
{ "Cancel", 641, 401, 45, UI_XSTR_COLOR_PINK, -1, &CC_Buttons[0][CANCEL_BUTTON].button },
{ "Undo", 1343, 586, 386, UI_XSTR_COLOR_GREEN, -1, &CC_Buttons[0][UNDO_BUTTON].button },
{ "Next Preset", 1661, 553, 45, UI_XSTR_COLOR_GREEN, -1, &CC_Buttons[0][RESET_BUTTON].button },
{ "Search", 1345, 458, 45, UI_XSTR_COLOR_GREEN, -1, &CC_Buttons[0][SEARCH_MODE].button },
{ "Bind", 1346, 517, 45, UI_XSTR_COLOR_PINK, -1, &CC_Buttons[0][BIND_BUTTON].button },
{ "Help", 928, 500, 440, UI_XSTR_COLOR_GREEN, -1, &CC_Buttons[0][HELP_BUTTON].button },
{ "Accept", 1035, 571, 412, UI_XSTR_COLOR_PINK, -1, &CC_Buttons[0][ACCEPT_BUTTON].button },
{ "Clear", 1347, 417, 386, UI_XSTR_COLOR_GREEN, -1, &CC_Buttons[0][CLEAR_OTHER_BUTTON].button },
{ "Conflict", 1348, 406, 396, UI_XSTR_COLOR_GREEN, -1, &CC_Buttons[0][CLEAR_OTHER_BUTTON].button },
{ "Clear", 1413, 474, 386, UI_XSTR_COLOR_GREEN, -1, &CC_Buttons[0][CLEAR_ALL_BUTTON].button },
{ "All", 1349, 483, 396, UI_XSTR_COLOR_GREEN, -1, &CC_Buttons[0][CLEAR_ALL_BUTTON].button },
{ "Clear", 1414, 529, 388, UI_XSTR_COLOR_PINK, -1, &CC_Buttons[0][CLEAR_BUTTON].button },
{ "Selected", 1350, 517, 396, UI_XSTR_COLOR_PINK, -1, &CC_Buttons[0][CLEAR_BUTTON].button },
},
{ // GR_1024
{ "Targeting", 1340, 47, 615, UI_XSTR_COLOR_GREEN, -1, &CC_Buttons[1][TARGET_TAB].button },
{ "Ship", 1341, 176, 615, UI_XSTR_COLOR_GREEN, -1, &CC_Buttons[1][SHIP_TAB].button },
{ "Weapons", 1065, 266, 615, UI_XSTR_COLOR_GREEN, -1, &CC_Buttons[1][WEAPON_TAB].button },
{ "Misc", 1411, 401, 615, UI_XSTR_COLOR_GREEN, -1, &CC_Buttons[1][COMPUTER_TAB].button },
{ "Alt", 1510, 29, 704, UI_XSTR_COLOR_GREEN, -1, &CC_Buttons[1][ALT_TOGGLE].button },
{ "Shift", 1511, 85, 704, UI_XSTR_COLOR_GREEN, -1, &CC_Buttons[1][SHIFT_TOGGLE].button },
{ "Invert", 1342, 254, 704, UI_XSTR_COLOR_GREEN, -1, &CC_Buttons[1][INVERT_AXIS].button },
{ "Cancel", 641, 655, 71, UI_XSTR_COLOR_PINK, -1, &CC_Buttons[1][CANCEL_BUTTON].button },
{ "Undo", 1343, 938, 619, UI_XSTR_COLOR_GREEN, -1, &CC_Buttons[1][UNDO_BUTTON].button },
{ "Next Preset", 1661, 913, 71, UI_XSTR_COLOR_GREEN, -1, &CC_Buttons[1][RESET_BUTTON].button },
{ "Search", 1345, 746, 71, UI_XSTR_COLOR_GREEN, -1, &CC_Buttons[1][SEARCH_MODE].button },
{ "Bind", 1346, 846, 71, UI_XSTR_COLOR_PINK, -1, &CC_Buttons[1][BIND_BUTTON].button },
{ "Help", 928, 800, 704, UI_XSTR_COLOR_GREEN, -1, &CC_Buttons[1][HELP_BUTTON].button },
{ "Accept", 1035, 914, 660, UI_XSTR_COLOR_PINK, -1, &CC_Buttons[1][ACCEPT_BUTTON].button },
{ "Clear", 1347, 683, 619, UI_XSTR_COLOR_GREEN, -1, &CC_Buttons[1][CLEAR_OTHER_BUTTON].button },
{ "Conflict", 1348, 666, 634, UI_XSTR_COLOR_GREEN, -1, &CC_Buttons[1][CLEAR_OTHER_BUTTON].button },
{ "Clear", 1413, 759, 619, UI_XSTR_COLOR_GREEN, -1, &CC_Buttons[1][CLEAR_ALL_BUTTON].button },
{ "All", 1349, 772, 634, UI_XSTR_COLOR_GREEN, -1, &CC_Buttons[1][CLEAR_ALL_BUTTON].button },
{ "Clear", 1414, 871, 619, UI_XSTR_COLOR_PINK, -1, &CC_Buttons[1][CLEAR_BUTTON].button },
{ "Selected", 1350, 852, 634, UI_XSTR_COLOR_PINK, -1, &CC_Buttons[1][CLEAR_BUTTON].button },
}
};
// same indices as Scan_code_text[]. Indicates if a scancode is allowed to be bound.
int Config_allowed[] = {
0, 0, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1,
1, 0, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 1, 1, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 1, 0, 0,
1, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 1,
1, 1, 0, 1, 0, 1, 0, 1,
1, 1, 1, 1, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
};
#ifndef NDEBUG
int Show_controls_info = 0;
DCF_BOOL(show_controls_info, Show_controls_info);
DCF(cc_adjust, "UI tool Used to adjust positioning and size of the controls config widgets") {
bool done = false;
if (dc_optional_string_either("help", "--help")) {
dc_printf("Available sub commands: \n");
dc_printf(" list - configures control_list_coords\n");
dc_printf(" col - configures the horizontal position and widths of control_list columns\n");
dc_printf("\n Each sub command has their own arguments, pass 'help' to them for further details.");
return;
}
if (dc_optional_string("list")) {
// listbox
if (dc_optional_string_either("help", "--help")) {
dc_printf("Configures control_list_coords\n");
dc_printf(" Available arguments: -x -y -w -h");
return;
}
if (dc_optional_string("-x")) {
dc_stuff_int(&Control_list_coords[gr_screen.res][CONTROL_X_COORD]);
done = true;
}
if (dc_optional_string("-y")) {
dc_stuff_int(&Control_list_coords[gr_screen.res][CONTROL_Y_COORD]);
done = true;
}
if (dc_optional_string("-w")) {
dc_stuff_int(&Control_list_coords[gr_screen.res][CONTROL_W_COORD]);
done = true;
}
if (dc_optional_string("-h")) {
dc_stuff_int(&Control_list_coords[gr_screen.res][CONTROL_H_COORD]);
done = true;
}
} else if (dc_optional_string("col")) {
if (dc_optional_string_either("help", "--help")) {
dc_printf("Configures the column positioning and sizes for Control_list\n");
dc_printf(" Avilable arguments: -x1 -w1 -x2");
return;
}
if (dc_optional_string("-x1")) {
dc_stuff_int(&Control_list_first_x[gr_screen.res]);
done = true;
}
if (dc_optional_string("-w1")) {
dc_stuff_int(&Control_list_first_w[gr_screen.res]);
done = true;
}
if (dc_optional_string("-x2")) {
dc_stuff_int(&Control_list_second_x[gr_screen.res]);
done = true;
}
}
if (!done) {
dc_printf("Nothing done. Enter 'help' or '--help' for available arguments.");
}
}
#endif
/**
* Values for all axes of all joysticks at the start of bind mode. Namely used to detect an axis that moved far enough
* from this "origin"
*/
static int Axes_origin[CID_JOY_MAX][JOY_NUM_AXES];
// Rotate selItem forwards
selItem operator++(selItem& item, int) {
Assert(item != selItem::selItem_END);
using type = typename std::underlying_type<selItem>::type;
item = static_cast<selItem>(static_cast<type>(item) + 1);
if (item == selItem::selItem_END) {
// Set to first item
item = static_cast<selItem>(static_cast<type>(selItem::selItem_REND) + 1);;
}
return item;
}
// Rotate selItem backwards
selItem operator--(selItem& item, int) {
Assert(item != selItem::selItem_REND);
using type = typename std::underlying_type<selItem>::type;
item = static_cast<selItem>( static_cast<type>(item) - 1 );
if (item == selItem::selItem_REND) {
// Set to last item
item = static_cast<selItem>(static_cast<type>(selItem::selItem_END) - 1);;
}
return item;
}
static int joy_get_unscaled_reading(int raw)
{
const int rng = JOY_AXIS_MAX - JOY_AXIS_MIN;
raw -= JOY_AXIS_MIN; // adjust for linear range starting at 0
// cap at limits
if (raw < 0)
raw = 0;
if (raw > rng)
raw = rng;
return (int) ((std::uint64_t) raw * (std::uint64_t) F1_0 / (std::uint64_t) rng); // convert to 0 - F1_0 range, 64bit ints used to avoid uint overflow
}
int joy_get_scaled_reading(int raw)
{
int x, d, dead_zone, rng;
float percent, sensitivity_percent, non_sensitivity_percent;
raw -= JOY_AXIS_CENTER;
if (Cmdline_deadzone >= 0) {
dead_zone = (JOY_AXIS_MAX - JOY_AXIS_MIN) * Cmdline_deadzone / 200;
//Making this div by 200 is what allows us to have the granularity of 0 to 100 in the cmdline.
//This allows a larger deadzone than the original maximum, all the way to the stick's full range.
} else {
dead_zone = (JOY_AXIS_MAX - JOY_AXIS_MIN) * Joy_dead_zone_size / 100;
}
if (raw < -dead_zone) {
rng = JOY_AXIS_CENTER - JOY_AXIS_MIN - dead_zone;
d = -raw - dead_zone;
} else if (raw > dead_zone) {
rng = JOY_AXIS_MAX - JOY_AXIS_CENTER - dead_zone;
d = raw - dead_zone;
} else
return 0;
if (d > rng)
d = rng;
Assert(Joy_sensitivity >= 0 && Joy_sensitivity <= 9);
// compute percentages as a range between 0 and 1
sensitivity_percent = (float) Joy_sensitivity / 9.0f;
non_sensitivity_percent = (float) (9 - Joy_sensitivity) / 9.0f;
// find percent of max axis is at
percent = (float) d / (float) rng;
// work sensitivity on axis value
percent = (percent * sensitivity_percent + percent * percent * percent * percent * percent * non_sensitivity_percent);
x = (int) ((float) F1_0 * percent);
//nprintf(("AI", "d=%6i, sens=%3i, percent=%6.3f, val=%6i, ratio=%6.3f\n", d, Joy_sensitivity, percent, (raw<0) ? -x : x, (float) d/x));
if (raw < 0)
return -x;
return x;
}
/**
* Initializes a reference point on all axes of all joysticks so that control_config_detect_axis can detect an axis
* that was deflected far enough
*/
void control_config_detect_axis_reset()
{
for (short j = CID_JOY0; j < CID_JOY_MAX; ++j) {
joystick_read_raw_axis(j, JOY_NUM_AXES, Axes_origin[j]);
}
}
/**
* Detects if a joystick axis was moved
*/
CC_bind control_config_detect_axis()
{
int dx, dy, dz;
int delta = 16384;
int axes_values[CID_JOY_MAX][JOY_NUM_AXES];
short j; // cid of the joy that moved
short axis = -1; // index of the detected axis
const int deadzone = 7; // Minor deadzone for mouse axis detection
// Find an axis among any of the joysticks that has deflected far enough
for (j = CID_JOY0; j < CID_JOY_MAX; ++j) {
if (!joy_present(j)) {
// Joy not present, skip
continue;
}
joystick_read_raw_axis(j, JOY_NUM_AXES, axes_values[j]);
for (short i = 0; i < JOY_NUM_AXES; i++) {
dx = abs(axes_values[j][i] - Axes_origin[j][i]);
if (dx > delta) {
axis = i;
delta = dx;
goto found_axis;
}
}
}
found_axis:;
if (j == CID_JOY_MAX) {
// Nothing found amongst the joysticks. Check the mouse.
mouse_get_delta( &dx, &dy, &dz );
if (Use_mouse_to_fly) {
// Treat mouse as Joy0
j = CID_JOY0;
if ((dx > deadzone) || (dx < -deadzone)) {
axis = JOY_X_AXIS;
} else if ((dy > deadzone) || (dy < -deadzone)) {
axis = JOY_Y_AXIS;
} else if ((dz > deadzone) || (dz < -deadzone)) {
axis = JOY_Z_AXIS;
} else {
// Nothing detected
j = CID_NONE;
}
} else {
// Treat mouse as mouse
j = CID_MOUSE;
if ((dx > deadzone) || (dx < -deadzone)) {
axis = MOUSE_X_AXIS;
} else if ((dy > deadzone) || (dy < -deadzone)) {
axis = MOUSE_Y_AXIS;
} else if ((dz > deadzone) || (dz < -deadzone)) {
axis = MOUSE_Z_AXIS;
} else {
// Nothing detected
j = CID_NONE;
}
}
}
return CC_bind(static_cast<CID>(j), axis, CCF_AXIS);;
}
/**
* @brief Checks all controls for conflicts. This should be called after any change to any bindings.
*/
void control_config_conflict_check()
{
int i, j;
const int CCFG_SIZE = static_cast<int>(Control_config.size());
for (i=0; i < CCFG_SIZE; i++) {
Conflicts[i].first = Conflicts[i].second = -1;
}
for (i=0; i<NUM_TABS; i++) {
Conflicts_tabs[i] = 0;
}
for (i = 0; i < (CCFG_SIZE - 1); i++) {
auto &item_i = Control_config[i];
if (item_i.empty()) {
// skip
continue;
}
for (j = i + 1; j < CCFG_SIZE; j++) {
auto &item_j = Control_config[j];
if (item_j.empty()) {
// skip
continue;
}
// Skip conflict check between items in different conflict groups
if ((item_i.conflict_groups & item_j.conflict_groups) == 0) {
continue;
}
if (item_i.disabled && (item_i.has_first_conflict(item_j) || item_i.has_second_conflict(item_j))) {
// item_i conflicts with item_j and is disabled. Silently clear item_i
item_i.clear();
}
if (item_j.disabled && (item_j.has_first_conflict(item_i) || item_j.has_second_conflict(item_i))) {
// item_j conflicts with item_i and is disabled. Silently clear item_j
item_j.clear();
}
// Clearly a headache
// This mess is needed so that only the conflicting bind is highlighted instead of both of them
if (item_i.has_first_conflict(item_j)) {
// item_i's first binding has conflict
Conflicts[i].first = j;
Conflicts_tabs[ item_i.tab ] = 1;
}
if (item_i.has_second_conflict(item_j)) {
// item_i's second binding has conflict
Conflicts[i].second = j;
Conflicts_tabs[ item_i.tab ] = 1;
}
if (item_j.has_first_conflict(item_i)) {
// item_j's first binding has conflict
Conflicts[j].first = i;
Conflicts_tabs[item_j.tab] = 1;
}
if (item_j.has_second_conflict(item_i)) {
// item_j's second binding has conflict
Conflicts[j].second = i;
Conflicts_tabs[ item_j.tab ] = 1;
}
}
}
}
// do list setup required prior to rendering and checking for the controls listing. Called when list changes
void control_config_list_prepare()
{
int y; // Offset, in pixels, the Cc_line has from the top
int z; // index into Control_config[]
int font_height = gr_get_font_height();
Num_cc_lines = y = z = 0;
// Populate the digital controls
for (const auto &item : Control_config) {
if (item.tab == Tab && !item.disabled) {
if (item.indexXSTR > 1) {
Cc_lines[Num_cc_lines].label = XSTR(item.text.c_str(), item.indexXSTR, true);
} else if (item.indexXSTR == 1) {
Cc_lines[Num_cc_lines].label = XSTR(item.text.c_str(), CONTROL_CONFIG_XSTR + z, true);
} else {
Cc_lines[Num_cc_lines].label = item.text.c_str();
}
Cc_lines[Num_cc_lines].cc_index = z;
Cc_lines[Num_cc_lines++].y = y;
y += font_height + 2;
} // Else, Ignore and hide items
z++; // z is the index position in Control_config[]
}
}
int cc_line_query_visible(int n)
{
int y;
if ((n < 0) || (n >= Num_cc_lines)) {
return 0;
}
y = Cc_lines[n].y - Cc_lines[Scroll_offset].y;
if ((y < 0) || (y + gr_get_font_height() > Control_list_coords[gr_screen.res][CONTROL_H_COORD])){
return 0;
}
return 1;
}
/**
* @brief Wrapper for CC_bind::take(), binds a given control
*/
void control_config_bind(int i, const CC_bind &new_bind, selItem order, bool API_Access = false)
{
int sel = -1;
switch (order) {
// Bind states. Saving covered by below
case selItem::Primary:
sel = 0;
break;
case selItem::Secondary:
sel = 1;
break;
case selItem::None:
sel = -1;
break;
// Error
default:
UNREACHABLE("Unknown order (%i) passed to control_config_bind_btn.", static_cast<int>(order));
}
// Save both bindings, because ::take() can clear the other binding if it is equal.
Undo_stack stack;
stack.save(Control_config[i].first);
stack.save(Control_config[i].second);
Undo_controls.save_stack(std::move(stack));
CCB old(Control_config[i]);
Control_config[i].take(new_bind, sel);
if (old == Control_config[i]) {
// Binding didn't take
Undo_controls.undo();
if (!API_Access) {
gamesnd_play_iface(InterfaceSounds::GENERAL_FAIL);
}
}
}
/**
* @brief Unbinds the selected control
*/
bool control_config_remove_binding(int ctrl, selItem item, bool API_Access)
{
bool success = true;
switch (item) {
case selItem::None:
// Clear both
if (!(Control_config[ctrl].empty())) {
Undo_stack stack;
stack.save(Control_config[ctrl].first);
stack.save(Control_config[ctrl].second);
Undo_controls.save_stack(std::move(stack));
Control_config[ctrl].first.clear();
Control_config[ctrl].second.clear();
} else {
success = false;
}
break;
case selItem::Primary:
// Clear only primary
if (!Control_config[ctrl].first.empty()) {
Undo_controls.save(Control_config[ctrl].first);
Control_config[ctrl].first.clear();
} else {
success = false;
}
break;
case selItem::Secondary:
// Clear only Secondary
if (!Control_config[ctrl].second.empty()) {
Undo_controls.save(Control_config[ctrl].second);
Control_config[ctrl].second.clear();
} else {
success = false;
}
break;
default:
// Coder forgot to add a case!
UNREACHABLE("Unhandled selItem case: %i", static_cast<int>(item));
}
if (success)
control_config_conflict_check();
if (!API_Access) {
if (success) {
control_config_list_prepare();
gamesnd_play_iface(InterfaceSounds::USER_SELECT);
Selected_item = selItem::None;
} else {
gamesnd_play_iface(InterfaceSounds::GENERAL_FAIL);
}
}
if (success) {
return true;
} else {
return false;
}
}
/**
* @brief Clears all conflicting control bindings, except the selected control
*/
bool control_config_clear_other(int ctrl, bool API_Access)
{
const auto& selected = Control_config[ctrl];
// Fail if selected item is empty
if (selected.empty()) {
if (!API_Access)
gamesnd_play_iface(InterfaceSounds::GENERAL_FAIL);
return false;
}
// Back up the old bindings
Undo_stack stack;
int total = 0;
for (int i = total = 0; i < (int)Control_config.size(); ++i) {
if (i == ctrl) {
// skip
continue;
}
auto &other = Control_config[i];
if (other.has_first_conflict(selected)) {
stack.save(other.first);
other.first.clear();
total++;
}
if (other.has_second_conflict(selected)) {
stack.save(other.second);
other.second.clear();
total++;
}
}
// Fail if no conflicts
if (total == 0) {
if (!API_Access)
gamesnd_play_iface(InterfaceSounds::GENERAL_FAIL);
return false;
}
Undo_controls.save_stack(std::move(stack));
control_config_conflict_check();
if (!API_Access) {
control_config_list_prepare();
gamesnd_play_iface(InterfaceSounds::USER_SELECT);
}
return true;
}
/**
* @brief Unbinds ALL controls
* TODO: unbind axes and reset inversion
*/
bool control_config_clear_all(bool API_Access)
{
int total = 0;
// Back up items for undo and then clear the item
Undo_stack stack;
for (auto &item : Control_config) {
if (!item.empty()) {
stack.save(item.first);
stack.save(item.second);
item.clear();
total++;
}
}
// Fail if nothing was cleared
if (total == 0) {
if (!API_Access)
gamesnd_play_iface(InterfaceSounds::GENERAL_FAIL);
return false;
}
Undo_controls.save_stack(std::move(stack));
control_config_conflict_check();
if (!API_Access) {
control_config_list_prepare();
gamesnd_play_iface(InterfaceSounds::RESET_PRESSED);
}
return true;
}
/**
* @brief Reverts all bindings to their preset. If already default, cycle to the next presets if cycle is true.
*/
bool control_config_do_reset(bool cycle, bool API_Access)
{
int total = 0;
Undo_stack stack;
auto &default_bindings = Control_config_presets[Defaults_cycle_pos].bindings;
// first, determine how many bindings need to be changed
for (size_t e = 0; e < Control_config.size(); ++e) {
const auto& item = Control_config[e];
const auto& default_item = default_bindings[e];
if (item.disabled) {
// skip
continue;
}
if ((item.first != default_item.first) ||
(item.second != default_item.second)) {
total++;
}
}
if ((total == 0) && (Control_config_presets.size() <= 1)) {
// Nothing to reset, no other presets besides default
if (!API_Access)
gamesnd_play_iface(InterfaceSounds::GENERAL_FAIL);
return false;
}
if (total == 0) {
if (cycle) {
// Select next preset
stack.save(Defaults_cycle_pos);
Defaults_cycle_pos++;
if (Defaults_cycle_pos >= Control_config_presets.size()) {
Defaults_cycle_pos = 0;
}
} else {
if (!API_Access)
gamesnd_play_iface(InterfaceSounds::GENERAL_FAIL);
return false;
}
}
// Save all item bindings for undo
for (auto &item : Control_config) {
stack.save(item.first);
stack.save(item.second);
}
Undo_controls.save_stack(std::move(stack));
control_config_use_preset(Control_config_presets[Defaults_cycle_pos]);
control_config_conflict_check();
if (!API_Access) {
control_config_list_prepare();
gamesnd_play_iface(InterfaceSounds::RESET_PRESSED);
}
return true;
}
void control_config_use_preset(CC_preset &preset)
{
// Reset all
const auto &bindings = preset.bindings;
const size_t size = MIN(bindings.size(), Control_config.size());
Assert(size >= CCFG_MAX);
for (size_t i = 0; i < size; ++i) {
// Don't use std::copy here, since the preset may contain more bindings than Control_config
Control_config[i].first = bindings[i].first;
Control_config[i].second = bindings[i].second;
}
}
bool control_config_use_preset_by_name(const SCP_string &name) {
auto it = std::find_if(Control_config_presets.begin(), Control_config_presets.end(),
[name](CC_preset &preset) {return preset.name == name;});
if (it == Control_config_presets.end()) {
// Couldn't find, use defaults
it = Control_config_presets.begin();
mprintf(("CCFG => Could not find preset with name %s, using defaults.", name.c_str()));
return false;
}
control_config_use_preset(*it);
return true;
}
void control_config_scroll_screen_up()
{
if (Scroll_offset) {
Scroll_offset--;
Assert(Selected_line > Scroll_offset);
while (!cc_line_query_visible(Selected_line)) {
Selected_line--;
}
Selected_item = selItem::None;
gamesnd_play_iface(InterfaceSounds::SCROLL);
} else {
gamesnd_play_iface(InterfaceSounds::GENERAL_FAIL);
}
}
void control_config_scroll_line_up()
{
if (Selected_line) {
Selected_line--;
if (Selected_line < Scroll_offset) {
Scroll_offset = Selected_line;
}
Selected_item = selItem::None;
gamesnd_play_iface(InterfaceSounds::SCROLL);
} else {
gamesnd_play_iface(InterfaceSounds::GENERAL_FAIL);
}
}
void control_config_scroll_screen_down()
{
if (Cc_lines[Num_cc_lines - 1].y + gr_get_font_height() > Cc_lines[Scroll_offset].y + Control_list_coords[gr_screen.res][CONTROL_H_COORD]) {
Scroll_offset++;
while (!cc_line_query_visible(Selected_line)) {
Selected_line++;
Assert(Selected_line < Num_cc_lines);
}
Selected_item = selItem::None;
gamesnd_play_iface(InterfaceSounds::SCROLL);
} else {
gamesnd_play_iface(InterfaceSounds::GENERAL_FAIL);
}
}
void control_config_scroll_line_down()
{
if (Selected_line < Num_cc_lines - 1) {
Selected_line++;
Assert(Selected_line > Scroll_offset);
while (!cc_line_query_visible(Selected_line)) {
Scroll_offset++;
}
Selected_item = selItem::None;
gamesnd_play_iface(InterfaceSounds::SCROLL);
} else {
gamesnd_play_iface(InterfaceSounds::GENERAL_FAIL);
}
}
bool control_config_toggle_modifier(int bit, int ctrl, bool API_Access)
{
int k = Control_config[ctrl].get_btn(CID_KEYBOARD);
if (k < 0) {
if (!API_Access)
gamesnd_play_iface(InterfaceSounds::GENERAL_FAIL);
return false;
}
Undo_stack stack;
stack.save(Control_config[ctrl].first);
stack.save(Control_config[ctrl].second);
Undo_controls.save_stack(std::move(stack));
Control_config[ctrl].take(CC_bind(CID_KEYBOARD, static_cast<short>(k ^ bit)), -1);
control_config_conflict_check();
if (!API_Access)
gamesnd_play_iface(InterfaceSounds::USER_SELECT);
return true;
}
/**
* @brief Toggles inversion for the selected axis control
*/
bool control_config_toggle_invert(int ctrl, selItem item, bool API_Access)
{
CCI& cur_bind = Control_config[ctrl];
// Only toggle inversion state for axis types
if (!((cur_bind.type == CC_TYPE_AXIS_ABS) || (cur_bind.type == CC_TYPE_AXIS_REL))) {
if (!API_Access)
gamesnd_play_iface(InterfaceSounds::USER_SELECT);
return false;
}
Undo_controls.save(item);
switch (item) {
case selItem::None:
// both
cur_bind.first.invert_toggle();
cur_bind.second.invert_toggle();
break;
case selItem::Primary:
// first
cur_bind.first.invert_toggle();
break;
case selItem::Secondary:
// second
cur_bind.second.invert_toggle();
break;
default:
UNREACHABLE("Unhandled selItem in control_config_toggle_invert(): %i\n", static_cast<int>(item));
}
return true;
}
/*!
* Sets menu in bind mode. Menu will watch controller input and bind to the currently selected item, if any.
*/
void control_config_do_bind(bool API_Access)
{
short i;
game_flush();
if (!API_Access) {
// if ((Selected_line < 0) || (Cc_lines[Selected_line].cc_index & JOY_AXIS)) {
if (Selected_line < 0) {
gamesnd_play_iface(InterfaceSounds::GENERAL_FAIL);
return;
}
for (i = 0; i < NUM_BUTTONS; i++) {
if (i != CANCEL_BUTTON) {
CC_Buttons[gr_screen.res][i].button.reset_status();
CC_Buttons[gr_screen.res][i].button.disable();
}
}
CC_Buttons[gr_screen.res][CANCEL_BUTTON].button.enable();
CC_Buttons[gr_screen.res][CANCEL_BUTTON].button.set_hotkey(KEY_ESC);
}
for (short j = CID_JOY0; j < CID_JOY_MAX; ++j) {
for (i=0; i<JOY_TOTAL_BUTTONS; ++i) {
joy_down_count(CC_bind(static_cast<CID>(j), i), 1); // clear checking status of all joystick buttons
}
}
control_config_detect_axis_reset();
Binding_mode = 1;
Bind_time = ui_timestamp();
Search_mode = 0;
Last_key = -1;
Axis_override.clear();
if (!API_Access) {
gamesnd_play_iface(InterfaceSounds::USER_SELECT);
}
}
/*!
* @brief Sets menu in search mode. Menu will watch for control input, search for the item that is bound to it, and then focus/highlight the item if found.
*/
void control_config_do_search(bool API_Access)
{
short i;
// flush for the API so mouse clicks can be detected
if (API_Access) {
game_flush();
}
if (!API_Access) {
for (i = 0; i < NUM_BUTTONS; i++) {
if (i != CANCEL_BUTTON) {
CC_Buttons[gr_screen.res][i].button.reset_status();
CC_Buttons[gr_screen.res][i].button.disable();
}
}
CC_Buttons[gr_screen.res][CANCEL_BUTTON].button.enable();
CC_Buttons[gr_screen.res][CANCEL_BUTTON].button.set_hotkey(KEY_ESC);
}
for (short j = CID_JOY0; j < CID_JOY_MAX; ++j) {
for (i=0; i<JOY_TOTAL_BUTTONS; i++) {
joy_down_count(CC_bind(static_cast<CID>(j), i), 1); // clear checking status of all joystick buttons
}
}
Binding_mode = 0;
Search_mode = 1;
Last_key = -1;
if (!API_Access) {
gamesnd_play_iface(InterfaceSounds::USER_SELECT);
}
}
/*!
* (Re)sets the menu mode to Browse mode. This mode lets users browse through the bindings using controller input to navigate the item lists
*/
void control_config_do_cancel(int fail = 0, bool API_Access = false)
{
int i;
game_flush();
if (!API_Access) {
for (i = 0; i < NUM_BUTTONS; i++) {
if ((i != CANCEL_BUTTON) && (i != INVERT_AXIS)) {
CC_Buttons[gr_screen.res][i].button.enable();
}
}
CC_Buttons[gr_screen.res][CANCEL_BUTTON].button.reset_status();
CC_Buttons[gr_screen.res][CANCEL_BUTTON].button.disable();
CC_Buttons[gr_screen.res][CANCEL_BUTTON].button.set_hotkey(-1);
CC_Buttons[gr_screen.res][BIND_BUTTON].button.reset_status();
CC_Buttons[gr_screen.res][SEARCH_MODE].button.reset_status();
}
Binding_mode = Search_mode = 0;
if (!API_Access) {
if (fail) {
gamesnd_play_iface(InterfaceSounds::GENERAL_FAIL);
} else {
gamesnd_play_iface(InterfaceSounds::USER_SELECT);
}
}
}
/*!
* @brief Performs a single undo opration, reverting the most recent change to bindings, if any
*/
void control_config_do_undo(bool API_Access) {
Undo_controls.undo();
control_config_conflict_check();
if (!API_Access)
gamesnd_play_iface(InterfaceSounds::USER_SELECT);
}
/*!
* Does a cursory conflict check, then accepts changes to the bindings, if any, and request the menu to close.
*/
bool control_config_accept(bool API_Access)
{
int i;
for (i=0; i<NUM_TABS; i++) {
if (Conflicts_tabs[i]) {
break;
}
}
if (i < NUM_TABS) {
if (!API_Access) {
gamesnd_play_iface(InterfaceSounds::GENERAL_FAIL);
}
return false;
}
if (control_config_get_current_preset() == Control_config_presets.end()) {
if (!API_Access) {
// We have a custom preset to save, prompt the user
int flags = PF_TITLE_WHITE;
char* cstr; // Must be a char *, because popup_input may return nullptr and std::string don't like it
retry:;
SCP_string default_string = (Current_preset_name.empty()) ? Player->callsign : Current_preset_name;
cstr = popup_input(flags,
XSTR( "Confirm new custom preset name.\n\nThe name must not be empty or a default preset.\n\n Press [Enter] to accept, [Esc] to abort to config menu.", 1867),
32 - 6,
default_string.c_str(),
"-_");
if (cstr == nullptr) {
// Abort
gamesnd_play_iface(InterfaceSounds::USER_SELECT);
return false;
} else if (strcmp(cstr, "") == 0) {
// retry
gamesnd_play_iface(InterfaceSounds::GENERAL_FAIL);
goto retry;
}
SCP_string str = cstr;
// Check if a hardcoded preset with name already exists. If so, complain to user and force retry
auto it = std::find_if(Control_config_presets.begin(), Control_config_presets.end(), [str](CC_preset& p) {
return ((p.name == str) && ((p.type == Preset_t::tbl) || (p.type == Preset_t::hardcode)));
});
if (it != Control_config_presets.end()) {
popup(flags | PF_USE_AFFIRMATIVE_ICON, 1, POPUP_OK, "You may not overwrite a default preset. Please choose another name.");
goto retry;
}
// Check if a preset file with name already exists. If so, prompt the user
CFILE* fp = cfopen((str + ".json").c_str(),
"r",
CF_TYPE_PLAYER_BINDS,
false,
CF_LOCATION_ROOT_USER | CF_LOCATION_ROOT_GAME | CF_LOCATION_TYPE_ROOT);
if (fp) {
cfclose(fp);
int n = popup(flags,
2,
POPUP_CANCEL,
POPUP_OK,
"'%s'\n Already exists!\n Press OK to overwrite existing preset, or CANCEL to input another name",
str.c_str());
if ((n == 0) || (n == -1)) {
// If Cancel button was pressed, or popup dismissed:
// retry
gamesnd_play_iface(InterfaceSounds::USER_SELECT);
goto retry;
}
}
// Pack the current bindings into a preset, then save the file
CC_preset preset;
preset.name = std::move(str);
std::copy(Control_config.begin(), Control_config.end(), std::back_inserter(preset.bindings));
Control_config_presets.push_back(preset);
save_preset_file(preset, true);
// Reload the presets from file. Do this instead of just pushing the preset to the vector direct to get
// consistant ordering
Control_config_presets.resize(1);
load_preset_files();
// finally, save the new preset so that changes will get saved to this preset
Current_preset_name = preset.name;
} else {
return false;
}
}
// adds scripting hook for 'On Control Config Menu Closed' --wookieejedi
if (scripting::hooks::OnControlConfigMenuClosed->isActive()) {
scripting::hooks::OnControlConfigMenuClosed->run(scripting::hook_param_list(scripting::hook_param("OptionsAccepted", 'b', true)));
}
if (!API_Access) {
gameseq_post_event(GS_EVENT_PREVIOUS_STATE);
gamesnd_play_iface(InterfaceSounds::COMMIT_PRESSED);
}
return true;
}
/*!
* Reverts all changes, if any, and requests the menu to close.
*/
void control_config_cancel_exit(bool API_Access)
{
// Check if any changes were made
if (!API_Access && (control_config_get_current_preset() == Control_config_presets.end())) {
// Changes were made, prompt the user first.
int flags = PF_TITLE_WHITE | PF_USE_NEGATIVE_ICON | PF_USE_AFFIRMATIVE_ICON;
int choice = popup(flags, 2, POPUP_NO, POPUP_YES, XSTR( "You have unsaved changes.\n\n\n Do you wish to continue without saving?", 1866));
switch (choice) {
case -1: // Aborted
case 1: // Selected Yes (continue without saving)
// Either aborted (with Esc) or selected Yes (do not save)
// continue with backup restore and post event
break;
case 0:
// Selected No (do not exit)
return;
break;
default:
UNREACHABLE("Unknown popup choice %i", choice);
}
}
// Restore all bindings with the backup
std::move(Control_config_backup.begin(), Control_config_backup.end(), Control_config.begin());
// adds scripting hook for 'On Control Config Menu Closed' --wookieejedi
if (scripting::hooks::OnControlConfigMenuClosed->isActive()) {
scripting::hooks::OnControlConfigMenuClosed->run(scripting::hook_param_list(scripting::hook_param("OptionsAccepted", 'b', false)));
}
if (!API_Access) {
gameseq_post_event(GS_EVENT_PREVIOUS_STATE);
}
}
/**
* Button widget handler
*/
void control_config_button_pressed(int n)
{
switch (n) {
case TARGET_TAB:
case SHIP_TAB:
case WEAPON_TAB:
case COMPUTER_TAB:
Tab = n;
Scroll_offset = Selected_line = 0;
control_config_list_prepare();
gamesnd_play_iface(InterfaceSounds::SCREEN_MODE_PRESSED);
break;
case BIND_BUTTON:
control_config_do_bind();
break;
case SEARCH_MODE:
control_config_do_search();
break;
case SHIFT_TOGGLE:
control_config_toggle_modifier(KEY_SHIFTED, Cc_lines[Selected_line].cc_index);
gamesnd_play_iface(InterfaceSounds::USER_SELECT);
break;
case ALT_TOGGLE:
control_config_toggle_modifier(KEY_ALTED, Cc_lines[Selected_line].cc_index);
gamesnd_play_iface(InterfaceSounds::USER_SELECT);
break;
case INVERT_AXIS:
control_config_toggle_invert(Cc_lines[Selected_line].cc_index, Selected_item);
gamesnd_play_iface(InterfaceSounds::USER_SELECT);
break;
case SCROLL_UP_BUTTON:
control_config_scroll_screen_up();
break;
case SCROLL_DOWN_BUTTON:
control_config_scroll_screen_down();
break;
case ACCEPT_BUTTON:
control_config_accept();
break;
case CLEAR_BUTTON:
if (Selected_line < 0) {
gamesnd_play_iface(InterfaceSounds::GENERAL_FAIL);
} else {
control_config_remove_binding(Cc_lines[Selected_line].cc_index, Selected_item);
}
break;
case HELP_BUTTON:
launch_context_help();
gamesnd_play_iface(InterfaceSounds::HELP_PRESSED);
break;
case RESET_BUTTON:
control_config_do_reset();
break;
case UNDO_BUTTON:
control_config_do_undo();
break;
case CANCEL_BUTTON:
control_config_do_cancel();
break;
case CLEAR_OTHER_BUTTON:
if (Selected_line < 0) {
gamesnd_play_iface(InterfaceSounds::GENERAL_FAIL);
} else {
control_config_clear_other(Cc_lines[Selected_line].cc_index);
}
break;
case CLEAR_ALL_BUTTON:
control_config_clear_all();
break;
}
}
/*!
* Localization handler for "@Conflict", should str equal it (case insensitive)
*/
const char *control_config_tooltip_handler(const char *str)
{
int i;
if (!stricmp(str, NOX("@conflict"))) {
for (i=0; i<NUM_TABS; i++) {
if (Conflicts_tabs[i]) {
return XSTR( "Conflict!", 205);
}
}
}
return NULL;
}
void control_config_init(bool API_Access)
{
int i;
ui_button_info *b;
// Init the backup vectors
Control_config_backup.clear();
Control_config_backup.reserve(Control_config.size());
std::copy(Control_config.begin(), Control_config.end(), std::back_inserter(Control_config_backup));
// Init conflict vector
Conflicts.clear();
Conflicts.resize(Control_config.size());
if (!API_Access) {
// Init Cc_lines
Cc_lines.clear();
Cc_lines.resize(Control_config.size()); // Can't use CCFG_MAX here, since scripts or might add controls
common_set_interface_palette(NOX("ControlConfigPalette")); // set the interface palette
Ui_window.create(0, 0, gr_screen.max_w_unscaled, gr_screen.max_h_unscaled, 0);
Ui_window.set_mask_bmap(Conflict_background_bitmap_mask_fname[gr_screen.res]);
Ui_window.tooltip_handler = control_config_tooltip_handler;
}
// Init preset cycling system
auto preset_it = control_config_get_current_preset();
if (preset_it == Control_config_presets.end()) {
Current_preset_name.clear();
Defaults_cycle_pos = 0;
} else {
Current_preset_name = preset_it->name;
Defaults_cycle_pos = static_cast<unsigned int>(std::distance(Control_config_presets.begin(), preset_it));
}
if (!API_Access) {
// load in help overlay bitmap
Control_config_overlay_id = help_overlay_get_index(CONTROL_CONFIG_OVERLAY);
help_overlay_set_state(Control_config_overlay_id, gr_screen.res, 0);
// reset conflict flashing
Conflict_stamp = UI_TIMESTAMP::invalid();
for (i = 0; i < NUM_BUTTONS; i++) {
b = &CC_Buttons[gr_screen.res][i];
if (b->hotspot < 0) { // temporary
b->button.create(&Ui_window, NOX("Clear other"), b->x, b->y, 150, 30, 0, 1); // temporary
b->button.set_highlight_action(common_play_highlight_sound);
continue;
}
b->button
.create(&Ui_window, "", b->x, b->y, 60, 30, ((i == SCROLL_UP_BUTTON) || (i == SCROLL_DOWN_BUTTON)), 1);
// set up callback for when a mouse first goes over a button
b->button.set_highlight_action(common_play_highlight_sound);
if (i < 4) {
b->button.set_bmaps(b->filename, 5, 1); // a bit of a hack here, but buttons 0-3 need 4 frames loaded
} else {
b->button.set_bmaps(b->filename);
}
b->button.link_hotspot(b->hotspot);
}
// create all text
for (i = 0; i < CC_NUM_TEXT; i++) {
Ui_window.add_XSTR(&CC_text[gr_screen.res][i]);
}
for (i = 0; i < LIST_BUTTONS_MAX; i++) {
List_buttons[i].create(&Ui_window, "", 0, 0, 60, 30, 0, 1);
List_buttons[i].hide();
List_buttons[i].disable();
}
// set up hotkeys for buttons so we draw the correct animation frame when a key is pressed
CC_Buttons[gr_screen.res][SCROLL_UP_BUTTON].button.set_hotkey(KEY_PAGEUP);
CC_Buttons[gr_screen.res][SCROLL_DOWN_BUTTON].button.set_hotkey(KEY_PAGEDOWN);
CC_Buttons[gr_screen.res][BIND_BUTTON].button.set_hotkey(KEY_ENTER);
CC_Buttons[gr_screen.res][CLEAR_OTHER_BUTTON].button.set_hotkey(KEY_CTRLED | KEY_DELETE);
CC_Buttons[gr_screen.res][UNDO_BUTTON].button.set_hotkey(KEY_CTRLED | KEY_Z);
CC_Buttons[gr_screen.res][CLEAR_BUTTON].button.set_hotkey(KEY_DELETE);
CC_Buttons[gr_screen.res][ACCEPT_BUTTON].button.set_hotkey(KEY_CTRLED | KEY_ENTER);
CC_Buttons[gr_screen.res][HELP_BUTTON].button.set_hotkey(KEY_F1);
CC_Buttons[gr_screen.res][RESET_BUTTON].button.set_hotkey(KEY_CTRLED | KEY_R);
CC_Buttons[gr_screen.res][INVERT_AXIS].button.set_hotkey(KEY_I);
CC_Buttons[gr_screen.res][CANCEL_BUTTON].button.disable();
CC_Buttons[gr_screen.res][CLEAR_OTHER_BUTTON].button.disable();
Background_bitmap = bm_load(Conflict_background_bitmap_fname[gr_screen.res]);
Scroll_offset = Selected_line = 0;
// setup strings
Invert_text[0] = vm_strdup(XSTR("N", 1032));
Invert_text[1] = vm_strdup(XSTR("Y", 1033));
}
control_config_conflict_check();
if (!API_Access) {
control_config_list_prepare();
}
}
void control_config_close(bool API_Access)
{
int idx;
if (!API_Access) {
if (Background_bitmap) {
bm_release(Background_bitmap);
}
Ui_window.destroy();
common_free_interface_palette(); // restore game palette
}
game_flush();
if (Game_mode & GM_MULTIPLAYER) {
Pilot.save_player();
} else {
Pilot.save_savefile();
}
if (!API_Access) {
// free strings
for (idx = 0; idx < NUM_INVERT_TEXT; idx++) {
if (Invert_text[idx] != nullptr) {
vm_free(Invert_text[idx]);
Invert_text[idx] = nullptr;
}
}
}
// Free up memory from dynamic containers
Control_config_backup.clear();
Cc_lines.clear();
Conflicts.clear();
Undo_controls.clear();
// Clear this, just to be tidy
Current_preset_name.clear();
}
SCP_vector<CC_preset>::iterator control_config_get_current_preset(bool invert_agnostic) {
// Find the matching preset.
// We do this instead of relying on Defaults_cycle_pos because the player may end up duplicating a preset
auto it = Control_config_presets.begin();
// While a match isn't found, and there more presets to check, do search.
for (bool is_match = false; it != Control_config_presets.end(); ++it) {
is_match = true; // Set to true at start of each loop
for (size_t i = 0; i < Control_config.size(); ++i) {
// Check disabled
if (Control_config[i].disabled) {
// Skip this item
continue;
}
if (invert_agnostic) {
// Check for binding equality, ignoring invert flag
// Check Primary
if (!Control_config[i].first.invert_agnostic_equals(it->bindings[i].first)) {
// Isn't a match, stop checking this preset
is_match = false;
break;
}
// Check Secondary
if (!Control_config[i].second.invert_agnostic_equals(it->bindings[i].second)) {
// Isn't a match, stop checking this preset
is_match = false;
break;
}
} else {
// Check for binding exact equality
// Check Primary
if (Control_config[i].first != it->bindings[i].first) {
// Isn't a match, stop checking this preset
is_match = false;
break;
}
// Check Secondary
if (Control_config[i].second != it->bindings[i].second) {
// Isn't a match, stop checking this preset
is_match = false;
break;
}
}
}
if (is_match) {
// Need to break out before iterator is advanced
break;
}
}
return it;
}
/**
* @brief Display the currently selected preset
*/
void control_config_draw_selected_preset() {
SCP_string preset_str;
// Find the matching preset.
// We do this instead of relying on Defaults_cycle_pos because the player may end up duplicating a preset
auto preset_it = control_config_get_current_preset();
if (preset_it != Control_config_presets.end()) {
sprintf(preset_str, XSTR("Preset: %s", 1659), preset_it->name.c_str());
} else {
sprintf(preset_str, XSTR("Preset: custom", 1660), "");
}
// Draw the string
int font_height = gr_get_font_height();
int w;
gr_get_string_size(&w, nullptr, preset_str.c_str());
gr_set_color_fast(&Color_text_normal);
if (gr_screen.res == GR_640) {
gr_string(16, (24 - font_height) / 2, preset_str.c_str(), GR_RESIZE_MENU);
} else {
gr_string(24, (40 - font_height) / 2, preset_str.c_str(), GR_RESIZE_MENU);
}
}
bool control_config_delete_preset(CC_preset preset) {
return delete_preset_file(preset);
}
bool control_config_create_new_preset(const SCP_string& newName, bool overwrite)
{
// Check if a hardcoded preset with name already exists. If so, complain to user and force retry
auto it = std::find_if(Control_config_presets.begin(), Control_config_presets.end(), [newName](CC_preset& p) {
return (p.name == newName) && ((p.type == Preset_t::tbl) || (p.type == Preset_t::hardcode));
});
if (it != Control_config_presets.end()) {
return false;
}
// Check if a preset file with name already exists.
if (!overwrite && ((cf_exists_full((newName + ".json").c_str(), CF_TYPE_PLAYER_BINDS)) != 0)) {
return false;
}
// Pack the current bindings into a preset, then save the file
CC_preset preset;
preset.name = newName;
preset.type = Preset_t::pst;
std::copy(Control_config.begin(), Control_config.end(), std::back_inserter(preset.bindings));
// Done with the file
auto clone = preset_find_duplicate(preset);
// Do not create a duplicate preset
if (clone == Control_config_presets.end()) {
Control_config_presets.push_back(preset);
save_preset_file(preset, true);
// Reload the presets from file.
Control_config_presets.resize(1);
load_preset_files(newName);
// use the newly created preset
control_config_use_preset_by_name(preset.name);
return true;
} else if ((clone->name != preset.name) || (clone->type != Preset_t::pst)) {
// Complain and ignore if the preset names or the type differs
return false;
}
return false; //should be unreachable, but just in case
}
bool control_config_clone_preset(const CC_preset& preset, const SCP_string& newName, bool overwrite) {
// Check if a hardcoded preset with name already exists. If so, complain to user and force retry
auto it = std::find_if(Control_config_presets.begin(), Control_config_presets.end(), [newName](CC_preset& p) {
return (p.name == newName) && ((p.type == Preset_t::tbl) || (p.type == Preset_t::hardcode));
});
if (it != Control_config_presets.end()) {
return false;
}
// Check if a preset file with name already exists.
if (!overwrite && ((cf_exists_full((newName + ".json").c_str(), CF_TYPE_PLAYER_BINDS)) != 0)) {
return false;
}
CC_preset newPreset = preset;
newPreset.name = newName;
bool success = save_preset_file(newPreset, false);
// Reload the presets from file.
Control_config_presets.resize(1);
load_preset_files(newName);
// use the newly cloned preset
control_config_use_preset_by_name(newPreset.name);
return success;
}
/**
* Sets the color for binding text according to various states
*
* @param[in] line Current line to render
* @param[in] select_tease_line Line that the mouse is hovering over
* @param[in] item Which selItem to test against
* @param[in] empty True if the item is empty or not
*
* @returns 0 if no conflicts found, or
* @returns 1 if a conflict was found
*/
int set_item_color(int line, int select_tease_line, selItem item, bool empty) {
int z = Cc_lines[line].cc_index;
int conflict_id = -1;
int found_conflict = 0; // Is this even needed?
// digital
switch (item) {
case selItem::Primary:
conflict_id = Conflicts[z].first;
break;
case selItem::Secondary:
conflict_id = Conflicts[z].second;
break;
default:
UNREACHABLE("Invalid selItem passed to set_item_color: %i", static_cast<int>(item));
}
if (conflict_id >= 0) {
// In conflict
if ((line == Selected_line) && (Selected_item == item)) {
// Highlight selected item
gr_set_color_fast(&Color_text_error_hi);
} else {
gr_set_color_fast(&Color_text_error);
}
found_conflict++;
} else if (line == Selected_line) {
// Highlight selected item
if (Selected_item == item) {
gr_set_color_fast(&Color_text_selected);
} else {
gr_set_color_fast(&Color_text_subselected);
}
} else if (line == select_tease_line) {
gr_set_color_fast(&Color_text_subselected);
} else if (empty) {
gr_set_color_fast(&Color_grey);
} else {
gr_set_color_fast(&Color_text_normal);
}
return found_conflict;
}
/*!
* Draws the list box of controls and their bindings
*/
int control_config_draw_list(int select_tease_line) {
color* c; // Color to draw the text
int conflict = 0; // 0 if no conflict found
int line; // Current line to render
int i; // Generic index
//int w; // width of the string to draw (pixels)
//int h; // height of the string to draw (pixels)
int x; // x coordinate of text anchor
int y; // y coordinate of text anchor
int z; // cc_index of the line being drawn
char buf[256]; // c_str buffer
int font_height = gr_get_font_height();
for (line = Scroll_offset; cc_line_query_visible(line) && (line - Scroll_offset < LIST_BUTTONS_MAX); ++line) {
z = Cc_lines[line].cc_index;
// screen coordinate y = list box origin y + (this item's relative y - topmost item's relative y)
y = Control_list_coords[gr_screen.res][CONTROL_Y_COORD] + Cc_lines[line].y - Cc_lines[Scroll_offset].y;
// Update the y position for this button
List_buttons[line - Scroll_offset].update_dimensions(Control_list_coords[gr_screen.res][CONTROL_X_COORD], y, Control_list_coords[gr_screen.res][CONTROL_W_COORD], font_height);
// Enable selection/hover of items when not in binding mode
List_buttons[line - Scroll_offset].enable(!Binding_mode);
Cc_lines[line].kw = Cc_lines[line].jw = 0;
if (line == Selected_line) {
c = &Color_text_selected;
} else if (line == select_tease_line) {
c = &Color_text_subselected;
} else {
c = &Color_text_normal;
}
gr_set_color_fast(c);
if (Cc_lines[line].label) {
strcpy_s(buf, Cc_lines[line].label);
font::force_fit_string(buf, 255, Control_list_ctrl_w[gr_screen.res]);
gr_printf_menu(Control_list_coords[gr_screen.res][CONTROL_X_COORD], y, "%s", buf);
}
// Textify and print the primary and secondary bindings
SCP_string first = Control_config[z].first.textify();
SCP_string second = Control_config[z].second.textify();
// Set color for primary according to state
conflict += set_item_color(line, select_tease_line, selItem::Primary, Control_config[z].first.empty());
// Print primary
x = Control_list_first_x[gr_screen.res];
*buf = 0;
strcpy_s(buf, first.c_str());
font::force_fit_string(buf, 255, Control_list_first_w[gr_screen.res]);
gr_printf_menu(x, y, "%s", buf);
Cc_lines[line].kx = x - Control_list_coords[gr_screen.res][CONTROL_X_COORD];
Cc_lines[line].kw = Control_list_first_w[gr_screen.res];
// Set color for secondary according to state
conflict += set_item_color(line, select_tease_line, selItem::Secondary, Control_config[z].second.empty());
// Print secondary
x = Control_list_second_x[gr_screen.res];
*buf = 0;
strcpy_s(buf, second.c_str());
font::force_fit_string(buf, 255, Control_list_first_w[gr_screen.res]);
gr_printf_menu(x, y, "%s", buf);
Cc_lines[line].jx = x - Control_list_coords[gr_screen.res][CONTROL_X_COORD];
Cc_lines[line].jw = Control_list_first_w[gr_screen.res];
}
// Disable remaining empty lines
i = line - Scroll_offset;
while (i < LIST_BUTTONS_MAX) {
List_buttons[i++].disable();
}
return conflict;
}
int control_config_bind_key_on_frame(int ctrl, selItem item, bool API_Access)
{
bool bind = false; // is true if binding should happen. Actually is an "Input detected" flag.
bool done = false; // is true if we're done binding and ready for exiting this mode
//API mode needs to run this once before beginning
if (API_Access && (Binding_mode != 1)) {
control_config_do_bind(API_Access);
}
// Poll for keypress
int k;
if (!API_Access)
k = game_poll(); // polled key. Can be masked with SHIFT and/or ALT
else {
// In API-Access mode, game_poll has already happened this frame. Either by the state we're actually running in,
// or by the game_poll hardcoded into overrided OnFrame hooks. Hence, polling again is incorrect. The key is
// available in Current_key_down, and mouse and joy states are still up-to-date
extern int Current_key_down;
k = Current_key_down;
}
if (!API_Access) {
Ui_window.use_hack_to_get_around_stupid_problem_flag = 1;
Ui_window.process(0);
}
if (k == KEY_ENTER) {
// Cancel axis bind if Enter is pressed
bind = true;
}
short j = JOY_TOTAL_BUTTONS; // polled joy button
short joy = -1; // polled joystick id
// Poll for joy btn presses
// Stop polling all joys if a btn was detected
for (joy = CID_JOY0; joy < CID_JOY_MAX; joy++) {
if (!joy_present(joy)) {
continue;
}
bool brk = false;
for (j = 0; j < JOY_TOTAL_BUTTONS; j++) {
if (joy_down_count(CC_bind(static_cast<CID>(joy), j), 1)) {
// btn is down, save it in j and joy
// Cancel axis bind if any button is pressed
bind = true;
brk = true;
break;
}
}
if (brk)
break;
}
// TODO Poll for mouse btn presses
if (!API_Access) {
if (help_overlay_active(Control_config_overlay_id)) {
// Help overlay is active. Reset the Help button state and ignore gadgets
CC_Buttons[gr_screen.res][HELP_BUTTON].button.reset_status();
Ui_window.set_ignore_gadgets(1);
if ((k > 0) || (j < JOY_TOTAL_BUTTONS) || B1_JUST_RELEASED) {
// If a key, joy, or mouse button was pressed, dismiss the overlay, watch gadgets, and consume them
help_overlay_set_state(Control_config_overlay_id, gr_screen.res, 0);
Ui_window.set_ignore_gadgets(0);
k = 0;
j = JOY_TOTAL_BUTTONS;
}
} else {
// Help overlay is not active, watch gadgets
Ui_window.set_ignore_gadgets(0);
}
}
if (k == KEY_ESC) {
// Cancel bind if ESC is pressed
strcpy_s(bound_string, XSTR("Canceled", 206));
bound_timestamp = ui_timestamp(2500);
control_config_do_cancel(0, API_Access);
if (API_Access) {
return 1;
}
} else if (Control_config[ctrl].is_axis()) {
// Is an analogue control
// Poll for joy axis
CC_bind ccb = control_config_detect_axis();
if (!ccb.empty()) {
Axis_override = ccb;
bind = true;
}
if (!done && bind) {
if (!Axis_override.empty()) {
control_config_bind(ctrl, Axis_override, item, API_Access);
done = true;
strcpy_s(bound_string, Axis_override.textify().c_str());
} else {
// Canceled
control_config_do_cancel(1, API_Access);
if (API_Access) {
return 1;
}
}
}
} else {
// Is a digital control
switch (k & KEY_MASK) {
case KEY_LSHIFT:
case KEY_RSHIFT:
case KEY_LALT:
case KEY_RALT:
// k is a modifier. Store the mask in Last_key and consume k
Last_key = k & KEY_MASK;
k = 0;
break;
}
if ((ctrl == BANK_WHEN_PRESSED || ctrl == GLIDE_WHEN_PRESSED) && (Last_key >= 0) && (k <= 0) &&
!key_is_pressed(Last_key)) {
// If the selected cc_item is BANK_WHEN_PRESSED or GLIDE_WHEN_PRESSED, and
// If the polled key is a modifier, and
// k was consumed, and
// the key was just released, then
// allow binding the modifier key by itself
k = Last_key;
}
if ((k > 0) && !Config_allowed[k & KEY_MASK]) {
// This key isn't allowed to be bound. Consume k and inform the player
if (!API_Access){
popup(PF_USE_AFFIRMATIVE_ICON, 1, POPUP_OK, XSTR("That is a non-bindable key. Please try again.", 207));
} else {
return -1;
}
k = 0;
}
k &= (KEY_MASK | KEY_SHIFTED | KEY_ALTED); // This shouldn't be needed, but just in case...
if (!done && (k > 0)) {
// Bind the key
Assert(!Control_config[ctrl].is_axis());
control_config_bind(ctrl, CC_bind(CID_KEYBOARD, static_cast<short>(k)), item, API_Access);
strcpy_s(bound_string, textify_scancode(k));
done = true;
}
if (!done && (j < JOY_TOTAL_BUTTONS)) {
// Bind the joy button
Assert(!Control_config[ctrl].is_axis());
CC_bind joy_bind(static_cast<CID>(joy), j);
control_config_bind(ctrl, joy_bind, item, API_Access);
strcpy_s(bound_string, joy_bind.textify().c_str());
done = true;
}
// Debounce timer to allow mouse double-click (maybe?)
if (!done && (ui_timestamp_since(Bind_time) > 375)) {
int i;
if (!API_Access) {
for (i = 0; i < NUM_BUTTONS; i++) {
if ((CC_Buttons[gr_screen.res][i].button.is_mouse_on()) &&
(CC_Buttons[gr_screen.res][i].button.enabled())) {
break;
}
}
} else {
i = NUM_BUTTONS; //this is kind of hacky but working out how to detangle the UI from the mouse binding is messy
}
if (i == NUM_BUTTONS) {
// no buttons pressed, go ahead with polling the mouse
CID cid;
if (Use_mouse_to_fly) {
// treat mouse as Joy0
cid = CID_JOY0;
} else {
// treat mouse as mouse
cid = CID_MOUSE;
}
for (i = 0; i < MOUSE_NUM_BUTTONS; i++) {
CC_bind mouse_bind(cid, static_cast<short>(i));
if (mouse_down(mouse_bind)) {
Assert(!Control_config[ctrl].is_axis());
control_config_bind(ctrl, mouse_bind, item, API_Access);
strcpy_s(bound_string, mouse_bind.textify().c_str());
done = true;
break;
}
}
}
}
}
// The API needs to reset itself if a bind was found as well as check for conflicts
if (API_Access && done) {
control_config_do_cancel();
control_config_conflict_check();
}
return (int)done;
}
// returns the index of the bind the detected key is matched to. returns -1 if no match or -2 if ESCAPE
int control_config_search_key_on_frame(bool API_Access)
{
// API mode needs to run this once before beginning
if (API_Access && (Search_mode != 1)) {
control_config_do_search(API_Access);
}
// Poll for keypress
int k;
if (!API_Access)
k = game_poll(); // polled key. Can be masked with SHIFT and/or ALT
else {
// In API-Access mode, game_poll has already happened this frame. Either by the state we're actually running in,
// or by the game_poll hardcoded into overrided OnFrame hooks. Hence, polling again is incorrect. The key is
// available in Current_key_down, and mouse and joy states are still up-to-date
extern int Current_key_down;
k = Current_key_down;
}
if (!API_Access) {
Ui_window.use_hack_to_get_around_stupid_problem_flag = 1;
Ui_window.process(0);
}
short j = JOY_TOTAL_BUTTONS; // polled joy button
short joy = -1; // polled joystick id
// Poll for joy btn presses
// Stop polling all joys if a btn was detected
for (joy = CID_JOY0; joy < CID_JOY_MAX; joy++) {
if (!joy_present(joy)) {
continue;
}
bool brk = false;
for (j = 0; j < JOY_TOTAL_BUTTONS; j++) {
if (joy_down_count(CC_bind(static_cast<CID>(joy), j), 1)) {
// btn is down, save it in j and joy
// Cancel axis bind if any button is pressed
brk = true;
break;
}
}
if (brk)
break;
}
if (!API_Access) {
if (help_overlay_active(Control_config_overlay_id)) {
// Help overlay is active. Reset the Help button state and ignore gadgets
CC_Buttons[gr_screen.res][HELP_BUTTON].button.reset_status();
Ui_window.set_ignore_gadgets(1);
if ((k > 0) || (j < JOY_TOTAL_BUTTONS) || B1_JUST_RELEASED) {
// If a key, joy, or mouse button was pressed, dismiss the overlay, watch gadgets, and consume them
help_overlay_set_state(Control_config_overlay_id, gr_screen.res, 0);
Ui_window.set_ignore_gadgets(0);
k = 0;
}
} else {
// Overlay not active, watch gadgets
Ui_window.set_ignore_gadgets(0);
}
}
if (k == KEY_ESC) {
// Cancel search if ESC is pressed
control_config_do_cancel(0, API_Access);
return -2;
} else {
if ((k > 0) && !Config_allowed[k & KEY_MASK]) {
// Ignore disallowed keys
k = 0;
}
const int CCFG_SIZE = static_cast<int>(Control_config.size()); // hack to get around signed/unsigned mismatch errors
k &= (KEY_MASK | KEY_SHIFTED | KEY_ALTED);
int ctrl = -1;
// If not done, Find the control bound to the given key
if ((ctrl < 0) && (k > 0)) {
for (int i = 0; i < CCFG_SIZE; ++i) {
if (Control_config[i].first == CC_bind(CID_KEYBOARD, static_cast<short>(k))) {
Selected_item = selItem::Primary;
ctrl = i;
break;
} else if (Control_config[i].second == CC_bind(CID_KEYBOARD, static_cast<short>(k))) {
Selected_item = selItem::Secondary;
ctrl = i;
break;
}
}
}
// If not done, Find the control bound to the given joy
if ((ctrl < 0) && (joy < CID_JOY_MAX)) {
for (int i = 0; i < CCFG_SIZE; ++i) {
if (Control_config[i].first == CC_bind(static_cast<CID>(joy), j)) {
Selected_item = selItem::Primary;
ctrl = i;
break;
} else if (Control_config[i].second == CC_bind(static_cast<CID>(joy), j)) {
Selected_item = selItem::Secondary;
ctrl = i;
break;
}
}
}
// check if not on enabled button
int i;
if (!API_Access) {
for (i = 0; i < NUM_BUTTONS; i++) {
if ((CC_Buttons[gr_screen.res][i].button.is_mouse_on()) &&
(CC_Buttons[gr_screen.res][i].button.enabled())) {
break;
}
}
} else {
i = NUM_BUTTONS; // this is kind of hacky but working out how to detangle the UI from the mouse binding is messy
}
// If not done, and no buttons pressed, poll the mouse and find controls bound to buttons
if ((ctrl < 0) && (i == NUM_BUTTONS)) {
for (j = 0; j < MOUSE_NUM_BUTTONS; ++j) {
if (mouse_down(CC_bind(CID_MOUSE, j))) {
// Find the control bound to the given mouse button
for (i = 0; i < CCFG_SIZE; ++i) {
if (Control_config[i].first == CC_bind(CID_MOUSE, j)) {
Selected_item = selItem::Primary;
ctrl = i;
break;
} else if (Control_config[i].second == CC_bind(CID_MOUSE, j)) {
Selected_item = selItem::Secondary;
ctrl = i;
break;
}
}
break;
}
}
}
// The API needs to reset itself if a bind was found
if (API_Access && (ctrl >= 0)) {
Search_mode = 0;
}
return ctrl;
}
}
void control_config_do_frame(float frametime)
{
const char *str;
char buf[256];
int i; // generic index
int w, x, y, conflict;
int k; // polled key. Can be masked with SHIFT and/or ALT
short j = JOY_TOTAL_BUTTONS; // polled joy button
short joy = -1; // polled joystick id
int z = Cc_lines[Selected_line].cc_index; // Selected line's cc_index; value: (z &= ~JOY_AXIS); Is an axis index if (z & JOY_AXIS) == true;
int font_height = gr_get_font_height();
int select_tease_line = -1; // line mouse is down on, but won't be selected until button released
static float timer = 0.0f;
timer += frametime;
if (Binding_mode) {
bool done = (bool)control_config_bind_key_on_frame(z, Selected_item);
if (done) {
// done with binding mode, clean up and prepare for display
font::force_fit_string(bound_string, 39, Conflict_wnd_coords[gr_screen.res][CONTROL_W_COORD]);
bound_timestamp = ui_timestamp(2500);
control_config_conflict_check();
control_config_list_prepare();
control_config_do_cancel();
for (j = 0; j < NUM_BUTTONS; j++) {
CC_Buttons[gr_screen.res][j].button.reset();
}
}
} else if (Search_mode) {
int ctrl = control_config_search_key_on_frame();
// If done, Focus on the found control
if (ctrl >= 0) {
Tab = Control_config[ctrl].tab;
control_config_list_prepare();
Selected_line = Scroll_offset = 0;
// Reverse Lookup cc_index to find the line its on
for (i = 0; i < Num_cc_lines; i++) {
if (Cc_lines[i].cc_index == ctrl) {
Selected_line = i;
break;
}
}
Assert(i != Num_cc_lines);
// Scroll to line if it is not visible
while (!cc_line_query_visible(Selected_line)) {
Scroll_offset++;
Assert(Scroll_offset < Num_cc_lines);
}
// Reset all nav buttons
for (size_t buttonid = 0; buttonid < NUM_BUTTONS; buttonid++) {
CC_Buttons[gr_screen.res][buttonid].button.reset();
}
}
} else {
// Browse/default mode
//Enable modifier buttons according to selected item type
z = Cc_lines[Selected_line].cc_index;
CC_Buttons[gr_screen.res][ALT_TOGGLE].button.enable(!Control_config[z].is_axis()); // Enabled for keys/buttons
CC_Buttons[gr_screen.res][SHIFT_TOGGLE].button.enable(!Control_config[z].is_axis()); // Enabled for keys/buttons
CC_Buttons[gr_screen.res][INVERT_AXIS].button.enable(Control_config[z].is_axis()); // Enabled for axes
// If selected item is not an axis, and
// If the bound key is a modifier, disable the modifier UI buttons
if (!Control_config[z].is_axis()) {
k = Control_config[z].get_btn(CID_KEYBOARD);
if ((k == KEY_LALT) || (k == KEY_RALT) || (k == KEY_LSHIFT) || (k == KEY_RSHIFT) ) {
CC_Buttons[gr_screen.res][ALT_TOGGLE].button.enable(0);
CC_Buttons[gr_screen.res][SHIFT_TOGGLE].button.enable(0);
}
}
// Enable the undo button if our undo stack has something in it
CC_Buttons[gr_screen.res][UNDO_BUTTON].button.enable(!Undo_controls.empty());
// Poll for keypress (navigational only)
k = Ui_window.process();
// Poll for joy buttons
for (joy = CID_JOY0; joy < CID_JOY_MAX; ++joy) {
if (!joy_present(joy)) {
// not present, skip
continue;
}
bool brk = false;
for (j = 0; j < JOY_TOTAL_BUTTONS; j++) {
if (joy_down_count(CC_bind(static_cast<CID>(joy), j), 1)) {
// btn is down, save it in j and joy
brk = true;
break;
}
}
if (brk)
break;
}
if ( help_overlay_active(Control_config_overlay_id) ) {
// If the help overlay is active, reset the help button state and ignore gadgets.
CC_Buttons[gr_screen.res][HELP_BUTTON].button.reset_status();
Ui_window.set_ignore_gadgets(1);
if ((k > 0) || (joy < CID_JOY_MAX) || B1_JUST_RELEASED) {
// If a key, joy, or mouse button was pressed, dismiss the overlay, watch gadgets, and consume them
help_overlay_set_state(Control_config_overlay_id, gr_screen.res, 0);
Ui_window.set_ignore_gadgets(0);
k = 0;
}
} else {
// Overlay not active, watch gadgets
Ui_window.set_ignore_gadgets(0);
}
// Navigate according to keypress
switch (k) {
case KEY_DOWN: // select next line
control_config_scroll_line_down();
break;
case KEY_UP: // select previous line
control_config_scroll_line_up();
break;
case KEY_SHIFTED | KEY_TAB: // activate previous tab
Tab--;
if (Tab < 0) {
Tab = NUM_TABS - 1;
}
Scroll_offset = Selected_line = 0;
control_config_list_prepare();
gamesnd_play_iface(InterfaceSounds::SCREEN_MODE_PRESSED);
break;
case KEY_TAB: // activate next tab
Tab++;
if (Tab >= NUM_TABS) {
Tab = 0;
}
Scroll_offset = Selected_line = 0;
control_config_list_prepare();
gamesnd_play_iface(InterfaceSounds::SCREEN_MODE_PRESSED);
break;
case KEY_LEFT:
// Select Previous item
Selected_item--;
gamesnd_play_iface(InterfaceSounds::SCROLL);
break;
case KEY_RIGHT:
// Next item
Selected_item++;
gamesnd_play_iface(InterfaceSounds::SCROLL);
break;
case KEY_BACKSP:
// undo last action
control_config_do_undo();
break;
case KEY_ESC:
// Escape from menu
control_config_cancel_exit();
break;
} // end switch
} // End mode specific logic
// Process UI Button presses
for (i=0; i<NUM_BUTTONS; i++){
if (CC_Buttons[gr_screen.res][i].button.pressed()){
control_config_button_pressed(i);
}
}
// Process list box
for (i=0; i<LIST_BUTTONS_MAX; i++) {
if (List_buttons[i].is_mouse_on()) {
// Set this line's state as mouse-over
select_tease_line = i + Scroll_offset;
}
if (List_buttons[i].pressed()) {
// Select the pressed line
Selected_line = i + Scroll_offset;
// Cyborg - Quick assertion for Coverity 1093701. Not likely to happen, but we check for a negative Selected Line later.
Assertion(Selected_line > -1, "control_config_do_frame() has somehow managed to get a negative Selected_line index of %d. Please get an SCP member.", Selected_line);
Selected_item = selItem::None;
List_buttons[i].get_mouse_pos(&x, &y);
// If the mouse is over the Primary binding, select it
if ((x >= Cc_lines[Selected_line].kx) && (x < Cc_lines[Selected_line].kx + Cc_lines[Selected_line].kw)) {
Selected_item = selItem::Primary;
}
// If the mouse is over the Secondary binding, select it
if ((x >= Cc_lines[Selected_line].jx) && (x < Cc_lines[Selected_line].jx + Cc_lines[Selected_line].jw)) {
Selected_item = selItem::Secondary;
}
gamesnd_play_iface(InterfaceSounds::USER_SELECT);
}
if (List_buttons[i].double_clicked()) {
control_config_do_bind();
}
}
GR_MAYBE_CLEAR_RES(Background_bitmap);
if (Background_bitmap >= 0) {
gr_set_bitmap(Background_bitmap);
gr_bitmap(0, 0, GR_RESIZE_MENU);
}
// highlight tab with conflict
Ui_window.draw();
for (i=z=0; i<NUM_TABS; i++) {
if (Conflicts_tabs[i]) {
CC_Buttons[gr_screen.res][i].button.draw_forced(4);
z++;
}
}
if (z) {
// maybe switch from bright to normal
if (!Conflict_stamp.isValid() || ui_timestamp_elapsed(Conflict_stamp)){
Conflict_bright = !Conflict_bright;
Conflict_stamp = ui_timestamp(CONFLICT_FLASH_TIME);
}
// set color and font
font::set_font(font::FONT2);
if(Conflict_bright){
gr_set_color_fast(&Color_bright_red);
} else {
gr_set_color_fast(&Color_red);
}
// setup the conflict string
char conflict_str[512] = "";
strncpy(conflict_str, XSTR("Conflict!", 205), 511);
int sw, sh;
gr_get_string_size(&sw, &sh, conflict_str);
x = Conflict_warning_coords[gr_screen.res][CONTROL_X_COORD] - (sw / 2);
y = Conflict_warning_coords[gr_screen.res][CONTROL_Y_COORD];
gr_printf_menu(x, y, "%s", conflict_str);
font::set_font(font::FONT1);
} else {
// might as well always reset the conflict stamp
Conflict_stamp = UI_TIMESTAMP::invalid();
}
// Find if a tab button was pressed
for (i=0; i<NUM_TABS; i++) {
if (CC_Buttons[gr_screen.res][i].button.button_down()) {
break;
}
}
// No tab buttons down, show selected tab button as down
if (i == NUM_TABS) {
CC_Buttons[gr_screen.res][Tab].button.draw_forced(2);
}
if (Search_mode) {
CC_Buttons[gr_screen.res][SEARCH_MODE].button.draw_forced(2);
}
if (Selected_line >= 0) {
z = Cc_lines[Selected_line].cc_index;
if (Control_config[z].is_axis()) {
// Show inversion button as down, if the selected axis is inverted
switch (Selected_item) {
case selItem::None:
case selItem::Primary:
if (Control_config[z].first.is_inverted()) {
CC_Buttons[gr_screen.res][INVERT_AXIS].button.draw_forced(2);
}
break;
case selItem::Secondary:
if (Control_config[z].second.is_inverted()) {
CC_Buttons[gr_screen.res][INVERT_AXIS].button.draw_forced(2);
}
break;
default:
break;
}
} else {
// Show modifier buttons as down, if the selected item uses them
k = Control_config[z].get_btn(CID_KEYBOARD);
if (k >= 0) {
if (k & KEY_SHIFTED) {
CC_Buttons[gr_screen.res][SHIFT_TOGGLE].button.draw_forced(2);
}
if (k & KEY_ALTED) {
CC_Buttons[gr_screen.res][ALT_TOGGLE].button.draw_forced(2);
}
}
}
}
if (Binding_mode) {
CC_Buttons[gr_screen.res][BIND_BUTTON].button.draw_forced(2);
}
z = Cc_lines[Selected_line].cc_index;
x = Conflict_wnd_coords[gr_screen.res][CONTROL_X_COORD] + Conflict_wnd_coords[gr_screen.res][CONTROL_W_COORD] / 2;
y = Conflict_wnd_coords[gr_screen.res][CONTROL_Y_COORD] + Conflict_wnd_coords[gr_screen.res][CONTROL_H_COORD] / 2;
if (Binding_mode) {
int t;
t = (int) (timer * 3);
if (t % 2) {
gr_set_color_fast(&Color_text_normal);
gr_get_string_size(&w, NULL, XSTR( "?", 208));
gr_printf_menu(x - w / 2, y - font_height / 2, "%s", XSTR( "?", 208));
}
} else if (!Control_config[z].is_axis() && ((Conflicts[z].first >= 0) || (Conflicts[z].second >= 0))) {
i = Conflicts[z].first;
if (i < 0) {
i = Conflicts[z].second;
}
gr_set_color_fast(&Color_text_normal);
str = XSTR( "Control conflicts with:", 209);
gr_get_string_size(&w, NULL, str);
gr_printf_menu(x - w / 2, y - font_height, "%s", str);
if (Control_config[i].indexXSTR > 1) {
strcpy_s(buf, XSTR(Control_config[i].text.c_str(), Control_config[i].indexXSTR, true));
} else if (Control_config[i].indexXSTR == 1) {
strcpy_s(buf, XSTR(Control_config[i].text.c_str(), CONTROL_CONFIG_XSTR + i, true));
} else {
strcpy_s(buf, Control_config[i].text.c_str());
}
w = font::force_fit_string(buf, 255, Conflict_wnd_coords[gr_screen.res][CONTROL_W_COORD]);
gr_printf_menu(x - w / 2, y, "%s", buf);
} else if (*bound_string) {
gr_set_color_fast(&Color_text_normal);
gr_get_string_size(&w, NULL, bound_string);
gr_printf_menu(x - w / 2, y - font_height / 2, "%s", bound_string);
if (ui_timestamp_elapsed(bound_timestamp)) {
*bound_string = 0;
}
}
if (Cc_lines[Num_cc_lines - 1].y + font_height > Cc_lines[Scroll_offset].y + Control_list_coords[gr_screen.res][CONTROL_H_COORD]) {
gr_set_color_fast(&Color_white);
gr_printf_menu(Control_more_coords[gr_screen.res][CONTROL_X_COORD], Control_more_coords[gr_screen.res][CONTROL_Y_COORD], "%s", XSTR( "More...", 210));
}
conflict = control_config_draw_list(select_tease_line);
CC_Buttons[gr_screen.res][CLEAR_OTHER_BUTTON].button.enable(conflict);
// Display preset in use
control_config_draw_selected_preset();
// blit help overlay if active
help_overlay_maybe_blit(Control_config_overlay_id, gr_screen.res);
gr_flip();
}
float check_control_timef(int id)
{
float t1, t2, t3;
// if type isn't continuous, we shouldn't be using this function, cause it won't work.
Assert(Control_config[id].type == CC_TYPE_CONTINUOUS);
// first, see if control actually used (makes sure modifiers match as well)
if (!check_control(id)) {
Control_config[id].continuous_ongoing = false;
return 0.0f;
}
t1 = key_down_timef(Control_config[id].get_btn(CID_KEYBOARD));
if (t1) {
control_used(id);
}
t2 = joy_down_time(Control_config[id].first);
if (t2) {
control_used(id);
}
t3 = joy_down_time(Control_config[id].second);
if (t3) {
control_used(id);
}
if (t1 + t2 + t3) {
// We want to set this to true only after visiting control_used() (above)
// to allow it to tell the difference between an ongoing continuous action
// started before and a continuous action being started right now.
Control_config[id].continuous_ongoing = true;
return t1 + t2 + t3;
}
return 1.0f;
}
void control_check_indicate()
{
#ifndef NDEBUG
if (Show_controls_info) {
gr_set_color_fast(&HUD_color_debug);
gr_printf_no_resize(gr_screen.center_offset_x + gr_screen.center_w - 154, gr_screen.center_offset_y + 5, NOX("Ctrls checked: %d"), Control_check_count);
}
#endif
Control_check_count = 0;
}
/**
* @brief Checks if a control is being used by the player this frame by checking the bound control input bindings.
*
* @param[in] id The IoActionId of the control to check
* @param[in] key DO NOT USE. Key combination acquired from game_poll(), or -1 to check against a previous poll
*
* @returns 0 If the control was disabled by a mod, or
* @returns 0 If locked by a script, or
* @returns 0 If controls need to be ignored for multiplayer, or
* @returns 0 If the control wasn't used, or
* @returns 1 If the control was used
*/
int check_control_used(int id, int key)
{
// Make sure mouse_down() is only called once during any logic path,
// or the mousewheel may be decayed multiple times!!
int mask;
static int last_key = 0;
auto & item = Control_config[id];
Control_check_count++;
if (key < 0) {
key = last_key;
}
last_key = key;
// if we're in multiplayer text enter (for chat) mode, check to see if we should ignore controls
if ((Game_mode & GM_MULTIPLAYER) && multi_ignore_controls()){
return 0;
}
if (item.disabled || item.locked)
return 0;
// Filter actions based on conflict groups and current mode
{
int active_group = game_is_photo_mode_active() ? CONFLICT_GROUP_PHOTO_MODE : CONFLICT_GROUP_DEFAULT;
if (!(item.conflict_groups & active_group))
return 0;
}
short z = item.get_btn(CID_KEYBOARD); // Get the key that's bound to this control
if (item.type == CC_TYPE_CONTINUOUS) {
// this is awful, need to make a reverse lookup table to do
// button -> control instead of this control -> button nonsense.
if ((joy_down(item.first) || joy_down_count(item.first, 1)) ||
(joy_down(item.second) || joy_down_count(item.second, 1))) {
// Joy button bound to this control was pressed, control activated
control_used(id);
return 1;
}
if ((mouse_down(item.first) || mouse_down_count(item.first, 1)) ||
(mouse_down(item.second) || mouse_down_count(item.second, 1))) {
// Mouse button bound to this control was pressed, control activated
control_used(id);
return 1;
}
// check what current modifiers are pressed
mask = 0;
if (key_is_pressed(KEY_LSHIFT, true) || key_is_pressed(KEY_RSHIFT, true)) {
// Any shift key is pressed, add KEY_SHIFTED mask
mask |= KEY_SHIFTED;
}
if (key_is_pressed(KEY_LALT, true) || key_is_pressed(KEY_RALT, true)) {
// Any alt key is pressed, add KEY_ALTED to the mask
mask |= KEY_ALTED;
}
if (z >= 0) {
if ( (z != KEY_LALT) && (z != KEY_RALT) && (z != KEY_LSHIFT) && (z != KEY_RSHIFT) ) {
// if current modifiers don't match action's modifiers, don't register control active.
if ((z & (KEY_SHIFTED | KEY_ALTED)) != mask) {
return 0;
}
}
z &= KEY_MASK;
if (key_is_pressed(z, true)) {
// Key combo is pressed, control activated
control_used(id);
return 1;
}
}
return 0;
}
if (((z >= 0) && (z == key)) ||
joy_down_count(item.first, 1) || joy_down_count(item.second, 1) ||
mouse_down_count(item.first, 1) || mouse_down_count(item.second, 1)) {
//mprintf(("Key used %d\n", key));
control_used(id);
return 1;
}
// special case to allow actual mouse wheel to work with trigger controls --wookieejedi
if (item.type == CC_TYPE_TRIGGER) {
if ( mouse_down(item.first, true) || mouse_down(item.second, true) ) {
// Mouse wheel bound to this trigger control was pressed, control activated
control_used(id);
return 1;
}
}
return 0;
}
int check_control(int id, int key)
{
const bool is_control_used = check_control_used(id, key) != 0;
const bool is_ignored = Ignored_keys[id] != 0;
//Decrement Ignored_keys if key was pressed and is ignored
if (is_control_used && Ignored_keys[id] > 0) {
Ignored_keys[id]--;
}
int control_triggered = 0; // boolean as int (0 = false, 1 = true); Weirdness for compatibility with control_run_lua();
if (is_control_used && !is_ignored) {
control_triggered = 1;
}
if (Control_config[id].type == CC_TYPE_CONTINUOUS) {
// Only call lua in here when it's a continuous button
if (control_run_lua(static_cast<IoActionId>(id), control_triggered)) {
// lua has taken care of the control already. Mark as not triggered to prevent hardcode from executing.
control_triggered = 0;
} // Else, let hardcode process the control
}
if (!is_control_used && Control_config[id].continuous_ongoing) {
// If we reach this point, then it means this is a continuous control
// which has just been released
if (scripting::hooks::OnActionStopped->isActive()) {
scripting::hooks::OnActionStopped->run(scripting::hooks::ControlActionConditions{ id },
scripting::hook_param_list(
scripting::hook_param("Action", 's', Control_config[id].text)
));
}
Control_config[id].continuous_ongoing = false;
}
return control_triggered;
}
/**
* Inverts the given raw axis value according to the action type
*
* @param[in] inv True for invert, False for noram
* @param[in] type Type of the axis value to invert, determines method of inversion
* @param[in,out] val raw axis value in, maybe inverted axis value out
*/
inline
void maybe_invert(bool inv, CC_type type, int &val)
{
if (!inv) {
return;
}
switch (type) {
case CC_TYPE_AXIS_ABS:
// Abs uses full range, 0 to 1
val = F1_0 - val;
break;
case CC_TYPE_AXIS_REL:
// Rel is centered around 0; range -1 to 1
val *= -1;
break;
default:
// ignore all others
break;
}
}
/*!
* Scales, and maybe inverts, the input axis values
*
* @param[in] bind The control's binding to check
* @param[in] action index into axis_out of the action
* @param[in] type CC_type of the control
* @param[in] frame_time Current frame time, used to scale mouse axis
* @param[in] axis_in[][] Array of raw axis values
* @param[out] axis_out Output array of the scaled axes
*
* @note C++ doesn't like passing multi-dim arrays as arguments
*/
void scale_invert(const CC_bind &bind,
int action,
CC_type type,
float frame_time,
int (&axis_in)[CID_JOY_MAX + 1][JOY_NUM_AXES],
int *axis_out)
{
const int MOUSE_ID = CID_JOY_MAX; // Joy axes go in front here, mouse gets tacked on the end
float factor = 0.0f;
int dx = 0;
const auto cid = bind.get_cid();
const auto btn = bind.get_btn();
factor = (float)Mouse_sensitivity + 1.77f;
factor = factor * factor / frame_time / 0.6f;
switch (cid) {
case CID_MOUSE:
if (!Use_mouse_to_fly) {
// Mouse is treated as mouse, get the axis values
dx = axis_in[MOUSE_ID][btn];
maybe_invert(bind.is_inverted(), type, dx);
axis_out[action] += (int)((float)dx * factor);
} // else, Mouse is treated as joy, ignore and let CID_JOY0 case handle it on next call
break;
case CID_JOY0:
if (Use_mouse_to_fly) {
// Mouse is treated as Joy0
dx = axis_in[MOUSE_ID][btn];
maybe_invert(bind.is_inverted(), type, dx);
axis_out[action] += (int)((float)dx * factor);
}
FALLTHROUGH;
case CID_JOY1:
case CID_JOY2:
case CID_JOY3:
switch (type) {
case CC_TYPE_AXIS_ABS:
dx = joy_get_unscaled_reading(axis_in[cid][btn]);
break;
case CC_TYPE_AXIS_REL:
case CC_TYPE_AXIS_BTN_NEG:
case CC_TYPE_AXIS_BTN_POS:
default:
dx = joy_get_scaled_reading(axis_in[cid][btn]);
break;
}
maybe_invert(bind.is_inverted(), type, dx);
axis_out[action] += dx;
break;
default:
// All others, ignore
break;
}
}
void control_get_axes_readings(int *axis_v, float frame_time)
{
int axe[CID_JOY_MAX + 1][JOY_NUM_AXES] = {{0}};
const int MOUSE_ID = CID_JOY_MAX; // Joy axes go in front here, mouse gets tacked on the end
Assert(axis_v != nullptr);
// Init output
for (int action = 0; action < Action::NUM_VALUES; ++action) {
axis_v[action] = 0;
}
// Read raw sticks.
for (short j = CID_JOY0; j < CID_JOY_MAX; ++j) {
joystick_read_raw_axis(j, JOY_NUM_AXES, axe[j]);
}
// Read raw mouse
mouse_get_delta(&axe[MOUSE_ID][MOUSE_X_AXIS], &axe[MOUSE_ID][MOUSE_Y_AXIS], &axe[MOUSE_ID][MOUSE_Z_AXIS]);
for (int action = 0; action < Action::NUM_VALUES; ++action) {
const auto action_id = static_cast<IoActionId>(action + JOY_AXIS_BEGIN);
CCI & item = Control_config[action_id];
// Assume actions are all axis actions, no need to check
// Assumes all axes are uniquely bound to an action
// Process first
if (!item.first.empty()) {
scale_invert(item.first, action, item.type, frame_time, axe, axis_v);
}
// Process second.
if (!item.second.empty()) {
scale_invert(item.second, action, item.type, frame_time, axe, axis_v);
}
//Call Lua hooks
if (control_run_lua(action_id, axis_v[action])) {
Assert(item.type == CC_TYPE_AXIS_ABS || item.type == CC_TYPE_AXIS_REL);
switch (item.type) {
case CC_TYPE_AXIS_ABS:
axis_v[action] = item.analog_value;
break;
case CC_TYPE_AXIS_REL:
axis_v[action] = 0;
break;
case CC_TYPE_AXIS_BTN_NEG:
case CC_TYPE_AXIS_BTN_POS:
default:
//This should never happen, especially with the above Assertion. This is required as incomplete switches on an enum generate warnings
UNREACHABLE("Unhandled control item type %d", static_cast<int>(item.type));
break;
}
}
//Store values for possible lua override
item.analog_value = axis_v[action];
}
}
void control_used(int id)
{
// if we have set this key to be ignored, ignore it
if (Ignored_keys[id]) {
return;
}
// This check needs to be done because the control code might call this function more than once per frame,
// and we don't want to run the hooks more than once per frame
if (!Control_config[id].digital_used.isValid() || timestamp_compare(Control_config[id].digital_used, Last_frame_timestamp) < 0) {
if (!Control_config[id].continuous_ongoing) {
if (scripting::hooks::OnAction->isActive()) {
scripting::hooks::OnAction->run(scripting::hooks::ControlActionConditions{ id },
scripting::hook_param_list(
scripting::hook_param("Action", 's', Control_config[id].text)
));
}
if (Control_config[id].type == CC_TYPE_CONTINUOUS)
Control_config[id].continuous_ongoing = true;
}
Control_config[id].digital_used = Last_frame_timestamp;
}
}
void control_config_clear_used_status()
{
// note: this is only for digital controls like button presses,
// so we don't need to clear the analog value
for (auto &item : Control_config) {
item.digital_used = TIMESTAMP::invalid();
}
}
void control_config_clear()
{
for (auto &item : Control_config) {
item.clear();
}
}