diff --git a/assets/config_menu.rml b/assets/config_menu.rml index 67eb999..696852a 100644 --- a/assets/config_menu.rml +++ b/assets/config_menu.rml @@ -28,6 +28,7 @@ + @@ -51,6 +52,13 @@ + + Control Options + + + + + Sound diff --git a/assets/config_menu/control_options.rml b/assets/config_menu/control_options.rml new file mode 100644 index 0000000..dfbab6b --- /dev/null +++ b/assets/config_menu/control_options.rml @@ -0,0 +1,28 @@ + + + + + + + + + Targeting Mode + + + Switch + + Hold + + + + Rumble Strength + + {{rumble_strength}} + + + + + + + + diff --git a/include/recomp_input.h b/include/recomp_input.h index 8c51221..b27faa0 100644 --- a/include/recomp_input.h +++ b/include/recomp_input.h @@ -9,6 +9,8 @@ #include #include +#include "json/json.hpp" + namespace recomp { // x-macros to build input enums and arrays. // First parameter is the enum name, second parameter is the bit field for the input (or 0 if there is no associated one), third is the readable name. @@ -112,7 +114,26 @@ namespace recomp { void set_input_binding(GameInput input, size_t binding_index, InputDevice device, InputField value); void get_n64_input(uint16_t* buttons_out, float* x_out, float* y_out); + void set_rumble(bool); void handle_events(); + + // Rumble strength ranges from 0 to 100. + int get_rumble_strength(); + void set_rumble_strength(int strength); + + enum class TargetingMode { + Switch, + Hold, + OptionCount + }; + + NLOHMANN_JSON_SERIALIZE_ENUM(recomp::TargetingMode, { + {recomp::TargetingMode::Switch, "Switch"}, + {recomp::TargetingMode::Hold, "Hold"} + }); + + TargetingMode get_targeting_mode(); + void set_targeting_mode(TargetingMode mode); bool game_input_disabled(); bool all_input_disabled(); diff --git a/patches/input.h b/patches/input.h index ac53e46..b0d5154 100644 --- a/patches/input.h +++ b/patches/input.h @@ -11,5 +11,6 @@ typedef enum { extern RecompCameraMode recomp_camera_mode; DECLARE_FUNC(void, recomp_get_gyro_deltas, float* x, float* y); +DECLARE_FUNC(int, recomp_get_targeting_mode); #endif diff --git a/patches/play_patches.c b/patches/play_patches.c index 804a351..6fb040e 100644 --- a/patches/play_patches.c +++ b/patches/play_patches.c @@ -1,8 +1,13 @@ #include "play_patches.h" #include "z64debug_display.h" +#include "input.h" extern Input D_801F6C18; +void controls_play_update(PlayState* play) { + gSaveContext.options.zTargetSetting = recomp_get_targeting_mode(); +} + // @recomp Patched to add hooks for various added functionality. void Play_Main(GameState* thisx) { static Input* prevInput = NULL; @@ -10,6 +15,7 @@ void Play_Main(GameState* thisx) { // @recomp debug_play_update(this); + controls_play_update(this); // @recomp avoid unused variable warning (void)prevInput; diff --git a/patches/syms.ld b/patches/syms.ld index 3202b30..d2d5b4e 100644 --- a/patches/syms.ld +++ b/patches/syms.ld @@ -4,14 +4,15 @@ __start = 0x80000000; sSceneEntranceTable = 0x801C5720; /* Dummy addresses that get recompiled into function calls */ -recomp_puts = 0x81000000; -recomp_exit = 0x81000004; -recomp_handle_quicksave_actions = 0x81000008; -recomp_handle_quicksave_actions_main = 0x8100000C; -osRecvMesg_recomp = 0x81000010; -osSendMesg_recomp = 0x81000014; -recomp_get_gyro_deltas = 0x81000018; -recomp_get_aspect_ratio = 0x8100001C; -recomp_get_pending_warp = 0x81000020; -recomp_powf = 0x81000024; -recomp_get_target_framerate = 0x81000028; +recomp_puts = 0x8F000000; +recomp_exit = 0x8F000004; +recomp_handle_quicksave_actions = 0x8F000008; +recomp_handle_quicksave_actions_main = 0x8F00000C; +osRecvMesg_recomp = 0x8F000010; +osSendMesg_recomp = 0x8F000014; +recomp_get_gyro_deltas = 0x8F000018; +recomp_get_aspect_ratio = 0x8F00001C; +recomp_get_pending_warp = 0x8F000020; +recomp_powf = 0x8F000024; +recomp_get_target_framerate = 0x8F000028; +recomp_get_targeting_mode = 0x8F00002C; diff --git a/src/game/config.cpp b/src/game/config.cpp index 8b227e1..c66e858 100644 --- a/src/game/config.cpp +++ b/src/game/config.cpp @@ -23,6 +23,17 @@ constexpr auto rr_default = RT64::UserConfiguration::RefreshRate::Di constexpr int rr_manual_default = 60; constexpr bool developer_mode_default = false; +template +void from_or_default(const json& j, const std::string& key, T& out, T default_value) { + auto find_it = j.find(key); + if (find_it != j.end()) { + find_it->get_to(out); + } + else { + out = default_value; + } +} + namespace ultramodern { void to_json(json& j, const GraphicsConfig& config) { j = json{ @@ -36,17 +47,6 @@ namespace ultramodern { }; } - template - void from_or_default(const json& j, const std::string& key, T& out, T default_value) { - auto find_it = j.find(key); - if (find_it != j.end()) { - find_it->get_to(out); - } - else { - out = default_value; - } - } - void from_json(const json& j, GraphicsConfig& config) { from_or_default(j, "res_option", config.res_option, res_default); from_or_default(j, "wm_option", config.wm_option, wm_default); @@ -171,6 +171,11 @@ void add_input_bindings(nlohmann::json& out, recomp::GameInput input, recomp::In void save_controls_config(const std::filesystem::path& path) { nlohmann::json config_json{}; + + config_json["options"] = {}; + recomp::to_json(config_json["options"]["targeting_mode"], recomp::get_targeting_mode()); + config_json["options"]["rumble_strength"] = recomp::get_rumble_strength(); + config_json["keyboard"] = {}; config_json["controller"] = {}; @@ -221,6 +226,14 @@ void load_controls_config(const std::filesystem::path& path) { nlohmann::json config_json{}; config_file >> config_json; + + recomp::TargetingMode targeting_mode; + from_or_default(config_json["options"], "targeting_mode", targeting_mode, recomp::TargetingMode::Switch); + recomp::set_targeting_mode(targeting_mode); + + int rumble_strength; + from_or_default(config_json["options"], "rumble_strength", rumble_strength, 25); + recomp::set_rumble_strength(rumble_strength); if (!load_input_device_from_json(config_json, recomp::InputDevice::Keyboard, "keyboard")) { assign_all_mappings(recomp::InputDevice::Keyboard, recomp::default_n64_keyboard_mappings); diff --git a/src/game/input.cpp b/src/game/input.cpp index e45d1ac..39d3193 100644 --- a/src/game/input.cpp +++ b/src/game/input.cpp @@ -345,6 +345,14 @@ void recomp::poll_inputs() { #endif } +void recomp::set_rumble(bool on) { + uint16_t rumble_strength = recomp::get_rumble_strength() * 0xFFFF / 100; + uint32_t duration = 1000000; // Dummy duration value that lasts long enough to matter as the game will reset rumble on its own. + for (const auto& controller : InputState.cur_controllers) { + SDL_GameControllerRumble(controller, 0, on ? rumble_strength : 0, duration); + } +} + bool controller_button_state(int32_t input_id) { if (input_id >= 0 && input_id < SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_MAX) { SDL_GameControllerButton button = (SDL_GameControllerButton)input_id; diff --git a/src/game/recomp_api.cpp b/src/game/recomp_api.cpp index 5e54d6d..dbb88b5 100644 --- a/src/game/recomp_api.cpp +++ b/src/game/recomp_api.cpp @@ -62,3 +62,7 @@ extern "C" void recomp_get_aspect_ratio(uint8_t* rdram, recomp_context* ctx) { return; } } + +extern "C" void recomp_get_targeting_mode(uint8_t* rdram, recomp_context* ctx) { + _return(ctx, static_cast(recomp::get_targeting_mode())); +} diff --git a/src/main/main.cpp b/src/main/main.cpp index f73bf78..f5c862a 100644 --- a/src/main/main.cpp +++ b/src/main/main.cpp @@ -253,6 +253,7 @@ int main(int argc, char** argv) { ultramodern::input_callbacks_t input_callbacks{ .poll_input = recomp::poll_inputs, .get_input = recomp::get_n64_input, + .set_rumble = recomp::set_rumble, }; recomp::start({}, audio_callbacks, input_callbacks, gfx_callbacks); diff --git a/src/recomp/cont.cpp b/src/recomp/cont.cpp index b279eac..473964e 100644 --- a/src/recomp/cont.cpp +++ b/src/recomp/cont.cpp @@ -75,7 +75,7 @@ extern "C" void osContGetQuery_recomp(uint8_t * rdram, recomp_context * ctx) { // Mark controller 0 as present MEM_H(0, status) = 0x0005; // type: CONT_TYPE_NORMAL (from joybus) - MEM_B(2, status) = 0x00; // status: 0 (from joybus) + MEM_B(2, status) = 0x01; // status: 0x01 (from joybus, indicates that a pak is plugged into the controller) MEM_B(3, status) = 0x00; // errno: 0 (from libultra) // Mark controllers 1-3 as not connected @@ -91,17 +91,46 @@ extern "C" void osContSetCh_recomp(uint8_t* rdram, recomp_context* ctx) { } extern "C" void __osMotorAccess_recomp(uint8_t* rdram, recomp_context* ctx) { + PTR(void) pfs = _arg<0, PTR(void)>(rdram, ctx); + s32 flag = _arg<1, s32>(rdram, ctx); + s32 channel = MEM_W(8, pfs); + // Only respect accesses to controller 0. + if (channel == 0) { + input_callbacks.set_rumble(flag); + } + + _return(ctx, 0); } extern "C" void osMotorInit_recomp(uint8_t* rdram, recomp_context* ctx) { - ; + PTR(void) pfs = _arg<1, PTR(void)>(rdram, ctx); + s32 channel = _arg<2, s32>(rdram, ctx); + MEM_W(8, pfs) = channel; + + _return(ctx, 0); } extern "C" void osMotorStart_recomp(uint8_t* rdram, recomp_context* ctx) { - ; + PTR(void) pfs = _arg<0, PTR(void)>(rdram, ctx); + s32 channel = MEM_W(8, pfs); + + // Only respect accesses to controller 0. + if (channel == 0) { + input_callbacks.set_rumble(true); + } + + _return(ctx, 0); } extern "C" void osMotorStop_recomp(uint8_t* rdram, recomp_context* ctx) { - ; + PTR(void) pfs = _arg<0, PTR(void)>(rdram, ctx); + s32 channel = MEM_W(8, pfs); + + // Only respect accesses to controller 0. + if (channel == 0) { + input_callbacks.set_rumble(false); + } + + _return(ctx, 0); } diff --git a/src/ui/ui_config.cpp b/src/ui/ui_config.cpp index 7c5a7c3..2a6202f 100644 --- a/src/ui/ui_config.cpp +++ b/src/ui/ui_config.cpp @@ -9,6 +9,7 @@ ultramodern::GraphicsConfig new_options; Rml::DataModelHandle graphics_model_handle; Rml::DataModelHandle controls_model_handle; +Rml::DataModelHandle control_options_model_handle; // True if controller config menu is open, false if keyboard config menu is open, undefined otherwise bool configuring_controller = false; @@ -70,6 +71,35 @@ void close_config_menu() { } } +struct ControlOptionsContext { + int rumble_strength = 50; // 0 to 100 + recomp::TargetingMode targeting_mode = recomp::TargetingMode::Switch; +}; + +ControlOptionsContext control_options_context; + +int recomp::get_rumble_strength() { + return control_options_context.rumble_strength; +} + +void recomp::set_rumble_strength(int strength) { + control_options_context.rumble_strength = strength; + if (control_options_model_handle) { + control_options_model_handle.DirtyVariable("rumble_strength"); + } +} + +recomp::TargetingMode recomp::get_targeting_mode() { + return control_options_context.targeting_mode; +} + +void recomp::set_targeting_mode(recomp::TargetingMode mode) { + control_options_context.targeting_mode = mode; + if (control_options_model_handle) { + control_options_model_handle.DirtyVariable("targeting_mode"); + } +} + struct DebugContext { Rml::DataModelHandle model_handle; std::vector area_names; @@ -345,6 +375,18 @@ public: controls_model_handle = constructor.GetModelHandle(); } + void make_control_options_bindings(Rml::Context* context) { + Rml::DataModelConstructor constructor = context->CreateDataModel("control_options_model"); + if (!constructor) { + throw std::runtime_error("Failed to make RmlUi data model for the control options menu"); + } + + constructor.Bind("rumble_strength", &control_options_context.rumble_strength); + bind_option(constructor, "targeting_mode", &control_options_context.targeting_mode); + + control_options_model_handle = constructor.GetModelHandle(); + } + void make_debug_bindings(Rml::Context* context) { Rml::DataModelConstructor constructor = context->CreateDataModel("debug_model"); if (!constructor) { @@ -370,6 +412,7 @@ public: void make_bindings(Rml::Context* context) override { make_graphics_bindings(context); make_controls_bindings(context); + make_control_options_bindings(context); make_debug_bindings(context); } };