#include "recomp_ui.h" #include "recomp_input.h" #include "recomp_sound.h" #include "recomp_config.h" #include "recomp_debug.h" #include "promptfont.h" #include "../../ultramodern/config.hpp" #include "../../ultramodern/ultramodern.hpp" #include "RmlUi/Core.h" #include "rt64_layer.h" ultramodern::GraphicsConfig new_options; Rml::DataModelHandle nav_help_model_handle; Rml::DataModelHandle general_model_handle; Rml::DataModelHandle controls_model_handle; Rml::DataModelHandle graphics_model_handle; Rml::DataModelHandle sound_options_model_handle; // True if controller config menu is open, false if keyboard config menu is open, undefined otherwise bool configuring_controller = false; template void get_option(const T& input, Rml::Variant& output) { std::string value = ""; to_json(value, input); if (value.empty()) { throw std::runtime_error("Invalid value :" + std::to_string(int(input))); } output = value; } template void set_option(T& output, const Rml::Variant& input) { T value = T::OptionCount; from_json(input.Get(), value); if (value == T::OptionCount) { throw std::runtime_error("Invalid value :" + input.Get()); } output = value; } template void bind_option(Rml::DataModelConstructor& constructor, const std::string& name, T* option) { constructor.BindFunc(name, [option](Rml::Variant& out) { get_option(*option, out); }, [option](const Rml::Variant& in) { set_option(*option, in); graphics_model_handle.DirtyVariable("options_changed"); graphics_model_handle.DirtyVariable("ds_info"); } ); }; template void bind_atomic(Rml::DataModelConstructor& constructor, Rml::DataModelHandle handle, const char* name, std::atomic* atomic_val) { constructor.BindFunc(name, [atomic_val](Rml::Variant& out) { out = atomic_val->load(); }, [atomic_val, handle, name](const Rml::Variant& in) mutable { atomic_val->store(in.Get()); handle.DirtyVariable(name); } ); } static int scanned_binding_index = -1; static int scanned_input_index = -1; static int focused_input_index = -1; static int focused_config_option_index = -1; static bool msaa2x_supported = false; static bool msaa4x_supported = false; static bool msaa8x_supported = false; static bool sample_positions_supported = false; static bool cont_active = true; static recomp::InputDevice cur_device = recomp::InputDevice::Controller; void recomp::finish_scanning_input(recomp::InputField scanned_field) { recomp::set_input_binding(static_cast(scanned_input_index), scanned_binding_index, cur_device, scanned_field); scanned_input_index = -1; scanned_binding_index = -1; controls_model_handle.DirtyVariable("inputs"); controls_model_handle.DirtyVariable("active_binding_input"); controls_model_handle.DirtyVariable("active_binding_slot"); } void recomp::cancel_scanning_input() { recomp::stop_scanning_input(); scanned_input_index = -1; scanned_binding_index = -1; controls_model_handle.DirtyVariable("inputs"); controls_model_handle.DirtyVariable("active_binding_input"); controls_model_handle.DirtyVariable("active_binding_slot"); } void recomp::set_cont_or_kb(bool cont_interacted) { if (nav_help_model_handle && cont_active != cont_interacted) { cont_active = cont_interacted; nav_help_model_handle.DirtyVariable("nav_help__navigate"); nav_help_model_handle.DirtyVariable("nav_help__accept"); nav_help_model_handle.DirtyVariable("nav_help__exit"); } } void close_config_menu() { recomp::save_config(); if (ultramodern::is_game_started()) { recomp::set_current_menu(recomp::Menu::None); } else { recomp::set_current_menu(recomp::Menu::Launcher); } } 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 (general_model_handle) { general_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 (general_model_handle) { general_model_handle.DirtyVariable("targeting_mode"); } } struct SoundOptionsContext { std::atomic bgm_volume; std::atomic low_health_beeps_enabled; // RmlUi doesn't seem to like "true"/"false" strings for setting variants so an int is used here instead. void reset() { bgm_volume = 100; low_health_beeps_enabled = (int)true; } SoundOptionsContext() { reset(); } }; SoundOptionsContext sound_options_context; void recomp::reset_sound_settings() { sound_options_context.reset(); if (sound_options_model_handle) { sound_options_model_handle.DirtyAllVariables(); } } void recomp::set_bgm_volume(int volume) { sound_options_context.bgm_volume.store(volume); if (sound_options_model_handle) { sound_options_model_handle.DirtyVariable("bgm_volume"); } } int recomp::get_bgm_volume() { return sound_options_context.bgm_volume.load(); } void recomp::set_low_health_beeps_enabled(bool enabled) { sound_options_context.low_health_beeps_enabled.store((int)enabled); if (sound_options_model_handle) { sound_options_model_handle.DirtyVariable("low_health_beeps_enabled"); } } bool recomp::get_low_health_beeps_enabled() { return (bool)sound_options_context.low_health_beeps_enabled.load(); } struct DebugContext { Rml::DataModelHandle model_handle; std::vector area_names; std::vector scene_names; std::vector entrance_names; int area_index = 0; int scene_index = 0; int entrance_index = 0; int set_time_day = 1; int set_time_hour = 12; int set_time_minute = 0; bool debug_enabled = false; DebugContext() { for (const auto& area : recomp::game_warps) { area_names.emplace_back(area.name); } update_warp_names(); } void update_warp_names() { scene_names.clear(); for (const auto& scene : recomp::game_warps[area_index].scenes) { scene_names.emplace_back(scene.name); } entrance_names = recomp::game_warps[area_index].scenes[scene_index].entrances; } }; DebugContext debug_context; class ConfigMenu : public recomp::MenuController { public: ConfigMenu() { } ~ConfigMenu() override { } Rml::ElementDocument* load_document(Rml::Context* context) override { return context->LoadDocument("assets/config_menu.rml"); } void register_events(recomp::UiEventListenerInstancer& listener) override { recomp::register_event(listener, "apply_options", [](const std::string& param, Rml::Event& event) { graphics_model_handle.DirtyVariable("options_changed"); ultramodern::set_graphics_config(new_options); }); recomp::register_event(listener, "config_keydown", [](const std::string& param, Rml::Event& event) { if (event.GetId() == Rml::EventId::Keydown) { if (event.GetParameter("key_identifier", Rml::Input::KeyIdentifier::KI_UNKNOWN) == Rml::Input::KeyIdentifier::KI_ESCAPE) { close_config_menu(); } } }); // This needs to be separate from `close_config_menu` so it ensures that the event is only on the target recomp::register_event(listener, "close_config_menu_backdrop", [](const std::string& param, Rml::Event& event) { if (event.GetPhase() == Rml::EventPhase::Target) { close_config_menu(); } }); recomp::register_event(listener, "close_config_menu", [](const std::string& param, Rml::Event& event) { close_config_menu(); }); recomp::register_event(listener, "toggle_input_device", [](const std::string& param, Rml::Event& event) { cur_device = cur_device == recomp::InputDevice::Controller ? recomp::InputDevice::Keyboard : recomp::InputDevice::Controller; controls_model_handle.DirtyVariable("input_device_is_keyboard"); controls_model_handle.DirtyVariable("inputs"); }); recomp::register_event(listener, "area_index_changed", [](const std::string& param, Rml::Event& event) { debug_context.area_index = event.GetParameter("value", 0); debug_context.scene_index = 0; debug_context.entrance_index = 0; debug_context.update_warp_names(); debug_context.model_handle.DirtyVariable("scene_index"); debug_context.model_handle.DirtyVariable("entrance_index"); debug_context.model_handle.DirtyVariable("scene_names"); debug_context.model_handle.DirtyVariable("entrance_names"); }); recomp::register_event(listener, "scene_index_changed", [](const std::string& param, Rml::Event& event) { debug_context.scene_index = event.GetParameter("value", 0); debug_context.entrance_index = 0; debug_context.update_warp_names(); debug_context.model_handle.DirtyVariable("entrance_index"); debug_context.model_handle.DirtyVariable("entrance_names"); }); recomp::register_event(listener, "do_warp", [](const std::string& param, Rml::Event& event) { recomp::do_warp(debug_context.area_index, debug_context.scene_index, debug_context.entrance_index); }); recomp::register_event(listener, "set_time", [](const std::string& param, Rml::Event& event) { recomp::set_time(debug_context.set_time_day, debug_context.set_time_hour, debug_context.set_time_minute); }); } void bind_config_list_events(Rml::DataModelConstructor &constructor) { constructor.BindEventCallback("set_cur_config_index", [](Rml::DataModelHandle model_handle, Rml::Event& event, const Rml::VariantList& inputs) { int option_index = inputs.at(0).Get(); // watch for mouseout being overzealous during event bubbling, only clear if the event's attached element matches the current if (option_index == -1 && event.GetType() == "mouseout" && event.GetCurrentElement() != event.GetTargetElement()) { return; } focused_config_option_index = option_index; model_handle.DirtyVariable("cur_config_index"); }); constructor.Bind("cur_config_index", &focused_config_option_index); } void make_graphics_bindings(Rml::Context* context) { Rml::DataModelConstructor constructor = context->CreateDataModel("graphics_model"); if (!constructor) { throw std::runtime_error("Failed to make RmlUi data model for the graphics config menu"); } ultramodern::sleep_milliseconds(50); new_options = ultramodern::get_graphics_config(); bind_config_list_events(constructor); constructor.BindFunc("res_option", [](Rml::Variant& out) { get_option(new_options.res_option, out); }, [](const Rml::Variant& in) { set_option(new_options.res_option, in); graphics_model_handle.DirtyVariable("options_changed"); graphics_model_handle.DirtyVariable("ds_info"); graphics_model_handle.DirtyVariable("ds_option"); } ); bind_option(constructor, "wm_option", &new_options.wm_option); bind_option(constructor, "ar_option", &new_options.ar_option); bind_option(constructor, "hr_option", &new_options.hr_option); bind_option(constructor, "msaa_option", &new_options.msaa_option); bind_option(constructor, "rr_option", &new_options.rr_option); constructor.BindFunc("rr_manual_value", [](Rml::Variant& out) { out = new_options.rr_manual_value; }, [](const Rml::Variant& in) { new_options.rr_manual_value = in.Get(); graphics_model_handle.DirtyVariable("options_changed"); }); constructor.BindFunc("ds_option", [](Rml::Variant& out) { if (new_options.res_option == ultramodern::Resolution::Auto) { out = 1; } else { out = new_options.ds_option; } }, [](const Rml::Variant& in) { new_options.ds_option = in.Get(); graphics_model_handle.DirtyVariable("options_changed"); graphics_model_handle.DirtyVariable("ds_info"); }); constructor.BindFunc("options_changed", [](Rml::Variant& out) { out = (ultramodern::get_graphics_config() != new_options); }); constructor.BindFunc("ds_info", [](Rml::Variant& out) { switch (new_options.res_option) { default: case ultramodern::Resolution::Auto: out = "Downsampling is not available at auto resolution"; return; case ultramodern::Resolution::Original: if (new_options.ds_option == 2) { out = "Rendered in 480p and scaled to 240p"; } else if (new_options.ds_option == 4) { out = "Rendered in 960p and scaled to 240p"; } return; case ultramodern::Resolution::Original2x: if (new_options.ds_option == 2) { out = "Rendered in 960p and scaled to 480p"; } else if (new_options.ds_option == 4) { out = "Rendered in 4K and scaled to 480p"; } return; } out = ""; }); constructor.Bind("msaa2x_supported", &msaa2x_supported); constructor.Bind("msaa4x_supported", &msaa4x_supported); constructor.Bind("msaa8x_supported", &msaa8x_supported); constructor.Bind("sample_positions_supported", &sample_positions_supported); graphics_model_handle = constructor.GetModelHandle(); } void make_controls_bindings(Rml::Context* context) { Rml::DataModelConstructor constructor = context->CreateDataModel("controls_model"); if (!constructor) { throw std::runtime_error("Failed to make RmlUi data model for the controls config menu"); } constructor.BindFunc("input_count", [](Rml::Variant& out) { out = recomp::get_num_inputs(); } ); constructor.BindFunc("input_device_is_keyboard", [](Rml::Variant& out) { out = cur_device == recomp::InputDevice::Keyboard; } ); constructor.RegisterTransformFunc("get_input_name", [](const Rml::VariantList& inputs) { return Rml::Variant{recomp::get_input_name(static_cast(inputs.at(0).Get()))}; }); constructor.RegisterTransformFunc("get_input_enum_name", [](const Rml::VariantList& inputs) { return Rml::Variant{recomp::get_input_enum_name(static_cast(inputs.at(0).Get()))}; }); constructor.BindEventCallback("set_input_binding", [](Rml::DataModelHandle model_handle, Rml::Event& event, const Rml::VariantList& inputs) { scanned_input_index = inputs.at(0).Get(); scanned_binding_index = inputs.at(1).Get(); recomp::start_scanning_input(cur_device); model_handle.DirtyVariable("active_binding_input"); model_handle.DirtyVariable("active_binding_slot"); }); constructor.BindEventCallback("clear_input_bindings", [](Rml::DataModelHandle model_handle, Rml::Event& event, const Rml::VariantList& inputs) { recomp::GameInput input = static_cast(inputs.at(0).Get()); for (size_t binding_index = 0; binding_index < recomp::bindings_per_input; binding_index++) { recomp::set_input_binding(input, binding_index, cur_device, recomp::InputField{}); } model_handle.DirtyVariable("inputs"); }); constructor.BindEventCallback("set_input_row_focus", [](Rml::DataModelHandle model_handle, Rml::Event& event, const Rml::VariantList& inputs) { int input_index = inputs.at(0).Get(); // watch for mouseout being overzealous during event bubbling, only clear if the event's attached element matches the current if (input_index == -1 && event.GetType() == "mouseout" && event.GetCurrentElement() != event.GetTargetElement()) { return; } focused_input_index = input_index; model_handle.DirtyVariable("cur_input_row"); }); // Rml variable definition for an individual InputField. struct InputFieldVariableDefinition : public Rml::VariableDefinition { InputFieldVariableDefinition() : Rml::VariableDefinition(Rml::DataVariableType::Scalar) {} virtual bool Get(void* ptr, Rml::Variant& variant) override { variant = reinterpret_cast(ptr)->to_string(); return true; } virtual bool Set(void* ptr, const Rml::Variant& variant) override { return false; } }; // Static instance of the InputField variable definition to have a pointer to return to RmlUi. static InputFieldVariableDefinition input_field_definition_instance{}; // Rml variable definition for an array of InputField values (e.g. all the bindings for a single input). struct BindingContainerVariableDefinition : public Rml::VariableDefinition { BindingContainerVariableDefinition() : Rml::VariableDefinition(Rml::DataVariableType::Array) {} virtual bool Get(void* ptr, Rml::Variant& variant) override { return false; } virtual bool Set(void* ptr, const Rml::Variant& variant) override { return false; } virtual int Size(void* ptr) override { return recomp::bindings_per_input; } virtual Rml::DataVariable Child(void* ptr, const Rml::DataAddressEntry& address) override { recomp::GameInput input = static_cast((uintptr_t)ptr); return Rml::DataVariable{&input_field_definition_instance, &recomp::get_input_binding(input, address.index, cur_device)}; } }; // Static instance of the InputField array variable definition to have a fixed pointer to return to RmlUi. static BindingContainerVariableDefinition binding_container_var_instance{}; // Rml variable definition for an array of an array of InputField values (e.g. all the bindings for all inputs). struct BindingArrayContainerVariableDefinition : public Rml::VariableDefinition { BindingArrayContainerVariableDefinition() : Rml::VariableDefinition(Rml::DataVariableType::Array) {} virtual bool Get(void* ptr, Rml::Variant& variant) override { return false; } virtual bool Set(void* ptr, const Rml::Variant& variant) override { return false; } virtual int Size(void* ptr) override { return recomp::get_num_inputs(); } virtual Rml::DataVariable Child(void* ptr, const Rml::DataAddressEntry& address) override { // Encode the input index as the pointer to avoid needing to do any allocations. return Rml::DataVariable(&binding_container_var_instance, (void*)(uintptr_t)address.index); } }; // Static instance of the BindingArrayContainerVariableDefinition variable definition to have a fixed pointer to return to RmlUi. static BindingArrayContainerVariableDefinition binding_array_var_instance{}; struct InputContainerVariableDefinition : public Rml::VariableDefinition { InputContainerVariableDefinition() : Rml::VariableDefinition(Rml::DataVariableType::Struct) {} virtual bool Get(void* ptr, Rml::Variant& variant) override { return true; } virtual bool Set(void* ptr, const Rml::Variant& variant) override { return false; } virtual int Size(void* ptr) override { return recomp::get_num_inputs(); } virtual Rml::DataVariable Child(void* ptr, const Rml::DataAddressEntry& address) override { if (address.name == "array") { return Rml::DataVariable(&binding_array_var_instance, nullptr); } else { recomp::GameInput input = recomp::get_input_from_enum_name(address.name); if (input != recomp::GameInput::COUNT) { return Rml::DataVariable(&binding_container_var_instance, (void*)(uintptr_t)input); } } return Rml::DataVariable{}; } }; // Dummy type to associate with the variable definition. struct InputContainer {}; constructor.RegisterCustomDataVariableDefinition(Rml::MakeUnique()); // Dummy instance of the dummy type to bind to the variable. static InputContainer dummy_container; constructor.Bind("inputs", &dummy_container); constructor.BindFunc("cur_input_row", [](Rml::Variant& out) { if (focused_input_index == -1) { out = "NONE"; } else { out = recomp::get_input_enum_name(static_cast(focused_input_index)); } }); constructor.BindFunc("active_binding_input", [](Rml::Variant& out) { if (scanned_input_index == -1) { out = "NONE"; } else { out = recomp::get_input_enum_name(static_cast(scanned_input_index)); } }); constructor.Bind("active_binding_slot", &scanned_binding_index); controls_model_handle = constructor.GetModelHandle(); } void make_nav_help_bindings(Rml::Context* context) { Rml::DataModelConstructor constructor = context->CreateDataModel("nav_help_model"); if (!constructor) { throw std::runtime_error("Failed to make RmlUi data model for nav help"); } constructor.BindFunc("nav_help__navigate", [](Rml::Variant& out) { if (cont_active) { out = PF_DPAD; } else { out = PF_KEYBOARD_ARROWS PF_KEYBOARD_TAB; } }); constructor.BindFunc("nav_help__accept", [](Rml::Variant& out) { if (cont_active) { out = PF_GAMEPAD_A; } else { out = PF_KEYBOARD_ENTER; } }); constructor.BindFunc("nav_help__exit", [](Rml::Variant& out) { if (cont_active) { out = PF_XBOX_VIEW; } else { out = PF_KEYBOARD_ESCAPE; } }); nav_help_model_handle = constructor.GetModelHandle(); } void make_general_bindings(Rml::Context* context) { Rml::DataModelConstructor constructor = context->CreateDataModel("general_model"); if (!constructor) { throw std::runtime_error("Failed to make RmlUi data model for the control options menu"); } bind_config_list_events(constructor); constructor.Bind("rumble_strength", &control_options_context.rumble_strength); bind_option(constructor, "targeting_mode", &control_options_context.targeting_mode); general_model_handle = constructor.GetModelHandle(); } void make_sound_options_bindings(Rml::Context* context) { Rml::DataModelConstructor constructor = context->CreateDataModel("sound_options_model"); if (!constructor) { throw std::runtime_error("Failed to make RmlUi data model for the sound options menu"); } bind_config_list_events(constructor); sound_options_model_handle = constructor.GetModelHandle(); bind_atomic(constructor, sound_options_model_handle, "bgm_volume", &sound_options_context.bgm_volume); bind_atomic(constructor, sound_options_model_handle, "low_health_beeps_enabled", &sound_options_context.low_health_beeps_enabled); } void make_debug_bindings(Rml::Context* context) { Rml::DataModelConstructor constructor = context->CreateDataModel("debug_model"); if (!constructor) { throw std::runtime_error("Failed to make RmlUi data model for the debug menu"); } bind_config_list_events(constructor); // Bind the debug mode enabled flag. constructor.Bind("debug_enabled", &debug_context.debug_enabled); // Register the array type for string vectors. constructor.RegisterArray>(); // Bind the warp parameter indices constructor.Bind("area_index", &debug_context.area_index); constructor.Bind("scene_index", &debug_context.scene_index); constructor.Bind("entrance_index", &debug_context.entrance_index); // Bind the vectors for warp names constructor.Bind("area_names", &debug_context.area_names); constructor.Bind("scene_names", &debug_context.scene_names); constructor.Bind("entrance_names", &debug_context.entrance_names); constructor.Bind("debug_time_day", &debug_context.set_time_day); constructor.Bind("debug_time_hour", &debug_context.set_time_hour); constructor.Bind("debug_time_minute", &debug_context.set_time_minute); debug_context.model_handle = constructor.GetModelHandle(); } void make_bindings(Rml::Context* context) override { make_nav_help_bindings(context); make_general_bindings(context); make_controls_bindings(context); make_graphics_bindings(context); make_sound_options_bindings(context); make_debug_bindings(context); } }; std::unique_ptr recomp::create_config_menu() { return std::make_unique(); } bool recomp::get_debug_mode_enabled() { return debug_context.debug_enabled; } void recomp::set_debug_mode_enabled(bool enabled) { debug_context.debug_enabled = enabled; if (debug_context.model_handle) { debug_context.model_handle.DirtyVariable("debug_enabled"); } } void recomp::update_supported_options() { msaa2x_supported = ultramodern::RT64MaxMSAA() >= RT64::UserConfiguration::Antialiasing::MSAA2X; msaa4x_supported = ultramodern::RT64MaxMSAA() >= RT64::UserConfiguration::Antialiasing::MSAA4X; msaa8x_supported = ultramodern::RT64MaxMSAA() >= RT64::UserConfiguration::Antialiasing::MSAA8X; sample_positions_supported = ultramodern::RT64SamplePositionsSupported(); new_options = ultramodern::get_graphics_config(); graphics_model_handle.DirtyAllVariables(); } void recomp::toggle_fullscreen() { new_options.wm_option = (new_options.wm_option == ultramodern::WindowMode::Windowed) ? ultramodern::WindowMode::Fullscreen : ultramodern::WindowMode::Windowed; ultramodern::set_graphics_config(new_options); graphics_model_handle.DirtyVariable("wm_option"); }