Controls 4 and 5 - Multi-joy and Preset Files (#2958)

* Updates the AxisInverted hooks to use the new CC_bind.  Currently assumes only Joy0, so this may need further tweaking or replacement

* * Add ability to claim up to 4 joysticks for player use, using the launcher (via registry or fs2_open.ini) or the new menu system
* Hack into existing control polling system to get the new primary/secondary binding schema to work.  This should be reworked into a reverse-lookup system.
* Finally break the mouse loose from overlapping with Joystick binidngs
* Fix bug where joy button wouldn't register on search function
* Fix bug with mousewheel not always decaying as it should.
* Fix rebase conflict with selecting item during search mode

* Fix bug where mouse buttons were incorrectly labeled.

* Move some enums around

* Fix bug with joy axis labels being displayed as buttons.
* Fix possible bug where Other conflict axis wouldn't clear
* Updated default joystick axis text. A new strings.tbl will need to be supplied for localization support

* Deprecate nAxisInverted ADE functions and replace with AxisInverted(int, int, bool)

* Merge Axis mappings into Control_config
* More utility functions for CC_bind and CCB
* Adds another ADE function for inverting the axis action, instead of individual bindings
* Hacks to get plr file to compile.  Currently slices out many of the new features like secondary axis and the mouse axis, and the additional joystick bindings (joy1, joy2, joy3)

* Fix bug with uncontrollable spin (slopily)
* Fix some warnings
* Add flags for Hats and Balls for future implementation

* Fix bug where inverting an axis would trigger an assert

* Fix bug where joy wasn't detected as present

* Most axis actions should be relative, instead of absolute
* Hopefully optimize joy_get_unscaled_reading
* Properly break out of double-for search function for search and bind modes in config menu
* Refactor control_get_axes_readings to provide more consistent behavior
* Use JOY_REL_THROTTLE instead of Action::REL_THROTTLE when indexing into Control_config

* Bank axis has a default binding, Throttle does not.

* Fix bug where conflicts weren't being detected.
Fix bug where default presets weren't being detectd

* Elegantly break out of double fors with goto.
Fix bug with bind mode not detecting axis

* Save new binding scheme to playerfile
* Add cid_assign() to standardize cid assignment with shorts
* Add write and read handlers for int8_t

* Remove saving controls to csg.  This will be replaced by presets.

* Fix bug with reading pilotfile

* Use "Joy-n" instead of "joystick n" to match default identifiers in config menu

* Fix bug with CC_TYPE_TRIGGER controls always marked as used, if not bound with a key.
Fix bug with Joy-0 btn 1 triggering multiple controls that are not bound to it.

* Improve readibility of the joy info output to log
* Message when a joystick connects and using legacy system.
* Fix bug where system joystick 0 was mapped in pJoystick that should've been nullptr

* Fix bug where *not* having a joystick will cause bind mode in the config menu to immediately cancel.

* Add reverse lookup functions for actions.  mActionToVal[] does string -> int, mValToAction() does int -> string

* Add the foundation for writing the presets files.
* Adds a bunch of reverse lookup functions to get the stringified version of various enums, etc.
* presets.h and presets.cpp are in something of a barebones at this point

* Add new subdir for preset files 'player/presets'
* add utility for finding the distance of a bitfield/bitmask.  Used to translate mouse button masks into mouse button id's
* Write PresetFileHandler as standalone, instead of a subclass.  Still mostly a copy of JSONFileHandler
* Make functions to map and reverse-map the bindings for use with the preset files and ultimately the ControlConfigDefaults.tbl as well

* Check presets from file to see if they are duplicate with an existing preset.

* Check for uniqueness of presets fron controlconfigdefaults.tbl before adding them, and warn the user if there are duplicates.

* Rename the test functions into proper load/save.
* save function has some standard checks
* load function is called on init
* make finding duplicates its own function outside of the preset label rendering function
* Initial draft of saving presets in-game

* Fix bug where all loaded presets were thought to be duplicate of defaults.

* Fix bug where preset cycling would get stuck on the first preset other than default

* Fix bug where saving a mouse button saves the wrong textified string.

* Prevent preset names from being too long.
* Crash still occurs if the name is too long, as if set outside of FSO
* Workaround text running off the popups in 640x480.  Doesn't look as nice, sadly

* CSG's now track preset names instead of containing bindings
* remake control_config_reset_defaults() into control_config_use_preset()
* provide a function to get the currently selected preset.

* Do PLR backward compatibility, hopefully.

* PLR now tracks presets instead of saving the bindings.

* On Loading pre PLR3 or CSG6 data, create a new preset.

* Strip off file extension before saving preset files. Jason.json.json just looks dumb

* Add "-joy_info" commandline option.  Will output SDL joystick info of all detected joysticks to stdout

* Fix non-standard behavior with registry_read_string().
* Returning NULL instead of the default value, or an empty string, will trigger an access violation with SCP_string and other string copy functions.

* Modify io::joystick::init() to take an optional param, no_register, to use with -joy_info

* Address CI complaints

* Address CI issues 2.

* Supress preset duplicate warning if in unit tests.

* Address scripting unittest failure due to incorrect argument type. (Ints don't technically exist in LUA)

* Use Mouse enums instead of Joy enums or magic numbers

* More signed/unsigned madness

* Address CI issues 3

* Address gcc-5 issues. (No auto in lambdas)
* Fix bug with bit_distance()

* Address gcc-5 issues 2

* Address clang issues

* Address clang-tidy issues

* Address clang-tidy issues in undosys.  May need to be cherry picked to Controls2.

* Address clang-tidy issues

* Improve logic of preset name prompt.

* Fix bug with selected presets not being saved

* No, a cstr is not a std::string.
* Can't use i there because its already defined.

* Fix bug where CSG's wouldn't save selected preset
* Fix bug where presets wouldn't be loaded correctly from player files
* Fix bug where loaded preset files would not update the preset cycle position correctly

* Some QoL changes for the preset name prompt

* Address Appveyor issues.

* Add forward compatibility for player file.  This is required because currently any "missing" data in the pilotfile will trigger an Error, and crash FSO.

* Fix bug where mouse X axis was unusable.
Apparently stating that MOUSE_Z_AXIS IS MOUSE_Z_AXIS doesn't get the point across.

* Address CI issues.

* Address CI issues.
* sel default assigned to -1
* static_cast enum to an int

* Assimilate -joy_info int -get_flags, replacing the existing method that launchers use to get joystick information.

* Add '\n' to PST log messages.

* Change logic of checking joystick .ini keys:  Should the multi-joy key not be found, try legacy joy key.

* Add some more missing '\n' to mprintf messages.

* Notify the player when converting to the new multi-joy system. (Using ReleaseWarning for now...)
* Warn the player if a preset defined in their .PLR could not be found.

* Actually disable control items without a valid tab, instead of relying on the fact that they're disabled  by default.

* Add CC_preset::is_duplicate_of() method to make checking for preset uniqueness less of a brain teaser.
* Doxy the first_override param of control_config_common_read_section

* Adds the os::dialogs::Information() method, which displays an information-only dialog in similar format to Warning and Error dialogs.  Does not throw Warning nor Error exceptions
* load_player now uses os::dialogs::Information() instead of ::ReleaseWarning, thus not halting unit tests.

* Breaks retail localization support for Joystick and Mice labels - This is because the new multi-joy axis end up very confusing "Joy-0 Joystick/Mouse X Axis"
* Renames Joy_axis_text to Axis_text, intended to be used both by joysticks and the mouse.
* Moves mouse axis and button labels into an array to ease future XSTR support
* Adds asserts to ensure indices are within the array sizes of label arrays
* Adds XSTR indices for Axis labels, mouse buttons, and mouse wheels.

* Seperates conflict check comparisons from equality comparisons.
*  Conflict checks can ignore certain combinations of CC_bind::flag's; i.e. an Axis bound to a different control but is inverted or relative will still cause a conflict with another control that has the same axis
*  Equality comparisons is exact equality, allowing presets to check if binds have the same binding and flag configurations.

* More UI string changes and localization
* Tweaks positioning of top button labels in CC menu
* Adds the following XSTR's:
1659 "Preset: %s"
1660 "Preset: custom"
1661 "Next Preset"

* Fix bug where mouse axis turned into mouse button when inverting control by its control name.
* Fix bug with the new conflict checking subroutine not working with non-axis controls

* Address CI issues.

* Don't trigger modal popups while in unit tests.

* Address clang tidy issues
* use nullptr
* be consistent with conditional style
* use booleans to stuff debug states, methods with just Assertions in it will trigger a false positive on clang tidy
* use string::empty() instead of checking for "".

* Remove dead code of -joy_info.  Functionality has been merged with -get_flags

* Address clang-tidy [readability-simplify-boolean-expr]
--z64 says "I don't like it!"

* fix axis check

* Implement revised axis action virtvars for scripting. Also protect against a control mismatch in the CSG file due to a script-inverted control

* Encapsulate CC_bind properly
* Add mouse and joystick cid's to AxisInverted description

* Update translate_key() and its user to correctly translate multijoy bindings for training messages.

* Prompt user trying to escape from config menu when they have changes.
* Remove dead function.

* Fix crash during training missions due to key translation that would overflow internal buffer.
* Went back with cstring as return value instead of SCP string, because caller would need a significant refactor
* Update doxy of the textify functions

* Unify textify_scancode() and textify_scancode_universal() to ensure common code path.

* Add "Multijoy" entry to json caps. (For Knossos)

* Make CI happy.
* Remove unused lambda captures.

* Instead of overwrite during plr2 => plr3 conversion, try to use the existing preset file.

* Fix conditional for checking if file exists within presets::save_preset_file()

* Revert Majority of PR#3739
* PLR and CSG versions are incremented past versions set by 3739
* Forward compatibility feature of #3739 (Only save sexp containers into the CSG if they are used) is removed

* Remove forward compatibilty of PLR file when going in between pre- and post-Controls5 FSO versions
* Add missing startSection() and endSection() for csg_write_controls()

* Fix clang-tidy error #1

* Fix clang-tidy error #2

* fix clandy tidy error #1 part 2/2

* Fix clang tidy error 3

Co-authored-by: Goober5000 <[email protected]>
Co-authored-by: Tyler Blount <[email protected]>
Co-authored-by: wookieejedi <[email protected]>
Co-authored-by: BMagnu <[email protected]>
This commit is contained in:
authored and GitHub committed 2022-01-04 12:01:32 -06:00
1 parent 62e9719712
commit 1fa6e00f39
35 files changed
+3296 -1156

No files matched your search

+1
View File
@@ -91,6 +91,7 @@ cf_pathtype Pathtypes[CF_MAX_PATH_TYPES] = {
{ CF_TYPE_FREDDOCS, "data" DIR_SEPARATOR_STR "freddocs", ".html", CF_TYPE_DATA },
{ CF_TYPE_INTERFACE_MARKUP, "data" DIR_SEPARATOR_STR "interface" DIR_SEPARATOR_STR "markup", ".rml", CF_TYPE_INTERFACE },
{ CF_TYPE_INTERFACE_CSS, "data" DIR_SEPARATOR_STR "interface" DIR_SEPARATOR_STR "css", ".rcss", CF_TYPE_INTERFACE },
{ CF_TYPE_PLAYER_BINDS, "data" DIR_SEPARATOR_STR "players" DIR_SEPARATOR_STR "presets", ".json", CF_TYPE_PLAYERS },
};
// clang-format on
+2 -1
View File
@@ -81,9 +81,10 @@ typedef struct {
#define CF_TYPE_FREDDOCS 37
#define CF_TYPE_INTERFACE_MARKUP 38
#define CF_TYPE_INTERFACE_CSS 39
#define CF_TYPE_PLAYER_BINDS 40
#define CF_MAX_PATH_TYPES \
40 // Can be as high as you'd like //DTP; yeah but beware alot of things uses CF_MAX_PATH_TYPES
41 // Can be as high as you'd like //DTP; yeah but beware alot of things uses CF_MAX_PATH_TYPES
// TRUE if type is specified and valid
#define CF_TYPE_SPECIFIED(path_type) (((path_type)>CF_TYPE_INVALID) && ((path_type)<CF_MAX_PATH_TYPES))
+2 -18
View File
@@ -1319,6 +1319,7 @@ static json_t* json_get_v1() {
json_array_append_new(caps_array, json_string("No D3D"));
json_array_append_new(caps_array, json_string("New Sound"));
json_array_append_new(caps_array, json_string("SDL"));
json_array_append_new(caps_array, json_string("Multijoy"));
json_object_set_new(root, "caps", caps_array);
}
@@ -1410,24 +1411,7 @@ static json_t* json_get_v1() {
json_object_set_new(root, "openal", openal_obj);
}
{
auto joystick_array = json_array();
auto joysticks = io::joystick::getJoystickInformations();
for (auto& info : joysticks) {
auto joystick_obj = json_object();
json_object_set_new(joystick_obj, "name", json_string(info.name.c_str()));
json_object_set_new(joystick_obj, "guid", json_string(info.guid.c_str()));
json_object_set_new(joystick_obj, "num_axes", json_integer(info.num_axes));
json_object_set_new(joystick_obj, "num_balls", json_integer(info.num_balls));
json_object_set_new(joystick_obj, "num_buttons", json_integer(info.num_buttons));
json_object_set_new(joystick_obj, "num_hats", json_integer(info.num_hats));
json_object_set_new(joystick_obj, "is_haptic", json_boolean(info.is_haptic));
json_array_append_new(joystick_array, joystick_obj);
}
auto joystick_array = io::joystick::getJsonArray();
json_object_set_new(root, "joysticks", joystick_array);
}
File diff suppressed because it is too large. Load diff
+385 -102
View File
@@ -15,68 +15,62 @@
#define CONTROL_CONFIG_XSTR 507
#define CCF_AXIS_BTN 0x40 //!< axis is a button on positive side. If also inverted, is on negative side instead
#define CCF_RELATIVE 0x20 //!< axis is relative
#define CCF_INVERTED 0x10 //!< axis is inverted
#define CCF_AXIS 0x08 //!< btn is an axis
#define CCF_HAT 0x04 //!< btn is a hat
#define CCF_BALL 0x02 //!< btn is a ball
/*!
* These are used to index a corresponding joystick axis value from an array.
* These are used to index a corresponding axis value from an array.
* Currently only used by ::Axis_map_to[] and ::Axis_map_to_defaults[]
*/
enum Joy_axis_index {
enum Joy_axis_index : short {
JOY_X_AXIS =0,
JOY_Y_AXIS,
JOY_Z_AXIS,
JOY_RX_AXIS,
JOY_RY_AXIS,
JOY_RZ_AXIS
JOY_RZ_AXIS,
JOY_NUM_AXES // Number of axes a joystick may have. Must be last enum in Joy_axis_index.
};
// Aliases for mouse axes. Really should unify this...
const short MOUSE_X_AXIS = JOY_X_AXIS;
const short MOUSE_Y_AXIS = JOY_Y_AXIS;
const short MOUSE_Z_AXIS = JOY_Z_AXIS;
const short MOUSE_NUM_AXES = 3;
const short MOUSE_NUM_BUTTONS = 9; // Keep this up to date with mouse.h until z64555 stops being lazy and does it right
/*!
* Controller index enumeration
* @details For use with hardcoded bindings and scripting API to allow human-readable translation
* @note These enums are hardcoded so that an int value of 0 would be JOY0
*/
enum CID : short {
CID_NONE = -3, // No device bound
CID_KEYBOARD = -2, // button belongs to keyboard
CID_MOUSE = -1, // to mouse
CID_JOY0 = 0, // to Joy0
CID_JOY1 = 1, // to Joy1 (Throttle?)
CID_JOY2 = 2, // to Joy2 (Pedals?)
CID_JOY3 = 3 // to Joy3 (Head tracker?)
// Any additional joystick is an int >3
};
/*!
* These are used to index a corresponding (analog) action, namely controlling the orientation angles and throttle.
*/
enum Joy_axis_action_index {
JOY_HEADING_AXIS =0, //
JOY_PITCH_AXIS,
JOY_BANK_AXIS,
JOY_ABS_THROTTLE_AXIS,
JOY_REL_THROTTLE_AXIS,
/*!
* This must always be below the last defined item
*/
NUM_JOY_AXIS_ACTIONS //!< The total number of actions an axis may map to
};
/*!
* Joystick action modes
* note: should this be in joy.h?
*/
enum Joy_axis_action_mode {
JAAM_ABS, //!< Absolute mode. Axis position = output value
JAAM_REL, //!< Relative mode. Axis postion away from its center adds or subtracts an output value register.
JAAM_BTN_NEG, //!< Button mode, negative side. Axis position in the negative side will trigger a button action
JAAM_BTN_POS, //!< Button mode, positive side. Axis position in the positive side will trigger a button action
enum CID : int8_t {
CID_NONE = -3, //!< No device bound
CID_KEYBOARD = -2, //!< belongs to keyboard
CID_MOUSE = -1, //!< to mouse
CID_JOY0 = 0, //!< to Joy0
CID_JOY1 = 1, //!< to Joy1 (Throttle?)
CID_JOY2 = 2, //!< to Joy2 (Pedals?)
CID_JOY3 = 3, //!< to Joy3 (Head tracker?)
CID_JOY_MAX //!< Maximum supported joysticks, must be last in the enum definition
};
/*!
* Control Configuration Types. Namely differ in how the control is activated
*/
enum CC_type {
CC_TYPE_TRIGGER =0, //!< A normal, one-shot type control that is activated when a key is or button is pressed
CC_TYPE_CONTINUOUS //!< A continous control that is activated as long as the key or button is held down
CC_TYPE_TRIGGER = 0, //!< A normal, one-shot type control that is activated when a key is or button is pressed
CC_TYPE_CONTINUOUS, //!< A continous control that is activated as long as the key or button is held down
CC_TYPE_AXIS_ABS, //!< Absolute Axis mode. Axis position = output value
CC_TYPE_AXIS_REL, //!< Relative Axis mode. Axis postion away from its center adds or subtracts an output value register
CC_TYPE_AXIS_BTN_NEG, //!< Axis Button mode, negative side. Axis position in the negative side will trigger a button action
CC_TYPE_AXIS_BTN_POS //!< Axis Button mode, positive side. Axis position in the positive side will trigger a button action
};
/*!
@@ -86,7 +80,7 @@ enum CC_type {
*
* Note: Do not adjust the order or numeric value
*/
enum IoActionId {
enum IoActionId : int {
TARGET_NEXT =0, //!< target next
TARGET_PREV =1, //!< target previous
TARGET_NEXT_CLOSEST_HOSTILE =2, //!< target the next hostile target
@@ -288,52 +282,77 @@ enum IoActionId {
CUSTOM_CONTROL_4 = 122,
CUSTOM_CONTROL_5 = 123,
//!< @n
//!< Analog controls
//!<-----------------------------
//!< All axis actions must be kept together
JOY_HEADING_AXIS = 124,
JOY_PITCH_AXIS = 125,
JOY_BANK_AXIS = 126,
JOY_ABS_THROTTLE_AXIS = 127,
JOY_REL_THROTTLE_AXIS = 128,
/*!
* This must always be below the last defined item
*/
CCFG_MAX //!< The total number of defined control actions (or last define + 1)
};
// Constants for array-based checking (within Control_config) for the axis actions.
// z64: Yes, this is hackish. No I don't really like it.
const int JOY_AXIS_BEGIN = JOY_HEADING_AXIS;
const int JOY_AXIS_END = JOY_REL_THROTTLE_AXIS + 1;
const int NUM_JOY_AXIS_ACTIONS = JOY_AXIS_END - JOY_AXIS_BEGIN;
/*!
* @brief Enum for indexing into axis action arrays.
* @note Keep this up to date with JOY_HEADING_AXIS and friends. Pay attention to their order and copy to here
*/
namespace Action {
enum {
HEADING = 0,
PITCH,
BANK,
ABS_THROTTLE,
REL_THROTTLE,
NUM_VALUES
};
}
class CCB;
/*!
* A singular button binding
*/
class CC_bind {
public:
CID cid = CID_NONE; //!< Which controller this belongs to
short btn = -1; //!< Which button to index; If cid == CID_KEYBOARD, this is a key hash.
public:
CC_bind() = default;
CC_bind(CID _cid, short _btn) : cid(_cid), btn(_btn) { validate(); };
CC_bind(CID _cid, short _btn, char _flags) : cid(_cid), flags(_flags), btn(_btn) { validate(); };
CC_bind(const CC_bind &A) = default;
CC_bind(CID _cid, short _btn) : cid(_cid), btn(_btn) {};
CC_bind& operator=(const CC_bind &A);
CC_bind& operator=(const CC_bind &A) = default;
/*!
* Checks if this CC_bind is equal to the given CC_bind
*
* @note EQUAL means EQUAL, this will return FALSE if the flags differ by a single bit
*/
bool operator==(const CC_bind &B) const;
/*!
* Checks if this CC_bind is equal to either in the pair
*/
bool operator==(const CCB &pair) const;
/*!
* Checks if this CC_bind is not equal to the given CC_bind
*/
bool operator!=(const CC_bind &B) const;
/*!
* Checks if this CC_bind is not equal to either in the pair
* Checks if this CC_bind is equal to the given CC_bind, disregarding the inversion flag
*/
bool operator!=(const CCB &pair) const;
bool invert_agnostic_equals(const CC_bind &B) const;
/*!
* Clears the binding
* Clears the binding and flags. Some flags are retained.
*/
void clear();
@@ -342,10 +361,62 @@ public:
*/
bool empty() const;
/*!
* Returns a copy of ::btn
*/
short get_btn() const;
/*!
* Returns a copy of ::cid
*/
CID get_cid() const;
/*!
* Returns a copy of ::flags
*/
char get_flags() const;
/**
* Takes the given binding.
*/
void take(CID _cid, short _btn, char _flags = 0);
/**
* Validates the binding, clearing it if necassary
*/
void validate();
/*!
* Returns a human-readable string of this binding
*/
SCP_string textify() const;
/*!
* Sets the inversion state of this binding according to the passed bool. True = inverted, false = normal
*/
void invert(bool);
/*!
* Toggles the inversion state
*/
void invert_toggle();
/*!
* Is true if inverted, false otherwise
*/
bool is_inverted() const;
/*!
* Checks if this CC_bind conflicts with the given argument
*
* @note Similar to operator==, but ignores certain combinations of flags.
*/
bool conflicts_with(const CC_bind&) const;
private:
CID cid = CID_NONE; //!< Which controller this belongs to
char flags = 0; //!< mask to determine various additional attributes of btn
short btn = -1; //!< The button, key combo, or axis that's bound.
};
/*!
@@ -392,17 +463,57 @@ public:
/*!
* Assigns the contents of the given CCB to this CCB
*/
CCB& operator=(const CCB&);
CCB& operator=(const CCB&) = default;
/*!
* Returns True if this CCB's first isn't empty and the given CCB has a binding equal to it
* Checks if the given CCB is exactly equal to this
*/
bool has_first(const CCB&) const;
bool operator==(const CCB&);
/*!
* Returns True if this CCB's second isn't empty and the given CCB has a binding equal to it
* Checks if the given CCB differs from this
*/
bool has_second(const CCB&) const;
bool operator!=(const CCB&);
/*!
* Returns True if this CCB's first isn't empty and the given CCB has a binding that can conflict with it
*/
bool has_first_conflict(const CCB&) const;
/*!
* Returns True if this CCB's second isn't empty and the given CCB has a binding that can conflict with it
*/
bool has_second_conflict(const CCB&) const;
/*!
* Returns a pointer to first, or second, whichever has a binding equal to the given CC_bind, or nullptr if neither
*/
CC_bind* find(const CC_bind&);
/*!
* Returns a pointer to first, or second, whichever has a CID equal to the given CID, or nullptr if neither
*/
CC_bind* find(CID);
/*!
* Returns a pointer to first, or second, whichever has *all* flags in the given mask
*/
CC_bind* find_flags(const char);
/*!
* Sets the inversion state of both bindings
*/
void invert(bool);
/*!
* Toggles the inversion state of Primary and copies it to Secondary
*/
void invert_toggle();
/*!
* Is true if both bindings are inverted, false otherwise
*/
bool is_inverted() const;
};
/*!
@@ -412,6 +523,12 @@ class CC_preset {
public:
SCP_vector<CCB> bindings;
SCP_string name;
public:
/*!
* Checks if the given preset is a duplicate of this one
*/
bool is_duplicate_of(CC_preset&);
};
/*!
@@ -449,6 +566,11 @@ public:
* @brief Takes the bindings of the given CCB, but leaves all other members alone
*/
CCI& operator=(const CCB&);
/*!
* Returns true if this item is analogue or an axis
*/
bool is_axis();
};
/*!
@@ -473,9 +595,23 @@ public:
/*!
* Assigns the hardcoded binding to the given action
*
* @note This differs from the original hardcode from :V: in the hopes of it being more intuitive to future IoAction additions
*
* param[in] action_id The IoActionId enum associated with the control
* param[in] primary The btn, key combo, or axis for the default primary binding
* param[in] secondary The btn, key combo, or axis for the default secondary binding
* param[in] tab Category tab this control should show up under on the config menu
* param[in] indexXSTR ID for the XSTR localization utility. 0 for no localization, 1 for automatic lookup, all other values are index for item in strings.tbl
* param[in] text Label text displayed in the config menu.
* param[in] type How the control should be treated. see doxy on CC_type for details
* param[in] disabled If true, this control will not be available in the game. Controlsconfigdefaults.tbl may override this. Don't use this to temporarily disable controls in mission.
*
* @details The CID for the default primary and secondary is determined by the type parameter.
* If the type is CC_TYPE_TRIGGER or CC_TYPE_CONTINOUS, the primary is assumed to be a key combo and secondary is assumed to be a joy button.
* If the type is CC_TYPE_AXIS_ABS, CC_TYPE_AXIS_REL, CC_TYPE_AXIS_BTN_POS, or CC_TYPE_AXIS_BTN_NEG, the primary is a Joy0 axis and the secondary is a Mouse axis
*/
CCI_builder& operator()(IoActionId action_id, short key_default, short joy_default, char tab, int indexXSTR, const char *text, CC_type type, bool disabled = false);
CCI_builder& operator()(IoActionId action_id, short primary, short secondary, char tab, int indexXSTR, const char *text, CC_type type, bool disabled = false);
private:
CCI_builder(); // Only one builder per Control Config, so a default constructor is useless
@@ -485,11 +621,6 @@ private:
extern int Failed_key_index;
extern int Axis_map_to[]; // Array to map an axis action to a joy axis. size() = NUM_JOY_AXIS_ACTIONS
extern int Axis_map_to_defaults[];
extern int Invert_axis[]; // Array to hold inversion bools for a joy axis. size() = JOY_NUM_AXES
extern int Invert_axis_defaults[];
extern int Joy_dead_zone_size;
extern int Joy_sensitivity;
@@ -498,7 +629,14 @@ extern int Control_config_overlay_id;
extern SCP_vector<CCI> Control_config; //!< Stores the keyboard configuration
extern SCP_vector<CC_preset> Control_config_presets; // tabled control presets; pointers to config_item arrays
extern const char **Scan_code_text;
extern const char **Joy_button_text;
extern const char **Joy_button_text; // String table of button labels. XSTR'd on init.
// string table constants for labels and stuff.
#define NUM_AXIS_TEXT JOY_NUM_AXES
#define NUM_MOUSE_TEXT MOUSE_NUM_BUTTONS
extern char *Axis_text[NUM_AXIS_TEXT]; // String table of axis labels (joystick and mice). XSTR'd on init.
extern char *Mouse_button_text[NUM_MOUSE_TEXT]; // String table of mouse button labels. XSTR'd on init.
/*!
* @brief Checks if either binding in the CCB has the given cid
@@ -509,6 +647,11 @@ extern const char **Joy_button_text;
*/
int is_cid_either(CID, const CCB);
/*!
* Allows CID to be assigned by a short. Has Assert to check values
*/
void cid_assign(CID&, const short);
/*!
* @brief initialize common control config stuff - call at game startup after localization has been initialized
*/
@@ -540,51 +683,74 @@ void control_config_close();
void control_config_cancel_exit();
/*!
* @brief Resets all controls to values within the currently selected preset
* @brief Copies all bindings within preset into Control_config
*/
void control_config_reset_defaults();
void control_config_use_preset(CC_preset &preset);
/*!
* Returns the IoActionId of a control bound to the given key
* @brief Search for a given preset by name and use it, if it exists.
*
* @returns TRUE if the preset was found and selected
* @returns FALSE if the preset was not found, and the defaults were selected
*
* @details If the preset does not exist, then defaults are used instead.
* @see control_config_use_preset()
*/
bool control_config_use_preset_by_name(const SCP_string &name);
/**
* @brief Gets the currently used preset
*
* @returns an iterator to the current preset, or
* @returns ::iterator Control_config_presets.end() if current bindings are not in a preset
*/
SCP_vector<CC_preset>::iterator control_config_get_current_preset();
/*!
* Returns the IoActionId (index within Control_config[]) of a control bound to the given key
*
* @param[in] key The key combo to look for
* @param[in] find_override If true, return the IoActionId of a control that has this key as its default
*
* @return -1 if the given key is NULL or not bound.
*
* @param[in] key The key combo to look for
* @param[in] find_override If true, return the IoActionId of a control that has this key as its default
* @details If find_override is set to true, then this returns the index of the action
*/
int translate_key_to_index(const char *key, bool find_override=true);
/*!
* @brief Given the system default key 'key', return the current key that is bound to that function.
* @brief Given the system default key 'key', return the current control input(s) that is bound to that function.
*
* @param[in] key The default key combo (as string) to a certain control
* @param[in] key The default key combo (as cstring) to a certain control
*
* @returns The key combo (as string) currently bound to the control
* @return The key combo (as cstring) currently bound to the control, or
* @return nullptr if the given key is not a system default key, or
* @return "None" if there is nothing bound to the control
*
* @details Both the 'key' and the return value are descriptive strings that can be displayed
* directly to the user. If 'key' isn't a real key, is not normally bound to anything,
* or there is no key currently bound to the function, NULL is returned.
* or there is no key currently bound to the function, nullptr is returned.
*
* @note Not thread safe. Has an internal buffer for the return value which is overwritten on each call.
*
* @note Uses CC_bind::textify for translations
*/
char *translate_key(char *key);
const char *translate_key(char *key);
/**
* @brief Converts the specified key code to a human readable string
* @brief Converts the specified key code to a human readable string, according to the selected locale
*
* @note The returned value is localized to the current language
* @param[in] code The key code to convert
* @param[in] use_default_locale If true, return the default locale (English) translation of the key
*
* @param code The key code to convert
* @return The text representation of the code. The returned value is stored in a temporary location, copy it to your
* own buffer if you want to continue using it.
* @return The text representation of the code.
*
* @note Not thread safe. Has an internal buffer for the return value which is overwritten on each call.
*
* @note The return value is translated according to localization settings
*/
const char *textify_scancode(int code);
/**
* @note Same as textify_scancode but always returns the same value regardless of current language
* @param code The key code to convert
* @return The name of the key
* @see textify_scancode
*/
const char *textify_scancode_universal(int code);
const char *textify_scancode(int code, bool use_default_locale = false);
/*!
* @brief Checks how long a control has been active
@@ -600,26 +766,29 @@ float check_control_timef(int id);
/**
* @brief Wrapper for check_control_used. Allows the game to ignore the key if told to do so by the ignore-key SEXP.
*
* @brief id The IoActionId of the control to check
* @brief key The key combo to check against the control.
* @param[in] id The IoActionId of the control to check
* @param[in] key The key combo to check against the control. If -1, re-check the last key passed to this function
*
* @returns 0 If the control wasn't used, or
* @returns 1 If the control was used
*
* @details If the given control is CC_CONTINOUS, param key is effectively ignored and instead keyd_press[] is checked.
* This function also checks against joy and mouse presses.
*/
int check_control(int id, int key = -1);
/**
* @brief Gets the scaled reading for all control axes.
*
* @param[out] h heading
* @param[out] p pitch
* @param[out] b bank
* @param[out] ta Throttle - Absolute
* @param[out] tr Throttle - Relative
* @param[in] frametime Frametime used to scale the mouse axes
* @param[out] axis_v Output array of all control axes, must have size of JOY_NUM_AXIS_ACTIONS
*
* @note None of the input pointers may be nullptr
* @details
* * Clamps output to stay within +/-JOY_AXIS_RANGE.
* * Should multiple axes be bound to a control, their values are blended with a simple sum. This is to save a bit of
* processing for speed.
*/
void control_get_axes_readings(int *h, int *p, int *b, int *ta, int *tr);
void control_get_axes_readings(int *axis_v, float frame_time);
/**
* @brief Markes the given control (by IoActionId) as used
@@ -652,4 +821,118 @@ void control_config_clear_used_status();
*/
int joy_get_scaled_reading(int raw);
/**
* Lookups the given stringified enum to find its value
* @return -1 if not found, or
* @return IoActionId if found
*/
int ActionToVal(const char * str);
/**
* Reverse lookups the IoActionId to get its stringified name
* @return Pointer to the stringified name of the action, or
* @return nullptr if not found
*/
const char * ValToAction(IoActionId id);
/**
* Reverse lookups the IoActionId to get its stringified name
* @return Pointer to the stringified name of the action, or
* @return nullptr if not found, or invalid id
*/
const char * ValToAction(int id);
/**
* Lookups the given stringified enum to find its value
*/
CID CIDToVal(const char * str);
/**
* Reverse lookups the CID to get its stringified name
* @return Pointer to the stringified name of the CID, or
* @return nullptr if not found
*/
const char * ValToCID(CID id);
/**
* Reverse lookups the CID to get its stringified name
* @return Pointer to the stringified name of the CID, or
* @return nullptr if not found, or invalid id
*/
const char * ValToCID(int id);
/**
* Lookups the given stringified enum to find its value
*/
char CCFToVal(const char * str);
/**
* Constructs a enum string from the CCF_FLAGS
* @return Pointer to the stringified name of the CCF, or
* @return nullptr if not found, or invalid id
*/
SCP_string ValToCCF(char id);
/**
* Lookups the given stringified enum to find its value
*/
short InputToVal(CID cid, const char * str);
/**
* Constructs a enumstring from the input binding, depending on the CID
*
* @return Pointer to the stringified name of the input, or
* @return nullptr if not found, or invalid CC_bind
*
* @note This requires a CCB due to the way things are encoded
*/
SCP_string ValToInput(const CC_bind &bind);
/**
* Lookups the given stringified enum to find its btn value
*/
short MouseToVal(const char * str);
/**
* Constructs a enum string from the mouse input
*
* @return Pointer to the stringified name of the input, or
* @return nullptr if not found, or invalid CC_bind
* TODO XSTR
*
* @note This requires a CCB due to the way things are encoded
*/
SCP_string ValToMouse(const CC_bind &bind);
/**
* Lookups the given stringified enum to find its btn value
*/
short KeyboardToVal(const char * str);
/**
* Constructs an enum string from the key binding
*
* @return Pointer to the stringified name of the input, or
* @return nullptr if not found, or invalid CC_bind
*
* @note This requires a CCB due to the way things are encoded
* TODO XSTR
*/
SCP_string ValToKeyboard(const CC_bind &bind);
/**
* Lookups the given stringified enum to find its value
*/
short JoyToVal(const char * str);
/**
* Constructs a enum string from the Joystick input
*
* @return Pointer to the stringified name of the input, or
* @return nullptr if not found, or invalid CC_bind
*
* @note This requires a CCB due to the way things are encoded
* TODO XSTR
*/
SCP_string ValToJoy(const CC_bind &bind);
#endif
File diff suppressed because it is too large. Load diff
+481
View File
@@ -0,0 +1,481 @@
#include "controlconfig/controlsconfig.h"
#include "controlconfig/presets.h"
#include "libs/jansson.h"
#include "parse/parselo.h"
#include <jansson.h>
using namespace io::presets;
const SCP_vector<std::pair<Section, const char*>> SectionMapping {
std::pair<Section, const char*>(Section::Unnamed, nullptr),
std::pair<Section, const char*>(Section::Actions, "actions"),
std::pair<Section, const char*>(Section::Primary, "primary"),
std::pair<Section, const char*>(Section::Secondary, "secondary"),
};
const char * lookupSectionName(Section id) {
for (const auto& pair: SectionMapping) {
if (pair.first == id) {
return pair.second;
}
}
return nullptr;
}
Section lookupSectionValue(const char* name) {
Assertion(name != nullptr, "Key name must be a valid pointer!");
for (auto& pair : SectionMapping) {
if (!strcmp(pair.second, name)) {
return pair.first;
}
}
return Section::Invalid;
}
void load_preset_files() {
SCP_vector<SCP_string> filelist;
cf_get_file_list(filelist, CF_TYPE_PLAYER_BINDS, NOX("*.json"), CF_SORT_NAME, nullptr,
CF_LOCATION_ROOT_USER | CF_LOCATION_ROOT_GAME | CF_LOCATION_TYPE_ROOT);
std::unique_ptr<PresetFileHandler> handler = nullptr;
for (const auto &file : filelist) {
CFILE* fp = cfopen((file + ".json").c_str(), "r", CFILE_NORMAL, CF_TYPE_PLAYER_BINDS, false,
CF_LOCATION_ROOT_USER | CF_LOCATION_ROOT_GAME | CF_LOCATION_TYPE_ROOT);
if (!fp) {
mprintf(("PLR => Unable to open '%s' for loading!\n", file.c_str()));
// try next
continue;
}
try {
handler.reset(new PresetFileHandler(fp, true));
} catch (const std::exception& e) {
mprintf(("PLR => Failed to open JSON: `%s`\n", e.what()));
continue;
}
// Check the signature
int pst_id = handler->readInt("signature");
if (pst_id != PST_FILE_ID) {
mprintf(("PST => Invalid header id for `%s`, skipping...\n", file.c_str()));
continue;
}
// Version
int version = handler->readInt("version");
mprintf(("PST => Loading `%s` with version %i\n", file.c_str(), version));
// Start reading in data
CC_preset preset;
preset.name = file;
preset.bindings.resize(Control_config.size());
size_t size;
handler->beginArrayRead(Section::Actions, size);
for (size_t i = 0; i < size; ++i, handler->nextArrayElement()) {
SCP_string str;
int id;
//handler->beginSectionRead(Section::Unnamed);
str = handler->readString("bind");
id = ActionToVal(str.c_str());
if (id < 0) {
// Unknown bind, possibly custom. Ignore for now
handler->endSectionRead(); // Unnamed
continue;
}
auto &item = preset.bindings[id];
handler->beginSectionRead(Section::Primary);
str = handler->readString("cid");
auto cid = CIDToVal(str.c_str());
str = handler->readString("flags");
auto flags = CCFToVal(str.c_str());
str = handler->readString("input");
auto btn = InputToVal(cid, str.c_str());
item.first.take(cid, btn, flags);
handler->endSectionRead(); // Primary
handler->beginSectionRead(Section::Secondary);
str = handler->readString("cid");
cid = CIDToVal(str.c_str());
str = handler->readString("flags");
flags = CCFToVal(str.c_str());
str = handler->readString("input");
btn = InputToVal(cid, str.c_str());
item.second.take(cid, btn, flags);
handler->endSectionRead(); // Secondary
//handler->endSectionRead(); // Unnamed
}
handler->endArrayRead(); // Actions
// Done with the file
bool unique = true;
auto it = Control_config_presets.begin();
for (; it != Control_config_presets.end(); ++it) {
if (preset.is_duplicate_of(*it)) {
unique = false;
break;
}
}
if (unique) {
Control_config_presets.push_back(preset);
} else {
Warning(LOCATION, "Preset '%s' is a duplicate of '%s', ignoring", preset.name.c_str(), it->name.c_str());
}
}
}
bool preset_file_exists(SCP_string name) {
if (name.find(".json") == std::string::npos) {
name.append(".json");
}
return cf_exists(name.c_str(), CF_TYPE_PLAYER_BINDS) != 0;
}
bool save_preset_file(CC_preset preset, bool overwrite) {
// Must have a name
if (preset.name.empty()) {
mprintf(("PST => Unable to save preset, missing name!\n"));
return false;
}
SCP_string filename = preset.name + ".json";
std::unique_ptr<PresetFileHandler> handler = nullptr;
// Check if there's a file already
CFILE* fp = cfopen(filename.c_str(), "r", CFILE_NORMAL, CF_TYPE_PLAYER_BINDS, false,
CF_LOCATION_ROOT_USER | CF_LOCATION_ROOT_GAME | CF_LOCATION_TYPE_ROOT);
if ((fp != nullptr) && !overwrite) {
mprintf(("PST => Unable to save '%s', file already exists!\n", filename.c_str()));
return false;
}
// Try opening file for write
fp = cfopen(filename.c_str(), "w", CFILE_NORMAL, CF_TYPE_PLAYER_BINDS, false,
CF_LOCATION_ROOT_USER | CF_LOCATION_ROOT_GAME | CF_LOCATION_TYPE_ROOT);
if (!fp) {
mprintf(("PST => Unable to save '%s', unknown error!\n", filename.c_str()));
}
try {
handler.reset(new PresetFileHandler(fp, false));
} catch (const std::exception& e) {
mprintf(("PST => Failed to open JSON: %s\n", e.what()));
return false;
}
// Write header and version
handler->writeInt("signature", PST_FILE_ID);
handler->writeInt("version", PST_VERSION);
mprintf(("PST => Saving %s with version %d...\n", filename.c_str(), (int)PST_VERSION));
handler->beginArrayWrite(Section::Actions);
for (int i = 0; static_cast<size_t>(i) < preset.bindings.size(); ++i) {
const auto& item = preset.bindings[i];
const auto& first = item.first;
const auto& second = item.second;
handler->beginSectionWrite(Section::Unnamed);
handler->writeString("bind", ValToAction(i));
handler->beginSectionWrite(Section::Primary);
handler->writeString("cid", ValToCID(first.get_cid()));
handler->writeString("flags", ValToCCF(first.get_flags()));
handler->writeString("input", ValToInput(first));
handler->endSectionWrite(); // Primary
handler->beginSectionWrite(Section::Secondary);
handler->writeString("cid", ValToCID(second.get_cid()));
handler->writeString("flags", ValToCCF(second.get_flags()));
handler->writeString("input", ValToInput(second));
handler->endSectionWrite(); // Secondary
handler->endSectionWrite(); // Control_config[i]
}
handler->endArrayWrite(); // Actions
handler->flush();
return true;
}
PresetFileHandler::PresetFileHandler(CFILE* cfp, bool reading) : _cfp(cfp) {
Assertion(cfp != nullptr, "File pointer must be valid!");
if (reading) {
json_error_t error;
_rootObj = json_load_cfile(cfp, 0, &error);
if (!_rootObj) {
SCP_string errorStr;
sprintf(errorStr, "Error while reading pilot file! %d: %s", error.line, error.text);
throw std::runtime_error(errorStr);
}
} else {
_rootObj = json_object();
}
_currentEl = _rootObj;
_elementStack.push_back(_currentEl);
};
PresetFileHandler::~PresetFileHandler() {
json_decref(_rootObj);
cfclose(_cfp);
_cfp = nullptr;
};
bool PresetFileHandler::beginSectionRead(Section s) {
const char * sectionName = lookupSectionName(s);
ensureExists(sectionName);
auto section = json_object_get(_currentEl, sectionName);
if (json_typeof(section) != JSON_OBJECT) {
Error(LOCATION, "Section must be a JSON object!");
return false;
}
pushElement(section);
return true;
};
void PresetFileHandler::beginSectionWrite(Section s) {
auto key_name = lookupSectionName(s);
json_t* obj = json_object();
if (json_is_array(_currentEl)) {
// Currently in an array, section must be unnamed
Assertion(key_name == nullptr, "Elements of array may not be named sections!");
json_array_append_new(_currentEl, obj);
} else {
Assertion(key_name != nullptr, "Section outside of arrays must be named!");
json_object_set_new(_currentEl, key_name, obj);
}
pushElement(obj);
};
bool PresetFileHandler::beginArrayRead(Section s, size_t &size) {
Assertion(_arrayIndex == INVALID_SIZE, "Array nesting is not supported yet!");
const char * name = lookupSectionName(s);
ensureExists(name);
auto array = json_object_get(_currentEl, name);
if (json_typeof(array) != JSON_ARRAY) {
Error(LOCATION, "Expected an array for '%s' but it was a different type!", name);
return false;
}
pushElement(array);
size = json_array_size(array);
if (size == 0) {
// Nothing to do here, avoid calling nextArraySection since that assumes that there is at least one element
return true;
} // Else, advance to the first element in the array
_arrayIndex = 0;
nextArrayElement();
return true;
}
void PresetFileHandler::beginArrayWrite(Section s) {
const char * name = lookupSectionName(s);
auto array = json_array();
if (json_is_array(_currentEl)) {
// We are in an array, section must be unnamed
Assertion(name == nullptr, "Inside an array there can be no named section!");
json_array_append_new(_currentEl, array);
} else {
Assertion(name != nullptr, "Section outside of arrays must be named!");
json_object_set_new(_currentEl, name, array);
}
pushElement(array);
}
void PresetFileHandler::endSectionRead() {
Assertion(json_typeof(_currentEl) == JSON_OBJECT, "Current element for section reading is not an object!");
popElement();
};
void PresetFileHandler::endSectionWrite() {
Assertion(json_is_object(_currentEl), "Section ended while not in a section!");
popElement();
};
void PresetFileHandler::endArrayRead() {
// First, remove the element we are currently in if it exists
if (json_typeof(_currentEl) != JSON_ARRAY) {
popElement();
}
Assertion(json_typeof(_currentEl) == JSON_ARRAY, "Current element must be an array!");
// TODO Sections are straightforward, but Arrays may need special consideration re: index.
popElement();
_arrayIndex = INVALID_SIZE;
};
void PresetFileHandler::endArrayWrite() {
Assertion(json_is_array(_currentEl), "Array ended while not in an array!");
popElement();
};
void PresetFileHandler::ensureExists(const char* name) {
if (json_typeof(_currentEl) != JSON_OBJECT) {
Error(LOCATION, "JSON reading requires a value with name '%s' but the current element is not an object!", name);
}
if (json_object_get(_currentEl, name) == nullptr) {
Error(LOCATION, "JSON reading requires a value with name '%s' but there is no such value!", name);
}
}
void PresetFileHandler::ensureNotExists(const char* name) {
// Stuff debug checks into booleans, otherwise clang will trigger a false positive for static method with just the asserts
// Make sure we're in an element that can support keys
bool supports_keys = json_is_object(_currentEl);
// Make sure we don't overwrite previous values
bool element_is_unique = json_object_get(_currentEl, name) == nullptr;
Assertion(supports_keys, "Currently not in an element that supports keys!");
Assertion(element_is_unique, "Entry with name %s already exists!", name);
}
bool PresetFileHandler::exists(const char* name) {
if (json_typeof(_currentEl) != JSON_OBJECT) {
return false;
}
if (json_object_get(_currentEl, name) == nullptr) {
return false;
}
return true;
}
void PresetFileHandler::flush() {
Assertion(_elementStack.size() == 1, "Not all sections or arrays have been ended!");
json_dump_cfile(_rootObj, _cfp, JSON_INDENT(4) | JSON_PRESERVE_ORDER);
}
void PresetFileHandler::nextArrayElement() {
Assertion(_arrayIndex != INVALID_SIZE, "Array index must be valid for this function!");
bool in_section = json_is_object(_currentEl);
if (in_section) {
// We have to pop the previous section first
popElement();
Assertion(json_typeof(_currentEl) == JSON_ARRAY, "The previous element should have been an array!");
}
auto max = json_array_size(_currentEl);
Assertion(_arrayIndex <= max, "Invalid array index detected!");
// Silently ignore if we are one past the last element since this function is used in a for loop where this function
// is executed one time after the index is incremented past the last element
if (_arrayIndex == max) {
// Increment the index so we catch usage errors the next time someone tries to call this function
++_arrayIndex;
return;
}
// We use the current array index and then increment it to avoid skipping the first element
auto el = json_array_get(_currentEl, _arrayIndex);
++_arrayIndex;
pushElement(el);
}
json_t * PresetFileHandler::popElement() {
Assertion(_elementStack.size() > 1, "Element stack may not get smaller than one element!");
_elementStack.pop_back();
_currentEl = _elementStack.back();
return _currentEl;
}
void PresetFileHandler::pushElement(json_t* el) {
Assertion(el != nullptr, "Invalid JSON element pointer passed!");
_currentEl = el;
_elementStack.push_back(_currentEl);
}
SCP_string PresetFileHandler::readString(const char* key) {
auto el = json_object_get(_currentEl, key);
if (el == nullptr || json_typeof(el) != JSON_STRING) {
Error(LOCATION, "JSON element %s must be a string but it is not valid!", key);
return SCP_string();
}
auto json_str = json_string_value(el);
SCP_string val;
val.assign(json_str, json_str + json_string_length(el));
return val;
}
int PresetFileHandler::readInt(const char* key) {
auto el = json_object_get(_currentEl, key);
if (el == nullptr || json_typeof(el) != JSON_INTEGER) {
Error(LOCATION, "JSON element %s must be an integer but it is not valid!", key);
return 0;
}
return static_cast<int>(json_integer_value(el));
}
void PresetFileHandler::writeString(const char* key, SCP_string s) {
ensureNotExists(key);
json_t *jstr = nullptr;
if (!s.empty()) {
// if this string isn't proper UTF-8, try to convert it
if (utf8::find_invalid(s.begin(), s.end()) != s.end()) {
SCP_string buffer;
coerce_to_utf8(buffer, s.c_str());
jstr = json_string(buffer.c_str());
} else {
jstr = json_string(s.c_str());
}
}
json_object_set_new(_currentEl, key, jstr);
};
void PresetFileHandler::writeInt(const char* key, int val) {
ensureNotExists(key);
json_object_set_new(_currentEl, key, json_integer(val));
}
+222
View File
@@ -0,0 +1,222 @@
#pragma once
#include "cfile/cfile.h"
#include "controlconfig/controlsconfig.h"
#include "globalincs/pstypes.h"
#include "globalincs/vmallocator.h"
#include <jansson.h>
/** API for saving and loading player custom bindings, known as Presets
Init. If the player/preset dir doesn't exist. Create it, and print a help.txt with all enums (as strings) to label
the button names and action names.
Example output:
{
"actions": {
"TARGET_NEXT": {
"first": {
"cid": 0,
"flags": 0,
"btn": 0
},
"second": {
"cid": 0,
"flags": 0,
"btn": 0
}
},
"TARGET_PREV" : {
"first": {
"cid": 0,
"flags": 0,
"btn": 0
},
"second": {
"cid": 0,
"flags": 0,
"btn": 0
}
},
},
"version": 0
}
*/
namespace io {
namespace presets {
static const unsigned int PST_VERSION = 0;
static const unsigned int PST_FILE_ID = 0x5f545350; // "PST_" in file
enum class Section {
Invalid = -1,
Unnamed = 0,
// arrays
Actions,
// subsections
Primary,
Secondary
};
class PresetFileHandler {
public:
PresetFileHandler(CFILE* cfp, bool reading);
~PresetFileHandler();
/**
* begins writing to a section using the presets::Section as its name
* @param[in] s id of the section
*/
void beginSectionWrite(presets::Section s);
/**
* begins reading a section, if it exists
* @param[in] s id of the section
*/
bool beginSectionRead(presets::Section s);
/**
* Begins writing to an array using the presets::Section as its name
*
* @param[in] s id of the array
*/
void beginArrayWrite(presets::Section s);
/**
* Begins reading from an array
* @param[int] s id of the array
* @param[out] size the size of the array
*/
bool beginArrayRead(presets::Section s, size_t &size);
/**
* ends writing to a section
*/
void endSectionWrite();
/**
* ends reading a section
*/
void endSectionRead();
/**
* ends writing to an array.
*/
void endArrayWrite();
/**
* ends reading from an array
*/
void endArrayRead();
/**
* Writes a string with the given key name
*/
void writeString(const char* key, SCP_string s);
/**
* Reads a string with the given key name
*/
SCP_string readString(const char* key);
/**
* Writes an int with the given key name
*/
void writeInt(const char* key, int val);
/**
* Reads an int with the given key name
*/
int readInt(const char* key);
/**
* For writing, Flushes the buffers and dumps to file
*/
void flush();
/**
* Advances the array parser to the next element
*/
void nextArrayElement();
private:
CFILE* _cfp = nullptr;
json_t* _rootObj = nullptr;
json_t* _currentEl = nullptr;
/**
* BufferStack of all json elements being read/written
*/
SCP_vector<json_t*> _elementStack;
/**
* Stack to keep track of the heirachy/nesting. Back is immediate parent, Front is root
*/
SCP_vector<void*> _parentStack;
size_t _arrayIndex = INVALID_SIZE;
//void* _sectionIterator = nullptr; //!< Pointer to the current section/array we're in
private:
/**
* Pushes an element onto the elementStack
*/
void pushElement(json_t* el);
/**
* Pops an element off the elementStack
*/
json_t * popElement();
/**
* Used for optionals, checks if an element exists with the given name
* @return True if the element exists
* @return False otherwise
*/
bool exists(const char* name);
/**
* Throws an error if an element with the given name does not exist
*/
void ensureExists(const char* name);
/**
* Throws an error if an element with the given name exists
*/
void ensureNotExists(const char* name);
};
} // namespace presets
} // namespace io
/**
* @brief Saves the given preset to file.
* @param[in] preset The preset to save
* @param[in] overwrite If true, overwrite existing preset file which have the same name as the given preset
*
* @returns True if successful, or
* @returns False Preset does not have a name, or
* @returns False Preset file exists and overwrite == false, or
* @returns False Preset file .JSON could not be written
*/
bool save_preset_file(CC_preset preset, bool overwrite);
/**
* @brief Loads in all preset .json files from 'players/presets'
* @details If a preset file is unique, it is loaded into the game and available for use by the player. However, if
* a preset is a duplicate of another preset, it is ignored, and the player is warned of it
*/
void load_preset_files();
/**
* @brief Returns true if a preset file with the given name exists.
*/
bool preset_file_exists(SCP_string name);
+295 -90
View File
@@ -1,3 +1,4 @@
#include "controlconfig/controlsconfig.h"
#include "io/joy.h"
#include "io/joy_ff.h"
#include "io/timer.h"
@@ -6,6 +7,7 @@
#include "osapi/osapi.h"
#include <algorithm>
#include <array>
#include <bitset>
#include <memory>
#include <utility>
@@ -19,8 +21,8 @@ namespace {
typedef std::unique_ptr<Joystick> JoystickPtr;
SCP_vector<JoystickPtr> joysticks;
Joystick *currentJoystick = nullptr;
SCP_vector<JoystickPtr> joysticks; //!< All joysticks detected by SDL
std::array<Joystick*, CID_JOY_MAX> pJoystick = {nullptr}; //!< The 4 joysticks FSO are using. pJoystick[0] replaces CurrentJoystick
bool initialized = false;
@@ -118,14 +120,65 @@ bool joystickMatchesGuid(Joystick* testStick, const SCP_string& guid, int id)
return testStick->getDeviceId() == id;
}
bool isCurrentJoystick(Joystick* testStick)
{
const auto currentGUID = os_config_read_string(nullptr, "CurrentJoystickGUID", "");
const auto currentId = static_cast<int>(os_config_read_uint(nullptr, "CurrentJoystick", 0));
/**
* Returns True if the given stick should be assigned the given cid
*
* @param[in] testStick The joystick ptr to test
* @param[in] cid The cid to test this against
*
* @details
* Should cid be CID_JOY0, it also checks for legacy .ini keys.
* Should Joy0ID key be null, legacy keys will be tried
*/
bool isPlayerJoystick(Joystick* testStick, short cid) {
SCP_string GUID;
int Id;
return joystickMatchesGuid(testStick, currentGUID, currentId);
SCP_string key_guid;
SCP_string key_id;
// Select key strings according to cid
switch (cid) {
case CID_JOY0:
key_guid = "Joy0GUID";
key_id = "Joy0ID";
break;
case CID_JOY1:
key_guid = "Joy1GUID";
key_id = "Joy1ID";
break;
case CID_JOY2:
key_guid = "Joy2GUID";
key_id = "Joy2ID";
break;
case CID_JOY3:
key_guid = "Joy3GUID";
key_id = "Joy3ID";
break;
case CID_NONE:
case CID_KEYBOARD:
case CID_MOUSE:
return false;
default:
mprintf(("Unknown CID %i\n", cid));
return false;
}
GUID = os_config_read_string(nullptr, key_guid.c_str(), "");
Id = static_cast<int>(os_config_read_uint(nullptr, key_id.c_str(), 0));
if ((cid == CID_JOY0) && GUID.empty()) {
// Check legacy keys
GUID = os_config_read_string(nullptr, "CurrentJoystickGUID", "");
Id = static_cast<int>(os_config_read_uint(nullptr, "CurrentJoystick", 0));
}
return joystickMatchesGuid(testStick, GUID, Id);
}
/**
* Reads/deserializes the given json line and retrieves the joystick that it references
*/
Joystick* joystick_deserialize(const json_t* value)
{
const char* guid;
@@ -145,6 +198,9 @@ Joystick* joystick_deserialize(const json_t* value)
return nullptr;
}
/**
* Converts/serializes joystick reference data into a json line
*/
json_t* joystick_serializer(Joystick* joystick)
{
if (joystick == nullptr) {
@@ -154,6 +210,9 @@ json_t* joystick_serializer(Joystick* joystick)
return json_pack("{sssi}", "guid", joystick->getGUID().c_str(), "id", joystick->getID());
}
/**
* Scripting callback for displaying the given joystick's name
*/
SCP_string joystick_display(Joystick* stick)
{
if (stick == nullptr) {
@@ -162,6 +221,9 @@ SCP_string joystick_display(Joystick* stick)
return stick->getName();
}
/**
* Scripting callback for enumerating a SCP_vector with all available joysticks
*/
SCP_vector<Joystick*> joystick_enumerator()
{
SCP_vector<Joystick*> out;
@@ -172,7 +234,23 @@ SCP_vector<Joystick*> joystick_enumerator()
return out;
}
auto JoystickOption = options::OptionBuilder<Joystick*>("Input.Joystick", "Joystick", "The current joystick.")
/**
* Joystick options for the new menu system
* These should be displayed as a dropdown box type widget
*/
auto JoystickOption = options::OptionBuilder<Joystick*>("Input.Joystick", "Joystick", "The current joystick 0.")
.category("Input") // Category this option shows up in the scripting heirachy
.deserializer(joystick_deserialize) // callback for json to C++
.serializer(joystick_serializer) // callback for C++ to json
.display(joystick_display) // callback for constructing the display label
.enumerator(joystick_enumerator) // callback for enumerating/constructing the values this option may take
.level(options::ExpertLevel::Beginner)
.default_val(nullptr) // initial/default value for this option
.flags({options::OptionFlags::ForceMultiValueSelection})
.importance(3)
.finish();
auto JoystickOption1 = options::OptionBuilder<Joystick*>("Input.Joystick1", "Joystick1", "The current joystick 1.")
.category("Input")
.deserializer(joystick_deserialize)
.serializer(joystick_serializer)
@@ -180,10 +258,34 @@ auto JoystickOption = options::OptionBuilder<Joystick*>("Input.Joystick", "Joyst
.enumerator(joystick_enumerator)
.level(options::ExpertLevel::Beginner)
.default_val(nullptr)
.flags({options::OptionFlags::ForceMultiValueSelection})
.flags({ options::OptionFlags::ForceMultiValueSelection })
.importance(3)
.finish();
auto JoystickOption2 = options::OptionBuilder<Joystick*>("Input.Joystick2", "Joystick2", "The current joystick 2.")
.category("Input")
.deserializer(joystick_deserialize)
.serializer(joystick_serializer)
.display(joystick_display)
.enumerator(joystick_enumerator)
.level(options::ExpertLevel::Beginner)
.default_val(nullptr)
.flags({ options::OptionFlags::ForceMultiValueSelection })
.importance(3)
.finish();
auto JoystickOption3 = options::OptionBuilder<Joystick*>("Input.Joystick3", "Joystick3", "The current joystick 3.")
.category("Input")
.deserializer(joystick_deserialize)
.serializer(joystick_serializer)
.display(joystick_display)
.enumerator(joystick_enumerator)
.level(options::ExpertLevel::Beginner)
.default_val(nullptr)
.flags({ options::OptionFlags::ForceMultiValueSelection })
.importance(3)
.finish();
HatPosition convertSDLHat(int val)
{
switch (val)
@@ -227,22 +329,30 @@ void enumerateJoysticks(SCP_vector<JoystickPtr>& outVec)
}
}
void setCurrentJoystick(Joystick *stick)
/**
* Assigns the given cid to the given stick
*
* @param[in] stick The stick that is being assigned
* @param[in] cid The cid being assigned, must be a valid short between CID_JOY0 and CID_JOY_MAX
*/
void setPlayerJoystick(Joystick *stick, short cid)
{
currentJoystick = stick;
Assert((cid >= CID_JOY0) && (cid < CID_JOY_MAX));
pJoystick[cid] = stick;
if (currentJoystick != nullptr)
if (pJoystick[cid] != nullptr)
{
mprintf((" Using '%s' as the primary joystick\n", currentJoystick->getName().c_str()));
mprintf((" Using '%s' as Joy-%i\n", pJoystick[cid]->getName().c_str(), cid));
mprintf(("\n"));
mprintf((" Number of axes: %d\n", pJoystick[cid]->numAxes()));
mprintf((" Number of buttons: %d\n", pJoystick[cid]->numButtons()));
mprintf((" Number of hats: %d\n", pJoystick[cid]->numHats()));
mprintf((" Number of trackballs: %d\n", pJoystick[cid]->numBalls()));
mprintf(("\n"));
mprintf((" Number of axes: %d\n", currentJoystick->numAxes()));
mprintf((" Number of buttons: %d\n", currentJoystick->numButtons()));
mprintf((" Number of hats: %d\n", currentJoystick->numHats()));
mprintf((" Number of trackballs: %d\n", currentJoystick->numBalls()));
}
else
{
mprintf((" Using no joystick.\n"));
mprintf((" Joystick %i removed\n", cid));
}
}
@@ -347,38 +457,76 @@ bool device_event_handler(const SDL_Event &evt)
joysticks.push_back(std::move(added));
}
if (currentJoystick != nullptr)
// Is true if all of the pJoysticks are bound
bool all_bound = std::all_of(pJoystick.begin(), pJoystick.end(), [](Joystick* pJoy){ return pJoy != nullptr; });
if (all_bound)
{
// We already have a valid joystick device, ignore any further events
// We already have all valid joystick devices, bail early
return true;
}
} // Else, at least one player stick is unbound
if (Using_in_game_options) {
auto value = JoystickOption->getValue();
// Bind the added stick, if we want it
if (Using_in_game_options)
{
// New menu system lets players select joysticks in-game
Joystick* value[CID_JOY_MAX];
value[0] = JoystickOption->getValue();
value[1] = JoystickOption1->getValue();
value[2] = JoystickOption2->getValue();
value[3] = JoystickOption3->getValue();
// Since the new stick has been added to the joystick list, the option will return the same pointer here
if (value == addedStick) {
setCurrentJoystick(addedStick);
for (short i = CID_JOY0; i < CID_JOY_MAX; ++i)
{
if (value[i] == addedStick)
{
mprintf(("Joystick %i connected\n", i));
setPlayerJoystick(addedStick, i);
break;
}
}
} else {
if (isCurrentJoystick(addedStick)) {
// found our wanted stick!
setCurrentJoystick(addedStick);
}
else
{
// Legacy system requires setting joysticks in the launcher
for (short i = CID_JOY0; i < CID_JOY_MAX; ++i)
{
if (isPlayerJoystick(addedStick, i))
{
mprintf(("Joystick %i connected\n", i));
setPlayerJoystick(addedStick, i);
break;
}
}
}
}
else if (evtType == SDL_JOYDEVICEREMOVED)
{
if (currentJoystick != nullptr && joyDeviceEvent.which == currentJoystick->getID())
bool any_bound = std::any_of(pJoystick.begin(), pJoystick.end(), [](Joystick* pJoy){ return pJoy != nullptr; });
if (!any_bound)
{
// We just lost our joystick, reset the data
setCurrentJoystick(nullptr);
// Nothing bound, ignore
return true;
}
// Else, Find if any of our joysticks were lost
for (short i = CID_JOY0; i < CID_JOY_MAX; ++i)
{
if (joyDeviceEvent.which == pJoystick[i]->getID())
{
// We just lost our joystick, reset the data
mprintf(("Joystick %i disconnected\n", i));
setPlayerJoystick(nullptr, i);
break;
}
}
}
return true;
}
}
} // namespace
namespace io
{
@@ -576,6 +724,7 @@ namespace joystick
_name.assign(SDL_JoystickName(_joystick));
_guidStr = getJoystickGUID(_joystick);
_id = SDL_JoystickInstanceID(_joystick);
_isHaptic = SDL_JoystickIsHaptic(_joystick);
// Initialize values of the axes
auto numSticks = SDL_JoystickNumAxes(_joystick);
@@ -771,11 +920,30 @@ namespace joystick
mprintf((" Joystick ID: %d\n", getID()));
mprintf((" Joystick device ID: %d\n", _device_id));
}
json_t* Joystick::getJSON() {
json_t* object;
object = json_pack("{s:s, s:s, s:i, s:i, s:i, s:i, s:i, s:i, s:b}",
"name", getName().c_str(), // s:s
"guid", getGUID().c_str(), // s:s
"id", static_cast<int>( getID() ), // s:i
"device", _device_id, // s:i
"num_axes", numAxes(), // s:i
"num_balls", numBalls(), // s:i
"num_buttons", numButtons(), // s:i
"num_hats", numHats(), // s:i
"is_haptic", _isHaptic // s:b
);
return object;
}
int Joystick::getDeviceId() const {
return _device_id;
}
bool init()
bool init(bool no_register)
{
using namespace os::events;
@@ -808,6 +976,11 @@ namespace joystick
// Get the initial list of connected joysticks
enumerateJoysticks(joysticks);
if (no_register) {
// bail before registering anything
return false;
}
// Register all the event handlers
addEventListener(SDL_JOYAXISMOTION, DEFAULT_LISTENER_WEIGHT, axis_event_handler);
addEventListener(SDL_JOYBALLMOTION, DEFAULT_LISTENER_WEIGHT, ball_event_handler);
@@ -821,25 +994,40 @@ namespace joystick
addEventListener(SDL_JOYDEVICEREMOVED, DEFAULT_LISTENER_WEIGHT, device_event_handler);
// Search for the correct stick
if (Using_in_game_options) {
// The new options system is in use
setCurrentJoystick(JoystickOption->getValue());
} else {
// Fall back to the legacy config values
for (auto& stick : joysticks) {
if (isCurrentJoystick(stick.get())) {
// Joystick found
setCurrentJoystick(stick.get());
break;
}
}
if (Using_in_game_options)
{
// The new options system is in use
setPlayerJoystick(JoystickOption->getValue(), 0);
}
else
{
// Fall back to the legacy config values
for (short i = CID_JOY0; i < CID_JOY_MAX; ++i)
{
for (auto& stick : joysticks)
{
if (isPlayerJoystick(stick.get(), i))
{
// Joystick found
setPlayerJoystick(stick.get(), i);
break;
}
}
}
}
if (currentJoystick == nullptr) {
mprintf((" No joystick is being used.\n"));
}
}
bool used = false;
for (const auto pJoy : pJoystick) {
if (pJoy != nullptr) {
used = true;
break;
}
}
if (!used) {
mprintf((" No joystick is being used.\n"));
}
initialized = true;
initialized = true;
joy_ff_init();
@@ -858,9 +1046,28 @@ namespace joystick
return joysticks[index].get();
}
Joystick *getCurrentJoystick()
Joystick *getPlayerJoystick(short cid)
{
return currentJoystick;
if ((cid >= CID_JOY0) && (cid < CID_JOY_MAX))
{
return pJoystick[cid];
}
else
{
return nullptr;
}
}
int getPlayerJoystickCount() {
int count = 0;
for (auto pJoy : pJoystick) {
if (pJoy != nullptr) {
count++;
}
}
return count;
}
void shutdown()
@@ -868,58 +1075,46 @@ namespace joystick
joy_ff_shutdown();
initialized = false;
currentJoystick = nullptr;
std::for_each(pJoystick.begin(), pJoystick.end(), [](Joystick*& pJoy){ pJoy = nullptr; });
// Automatically frees joystick resources
joysticks.clear();
SDL_QuitSubSystem(SDL_INIT_JOYSTICK);
}
SCP_vector<JoystickInformation> getJoystickInformations() {
SCP_vector<JoystickInformation> joystickInfo;
json_t* getJsonArray() {
// Do a full init of the SDL_JOYSTICK systems
init();
if (SDL_InitSubSystem(SDL_INIT_JOYSTICK) < 0) {
return SCP_vector<JoystickInformation>();
}
json_t* array = json_array();
size_t len = 0;
auto num_joysticks = SDL_NumJoysticks();
for (auto i = 0; i < num_joysticks; ++i) {
auto joystick = SDL_JoystickOpen(i);
// Get the JSON info of each detected joystick and attach it to array
for (auto& joystick : joysticks) {
json_t* object = joystick->getJSON();
JoystickInformation info{};
auto name = SDL_JoystickName(joystick);
if (name != nullptr) {
info.name = name;
if (object != nullptr) {
json_array_append_new(array, object);
len++;
}
info.guid = getJoystickGUID(joystick);
info.num_axes = static_cast<uint32_t>(SDL_JoystickNumAxes(joystick));
info.num_balls = static_cast<uint32_t>(SDL_JoystickNumBalls(joystick));
info.num_buttons = static_cast<uint32_t>(SDL_JoystickNumButtons(joystick));
info.num_hats = static_cast<uint32_t>(SDL_JoystickNumHats(joystick));
info.is_haptic = SDL_JoystickIsHaptic(joystick) == SDL_TRUE;
joystickInfo.push_back(info);
SDL_JoystickClose(joystick);
}
SDL_QuitSubSystem(SDL_INIT_JOYSTICK);
// Do a full de-init of the SDL_JOYSTICK systems
shutdown();
return joystickInfo;
return array;
}
}
}
// Compatibility for the rest of the engine follows
int joystick_read_raw_axis(int num_axes, int *axis)
int joystick_read_raw_axis(short cid, int num_axes, int *axis)
{
int i;
auto current = io::joystick::getCurrentJoystick();
auto current = io::joystick::getPlayerJoystick(cid);
if (!initialized || current == nullptr)
{
// fake a return value so that controlconfig doesn't get freaky with no joystick
@@ -946,11 +1141,12 @@ int joystick_read_raw_axis(int num_axes, int *axis)
return 1;
}
float joy_down_time(int btn)
float joy_down_time(const CC_bind &bind)
{
int btn = bind.get_btn();
if (btn < 0) return 0.f;
auto current = io::joystick::getCurrentJoystick();
auto current = io::joystick::getPlayerJoystick(bind.get_cid());
if (current == nullptr)
{
@@ -978,11 +1174,12 @@ float joy_down_time(int btn)
return current->getButtonDownTime(btn);
}
int joy_down_count(int btn, int reset_count)
int joy_down_count(const CC_bind &bind, int reset_count)
{
int btn = bind.get_btn();
if (btn < 0) return 0;
auto current = io::joystick::getCurrentJoystick();
auto current = io::joystick::getPlayerJoystick(bind.get_cid());
if (current == nullptr)
{
@@ -1010,7 +1207,15 @@ int joy_down_count(int btn, int reset_count)
return current->getButtonDownCount(btn, reset_count != 0);
}
int joy_down(int btn)
int joy_down(const CC_bind &bind)
{
return joy_down_time(btn) > 0.0f;
return joy_down_time(bind) > 0.0f;
}
bool joy_present(short cid) {
if ((cid < CID_JOY0) || (cid >= CID_JOY_MAX)) {
return false;
}
return pJoystick[cid] != nullptr;
}
+48 -32
View File
@@ -10,8 +10,10 @@
#ifndef __JOY_H__
#define __JOY_H__
#include "controlconfig/controlsconfig.h"
#include "globalincs/pstypes.h"
#include "libs/jansson.h"
#include "SDL_joystick.h"
// z64: Moved up here for compatibility. Bye bye, organization!
@@ -226,7 +228,16 @@ namespace io
*/
void handleJoyEvent(const SDL_Event &evt);
/**
* @brief Prints joystick info to log
*/
void printInfo();
/**
* @brief Like printInfo, but returns info as a JSON object
*/
json_t* getJSON();
private:
Joystick(const Joystick &);
Joystick &operator=(const Joystick &);
@@ -241,13 +252,13 @@ namespace io
void handleButtonEvent(const SDL_JoyButtonEvent& evt);
void handleHatEvent(const SDL_JoyHatEvent& evt);
int _device_id; //!< The SDL device index
int _device_id; //!< The OS device index
SDL_Joystick *_joystick; //!< The SDL joystick handle
SCP_string _guidStr; //!< The GUID string
SCP_string _name; //!< The joystick name
SDL_JoystickID _id; //!< The instance ID
SCP_string _guidStr; //!< The GUID string
SCP_string _name; //!< The joystick name
SDL_JoystickID _id; //!< The instance ID
bool _isHaptic = false; //!< If this joystick supports haptic feedback
SCP_vector<Sint16> _axisValues; //!< The current axes values
SCP_vector<coord2d> _ballValues; //!< The ball values
@@ -277,9 +288,15 @@ namespace io
/**
* @brief Initializes the joystick subsystem
*
* @param[in] no_register if true, init won't try to register joysticks for use.
*
* @return @c true when successfully initialized
*
* @details Registration involves setting up listeners and grabbing the selected joystick from the registry.
* Should no_register be true, the only init done is the SDL systems needed to query joystick information
*/
bool init();
bool init(bool no_register = false);
/**
* @brief Gets number of connected joysticks
@@ -297,48 +314,47 @@ namespace io
Joystick *getJoystick(size_t index);
/**
* @brief Gets the primary joystick
* @return The joystick pointer, may be @c nullptr of no primary joystick
*
* @warning This function may be removed in the future when multi-joystick support is implemented
* @brief Gets the Joystick with the given cid
* @return The joystick pointer, may be @c nullptr if unassigned or invalid cid
*/
Joystick *getCurrentJoystick();
Joystick *getPlayerJoystick(short cid);
/**
* @brief Gets the number of active player joysticks
* @return 0 If no player joysticks are bound or active, or
* @return the number of player joysticks
*/
int getPlayerJoystickCount();
/**
* @brief Frees resources of the joystick subsystem
*/
void shutdown();
struct JoystickInformation {
SCP_string name;
SCP_string guid;
uint32_t num_axes;
uint32_t num_balls;
uint32_t num_buttons;
uint32_t num_hats;
bool is_haptic;
};
SCP_vector<JoystickInformation> getJoystickInformations();
/**
* @brief Called from cmdline -get_info, gets Joy info JSON and shuts down SDL_JOYSTICK subsystem.
*/
json_t* getJsonArray();
}
}
// For now this is a constant for the rest of the engine
const int JOY_NUM_AXES = 6;
const int JOY_AXIS_MIN = 0;
const int JOY_AXIS_CENTER = 32768;
const int JOY_AXIS_CENTER = 32768; // = JOY_AXIS_MIN + ((JOY_AXIS_MAX - JOY_AXIS_MIN) / 2)
const int JOY_AXIS_MAX = 65536;
const int JOY_AXIS_RANGE = (JOY_AXIS_MAX - JOY_AXIS_MIN) / 2; // Currently equal to JOY_AXIS_CENTER
int joystick_read_raw_axis(int num_axes, int *axis);
int joystick_read_raw_axis(short cid, int num_axes, int *axis);
float joy_down_time(int btn);
float joy_down_time(const CC_bind &bind);
int joy_down_count(int btn, int reset_count);
int joy_down_count(const CC_bind &bind, int reset_count);
int joy_down(int btn);
int joy_down(const CC_bind &bind);
/**
* Checks if the given joystick is present or not
*/
bool joy_present(short cid);
#endif /* __JOY_H__ */
+3 -3
View File
@@ -110,8 +110,8 @@ int joy_ff_init()
} else {
ff_enabled = os_config_read_uint(nullptr, "EnableJoystickFF", 1) != 0;
}
if ( !ff_enabled || io::joystick::getCurrentJoystick() == nullptr) {
if ( !ff_enabled || (io::joystick::getPlayerJoystick(CID_JOY0) == nullptr) ) {
return 0;
}
@@ -140,7 +140,7 @@ int joy_ff_init()
}
#endif
haptic = SDL_HapticOpenFromJoystick(io::joystick::getCurrentJoystick()->getDevice());
haptic = SDL_HapticOpenFromJoystick(io::joystick::getPlayerJoystick(CID_JOY0)->getDevice());
if (haptic == NULL) {
mprintf((" ERROR: Unable to open haptic joystick: %s\n", SDL_GetError()));
+2 -2
View File
@@ -543,7 +543,7 @@ void key_mark( uint code, int state, uint latency )
}
if (Script_system.IsActiveAction(CHA_KEYRELEASED)) {
Script_system.SetHookVar("Key", 's', textify_scancode_universal(Current_key_down));
Script_system.SetHookVar("Key", 's', textify_scancode(Current_key_down, true));
Script_system.RunCondition(CHA_KEYRELEASED);
Script_system.RemHookVar("Key");
}
@@ -574,7 +574,7 @@ void key_mark( uint code, int state, uint latency )
if (Script_system.IsActiveAction(CHA_KEYPRESSED)) {
// We use the universal value here to keep the scripting interface consistent regardless of the current language
Script_system.SetHookVar("Key", 's', textify_scancode_universal(Current_key_down));
Script_system.SetHookVar("Key", 's', textify_scancode(Current_key_down, true));
Script_system.RunCondition(CHA_KEYPRESSED);
Script_system.RemHookVar("Key");
}
+54 -12
View File
@@ -14,6 +14,7 @@
#include <windowsx.h>
#endif
#include "controlconfig/controlsconfig.h"
#include "graphics/2d.h"
#include "io/mouse.h"
#include "options/Option.h"
@@ -300,8 +301,17 @@ void mouse_flush()
SDL_UnlockMutex( mouse_lock );
}
int mouse_down_count(int n, int reset_count)
int mouse_down_count(const CC_bind &bind, int reset_count)
{
if (bind.get_cid() != CID_MOUSE) {
return 0;
}
int n = 1 << bind.get_btn();
return mouse_down_count(n, reset_count);
}
int mouse_down_count(int n, int reset_count) {
int tmp = 0;
if ( !mouse_inited ) return 0;
@@ -356,8 +366,16 @@ int mouse_down_count(int n, int reset_count)
//
// parameters: n - button of mouse (see #define's in mouse.h)
//
int mouse_up_count(int n)
int mouse_up_count(const CC_bind &bind)
{
if (bind.get_cid() != CID_MOUSE) {
return 0;
}
int n = 1 << bind.get_btn();
return mouse_up_count(n);
}
int mouse_up_count(int n) {
int tmp = 0;
if ( !mouse_inited ) return 0;
@@ -403,8 +421,17 @@ int mouse_up_count(int n)
// returns 1 if mouse button btn is down, 0 otherwise
int mouse_down(int btn)
int mouse_down(const CC_bind &bind)
{
if (bind.get_cid() != CID_MOUSE) {
return 0;
}
int btn = 1 << bind.get_btn();
return mouse_down(btn);
}
int mouse_down(int btn) {
int tmp;
if ( !mouse_inited ) return 0;
@@ -574,19 +601,17 @@ void mousewheel_motion(int x, int y, bool reversed) {
}
void mousewheel_decay(int btn) {
switch (btn) {
case MOUSE_WHEEL_UP:
if (btn & MOUSE_WHEEL_UP) {
Mouse_wheel_y -= 1;
break;
case MOUSE_WHEEL_DOWN:
}
if (btn & MOUSE_WHEEL_DOWN) {
Mouse_wheel_y += 1;
break;
case MOUSE_WHEEL_LEFT:
}
if (btn & MOUSE_WHEEL_LEFT) {
Mouse_wheel_x -= 1;
break;
case MOUSE_WHEEL_RIGHT:
}
if (btn & MOUSE_WHEEL_RIGHT) {
Mouse_wheel_x += 1;
break;
}
if (Mouse_wheel_x == 0) {
@@ -596,3 +621,20 @@ void mousewheel_decay(int btn) {
mouse_flags &= ~(MOUSE_WHEEL_RIGHT | MOUSE_WHEEL_LEFT);
}
}
short bit_distance(short x) {
short i;
const short max_dist = sizeof(short) * 8;
for (i = 0; i < max_dist; ++i, x >>= 1) {
if (x & 0x01) {
break;
}
}
if (i >= max_dist) {
return -1;
}
return i;
}
+20 -1
View File
@@ -12,6 +12,7 @@
#ifndef _MOUSE_H
#define _MOUSE_H
#include "controlconfig/controlsconfig.h"
#include "globalincs/pstypes.h"
extern int Mouse_sensitivity;
@@ -58,11 +59,15 @@ extern void mouse_set_pos(int xpos,int ypos);
/**
* @brief Returns the number of times button n went from up to down since last call
*/
int mouse_down_count(const CC_bind &bind, int reset_count = 1);
int mouse_down_count(int n, int reset_count = 1);
/**
* @brief Returns the number of times button n went from down to up since last call
*/
int mouse_up_count(const CC_bind &bind);
int mouse_up_count(int n);
/**
@@ -70,7 +75,15 @@ int mouse_up_count(int n);
*/
void mouse_flush();
int mouse_down(int btn); // returns 1 if mouse button btn is down, 0 otherwise
/**
* Returns 1 if the given mouse button down, 0 otherwise
*/
int mouse_down(const CC_bind &bind);
/**
* Returns 1 if any of the given mouse buttons are down. 0 otherwise
*/
int mouse_down(int btn);
void mouse_reset_deltas();
void mouse_get_delta(int *dx = NULL, int *dy = NULL, int *dz = NULL);
@@ -84,4 +97,10 @@ void mousewheel_motion(int x, int y, bool reversed);
extern void mouse_force_pos(int x, int y);
/**
* Get the closest distance of a bit mask to 0
* @note use this to translate mouse button masks to button id's
*/
short bit_distance(short x);
#endif
+2 -1
View File
@@ -59,7 +59,8 @@ bool *Lcl_unexpected_tstring_check = nullptr;
// the english version (in the code) to a foreign version (in the table). Thus, if you
// add a new string to the code, you must assign it a new index. Use the number below for
// that index and increase the number below by one.
#define XSTR_SIZE 1647
// retail XSTR_SIZE = 1570
#define XSTR_SIZE 1662
// struct to allow for strings.tbl-determined x offset
+1 -1
View File
@@ -1587,7 +1587,7 @@ void barracks_do_frame(float /*frametime*/)
}
// check mouse over help
if (mouse_down(MOUSE_LEFT_BUTTON)) {
if (mouse_down(CC_bind(CID_MOUSE, MOUSE_LEFT_BUTTON))) {
help_overlay_set_state(Barracks_overlay_id, gr_screen.res, 0);
}
+3 -3
View File
@@ -636,8 +636,8 @@ char *translate_message_token(char *str)
*/
SCP_string message_translate_tokens(const char *text)
{
char temp[40], *ptr;
const char *toke1, *toke2;
char temp[40];
const char *ptr, *toke1, *toke2;
int r;
SCP_string buf;
@@ -659,7 +659,7 @@ SCP_string message_translate_tokens(const char *text)
strncpy(temp, text, toke2 - text); // isolate token into seperate buffer
temp[toke2 - text] = 0; // null terminate string
ptr = translate_key(temp); // try and translate key
if (ptr) { // was key translated properly?
if (ptr != nullptr) { // was key translated properly?
if (!stricmp(ptr, NOX("none")) && (Training_bind_warning != Missiontime)) {
if ( The_mission.game_type & MISSION_TYPE_TRAINING ) {
r = popup(PF_TITLE_BIG | PF_TITLE_RED, 2, XSTR( "&Bind Control", 424), XSTR( "&Abort mission", 425),
+38
View File
@@ -465,6 +465,44 @@ namespace os
#endif
}
void Information(const char* filename, int line, const char* format, ...) {
SCP_string msg;
va_list args;
va_start(args, format);
vsprintf(msg, format, args);
va_end(args);
// Below is essentially a stripped down copy pasta of WarningImpl
filename = clean_filename(filename);
// output to the debug log before anything else (so that we have a complete record)
mprintf(("INFO: \"%s\" at %s:%d\n", msg.c_str(), filename, line));
// now go for the additional popup window, if we want it ...
if (Cmdline_noninteractive || running_unittests) {
return;
}
SCP_stringstream boxMsgStream;
boxMsgStream << "Information: " << msg << "\n";
boxMsgStream << "File: " << filename << "\n";
boxMsgStream << "Line: " << line << "\n";
set_clipboard_text(boxMsgStream.str().c_str());
boxMsgStream << "\n";
SCP_string boxMessage = truncateLines(boxMsgStream, Messagebox_lines);
boxMessage += "\n[ This info is in the clipboard so you can paste it somewhere now ]\n";
gr_activate(0);
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_INFORMATION, "Information", boxMessage.c_str(), getDialogParent());
gr_activate(1);
}
void Message(MessageType type, const char* message, const char* title)
{
if (running_unittests) {
+19 -5
View File
@@ -114,15 +114,29 @@ namespace os
*/
void ReleaseWarning(const char* filename, int line, SCP_FORMAT_STRING const char* format, ...) SCP_FORMAT_STRING_ARGS(3, 4);
/**
* @brief Shows an information dialog
* Displays a modal dialog with at most an OK button and some text. Use instead of ReleaseWarning if
* debugging is not necassary and you just want to inform the player of something important and can't use
* other UI. Use sparingly.
*
* @param filename The source code filename where this function was called
* @param line The source code line number where this function was called
* @param format The message to display
*/
void Information(const char* filename, int line, SCP_FORMAT_STRING const char* format, ...) SCP_FORMAT_STRING_ARGS(3, 4);
void Message(MessageType type, const char* message, const char* title = NULL);
}
}
// Make these available in the global namespace for compatibility
using os::dialogs::LuaError;
using os::dialogs::Error;
using os::dialogs::Warning;
using os::dialogs::ReleaseWarning;
using os::dialogs::WarningEx;
using os::dialogs::LuaError; //NOLINT
using os::dialogs::Error; //NOLINT
using os::dialogs::Warning; //NOLINT
using os::dialogs::ReleaseWarning; //NOLINT
using os::dialogs::WarningEx; //NOLINT
using os::dialogs::Information; //NOLINT
#endif // _OSAPI_DIALOGS_H
+13 -6
View File
@@ -274,11 +274,18 @@ namespace
}
// Reads a string from the INI file. If default is passed,
// and the string isn't found, returns ptr to default otherwise
// returns NULL; Copy the return value somewhere before
// calling os_read_string again, because it might reuse the
// same buffer.
/**
* @brief Reads a string from the INI file or registry.
*
* @param[in] section Section to look in
* @param[in] name Key name of the value
* @param[in] default_value Value to return, if not found or if OS_reg_inited is false
*
* @returns The value of the string with the given name in the given section if found
*
* @note Copy the return value somewhere before calling os_read_string again, because it might reuse the same
* buffer
*/
static char registry_tmp_string_data[1024];
const char * registry_read_string(const char *section, const char *name, const char *default_value)
{
@@ -288,7 +295,7 @@ namespace
LONG lResult;
if (!Os_reg_inited) {
return NULL;
return default_value;
}
HKEY useHKey = get_registry_keyname(keyname, section, Cmdline_alternate_registry_path);
+6
View File
@@ -10,6 +10,9 @@ pilot::BinaryFileHandler::~BinaryFileHandler() {
_cfp = nullptr;
}
void pilot::BinaryFileHandler::writeByte(const char*, std::int8_t value) {
cfwrite_char(value, _cfp);
}
void pilot::BinaryFileHandler::writeUByte(const char*, std::uint8_t value) {
cfwrite_ubyte(value, _cfp);
}
@@ -95,6 +98,9 @@ void pilot::BinaryFileHandler::endArrayWrite() {
void pilot::BinaryFileHandler::flush() {
cflush(_cfp);
}
std::int8_t pilot::BinaryFileHandler::readByte(const char*) {
return cfread_char(_cfp);
}
std::uint8_t pilot::BinaryFileHandler::readUByte(const char*) {
return cfread_ubyte(_cfp);
}
+4
View File
@@ -30,6 +30,8 @@ class BinaryFileHandler: public FileHandler {
~BinaryFileHandler() override;
void writeByte(const char* name, std::int8_t value) override;
void writeUByte(const char* name, std::uint8_t value) override;
void writeShort(const char* name, std::int16_t value) override;
@@ -59,6 +61,8 @@ class BinaryFileHandler: public FileHandler {
void flush() override;
std::int8_t readByte(const char* name) override;
std::uint8_t readUByte(const char* name) override;
std::int16_t readShort(const char* name) override;
+4
View File
@@ -44,6 +44,8 @@ class FileHandler {
public:
virtual ~FileHandler() {}
virtual void writeByte(const char* name, std::int8_t value) = 0;
virtual void writeUByte(const char* name, std::uint8_t value) = 0;
virtual void writeShort(const char* name, std::int16_t value) = 0;
@@ -70,6 +72,8 @@ class FileHandler {
virtual void flush() = 0;
virtual std::int8_t readByte(const char* name) = 0;
virtual std::uint8_t readUByte(const char* name) = 0;
virtual std::int16_t readShort(const char* name) = 0;
+6
View File
@@ -87,6 +87,9 @@ void pilot::JSONFileHandler::popElement() {
_elementStack.pop_back();
_currentEl = _elementStack.back();
}
void pilot::JSONFileHandler::writeByte(const char* name, std::int8_t value) {
writeInteger(name, value);
}
void pilot::JSONFileHandler::writeUByte(const char* name, std::uint8_t value) {
writeInteger(name, value);
}
@@ -209,6 +212,9 @@ void pilot::JSONFileHandler::ensureExists(const char* name) {
Error(LOCATION, "JSON reading requires a value with name '%s' but there is no such value!", name);
}
}
std::int8_t pilot::JSONFileHandler::readByte(const char* name) {
return (std::int8_t)readInteger(name);
}
std::uint8_t pilot::JSONFileHandler::readUByte(const char* name) {
return (std::uint8_t)readInteger(name);
}
+3
View File
@@ -38,6 +38,8 @@ class JSONFileHandler: public FileHandler {
~JSONFileHandler() override;
void writeByte(const char* name, std::int8_t value) override;
void writeUByte(const char* name, std::uint8_t value) override;
void writeShort(const char* name, std::int16_t value) override;
@@ -66,6 +68,7 @@ class JSONFileHandler: public FileHandler {
void flush() override;
std::int8_t readByte(const char* name) override;
std::uint8_t readUByte(const char* name) override;
+54 -34
View File
@@ -1,4 +1,6 @@
#include "controlconfig/controlsconfig.h"
#include "controlconfig/presets.h"
#include "cutscene/cutscenes.h"
#include "freespace.h"
#include "gamesnd/eventmusic.h"
@@ -1209,37 +1211,67 @@ void pilotfile::csg_write_settings()
void pilotfile::csg_read_controls()
{
int idx, list_size;
short id1, id2, id3 __UNUSED;
if (version < 7) {
// Pre CSG-7 compatibility
int idx, list_size;
short id1, id2, id3 __UNUSED;
list_size = (int)cfread_ushort(cfp);
list_size = (int)cfread_ushort(cfp);
for (idx = 0; idx < list_size; idx++) {
id1 = cfread_short(cfp);
id2 = cfread_short(cfp);
id3 = cfread_short(cfp); // unused, at the moment
for (idx = 0; idx < list_size; idx++) {
id1 = cfread_short(cfp);
id2 = cfread_short(cfp);
id3 = cfread_short(cfp); // unused, at the moment
if (idx < CCFG_MAX) {
Control_config[idx].take(CC_bind(CID_KEYBOARD, id1), 0);
Control_config[idx].take(CC_bind(CID_JOY0, id2), 1);
if (idx < CCFG_MAX) {
Control_config[idx].take(CC_bind(CID_KEYBOARD, id1), 0);
Control_config[idx].take(CC_bind(CID_JOY0, id2), 1);
}
}
// Check that these bindings are in a preset.
auto it = control_config_get_current_preset();
if (it == Control_config_presets.end()) {
// Not a preset, create one and its file
CC_preset preset;
preset.name = filename;
// strip off extension
auto n = preset.name.find_last_of('.');
preset.name.resize(n);
std::copy(Control_config.begin(), Control_config.end(), std::back_inserter(preset.bindings));
Control_config_presets.push_back(preset);
save_preset_file(preset, true);
}
return;
} else {
// >= CSG-7
char buf[MAX_FILENAME_LEN];
memset(buf, '\0', sizeof(buf));
cfread_string(buf, sizeof(buf), cfp);
auto it = std::find_if(Control_config_presets.begin(), Control_config_presets.end(),
[buf](const CC_preset& preset) { return preset.name == buf; });
if (it == Control_config_presets.end()) {
// Couldn't find the preset, use defaults
it = Control_config_presets.begin();
}
control_config_use_preset(*it);
return;
}
}
void pilotfile::csg_write_controls()
{
int idx;
auto it = control_config_get_current_preset();
startSection(Section::Controls);
cfwrite_ushort(CCFG_MAX, cfp);
for (idx = 0; idx < CCFG_MAX; idx++) {
cfwrite_short(Control_config[idx].get_btn(CID_KEYBOARD), cfp);
cfwrite_short(Control_config[idx].get_btn(CID_JOY0), cfp);
// placeholder? for future mouse_id?
cfwrite_short(-1, cfp);
}
cfwrite_string(it->name.c_str(), cfp);
endSection();
}
@@ -1374,11 +1406,6 @@ void pilotfile::csg_read_container(sexp_container& container)
}
}
bool pilotfile::csg_has_persistent_containers()
{
return !Campaign.persistent_containers.empty() || !Campaign.red_alert_containers.empty();
}
void pilotfile::csg_write_containers()
{
startSection(Section::Containers);
@@ -1728,10 +1755,7 @@ bool pilotfile::save_savefile()
// header and version
cfwrite_int(CSG_FILE_ID, cfp);
static_assert(CSG_VERSION == PRE_CONTAINERS_CSG_VERSION + 1,
"CSG_VERSION shouldn't be bumped beyond containers without removing compatibility fix");
const ubyte file_version = csg_has_persistent_containers() ? CSG_VERSION : PRE_CONTAINERS_CSG_VERSION;
cfwrite_ubyte(file_version, cfp);
cfwrite_ubyte(CSG_VERSION, cfp);
mprintf(("CSG => Saving '%s' with version %d...\n", filename.c_str(), (int)CSG_VERSION));
@@ -1764,12 +1788,8 @@ bool pilotfile::save_savefile()
csg_write_cutscenes();
mprintf(("CSG => Saving: Last Missions...\n"));
csg_write_lastmissions();
if (csg_has_persistent_containers()) {
mprintf(("CSG => Saving: Containers...\n"));
csg_write_containers();
} else {
mprintf(("CSG => No containers to save.\n"));
}
mprintf(("CSG => Saving: Containers...\n"));
csg_write_containers();
// Done!
mprintf(("CSG => Saving complete!\n"));
+4 -6
View File
@@ -23,18 +23,18 @@ static const unsigned int CSG_FILE_ID = 0x5f475343; // "CSG_" in file
// 1 - Adding support for the player is multi flag
// 2 - Add language in use when pilot was created
// (due to intel entries using translated text as the primary key)
static const ubyte PRE_CONTAINERS_PLR_VERSION = 2;
// 3 - Add SEXP containers
static const ubyte PLR_VERSION = 3;
// 4 Controls are removed, and instead a preset name is saved/loaded
static const ubyte PLR_VERSION = 4;
// 0 - initial version
// 1 - re-add recent missions
// 2 - separate single/multi squad name & pic
// 3 - remove separate detail settings for campaigns
// 4 - add CPV rollback for Red Alert missions
// 5 - save rank to flags for quick access
static const ubyte PRE_CONTAINERS_CSG_VERSION = 5;
// 6 - add SEXP containers
static const ubyte CSG_VERSION = 6;
// 7 - Controls are removed, and instead a preset name is saved/loaded.
static const ubyte CSG_VERSION = 7;
typedef struct index_list_t {
SCP_string name;
@@ -155,7 +155,6 @@ class pilotfile {
void plr_write_stats_multi();
void plr_write_multiplayer();
void plr_write_variables();
bool plr_has_persistent_containers() const;
void plr_write_containers();
void plr_write_hud();
void plr_write_controls();
@@ -195,7 +194,6 @@ class pilotfile {
void csg_write_controls();
void csg_write_cutscenes();
void csg_write_lastmissions();
static bool csg_has_persistent_containers();
void csg_write_containers();
void csg_write_container(const sexp_container& container);
+77 -61
View File
@@ -1,4 +1,6 @@
#include "controlconfig/controlsconfig.h"
#include "controlconfig/presets.h"
#include "freespace.h"
#include "gamesnd/eventmusic.h"
#include "hud/hudconfig.h"
@@ -346,11 +348,6 @@ void pilotfile::plr_read_containers()
handler->endArrayRead();
}
bool pilotfile::plr_has_persistent_containers() const
{
return !p->containers.empty();
}
void pilotfile::plr_write_containers()
{
handler->startSectionWrite(Section::Containers);
@@ -651,65 +648,91 @@ void pilotfile::plr_write_stats_multi()
void pilotfile::plr_read_controls()
{
short id1, id2;
int axi, inv;
if (version < 4) {
// PLR < 4
short id1, id2;
int axi, inv;
auto list_size = handler->startArrayRead("controls", true);
for (size_t idx = 0; idx < list_size; idx++, handler->nextArraySection()) {
id1 = handler->readShort("key");
id2 = handler->readShort("joystick");
handler->readShort("mouse"); // unused, at the moment
// Set up preset name, we'll populate the rest of the preset's data later
CC_preset preset;
preset.name = filename;
if (idx < Control_config.size()) {
Control_config[idx].take(CC_bind(CID_KEYBOARD, id1), 0);
Control_config[idx].take(CC_bind(CID_JOY0, id2), 1);
// strip off extension
auto n = preset.name.find_last_of('.');
preset.name.resize(n);
// Load in the bindings to Control_config
// ...First the digital controls
auto list_size = handler->startArrayRead("controls", true);
for (size_t idx = 0; idx < list_size; idx++, handler->nextArraySection()) {
id1 = handler->readShort("key");
id2 = handler->readShort("joystick");
handler->readShort("mouse");
if (idx < Control_config.size()) {
// Force the bindings into Control_config
Control_config[idx].take(CC_bind(CID_KEYBOARD, id1), 0);
Control_config[idx].take(CC_bind(CID_JOY0, id2), 1);
}
}
}
handler->endArrayRead();
handler->endArrayRead();
auto list_axis = handler->startArrayRead("axes");
for (size_t idx = 0; idx < list_axis; idx++, handler->nextArraySection()) {
axi = handler->readInt("axis_map");
inv = handler->readInt("invert_axis");
// ...Then the analog controls
auto list_axis = handler->startArrayRead("axes");
for (size_t idx = 0; idx < list_axis; idx++, handler->nextArraySection()) {
axi = handler->readInt("axis_map");
inv = handler->readInt("invert_axis");
if (idx < NUM_JOY_AXIS_ACTIONS) {
Axis_map_to[idx] = axi;
Invert_axis[idx] = inv;
if (idx < NUM_JOY_AXIS_ACTIONS) {
Control_config[idx + JOY_AXIS_BEGIN].take(CC_bind(CID_JOY0, static_cast<short>(axi), CCF_AXIS), 0);
Control_config[idx + JOY_AXIS_BEGIN].invert(inv != 0);
}
}
handler->endArrayRead();
// Check that these bindings are in a preset. If it is not, create a new preset file
auto it = control_config_get_current_preset();
if (it == Control_config_presets.end()) {
std::copy(Control_config.begin(), Control_config.end(), std::back_inserter(preset.bindings));
Control_config_presets.push_back(preset);
// Try to save the preset file
if (save_preset_file(preset, false)) {
Information(LOCATION, "Successfully converted playerfile to v4. Please rebind your mouse controls, if any.");
} else {
Warning(LOCATION, "Could not save controls preset file (%s) when converting playerfile to v3.", preset.name.c_str());
}
}
return;
} else {
// read PLR >= 4
SCP_string buf = handler->readString("preset");
auto it = std::find_if(Control_config_presets.begin(), Control_config_presets.end(),
[buf](const CC_preset& preset) { return preset.name == buf; });
if (it == Control_config_presets.end()) {
// Couldn't find the preset, use defaults
ReleaseWarning(LOCATION, "Could not find preset %s, using defaults\n", buf.c_str());
it = Control_config_presets.begin();
}
control_config_use_preset(*it);
return;
}
handler->endArrayRead();
}
void pilotfile::plr_write_controls()
{
handler->startSectionWrite(Section::Controls);
// As of PLR v4, control bindings are saved outside of the PLR file into PST files.
// We now only save the currently selected preset in the PLR file itself.
auto it = control_config_get_current_preset();
handler->writeString("preset", it->name.c_str());
// For some unknown reason, the old code used a short for the array length here...
handler->startArrayWrite("controls", Control_config.size(), true);
for (auto &item : Control_config) {
handler->startSectionWrite(Section::Unnamed);
handler->writeShort("key", item.get_btn(CID_KEYBOARD));
handler->writeShort("joystick", item.get_btn(CID_JOY0));
// placeholder? for future mouse_id?
handler->writeShort("mouse", -1);
handler->endSectionWrite();
}
handler->endArrayWrite();
handler->startArrayWrite("axes", NUM_JOY_AXIS_ACTIONS);
for (size_t idx = 0; idx < NUM_JOY_AXIS_ACTIONS; idx++) {
handler->startSectionWrite(Section::Unnamed);
handler->writeInt("axis_map", Axis_map_to[idx]);
handler->writeInt("invert_axis", Invert_axis[idx]);
handler->endSectionWrite();
}
handler->endArrayWrite();
handler->endSectionWrite();
handler->endSectionWrite(); // Section::controls
}
void pilotfile::plr_read_settings()
@@ -1081,10 +1104,7 @@ bool pilotfile::save_player(player *_p)
// header and version
handler->writeInt("signature", PLR_FILE_ID);
static_assert(PLR_VERSION == PRE_CONTAINERS_PLR_VERSION + 1,
"PLR_VERSION shouldn't be bumped beyond containers without removing compatibility fix");
const ubyte file_version = plr_has_persistent_containers() ? PLR_VERSION : PRE_CONTAINERS_PLR_VERSION;
handler->writeUByte("version", file_version);
handler->writeUByte("version", PLR_VERSION);
mprintf(("PLR => Saving '%s' with version %d...\n", filename.c_str(), (int)PLR_VERSION));
@@ -1105,12 +1125,8 @@ bool pilotfile::save_player(player *_p)
plr_write_hud();
mprintf(("PLR => Saving: Variables...\n"));
plr_write_variables();
if (plr_has_persistent_containers()) {
mprintf(("PLR => Saving: Containers...\n"));
plr_write_containers();
} else {
mprintf(("PLR => No containers to save.\n"));
}
mprintf(("PLR => Saving: Containers...\n"));
plr_write_containers();
mprintf(("PLR => Saving: Multiplayer...\n"));
plr_write_multiplayer();
mprintf(("PLR => Saving: Controls...\n"));
+1 -1
View File
@@ -88,7 +88,7 @@ void init_new_pilot(player *p, int reset)
if (reset) {
hud_set_default_hud_config(p); // use a default hud config
control_config_reset_defaults(); // get a default keyboard config
control_config_use_preset(Control_config_presets[0]); // get a default keyboard config
player_set_pilot_defaults(p); // set up any player struct defaults
// set the default detail level based on tabling rather than the above method
+22 -98
View File
@@ -79,13 +79,6 @@ float Minimum_player_warpout_time = 3.0f;
DCF_BOOL( show_killer_weapon, Show_killer_weapon )
#endif
/**
* @brief Reads and combines the axes from the player's joystick and mouse.
* @param[out] axis Pointer to axes array
* @param[in] frame_time The frame time when this was called
*/
void playercontrol_read_stick(int *axis, float frame_time);
/**
* @brief Slew angles chase towards a value like they're on a spring.
*
@@ -151,7 +144,7 @@ void view_modify(angles *ma, angles *da, float max_p, float max_h)
float u = 0;
// Analog inputs
int axis[NUM_JOY_AXIS_ACTIONS];
int axis[Action::NUM_VALUES];
float h = 0.0f;
float p = 0.0f;
@@ -223,11 +216,11 @@ void view_modify(angles *ma, angles *da, float max_p, float max_h)
t = (check_control_timef(YAW_RIGHT) - check_control_timef(YAW_LEFT));
u = (check_control_timef(PITCH_FORWARD) - check_control_timef(PITCH_BACK));
playercontrol_read_stick(axis, flRealframetime);
control_get_axes_readings(axis, flRealframetime);
// Does the same thing as t and u but for the joystick input
h = f2fl(axis[JOY_HEADING_AXIS]);
p = -f2fl(axis[JOY_PITCH_AXIS]);
h = f2fl(axis[Action::HEADING]);
p = -f2fl(axis[Action::PITCH]);
}
// Combine Analog and Digital slew commands
@@ -543,74 +536,10 @@ void player_control_reset_ci( control_info *ci )
ci->forward_cruise_percent = oldspeed;
}
/**
* @brief Reads the joystick Pitch, Bank, and Heading axes.
*
* @param[out] axis The axis array to store the scaled joystick position. (Must have a size of NUM_JOY_AXIS_ACTIONS)
* @param[in] frame_time The frame time at which this function is called.
*
* @details This is its own function because we only want to read it at a certain rate, since it takes time.
*/
void playercontrol_read_stick(int *axis, float frame_time)
{
int i;
// Read the stick
control_get_axes_readings(&axis[0], &axis[1], &axis[2], &axis[3], &axis[4]);
if (Use_mouse_to_fly) {
int dx, dy, dz;
float factor;
factor = (float) Mouse_sensitivity + 1.77f;
factor = factor * factor / frame_time / 0.6f;
mouse_get_delta(&dx, &dy, &dz);
int x_axis, y_axis, z_axis;
x_axis = y_axis = z_axis = -1;
for (i = 0; i < NUM_JOY_AXIS_ACTIONS; i++) {
switch(Axis_map_to[i])
{
case JOY_X_AXIS:
x_axis = i;
break;
case JOY_Y_AXIS:
y_axis = i;
break;
case JOY_Z_AXIS:
z_axis = i;
break;
}
}
if (x_axis >= 0) {
if (Invert_axis[x_axis]) {
dx = -dx;
}
axis[x_axis] += (int) ((float) dx * factor);
}
if (y_axis >= 0) {
if (Invert_axis[y_axis]) {
dy = -dy;
}
axis[y_axis] += (int) ((float) dy * factor);
}
if (z_axis >= 0) {
if (Invert_axis[z_axis]) {
dz = -dz;
}
axis[z_axis] += (int) ((float) dz * factor);
}
}
}
void read_keyboard_controls( control_info * ci, float frame_time, physics_info *pi )
{
float kh=0.0f, scaled, newspeed, delta, oldspeed;
int axis[NUM_JOY_AXIS_ACTIONS];
int axis[Action::NUM_VALUES];
static int afterburner_last = 0;
static float analog_throttle_last = 9e9f;
static int override_analog_throttle = 0;
@@ -805,41 +734,36 @@ void read_keyboard_controls( control_info * ci, float frame_time, physics_info *
// code to read joystick axis for pitch/heading. Code to read joystick buttons
// for bank.
if ( !(Game_mode & GM_DEAD) ) {
playercontrol_read_stick(axis, frame_time);
control_get_axes_readings(axis, frame_time);
} else {
axis[0] = axis[1] = axis[2] = axis[3] = axis[4] = 0;
}
if (Viewer_mode & VM_CAMERA_LOCKED) {
// Player has control of the ship
if (Axis_map_to[JOY_HEADING_AXIS] >= 0) {
// check the heading on the x axis
if ( check_control(BANK_WHEN_PRESSED) ) {
delta = f2fl( axis[JOY_HEADING_AXIS] );
if ( (delta > 0.05f) || (delta < -0.05f) ) {
ci->bank -= delta;
}
} else {
ci->heading += f2fl( axis[JOY_HEADING_AXIS] );
// Set heading
if ( check_control(BANK_WHEN_PRESSED) ) {
delta = f2fl( axis[Action::HEADING] );
if ( (delta > 0.05f) || (delta < -0.05f) ) {
ci->bank -= delta;
}
} else {
ci->heading += f2fl( axis[Action::HEADING] );
}
// check the pitch on the y axis
if (Axis_map_to[JOY_PITCH_AXIS] >= 0) {
ci->pitch -= f2fl( axis[JOY_PITCH_AXIS] );
}
// Set pitch
ci->pitch -= f2fl( axis[Action::PITCH] );
} else {
// Player has control of the camera
ci->pitch = 0.0f;
ci->heading = 0.0f;
}
if (Axis_map_to[JOY_BANK_AXIS] >= 0) {
ci->bank -= f2fl( axis[JOY_BANK_AXIS] ) * 1.5f;
}
// Set bank
ci->bank -= f2fl( axis[Action::BANK] ) * 1.5f;
// axis 2 is for throttle
if (Axis_map_to[JOY_ABS_THROTTLE_AXIS] >= 0) {
scaled = (float) axis[JOY_ABS_THROTTLE_AXIS] * 1.2f / (float) F1_0 - 0.1f; // convert to -0.1 - 1.1 range
if (!Control_config[JOY_ABS_THROTTLE_AXIS].empty()) {
scaled = (float) axis[Action::ABS_THROTTLE] * 1.2f / (float) F1_0 - 0.1f; // convert to -0.1 - 1.1 range
oldspeed = ci->forward_cruise_percent;
newspeed = (1.0f - scaled) * 100.0f;
@@ -852,8 +776,8 @@ void read_keyboard_controls( control_info * ci, float frame_time, physics_info *
}
}
if (Axis_map_to[JOY_REL_THROTTLE_AXIS] >= 0)
ci->forward_cruise_percent += f2fl(axis[JOY_REL_THROTTLE_AXIS]) * 100.0f * frame_time;
if (!Control_config[JOY_REL_THROTTLE_AXIS].empty())
ci->forward_cruise_percent += f2fl(axis[Action::REL_THROTTLE]) * 100.0f * frame_time;
CLAMP(ci->forward_cruise_percent, 0, 100);
+4 -3
View File
@@ -529,7 +529,7 @@ int popup_init(popup_info *pi, int flags)
// if this is an input popup, create and center the popup
if(flags & PF_INPUT){
Popup_input.create(&Popup_window, Popup_text_coords[gr_screen.res][0], pbg->coords[1] + Popup_input_y_offset[gr_screen.res], Popup_text_coords[gr_screen.res][2], pi->max_input_text_len, "", UI_INPUTBOX_FLAG_INVIS | UI_INPUTBOX_FLAG_ESC_CLR | UI_INPUTBOX_FLAG_ESC_FOC | UI_INPUTBOX_FLAG_KEYTHRU | UI_INPUTBOX_FLAG_TEXT_CEN);
Popup_input.create(&Popup_window, Popup_text_coords[gr_screen.res][0], pbg->coords[1] + Popup_input_y_offset[gr_screen.res], Popup_text_coords[gr_screen.res][2], pi->max_input_text_len, pi->input_text, UI_INPUTBOX_FLAG_INVIS | UI_INPUTBOX_FLAG_ESC_CLR | UI_INPUTBOX_FLAG_ESC_FOC | UI_INPUTBOX_FLAG_KEYTHRU | UI_INPUTBOX_FLAG_TEXT_CEN);
Popup_input.set_focus();
}
@@ -1102,7 +1102,7 @@ int popup_till_condition(int (*condition)(), ...)
}
// popup to return the value from an input box
char *popup_input(int flags, const char *caption, int max_output_len)
char *popup_input(int flags, const char *caption, int max_output_len, const char *default_input)
{
if ( Popup_is_active ) {
Int3(); // should never happen
@@ -1129,8 +1129,9 @@ char *popup_input(int flags, const char *caption, int max_output_len)
Popup_info.max_input_text_len = max_output_len;
}
// zero the popup input text
// zero the popup input text and set default, if provided
memset(Popup_info.input_text, 0, POPUP_INPUT_MAX_CHARS);
strcpy_s(Popup_info.input_text, default_input);
gamesnd_play_iface(InterfaceSounds::POPUP_APPEAR); // play sound when popup appears
+8 -2
View File
@@ -94,8 +94,14 @@ int popup(int flags, int nchoices, ... );
// rval = popup_till_condition( condition_function, "Cancel", "Checking to see if %s is an idiot.", pl->callsign);
int popup_till_condition( int(*condition)() , ...);
// popup to return the value from an input box
char *popup_input(int flags, const char *caption, int max_output_len = -1);
/*!
* Popup that prompts the user for input
* @param[in] flags Apperance and behavior flags. See the PF_* defines in popup.h
* @param[in] caption Caption text of the prompt
* @param[in] max_output_len (Optional) Maximum string length of the input text
* @param[in] default_input (Optional) Default input text
*/
char *popup_input(int flags, const char *caption, int max_output_len = -1, const char *default_input = "");
int popup_active();
+89 -24
View File
@@ -80,52 +80,117 @@ ADE_FUNC(isMouseButtonDown,
return ade_set_args(L, "b", rtn);
}
static int AxisInverted_sub(Joy_axis_index joy_axis, lua_State* L)
static int AxisActionInverted_sub(int AxisAction, lua_State* L)
{
Assertion(joy_axis == JOY_X_AXIS || joy_axis == JOY_Y_AXIS || joy_axis == JOY_Z_AXIS, "Only the X, Y, and Z axes are supported for now.");
// z64: If so desired, it may be possible to have two inversion states, one set by the player in the config menu,
// and one set by scripting. Essentially, the CCI will have its own inversion flag in addition to the inversion
// flags to both bindings. Thus, its possible for the player to invert one binding while keeping the other
// binding normal instead of having the script try to set each bind's inversion state individually.
bool b;
if (!ade_get_args(L, "*|b", &b))
return ade_set_error(L, "b", false);
int axis = -1;
for (int i = 0; i < NUM_JOY_AXIS_ACTIONS; ++i)
{
if (Axis_map_to[i] == joy_axis)
{
axis = i;
break;
}
}
if ((AxisAction < JOY_HEADING_AXIS) || (AxisAction > JOY_REL_THROTTLE_AXIS))
return ade_set_error(L, "b", false); // invalid IoActionId
if (axis < 0)
{
LuaError(L, "Control axis %d not found!", static_cast<int>(joy_axis));
return ADE_RETURN_NIL;
}
auto& item = Control_config[AxisAction];
if (ADE_SETTING_VAR)
Invert_axis[axis] = b ? 1 : 0;
item.invert(b);
if (Invert_axis[axis] != 0)
if (item.is_inverted())
return ADE_RETURN_TRUE;
else
return ADE_RETURN_FALSE;
}
ADE_VIRTVAR(XAxisInverted, l_Mouse, "boolean inverted", "Gets or sets whether the X-axis is inverted", "boolean", "True/false")
ADE_VIRTVAR_DEPRECATED(XAxisInverted, l_Mouse, "boolean inverted", "Gets or sets whether the X-axis action is inverted", "boolean", "True/false", gameversion::version(21, 6), "Deprecated in favor of HeadingAxisInverted")
{
return AxisInverted_sub(JOY_X_AXIS, L);
return AxisActionInverted_sub(JOY_HEADING_AXIS, L);
}
ADE_VIRTVAR(YAxisInverted, l_Mouse, "boolean inverted", "Gets or sets whether the Y-axis is inverted", "boolean", "True/false")
ADE_VIRTVAR_DEPRECATED(YAxisInverted, l_Mouse, "boolean inverted", "Gets or sets whether the Y-axis action is inverted", "boolean", "True/false", gameversion::version(21, 6), "Deprecated in favor of PitchAxisInverted")
{
return AxisInverted_sub(JOY_Y_AXIS, L);
return AxisActionInverted_sub(JOY_PITCH_AXIS, L);
}
ADE_VIRTVAR(ZAxisInverted, l_Mouse, "boolean inverted", "Gets or sets whether the Z-axis is inverted", "boolean", "True/false")
ADE_VIRTVAR_DEPRECATED(ZAxisInverted, l_Mouse, "boolean inverted", "Gets or sets whether the Z-axis action is inverted", "boolean", "True/false", gameversion::version(21, 6), "Deprecated in favor of BankAxisInverted")
{
return AxisInverted_sub(JOY_Z_AXIS, L);
return AxisActionInverted_sub(JOY_BANK_AXIS, L);
}
ADE_VIRTVAR(HeadingAxisInverted, l_Mouse, "boolean inverted", "Gets or sets whether the heading axis action is inverted", "boolean", "True/false")
{
return AxisActionInverted_sub(JOY_HEADING_AXIS, L);
}
ADE_VIRTVAR(PitchAxisInverted, l_Mouse, "boolean inverted", "Gets or sets whether the pitch axis action is inverted", "boolean", "True/false")
{
return AxisActionInverted_sub(JOY_PITCH_AXIS, L);
}
ADE_VIRTVAR(BankAxisInverted, l_Mouse, "boolean inverted", "Gets or sets whether the bank axis action is inverted", "boolean", "True/false")
{
return AxisActionInverted_sub(JOY_BANK_AXIS, L);
}
ADE_VIRTVAR(AbsoluteThrottleAxisInverted, l_Mouse, "boolean inverted", "Gets or sets whether the absolute throttle axis action is inverted", "boolean", "True/false")
{
return AxisActionInverted_sub(JOY_ABS_THROTTLE_AXIS, L);
}
ADE_VIRTVAR(RelativeThrottleAxisInverted, l_Mouse, "boolean inverted", "Gets or sets whether the relative throttle axis action is inverted", "boolean", "True/false")
{
return AxisActionInverted_sub(JOY_REL_THROTTLE_AXIS, L);
}
ADE_FUNC(AxisInverted, l_Mouse, "number cid, number axis, boolean inverted", "Gets or sets the given Joystick or Mouse axis inversion state. Mouse cid = -1, Joystick cid = [0, 3]", "boolean", "True/false")
{
int joy = CID_NONE;
int axis = -1;
bool inverted = false;
int n = ade_get_args(L, "ii|b", &joy, &axis, &inverted);
if (n == 0)
return ade_set_error(L, "b", false); // no arguments passed
if ((joy < CID_JOY0) || (CID_JOY_MAX <= joy) || (joy != CID_MOUSE))
return ade_set_error(L, "b", false); // Invalid cid
if ((axis < 0) || (axis > JOY_NUM_AXES))
return ade_set_error(L, "b", false); // invalid axis
// Find the binding that has the given joy.
// This is a bit obtuse since inversion is on the config side instead of on the joystick side
CC_bind *A = nullptr;
for (int i = JOY_HEADING_AXIS; i <= JOY_REL_THROTTLE_AXIS; ++i)
{
CC_bind B(static_cast<CID>(joy), static_cast<short>(axis), CCF_AXIS);
A = Control_config[i].find(B);
if (A != nullptr)
break;
// the binding we're looking for could be inverted
CC_bind C(static_cast<CID>(joy), static_cast<short>(axis), CCF_AXIS | CCF_INVERTED);
A = Control_config[i].find(C);
if (A != nullptr)
break;
}
// TODO Should this be an error or silent false?
if (A == nullptr)
return ade_set_error(L, "b", false); // Axis not bound
if (n > 2)
{
A->invert(inverted);
}
if (A->is_inverted())
return ADE_RETURN_TRUE;
else
return ADE_RETURN_FALSE;
}
ADE_FUNC(setCursorImage, l_Mouse, "string filename", "Sets mouse cursor image, and allows you to lock/unlock the image. (A locked cursor may only be changed with the unlock parameter)", "boolean", "true if successful, false otherwise")
+2
View File
@@ -145,6 +145,8 @@ add_file_folder("ControlConfig"
controlconfig/controlsconfig.cpp
controlconfig/controlsconfig.h
controlconfig/controlsconfigcommon.cpp
controlconfig/presets.cpp
controlconfig/presets.h
)
# Cutscene files
+3 -3
View File
@@ -4412,14 +4412,14 @@ int game_poll()
int k = key_inkey();
// Move the mouse cursor with the joystick.
// Move the mouse cursor with the joystick. Currently uses Joystick0
if (os_foreground() && !io::mouse::CursorManager::get()->isCursorShown() && (Use_joy_mouse)) {
// Move the mouse cursor with the joystick
int mx, my;
int raw_axis[2];
joystick_read_raw_axis(2, raw_axis);
joystick_read_raw_axis(CID_JOY0, 2, raw_axis);
int jx = joy_get_scaled_reading(raw_axis[0]);
int jy = joy_get_scaled_reading(raw_axis[1]);
@@ -4432,7 +4432,7 @@ int game_poll()
mouse_set_pos( mx+dx, my+dy );
}
int j = joy_down(0);
int j = joy_down(CC_bind(CID_JOY0, 0));
int m = mouse_down(MOUSE_LEFT_BUTTON);
if ( j != Joymouse_button_status ) {