/* * Copyright (C) 2023 Christopher J. Howard * * This file is part of Antkeeper source code. * * Antkeeper source code is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Antkeeper source code is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Antkeeper source code. If not, see . */ #include "game/controls.hpp" #include "game/graphics.hpp" #include "game/menu.hpp" #include "game/control-profile.hpp" #include "game/states/pause-menu-state.hpp" #include #include #include #include void reset_control_profile(::control_profile& profile) { auto& mappings = profile.mappings; auto& settings = profile.settings; mappings.clear(); settings.clear(); // Fullscreen mappings.emplace("fullscreen", std::make_unique(nullptr, input::scancode::f11, 0, false)); mappings.emplace("fullscreen", std::make_unique(nullptr, input::scancode::enter, input::modifier_key::alt, false)); // Screenshot mappings.emplace("screenshot", std::make_unique(nullptr, input::scancode::f12, 0, false)); mappings.emplace("screenshot", std::make_unique(nullptr, input::scancode::print_screen, 0, false)); // Menu up mappings.emplace("menu_up", std::make_unique(nullptr, input::scancode::up, 0, true)); mappings.emplace("menu_up", std::make_unique(nullptr, input::scancode::w, 0, true)); mappings.emplace("menu_up", std::make_unique(nullptr, input::scancode::i, 0, true)); mappings.emplace("menu_up", std::make_unique(nullptr, input::gamepad_axis::left_stick_y, true)); mappings.emplace("menu_up", std::make_unique(nullptr, input::gamepad_axis::right_stick_y, true)); mappings.emplace("menu_up", std::make_unique(nullptr, input::gamepad_button::dpad_up)); // Menu down mappings.emplace("menu_down", std::make_unique(nullptr, input::scancode::down, 0, true)); mappings.emplace("menu_down", std::make_unique(nullptr, input::scancode::s, 0, true)); mappings.emplace("menu_down", std::make_unique(nullptr, input::scancode::k, 0, true)); mappings.emplace("menu_down", std::make_unique(nullptr, input::gamepad_axis::left_stick_y, false)); mappings.emplace("menu_down", std::make_unique(nullptr, input::gamepad_axis::right_stick_y, false)); mappings.emplace("menu_down", std::make_unique(nullptr, input::gamepad_button::dpad_down)); // Menu left mappings.emplace("menu_left", std::make_unique(nullptr, input::scancode::left, 0, true)); mappings.emplace("menu_left", std::make_unique(nullptr, input::scancode::a, 0, true)); mappings.emplace("menu_left", std::make_unique(nullptr, input::scancode::j, 0, true)); mappings.emplace("menu_left", std::make_unique(nullptr, input::gamepad_axis::left_stick_x, true)); mappings.emplace("menu_left", std::make_unique(nullptr, input::gamepad_axis::right_stick_x, true)); mappings.emplace("menu_left", std::make_unique(nullptr, input::gamepad_button::dpad_left)); mappings.emplace("menu_left", std::make_unique(nullptr, input::mouse_scroll_axis::x, true)); mappings.emplace("menu_left", std::make_unique(nullptr, input::mouse_scroll_axis::y, true)); // Menu right mappings.emplace("menu_right", std::make_unique(nullptr, input::scancode::right, 0, true)); mappings.emplace("menu_right", std::make_unique(nullptr, input::scancode::d, 0, true)); mappings.emplace("menu_right", std::make_unique(nullptr, input::scancode::l, 0, true)); mappings.emplace("menu_right", std::make_unique(nullptr, input::gamepad_axis::left_stick_x, false)); mappings.emplace("menu_right", std::make_unique(nullptr, input::gamepad_axis::right_stick_x, false)); mappings.emplace("menu_right", std::make_unique(nullptr, input::gamepad_button::dpad_right)); mappings.emplace("menu_right", std::make_unique(nullptr, input::mouse_scroll_axis::x, false)); mappings.emplace("menu_right", std::make_unique(nullptr, input::mouse_scroll_axis::y, false)); // Menu select mappings.emplace("menu_select", std::make_unique(nullptr, input::scancode::enter, 0, false)); mappings.emplace("menu_select", std::make_unique(nullptr, input::scancode::space, 0, false)); mappings.emplace("menu_select", std::make_unique(nullptr, input::scancode::e, 0, false)); mappings.emplace("menu_select", std::make_unique(nullptr, input::gamepad_button::a)); // Menu back mappings.emplace("menu_back", std::make_unique(nullptr, input::scancode::escape, 0, false)); mappings.emplace("menu_back", std::make_unique(nullptr, input::scancode::backspace, 0, false)); mappings.emplace("menu_back", std::make_unique(nullptr, input::scancode::q, 0, false)); mappings.emplace("menu_back", std::make_unique(nullptr, input::gamepad_button::b)); mappings.emplace("menu_back", std::make_unique(nullptr, input::gamepad_button::back)); // Menu modifier mappings.emplace("menu_modifier", std::make_unique(nullptr, input::scancode::left_shift, 0, false)); mappings.emplace("menu_modifier", std::make_unique(nullptr, input::scancode::right_shift, 0, false)); // Move forward mappings.emplace("move_forward", std::make_unique(nullptr, input::scancode::w, 0, false)); mappings.emplace("move_forward", std::make_unique(nullptr, input::gamepad_axis::left_stick_y, true)); // Move back mappings.emplace("move_back", std::make_unique(nullptr, input::scancode::s, 0, false)); mappings.emplace("move_back", std::make_unique(nullptr, input::gamepad_axis::left_stick_y, false)); // Move left mappings.emplace("move_left", std::make_unique(nullptr, input::scancode::a, 0, false)); mappings.emplace("move_left", std::make_unique(nullptr, input::gamepad_axis::left_stick_x, true)); // Move right mappings.emplace("move_right", std::make_unique(nullptr, input::scancode::d, 0, false)); mappings.emplace("move_right", std::make_unique(nullptr, input::gamepad_axis::left_stick_x, false)); // Move up mappings.emplace("move_up", std::make_unique(nullptr, input::mouse_scroll_axis::y, false)); mappings.emplace("move_up", std::make_unique(nullptr, input::gamepad_axis::right_trigger, false)); // Move down mappings.emplace("move_down", std::make_unique(nullptr, input::mouse_scroll_axis::y, true)); mappings.emplace("move_down", std::make_unique(nullptr, input::gamepad_axis::left_trigger, false)); // Pause mappings.emplace("pause", std::make_unique(nullptr, input::scancode::escape, 0, false)); mappings.emplace("pause", std::make_unique(nullptr, input::gamepad_button::start)); // Mouse pick mappings.emplace("mouse_pick", std::make_unique(nullptr, input::mouse_button::left)); // Mouse look mappings.emplace("mouse_look", std::make_unique(nullptr, input::mouse_button::right)); // Focus mappings.emplace("focus", std::make_unique(nullptr, input::scancode::left_shift, 0, false)); } void apply_control_profile(::game& ctx, const ::control_profile& profile) { auto add_mappings = [&profile](input::action_map& map, input::action& action, hash::fnv1a32_t key) { auto range = profile.mappings.equal_range(key); for (auto i = range.first; i != range.second; ++i) { map.add_mapping(action, *i->second); } }; // Window controls ctx.window_action_map.remove_mappings(); add_mappings(ctx.window_action_map, ctx.fullscreen_action, "fullscreen"); add_mappings(ctx.window_action_map, ctx.screenshot_action, "screenshot"); // Menu controls ctx.menu_action_map.remove_mappings(); add_mappings(ctx.menu_action_map, ctx.menu_up_action, "menu_up"); add_mappings(ctx.menu_action_map, ctx.menu_down_action, "menu_down"); add_mappings(ctx.menu_action_map, ctx.menu_left_action, "menu_left"); add_mappings(ctx.menu_action_map, ctx.menu_right_action, "menu_right"); add_mappings(ctx.menu_action_map, ctx.menu_select_action, "menu_select"); add_mappings(ctx.menu_action_map, ctx.menu_back_action, "menu_back"); add_mappings(ctx.menu_action_map, ctx.menu_modifier_action, "menu_modifier"); // Movement controls ctx.movement_action_map.remove_mappings(); add_mappings(ctx.movement_action_map, ctx.move_forward_action, "move_forward"); add_mappings(ctx.movement_action_map, ctx.move_back_action, "move_back"); add_mappings(ctx.movement_action_map, ctx.move_left_action, "move_left"); add_mappings(ctx.movement_action_map, ctx.move_right_action, "move_right"); add_mappings(ctx.movement_action_map, ctx.move_up_action, "move_up"); add_mappings(ctx.movement_action_map, ctx.move_down_action, "move_down"); add_mappings(ctx.movement_action_map, ctx.pause_action, "pause"); // Keeper controls ctx.keeper_action_map.remove_mappings(); add_mappings(ctx.keeper_action_map, ctx.mouse_pick_action, "mouse_pick"); add_mappings(ctx.keeper_action_map, ctx.mouse_look_action, "mouse_look"); add_mappings(ctx.keeper_action_map, ctx.focus_action, "focus"); } void update_control_profile(::game& ctx, ::control_profile& profile) { auto add_mappings = [&profile](const input::action_map& map, const input::action& action, hash::fnv1a32_t key) { auto gamepad_axis_mappings = map.get_gamepad_axis_mappings(action); auto gamepad_button_mappings = map.get_gamepad_button_mappings(action); auto key_mappings = map.get_key_mappings(action); auto mouse_button_mappings = map.get_mouse_button_mappings(action); auto mouse_motion_mappings = map.get_mouse_motion_mappings(action); auto mouse_scroll_mappings = map.get_mouse_scroll_mappings(action); for (const auto& mapping: gamepad_axis_mappings) { profile.mappings.emplace(key, std::make_unique(mapping)); } for (const auto& mapping: gamepad_button_mappings) { profile.mappings.emplace(key, std::make_unique(mapping)); } for (const auto& mapping: key_mappings) { profile.mappings.emplace(key, std::make_unique(mapping)); } for (const auto& mapping: mouse_button_mappings) { profile.mappings.emplace(key, std::make_unique(mapping)); } for (const auto& mapping: mouse_motion_mappings) { profile.mappings.emplace(key, std::make_unique(mapping)); } for (const auto& mapping: mouse_scroll_mappings) { profile.mappings.emplace(key, std::make_unique(mapping)); } }; profile.mappings.clear(); // Window controls add_mappings(ctx.window_action_map, ctx.fullscreen_action, "fullscreen"); add_mappings(ctx.window_action_map, ctx.screenshot_action, "screenshot"); // Menu controls add_mappings(ctx.menu_action_map, ctx.menu_up_action, "menu_up"); add_mappings(ctx.menu_action_map, ctx.menu_down_action, "menu_down"); add_mappings(ctx.menu_action_map, ctx.menu_left_action, "menu_left"); add_mappings(ctx.menu_action_map, ctx.menu_right_action, "menu_right"); add_mappings(ctx.menu_action_map, ctx.menu_select_action, "menu_select"); add_mappings(ctx.menu_action_map, ctx.menu_back_action, "menu_back"); add_mappings(ctx.menu_action_map, ctx.menu_modifier_action, "menu_modifier"); // Movement controls add_mappings(ctx.movement_action_map, ctx.move_forward_action, "move_forward"); add_mappings(ctx.movement_action_map, ctx.move_back_action, "move_back"); add_mappings(ctx.movement_action_map, ctx.move_left_action, "move_left"); add_mappings(ctx.movement_action_map, ctx.move_right_action, "move_right"); add_mappings(ctx.movement_action_map, ctx.move_up_action, "move_up"); add_mappings(ctx.movement_action_map, ctx.move_down_action, "move_down"); add_mappings(ctx.movement_action_map, ctx.pause_action, "pause"); // Keeper controls add_mappings(ctx.keeper_action_map, ctx.mouse_pick_action, "mouse_pick"); add_mappings(ctx.keeper_action_map, ctx.mouse_look_action, "mouse_look"); add_mappings(ctx.keeper_action_map, ctx.focus_action, "focus"); } void setup_window_controls(::game& ctx) { // Setup fullscreen control ctx.window_action_subscriptions.emplace_back ( ctx.fullscreen_action.get_activated_channel().subscribe ( [&ctx](const auto& event) { ctx.window->set_fullscreen(!ctx.window->is_fullscreen()); } ) ); // Setup screenshot control ctx.window_action_subscriptions.emplace_back ( ctx.screenshot_action.get_activated_channel().subscribe ( [&ctx](const auto& event) { ::graphics::save_screenshot(ctx); } ) ); } void setup_menu_controls(::game& ctx) { // Setup menu controls ctx.menu_action_subscriptions.emplace_back ( ctx.menu_up_action.get_activated_channel().subscribe ( [&ctx](const auto& event) { --(*ctx.menu_item_index); if (*ctx.menu_item_index < 0) *ctx.menu_item_index = static_cast(ctx.menu_item_texts.size()) - 1; ::menu::update_text_color(ctx); } ) ); ctx.menu_action_subscriptions.emplace_back ( ctx.menu_down_action.get_activated_channel().subscribe ( [&ctx](const auto& event) { ++(*ctx.menu_item_index); if (*ctx.menu_item_index >= ctx.menu_item_texts.size()) *ctx.menu_item_index = 0; ::menu::update_text_color(ctx); } ) ); ctx.menu_action_subscriptions.emplace_back ( ctx.menu_left_action.get_activated_channel().subscribe ( [&ctx](const auto& event) { auto callback = ctx.menu_left_callbacks[*ctx.menu_item_index]; if (callback != nullptr) callback(); } ) ); ctx.menu_action_subscriptions.emplace_back ( ctx.menu_right_action.get_activated_channel().subscribe ( [&ctx](const auto& event) { auto callback = ctx.menu_right_callbacks[*ctx.menu_item_index]; if (callback != nullptr) callback(); } ) ); ctx.menu_action_subscriptions.emplace_back ( ctx.menu_select_action.get_activated_channel().subscribe ( [&ctx](const auto& event) { const auto& callback = ctx.menu_select_callbacks[*ctx.menu_item_index]; if (callback != nullptr) callback(); } ) ); ctx.menu_action_subscriptions.emplace_back ( ctx.menu_back_action.get_activated_channel().subscribe ( [&ctx](const auto& event) { if (ctx.menu_back_callback != nullptr) ctx.menu_back_callback(); } ) ); // Set activation threshold for menu navigation controls to mitigate drifting gamepad axes auto menu_action_threshold = [](float x) -> bool { return x > 0.5f; }; ctx.menu_up_action.set_threshold_function(menu_action_threshold); ctx.menu_down_action.set_threshold_function(menu_action_threshold); ctx.menu_left_action.set_threshold_function(menu_action_threshold); ctx.menu_right_action.set_threshold_function(menu_action_threshold); } void setup_game_controls(::game& ctx) { // Setup pause control ctx.movement_action_subscriptions.emplace_back ( ctx.pause_action.get_activated_channel().subscribe ( [&ctx](const auto& event) { if (!ctx.resume_callback) { // Queue disable game controls and push pause state ctx.function_queue.push ( [&ctx]() { // Disable game controls ::disable_game_controls(ctx); // Push pause menu state ctx.state_machine.emplace(std::make_unique(ctx)); } ); // Set resume callback ctx.resume_callback = [&ctx]() { enable_game_controls(ctx); ctx.resume_callback = nullptr; }; } } ) ); } void enable_window_controls(::game& ctx) { ctx.window_action_map.enable(); } void enable_menu_controls(::game& ctx) { ctx.menu_action_map.enable(); // Function to select menu item at mouse position auto select_menu_item = [&ctx](const math::vector& mouse_position) -> bool { const float padding = config::menu_mouseover_padding * ctx.menu_font.get_font_metrics().size; for (std::size_t i = 0; i < ctx.menu_item_texts.size(); ++i) { auto [name, value] = ctx.menu_item_texts[i]; const auto& name_bounds = static_cast&>(name->get_bounds()); float min_x = name_bounds.min_point.x(); float min_y = name_bounds.min_point.y(); float max_x = name_bounds.max_point.x(); float max_y = name_bounds.max_point.y(); if (value) { const auto& value_bounds = static_cast&>(value->get_bounds()); min_x = std::min(min_x, value_bounds.min_point.x()); min_y = std::min(min_y, value_bounds.min_point.y()); max_x = std::max(max_x, value_bounds.max_point.x()); max_y = std::max(max_y, value_bounds.max_point.y()); } min_x -= padding; min_y -= padding; max_x += padding; max_y += padding; const auto& viewport = ctx.window->get_viewport_size(); const float x = mouse_position.x(); const float y = static_cast((viewport[1] - mouse_position.y() + 1)); if (x >= min_x && x <= max_x) { if (y >= min_y && y <= max_y) { *ctx.menu_item_index = static_cast(i); ::menu::update_text_color(ctx); return true; } } } return false; }; // Enable menu mouse tracking ctx.menu_mouse_subscriptions.clear(); ctx.menu_mouse_subscriptions.emplace_back ( ctx.input_manager->get_event_dispatcher().subscribe ( [&ctx, select_menu_item](const auto& event) { // Select menu item at mouse position (if any) select_menu_item(math::vector(event.position)); } ) ); ctx.menu_mouse_subscriptions.emplace_back ( ctx.input_manager->get_event_dispatcher().subscribe ( [&ctx, select_menu_item](const auto& event) { // Select menu item at mouse position (if any) if (select_menu_item(math::vector(event.position))) { // Determine appropriate menu item callback auto callback = ctx.menu_select_callbacks[*ctx.menu_item_index]; if (event.button == input::mouse_button::left) { if (ctx.menu_left_callbacks[*ctx.menu_item_index]) { callback = ctx.menu_left_callbacks[*ctx.menu_item_index]; } } else if (event.button == input::mouse_button::right) { if (ctx.menu_right_callbacks[*ctx.menu_item_index]) { callback = ctx.menu_right_callbacks[*ctx.menu_item_index]; } } // Invoke menu item callback if (callback) { callback(); } } } ) ); } void enable_game_controls(::game& ctx) { ctx.movement_action_map.enable(); } void enable_keeper_controls(::game& ctx) { ctx.keeper_action_map.enable(); } void disable_window_controls(::game& ctx) { ctx.window_action_map.disable(); } void disable_menu_controls(::game& ctx) { ctx.menu_action_map.disable(); ctx.menu_mouse_subscriptions.clear(); ctx.menu_up_action.reset(); ctx.menu_down_action.reset(); ctx.menu_left_action.reset(); ctx.menu_right_action.reset(); ctx.menu_select_action.reset(); ctx.menu_back_action.reset(); ctx.menu_modifier_action.reset(); } void disable_game_controls(::game& ctx) { ctx.movement_action_map.disable(); ctx.move_forward_action.reset(); ctx.move_back_action.reset(); ctx.move_left_action.reset(); ctx.move_right_action.reset(); ctx.move_up_action.reset(); ctx.move_down_action.reset(); ctx.pause_action.reset(); } void disable_keeper_controls(::game& ctx) { ctx.keeper_action_map.disable(); ctx.mouse_pick_action.reset(); ctx.mouse_look_action.reset(); ctx.focus_action.reset(); }