Implemented rumble, added control options menu with rumble strength and targeting mode

This commit is contained in:
Mr-Wiseguy 2024-03-04 02:13:12 -05:00
parent 9ef243bc05
commit 57475d058b
12 changed files with 189 additions and 26 deletions

View File

@ -28,6 +28,7 @@
</style>
<link type="text/template" href="config_menu/graphics.rml" />
<link type="text/template" href="config_menu/controls.rml" />
<link type="text/template" href="config_menu/control_options.rml" />
<link type="text/template" href="config_menu/sound.rml" />
<link type="text/template" href="config_menu/debug.rml" />
</head>
@ -51,6 +52,13 @@
<panel class="config" data-model="controls_model">
<template src="config-menu__controls" />
</panel>
<tab class="tab">
<div>Control Options</div>
<div class="tab__indicator"></div>
</tab>
<panel class="config" data-model="control_options_model">
<template src="config-menu__control-options" />
</panel>
<tab class="tab">
<div>Sound</div>
<div class="tab__indicator"></div>

View File

@ -0,0 +1,28 @@
<template name="config-menu__control-options">
<head>
</head>
<body>
<form class="config__form">
<div class="config__wrapper">
<div class="config__row">
<div class="config-option">
<label class="config-option__title">Targeting Mode</label>
<div class="config-option__list config-option__list">
<input type="radio" name="targeting_mode" data-checked="targeting_mode" value="Switch" id="tm_switch"/>
<label class="config-option__tab-label" for="tm_switch">Switch</label>
<input type="radio" name="targeting_mode" data-checked="targeting_mode" value="Hold" id="tm_hold"/>
<label class="config-option__tab-label" for="tm_hold">Hold</label>
</div>
</div>
<div class="config-option">
<label class="config-option__title">Rumble Strength</label>
<div class="config-option__list config-option__list">
<label class="config-option__range-label">{{rumble_strength}}</label>
<input id="rumble_strength_input" type="range" min="0" max="100" style="flex:1;margin: 0dp;nav-up:auto;nav-down:auto;" data-value="rumble_strength"/>
</div>
</div>
</div>
</div>
</form>
</body>
</template>

View File

@ -9,6 +9,8 @@
#include <string>
#include <string_view>
#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();

View File

@ -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

View File

@ -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;

View File

@ -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;

View File

@ -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 <typename T>
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 <typename T>
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);

View File

@ -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;

View File

@ -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<int>(recomp::get_targeting_mode()));
}

View File

@ -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);

View File

@ -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<s32>(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<s32>(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<s32>(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<s32>(ctx, 0);
}

View File

@ -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<std::string> 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);
}
};