diff --git a/src/game/controls.cpp b/src/game/controls.cpp index 8f316d4..5479406 100644 --- a/src/game/controls.cpp +++ b/src/game/controls.cpp @@ -19,6 +19,7 @@ #include "controls.hpp" #include "resources/resource-manager.hpp" +#include "application.hpp" #include namespace game { @@ -28,6 +29,11 @@ std::string gamepad_calibration_path(const game::context* ctx, const input::game return "gamepad-" + gamepad->get_guid() + ".json"; } +json default_control_profile() +{ + return json(); +} + json default_gamepad_calibration() { const float activation_min = 0.15f; @@ -94,6 +100,251 @@ bool save_gamepad_calibration(const game::context* ctx, const input::gamepad* ga return true; } +void apply_control_profile(game::context* ctx, const json& profile) +{ + // Map gamepad buttons to strings + const std::unordered_map gamepad_button_map = + { + {"a", input::gamepad_button::a}, + {"b", input::gamepad_button::b}, + {"x", input::gamepad_button::x}, + {"y", input::gamepad_button::y}, + {"back", input::gamepad_button::back}, + {"guide", input::gamepad_button::guide}, + {"start", input::gamepad_button::start}, + {"leftstick", input::gamepad_button::left_stick}, + {"rightstick", input::gamepad_button::right_stick}, + {"leftshoulder", input::gamepad_button::left_shoulder}, + {"rightshoulder", input::gamepad_button::right_shoulder}, + {"dpup", input::gamepad_button::dpad_up}, + {"dpdown", input::gamepad_button::dpad_down}, + {"dpleft", input::gamepad_button::dpad_left}, + {"dpright", input::gamepad_button::dpad_right} + }; + + // Map gamepad axes to strings + const std::unordered_map gamepad_axis_map = + { + {"leftx", input::gamepad_axis::left_x}, + {"lefty", input::gamepad_axis::left_y}, + {"rightx", input::gamepad_axis::right_x}, + {"righty", input::gamepad_axis::right_y}, + {"lefttrigger", input::gamepad_axis::left_trigger}, + {"righttrigger", input::gamepad_axis::right_trigger} + }; + + // Remove all existing input mappings + for (auto control = ctx->controls.begin(); control != ctx->controls.end(); ++control) + { + ctx->input_event_router->remove_mappings(control->second); + } + + // Get keyboard and mouse devices + input::keyboard* keyboard = ctx->app->get_keyboard(); + input::mouse* mouse = ctx->app->get_mouse(); + + // Find profile gamepad device + input::gamepad* gamepad = nullptr; + auto gamepad_element = profile.find("gamepad"); + if (gamepad_element != profile.end()) + { + // Get gamepad GUID + const std::string gamepad_guid = gamepad_element->get(); + + // Find gamepad with matching GUID + for (input::gamepad* device: ctx->app->get_gamepads()) + { + if (device->get_guid() == gamepad_guid) + { + gamepad = device; + break; + } + } + } + + // Find controls element + auto controls_element = profile.find("controls"); + if (controls_element != profile.end()) + { + // For each control in the profile + for (auto control_element = controls_element->cbegin(); control_element != controls_element->cend(); ++control_element) + { + // Get the control name + std::string control_name = control_element.key(); + + // Find or create control + input::control* control; + if (ctx->controls.count(control_name)) + { + control = ctx->controls[control_name]; + } + else + { + control = new input::control(); + ctx->controls[control_name] = control; + } + + // For each mapping in the control + for (auto mapping_element = control_element.value().cbegin(); mapping_element != control_element.value().cend(); ++mapping_element) + { + if (!mapping_element->contains("device")) + { + ctx->logger->warning("Control \"" + control_name + "\" not mapped to a device"); + continue; + } + + // Get the mapping device + const std::string device = (*mapping_element)["device"]; + + if (device == "keyboard") + { + // Parse key name + if (!mapping_element->contains("key")) + { + ctx->logger->warning("Control \"" + control_name + "\" has invalid keyboard mapping"); + continue; + } + std::string key = (*mapping_element)["key"].get(); + + // Get scancode from key name + input::scancode scancode = keyboard->get_scancode_from_name(key.c_str()); + if (scancode == input::scancode::unknown) + { + ctx->logger->warning("Control \"" + control_name + "\" mapped to unknown keyboard key \"" + key + "\""); + continue; + } + + // Map control to keyboard key + ctx->input_event_router->add_mapping(input::key_mapping(control, keyboard, scancode)); + + ctx->logger->log("Mapped control \"" + control_name + "\" to keyboard key \"" + key + "\""); + } + else if (device == "mouse") + { + if (mapping_element->contains("button")) + { + // Parse mouse button index + int button = (*mapping_element)["button"].get(); + + // Map control to mouse button + ctx->input_event_router->add_mapping(input::mouse_button_mapping(control, mouse, button)); + + ctx->logger->log("Mapped control \"" + control_name + "\" to mouse button " + std::to_string(button)); + } + else if (mapping_element->contains("wheel")) + { + // Parse mouse wheel axis + std::string wheel = (*mapping_element)["wheel"].get(); + input::mouse_wheel_axis axis; + if (wheel == "x+") + axis = input::mouse_wheel_axis::positive_x; + else if (wheel == "x-") + axis = input::mouse_wheel_axis::negative_x; + else if (wheel == "y+") + axis = input::mouse_wheel_axis::positive_y; + else if (wheel == "y-") + axis = input::mouse_wheel_axis::negative_y; + else + { + ctx->logger->warning("Control \"" + control_name + "\" is mapped to invalid mouse wheel axis \"" + wheel + "\""); + continue; + } + + // Map control to mouse wheel axis + ctx->input_event_router->add_mapping(input::mouse_wheel_mapping(control, mouse, axis)); + + ctx->logger->log("Mapped control \"" + control_name + "\" to mouse wheel axis " + wheel); + } + else if (mapping_element->contains("motion")) + { + std::string motion = (*mapping_element)["motion"].get(); + input::mouse_motion_axis axis; + if (motion == "x+") + axis = input::mouse_motion_axis::positive_x; + else if (motion == "x-") + axis = input::mouse_motion_axis::negative_x; + else if (motion == "y+") + axis = input::mouse_motion_axis::positive_y; + else if (motion == "y-") + axis = input::mouse_motion_axis::negative_y; + else + { + ctx->logger->warning("Control \"" + control_name + "\" is mapped to invalid mouse motion axis \"" + motion + "\""); + continue; + } + + // Map control to mouse motion axis + ctx->input_event_router->add_mapping(input::mouse_motion_mapping(control, mouse, axis)); + + ctx->logger->log("Mapped control \"" + control_name + "\" to mouse motion axis " + motion); + } + else + { + ctx->logger->warning("Control \"" + control_name + "\" has invalid mouse mapping"); + continue; + } + } + else if (device == "gamepad") + { + if (mapping_element->contains("button")) + { + // Parse gamepad button + std::string button = (*mapping_element)["button"].get(); + + auto button_it = gamepad_button_map.find(button); + if (button_it == gamepad_button_map.end()) + { + ctx->logger->warning("Control \"" + control_name + "\" is mapped to invalid gamepad button \"" + button + "\""); + continue; + } + + // Map control to gamepad button + ctx->input_event_router->add_mapping(input::gamepad_button_mapping(control, gamepad, button_it->second)); + + ctx->logger->log("Mapped control \"" + control_name + "\" to gamepad button " + button); + } + else if (mapping_element->contains("axis")) + { + std::string axis = (*mapping_element)["axis"].get(); + + // Parse gamepad axis name + const std::string axis_name = axis.substr(0, axis.length() - 1); + auto axis_it = gamepad_axis_map.find(axis_name); + if (axis_it == gamepad_axis_map.end()) + { + ctx->logger->warning("Control \"" + control_name + "\" is mapped to invalid gamepad axis \"" + axis_name + "\""); + continue; + } + + // Parse gamepad axis sign + const char axis_sign = axis.back(); + if (axis_sign != '-' && axis_sign != '+') + { + ctx->logger->warning("Control \"" + control_name + "\" is mapped to gamepad axis with invalid sign \"" + axis_sign + "\""); + continue; + } + bool axis_negative = (axis_sign == '-'); + + // Map control to gamepad axis + ctx->input_event_router->add_mapping(input::gamepad_axis_mapping(control, gamepad, axis_it->second, axis_negative)); + + ctx->logger->log("Mapped control \"" + control_name + "\" to gamepad axis " + axis); + } + else + { + ctx->logger->log("Control \"" + control_name + "\" has invalid gamepad mapping"); + continue; + } + } + else + { + ctx->logger->warning("Control \"" + control_name + "\" bound to unknown device \"" + device + "\""); + } + } + } + } +} + void apply_gamepad_calibration(input::gamepad* gamepad, const json& calibration) { // Parse and apply activation thresholds diff --git a/src/game/controls.hpp b/src/game/controls.hpp index f35ed9f..c266bc5 100644 --- a/src/game/controls.hpp +++ b/src/game/controls.hpp @@ -26,6 +26,21 @@ namespace game { +/** + * Applies a control profile to the game context. + * + * @param ctx Game context. + * @param profile Control profile. + */ +void apply_control_profile(game::context* ctx, const json& profile); + +/** + * Generates a default control profile. + * + * @return Default control profile. + */ +json default_control_profile(); + /** * Returns a string containing the path to the gamepad calibration file. */ @@ -43,6 +58,7 @@ json default_gamepad_calibration(); * * @param ctx Game context. * @param gamepad Gamepad for which to load calibration settings. + * @return Gamepad calibration settings, or `nullptr` if not loaded. */ json* load_gamepad_calibration(game::context* ctx, input::gamepad* gamepad); diff --git a/src/game/states/loading.cpp b/src/game/states/loading.cpp index 07419bd..c0489f5 100644 --- a/src/game/states/loading.cpp +++ b/src/game/states/loading.cpp @@ -55,7 +55,7 @@ namespace game { namespace state { namespace loading { -/// Creates or loads control configuration +/// Loads control profile and calibrates gamepads static void load_controls(game::context* ctx); /// Creates the universe and solar system. @@ -126,259 +126,21 @@ void exit(game::context* ctx) {} void load_controls(game::context* ctx) -{ - // Allocate known controls - ctx->controls["toggle_fullscreen"] = new input::control(); - ctx->controls["screenshot"] = new input::control(); - ctx->controls["menu_up"] = new input::control(); - ctx->controls["menu_down"] = new input::control(); - ctx->controls["menu_left"] = new input::control(); - ctx->controls["menu_right"] = new input::control(); - ctx->controls["menu_select"] = new input::control(); - ctx->controls["menu_back"] = new input::control(); - ctx->controls["dolly_forward"] = new input::control(); - ctx->controls["dolly_backward"] = new input::control(); - ctx->controls["truck_left"] = new input::control(); - ctx->controls["truck_right"] = new input::control(); - ctx->controls["pedestal_up"] = new input::control(); - ctx->controls["pedestal_down"] = new input::control(); - ctx->controls["move_slow"] = new input::control(); - ctx->controls["move_fast"] = new input::control(); - ctx->controls["mouse_look"] = new input::control(); - ctx->controls["pan_left_gamepad"] = new input::control(); - ctx->controls["pan_left_mouse"] = new input::control(); - ctx->controls["pan_right_gamepad"] = new input::control(); - ctx->controls["pan_right_mouse"] = new input::control(); - ctx->controls["tilt_up_gamepad"] = new input::control(); - ctx->controls["tilt_up_mouse"] = new input::control(); - ctx->controls["tilt_down_gamepad"] = new input::control(); - ctx->controls["tilt_down_mouse"] = new input::control(); - ctx->controls["use_tool"] = new input::control(); - - // Set activation threshold for menu navigation controls to mitigate drifting gamepad axes - const float menu_activation_threshold = 0.1f; - ctx->controls["menu_up"]->set_activation_threshold(menu_activation_threshold); - ctx->controls["menu_down"]->set_activation_threshold(menu_activation_threshold); - ctx->controls["menu_left"]->set_activation_threshold(menu_activation_threshold); - ctx->controls["menu_right"]->set_activation_threshold(menu_activation_threshold); - - // Get keyboard and mouse devices - input::keyboard* keyboard = ctx->app->get_keyboard(); - input::mouse* mouse = ctx->app->get_mouse(); - - const std::unordered_map gamepad_button_map = - { - {"a", input::gamepad_button::a}, - {"b", input::gamepad_button::b}, - {"x", input::gamepad_button::x}, - {"y", input::gamepad_button::y}, - {"back", input::gamepad_button::back}, - {"guide", input::gamepad_button::guide}, - {"start", input::gamepad_button::start}, - {"leftstick", input::gamepad_button::left_stick}, - {"rightstick", input::gamepad_button::right_stick}, - {"leftshoulder", input::gamepad_button::left_shoulder}, - {"rightshoulder", input::gamepad_button::right_shoulder}, - {"dpup", input::gamepad_button::dpad_up}, - {"dpdown", input::gamepad_button::dpad_down}, - {"dpleft", input::gamepad_button::dpad_left}, - {"dpright", input::gamepad_button::dpad_right} - }; - - const std::unordered_map gamepad_axis_map = - { - {"leftx", input::gamepad_axis::left_x}, - {"lefty", input::gamepad_axis::left_y}, - {"rightx", input::gamepad_axis::right_x}, - {"righty", input::gamepad_axis::right_y}, - {"lefttrigger", input::gamepad_axis::left_trigger}, - {"righttrigger", input::gamepad_axis::right_trigger} - }; - - // Check if a control profile is set in the config file +{ + // If a control profile is set in the config file if (ctx->config->contains("control_profile")) { // Load control profile json* profile = ctx->resource_manager->load((*ctx->config)["control_profile"].get()); - // Parse control profile - for (auto it = profile->begin(); it != profile->end(); ++it) + // Apply control profile + if (profile) { - // Parse control name - if (!it->contains("name")) - { - ctx->logger->warning("Unnamed control in control profile"); - continue; - } - std::string name = (*it)["name"].get(); - - // Find or create control - input::control* control; - if (ctx->controls.count(name)) - { - control = ctx->controls[name]; - } - else - { - control = new input::control; - ctx->controls[name] = control; - } - - // Parse control device - if (!it->contains("device")) - { - ctx->logger->warning("Control \"" + name + "\" not mapped to a device"); - continue; - } - std::string device = (*it)["device"].get(); - - if (device == "keyboard") - { - // Parse key name - if (!it->contains("key")) - { - ctx->logger->warning("Control \"" + name + "\" has invalid keyboard mapping"); - continue; - } - std::string key = (*it)["key"].get(); - - // Get scancode from key name - input::scancode scancode = keyboard->get_scancode_from_name(key.c_str()); - if (scancode == input::scancode::unknown) - { - ctx->logger->warning("Control \"" + name + "\" mapped to unknown keyboard key \"" + key + "\""); - continue; - } - - // Map control to keyboard key - ctx->input_event_router->add_mapping(input::key_mapping(control, nullptr, scancode)); - - ctx->logger->log("Mapped control \"" + name + "\" to keyboard key \"" + key + "\""); - } - else if (device == "mouse") - { - if (it->contains("button")) - { - // Parse mouse button index - int button = (*it)["button"].get(); - - // Map control to mouse button - ctx->input_event_router->add_mapping(input::mouse_button_mapping(control, nullptr, button)); - - ctx->logger->log("Mapped control \"" + name + "\" to mouse button " + std::to_string(button)); - } - else if (it->contains("wheel")) - { - // Parse mouse wheel axis - std::string wheel = (*it)["wheel"].get(); - input::mouse_wheel_axis axis; - if (wheel == "x+") - axis = input::mouse_wheel_axis::positive_x; - else if (wheel == "x-") - axis = input::mouse_wheel_axis::negative_x; - else if (wheel == "y+") - axis = input::mouse_wheel_axis::positive_y; - else if (wheel == "y-") - axis = input::mouse_wheel_axis::negative_y; - else - { - ctx->logger->warning("Control \"" + name + "\" is mapped to invalid mouse wheel axis \"" + wheel + "\""); - continue; - } - - // Map control to mouse wheel axis - ctx->input_event_router->add_mapping(input::mouse_wheel_mapping(control, nullptr, axis)); - - ctx->logger->log("Mapped control \"" + name + "\" to mouse wheel axis " + wheel); - } - else if (it->contains("motion")) - { - std::string motion = (*it)["motion"].get(); - input::mouse_motion_axis axis; - if (motion == "x+") - axis = input::mouse_motion_axis::positive_x; - else if (motion == "x-") - axis = input::mouse_motion_axis::negative_x; - else if (motion == "y+") - axis = input::mouse_motion_axis::positive_y; - else if (motion == "y-") - axis = input::mouse_motion_axis::negative_y; - else - { - ctx->logger->warning("Control \"" + name + "\" is mapped to invalid mouse motion axis \"" + motion + "\""); - continue; - } - - // Map control to mouse motion axis - ctx->input_event_router->add_mapping(input::mouse_motion_mapping(control, nullptr, axis)); - - ctx->logger->log("Mapped control \"" + name + "\" to mouse motion axis " + motion); - } - else - { - ctx->logger->warning("Control \"" + name + "\" has invalid mouse mapping"); - continue; - } - } - else if (device == "gamepad") - { - if (it->contains("button")) - { - // Parse gamepad button - std::string button = (*it)["button"].get(); - - auto button_it = gamepad_button_map.find(button); - if (button_it == gamepad_button_map.end()) - { - ctx->logger->warning("Control \"" + name + "\" is mapped to invalid gamepad button \"" + button + "\""); - continue; - } - - // Map control to gamepad button - ctx->input_event_router->add_mapping(input::gamepad_button_mapping(control, nullptr, button_it->second)); - - ctx->logger->log("Mapped control \"" + name + "\" to gamepad button " + button); - } - else if (it->contains("axis")) - { - std::string axis = (*it)["axis"].get(); - - // Parse gamepad axis name - const std::string axis_name = axis.substr(0, axis.length() - 1); - auto axis_it = gamepad_axis_map.find(axis_name); - if (axis_it == gamepad_axis_map.end()) - { - ctx->logger->warning("Control \"" + name + "\" is mapped to invalid gamepad axis \"" + axis_name + "\""); - continue; - } - - // Parse gamepad axis sign - const char axis_sign = axis.back(); - if (axis_sign != '-' && axis_sign != '+') - { - ctx->logger->warning("Control \"" + name + "\" is mapped to gamepad axis with invalid sign \"" + axis_sign + "\""); - continue; - } - bool axis_negative = (axis_sign == '-'); - - // Map control to gamepad axis - ctx->input_event_router->add_mapping(input::gamepad_axis_mapping(control, nullptr, axis_it->second, axis_negative)); - - ctx->logger->log("Mapped control \"" + name + "\" to gamepad axis " + axis); - } - else - { - ctx->logger->log("Control \"" + name + "\" has invalid gamepad mapping"); - continue; - } - } - else - { - ctx->logger->warning("Control \"" + name + "\" bound to unknown device \"" + device + "\""); - } + game::apply_control_profile(ctx, *profile); } } + // Calibrate gamepads for (input::gamepad* gamepad: ctx->app->get_gamepads()) { ctx->logger->push_task("Loading calibration for gamepad " + gamepad->get_guid()); @@ -440,6 +202,13 @@ void load_controls(game::context* ctx) ( std::bind(&application::close, ctx->app, 0) ); + + // Set activation threshold for menu navigation controls to mitigate drifting gamepad axes + const float menu_activation_threshold = 0.1f; + ctx->controls["menu_up"]->set_activation_threshold(menu_activation_threshold); + ctx->controls["menu_down"]->set_activation_threshold(menu_activation_threshold); + ctx->controls["menu_left"]->set_activation_threshold(menu_activation_threshold); + ctx->controls["menu_right"]->set_activation_threshold(menu_activation_threshold); } void cosmogenesis(game::context* ctx)