diff options
-rw-r--r-- | src/engine/user/cursor.cpp | 15 | ||||
-rw-r--r-- | src/engine/user/cursor.hpp | 6 | ||||
-rw-r--r-- | src/game/game.cpp | 248 | ||||
-rw-r--r-- | src/game/game.hpp | 52 | ||||
-rw-r--r-- | src/game/keycodes.hpp | 15 | ||||
-rw-r--r-- | src/interfaces/cursor.hpp | 22 | ||||
-rw-r--r-- | src/util/string.hpp | 16 | ||||
-rw-r--r-- | src/util/string_impl.hpp | 29 |
8 files changed, 382 insertions, 21 deletions
diff --git a/src/engine/user/cursor.cpp b/src/engine/user/cursor.cpp index d348d9e..963531f 100644 --- a/src/engine/user/cursor.cpp +++ b/src/engine/user/cursor.cpp @@ -55,6 +55,11 @@ void CursorController::ensure_position() noexcept _position = _invert_position_y(Vector2(vector2_options)); } +void CursorController::update_position(const Vector2 &position) noexcept +{ + _position = position; +} + void CursorController::hide() noexcept { fmt::print(CURSOR_INVISIBLE, fmt::arg("esc", ESC)); @@ -67,6 +72,16 @@ void CursorController::show() noexcept std::cout.flush(); } +void CursorController::set_cursor_style(CursorStyle cursor_style) noexcept +{ + fmt::print( + SET_CURSOR_STYLE, + fmt::arg("esc", ESC), + fmt::arg("style", static_cast<int>(cursor_style))); + + std::cout.flush(); +} + void CursorController::set_bounds(const Bounds &bounds) noexcept { _bounds = bounds; diff --git a/src/engine/user/cursor.hpp b/src/engine/user/cursor.hpp index 199c86b..ace47ee 100644 --- a/src/engine/user/cursor.hpp +++ b/src/engine/user/cursor.hpp @@ -25,6 +25,8 @@ constexpr fmt::string_view REQUEST_CURSOR_POSITION = "{esc}[6n"; constexpr fmt::string_view CURSOR_VISIBLE = "{esc}[?25h"; constexpr fmt::string_view CURSOR_INVISIBLE = "{esc}[?25l"; +constexpr fmt::string_view SET_CURSOR_STYLE = "{esc}[{style} q"; + const std::unordered_map<Vector2, std::string_view, Vector2Hasher> direction_format_map = {{Vector2::up(), MOVE_CURSOR_UP}, {Vector2::down(), MOVE_CURSOR_DOWN}, @@ -45,10 +47,14 @@ public: void ensure_position() noexcept override; + void update_position(const Vector2 &position) noexcept override; + void hide() noexcept override; void show() noexcept override; + void set_cursor_style(CursorStyle cursor_style) noexcept override; + void set_bounds(const Bounds &bounds) noexcept override; private: diff --git a/src/game/game.cpp b/src/game/game.cpp index 3127d40..0b3e85e 100644 --- a/src/game/game.cpp +++ b/src/game/game.cpp @@ -1,8 +1,12 @@ #include "game.hpp" #include "engine/data/bounds.hpp" +#include "engine/escape.hpp" +#include "game/keycodes.hpp" #include "util/algorithm.hpp" +#include "util/string.hpp" +#include <fmt/color.h> #include <fmt/core.h> #include <algorithm> @@ -24,7 +28,8 @@ Game::Game( _generation_tracker(std::move(generation_tracker)), _status_manager(std::move(status_manager)), _user_input_observer(std::move(user_input_observer)), - _cell_helper(std::move(cell_helper)) + _cell_helper(std::move(cell_helper)), + _current_mode(Mode::NORMAL) { } @@ -35,7 +40,7 @@ void Game::on_start() noexcept std::shared_ptr<IStatusLine> statusline = _statusline_factory(Bounds({.width = scene_size.get_width(), .height = 1})); - _scene->register_component(statusline, Vector2({0, 0})); + _scene->register_component(statusline, Vector2({.x = 0, .y = 1})); _status_manager->bind(statusline); @@ -81,11 +86,49 @@ void Game::on_start() noexcept scene_size.get_width(), scene_size.get_height())); - _last_update_time = std::chrono::system_clock::now(); + _commands["ping"] = CommandInfo( + {.option_cnt = 0, + .function = [this](CommandInfo::Options /*options*/) + { + _erase_entire_line(); + + const auto position = _cursor_controller->where(); + + _cursor_controller->move_to(Vector2({.x = 0, .y = position.get_y()})); + + std::cout << "pong!"; + std::cout.flush(); + }}); } void Game::on_update() noexcept { + if (_current_mode == Mode::COMMAND) + { + _on_command_mode_update(); + return; + } + + _on_normal_mode_update(); +} + +void Game::on_exit() const noexcept +{ + for (auto row : *_scene->get_matrix()) + { + for (auto &col : row) + { + fmt::print("{}", col); + } + + fmt::print("\n"); + } + + std::cout.flush(); +} + +void Game::_on_normal_mode_update() noexcept +{ const auto pressed_key = _user_input_observer->get_currently_pressed_key(); auto cursor_has_moved = false; @@ -124,6 +167,26 @@ void Game::on_update() noexcept case 'q': std::exit(EXIT_SUCCESS); + case ':': + _last_pos_before_command_mode = _cursor_controller->where(); + + _cursor_controller->move_to(Vector2({.x = 0, .y = 0})); + + _erase_entire_line(); + + std::cout << ":"; + std::cout.flush(); + + _cursor_controller->update_position( + _cursor_controller->where() + Vector2::right()); + + _cursor_controller->set_cursor_style(CursorStyle::BlinkingBar); + + _command_mode_input = ""; + + _current_mode = Mode::COMMAND; + return; + case 'i': { const auto position = _cursor_controller->where(); @@ -208,7 +271,6 @@ void Game::on_update() noexcept if (_generation_tracker->get_is_paused() && !is_generation_stepping) { - _last_update_time = time_now; return; } @@ -218,7 +280,6 @@ void Game::on_update() noexcept if (time_since_last_gen.count() <= _min_time_since_last_gen_millis) { - _last_update_time = time_now; return; } @@ -261,23 +322,171 @@ void Game::on_update() noexcept _set_space(matrix, birth_cell_pos, 'x'); _living_cell_positions.push_back(birth_cell_pos); } +} + +void Game::_on_command_mode_update() noexcept +{ + const auto pressed_key = _user_input_observer->get_currently_pressed_key(); + + switch (pressed_key) + { + case keycodes::ESCAPE: + _erase_entire_line(); + _return_to_normal_mode(); + break; + + case keycodes::ENTER: + if (!_command_mode_input.empty()) + { + _run_command(_command_mode_input); + } + + if (_current_mode != Mode::NORMAL) + { + _return_to_normal_mode(); + } + + break; + + case keycodes::BACKSPACE: + { + const auto position = _cursor_controller->where(); + + const auto input_pos_x = static_cast<uint32_t>(position.get_x()) - 1U; + + if (input_pos_x == 0U) + { + break; + } + + _command_mode_input.erase(input_pos_x - 1U, 1U); + + _cursor_controller->move(Vector2::left(), 1U); + + std::cout.put(' '); + std::cout.flush(); + + _cursor_controller->update_position( + _cursor_controller->where() + Vector2::right()); + + _cursor_controller->move(Vector2::left(), 1U); + + _erase_line_from_cursor(); + + std::cout << _command_mode_input.substr(input_pos_x - 1U); + std::cout.flush(); + + _cursor_controller->move_to(position); + _cursor_controller->move(Vector2::left(), 1U); - _last_update_time = time_now; + break; + } + + case keycodes::LEFT_ARROW: + if (_cursor_controller->where().get_x() >= 2) + { + _cursor_controller->move(Vector2::left(), 1U); + } + break; + + case keycodes::RIGHT_ARROW: + if (static_cast<uint32_t>(_cursor_controller->where().get_x()) - 1U < + _command_mode_input.size()) + { + _cursor_controller->move(Vector2::right(), 1U); + } + break; + + case keycodes::UP_ARROW: + case keycodes::DOWN_ARROW: + case 0: + break; + + default: + const auto position = _cursor_controller->where(); + + const auto input_pos_x = static_cast<uint32_t>(position.get_x()) - 1U; + + if (input_pos_x < _command_mode_input.length()) + { + _command_mode_input.insert(input_pos_x, 1, pressed_key); + + _erase_line_from_cursor(); + + std::cout << pressed_key << _command_mode_input.substr(input_pos_x + 1U); + std::cout.flush(); + + _cursor_controller->move_to(position + Vector2::right()); + break; + } + + _command_mode_input += pressed_key; + + std::cout << pressed_key; + std::cout.flush(); + + _cursor_controller->update_position( + _cursor_controller->where() + Vector2::right()); + + break; + } } -void Game::on_exit() const noexcept +void Game::_return_to_normal_mode() noexcept { - for (auto row : *_scene->get_matrix()) + _cursor_controller->move_to(_last_pos_before_command_mode.value_or( + Vector2({.x = CURSOR_FALLBACK_POS_X, .y = CURSOR_FALLBACK_POS_Y}))); + + _cursor_controller->set_cursor_style(CursorStyle::BlinkingBlock); + + _current_mode = Mode::NORMAL; +} + +void Game::_run_command(const std::string &command) noexcept +{ + const auto split_command = split_string<std::vector<std::string>>(command, ' '); + + if (split_command.size() == 1) { - for (auto &col : row) + if (!_commands.contains(command)) { - fmt::print("{}", col); + _show_command_error(fmt::format("Error: Not a command: {}", command)); + return; } - fmt::print("\n"); + if (_commands.at(command).option_cnt != 0U) + { + _show_command_error( + fmt::format("Error: Insufficient arguments for command: {}", command)); + return; + } } - std::cout.flush(); + const auto &command_name = split_command[0]; + + if (!_commands.contains(command_name)) + { + _show_command_error(fmt::format("Error: Not a command: {}", command)); + return; + } + + const auto &command_info = _commands.at(command_name); + + const auto args = + decltype(split_command)(split_command.begin() + 1, split_command.end()); + + command_info.function(args); +} + +void Game::_show_command_error(const std::string_view &error_message) noexcept +{ + _erase_entire_line(); + + const auto position = _cursor_controller->where(); + + _cursor_controller->move_to(Vector2({.x = 0, .y = position.get_y()})); + + fmt::print(fmt::fg(fmt::color::red), "{}", error_message); } auto Game::_move_cursor(const Vector2 &direction) noexcept -> bool @@ -293,7 +502,7 @@ auto Game::_move_cursor(const Vector2 &direction) noexcept -> bool return false; } - if (dest_position.get_y() < 1) + if (dest_position.get_y() <= 1) { return false; } @@ -319,3 +528,16 @@ void Game::_set_space( _cursor_controller->move_to(prev_position); } + +void Game::_erase_entire_line() noexcept +{ + fmt::print(ERASE_ENTIRE_LINE, fmt::arg("esc", ESC)); + std::cout.flush(); +} + +void Game::_erase_line_from_cursor() noexcept +{ + fmt::print(ERASE_LINE_FROM_CURSOR, fmt::arg("esc", ESC)); + std::cout.flush(); +} + diff --git a/src/game/game.hpp b/src/game/game.hpp index 8363c4d..a35c0ce 100644 --- a/src/game/game.hpp +++ b/src/game/game.hpp @@ -14,14 +14,41 @@ #include <chrono> #include <cstddef> +#include <functional> #include <list> #include <memory> +#include <optional> +#include <string> +#include <string_view> +#include <vector> constexpr auto DEFAULT_MIN_TIME_SINCE_LAST_GEN_MILLIS = 200; constexpr auto MIN_TIME_SINCE_LAST_GEN_INCREMENT = 50; constexpr auto MIN_TIME_SINCE_LAST_GEN_DECREMENT = 50; +constexpr auto CURSOR_FALLBACK_POS_X = 10; +constexpr auto CURSOR_FALLBACK_POS_Y = 10; + +constexpr std::string_view ERASE_ENTIRE_LINE = "{esc}[2K"; +constexpr std::string_view ERASE_LINE_FROM_CURSOR = "{esc}[0K"; + +enum Mode +{ + NORMAL, + COMMAND +}; + +class CommandInfo +{ +public: + using Options = const std::vector<std::string> &; + using CommandFunction = std::function<void(Options)>; + + std::size_t option_cnt; + CommandFunction function; +}; + class Game : public IGame { public: @@ -50,19 +77,38 @@ private: std::shared_ptr<IUserInputObserver> _user_input_observer; std::shared_ptr<ICellHelper> _cell_helper; - using TimePoint = std::chrono::system_clock::time_point; + Mode _current_mode; - TimePoint _last_update_time; - TimePoint _last_gen_update_time; + std::optional<Vector2> _last_pos_before_command_mode; + + std::string _command_mode_input; + + std::unordered_map<std::string, CommandInfo> _commands; + + std::chrono::system_clock::time_point _last_gen_update_time; int32_t _min_time_since_last_gen_millis = DEFAULT_MIN_TIME_SINCE_LAST_GEN_MILLIS; std::list<Vector2> _living_cell_positions; + void _on_normal_mode_update() noexcept; + + void _on_command_mode_update() noexcept; + + void _return_to_normal_mode() noexcept; + + void _run_command(const std::string &command) noexcept; + + void _show_command_error(const std::string_view &error_message) noexcept; + auto _move_cursor(const Vector2 &direction) noexcept -> bool; void _set_space( const std::shared_ptr<IMatrix<IScene::MatrixElement>> &matrix, const Vector2 &position, char character) noexcept; + + static void _erase_entire_line() noexcept; + + static void _erase_line_from_cursor() noexcept; }; diff --git a/src/game/keycodes.hpp b/src/game/keycodes.hpp new file mode 100644 index 0000000..84ca5a7 --- /dev/null +++ b/src/game/keycodes.hpp @@ -0,0 +1,15 @@ +#pragma once + +namespace keycodes +{ + +constexpr auto ENTER = 10; +constexpr auto ESCAPE = 27; +constexpr auto BACKSPACE = 127; + +constexpr auto UP_ARROW = 65; +constexpr auto DOWN_ARROW = 66; +constexpr auto RIGHT_ARROW = 67; +constexpr auto LEFT_ARROW = 68; + +} // namespace keycodes diff --git a/src/interfaces/cursor.hpp b/src/interfaces/cursor.hpp index 377fe25..b09f06f 100644 --- a/src/interfaces/cursor.hpp +++ b/src/interfaces/cursor.hpp @@ -5,18 +5,21 @@ #include <memory> -enum CursorEvent +enum class CursorStyle { - POSITION_CHANGE + BlinkingBlock = 0, + BlinkingBlockDefault = 1, + SteadyBlock = 2, + BlinkingUnderline = 3, + SteadyUnderline = 4, + BlinkingBar = 5, + SteadyBar = 6 }; // NOLINTNEXTLINE(cppcoreguidelines-special-member-functions) class ICursorController { public: - using Event = CursorEvent; - using Context = Vector2; - virtual ~ICursorController() noexcept = default; virtual void move(const Vector2 &direction, const uint32_t &amount) noexcept = 0; @@ -28,9 +31,18 @@ public: virtual void ensure_position() noexcept = 0; + /** + * Updates the stored cursor position. + * + * This will NOT change the position of the actual cursor! + */ + virtual void update_position(const Vector2 &position) noexcept = 0; + virtual void hide() noexcept = 0; virtual void show() noexcept = 0; + virtual void set_cursor_style(CursorStyle cursor_style) noexcept = 0; + virtual void set_bounds(const Bounds &bounds) noexcept = 0; }; diff --git a/src/util/string.hpp b/src/util/string.hpp new file mode 100644 index 0000000..a571c50 --- /dev/null +++ b/src/util/string.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include "util/concepts.hpp" + +#include <concepts> +#include <string> + +template < + typename ContainerType, + typename Char = char, + typename String = std::basic_string<Char>> +requires Container<ContainerType> && HasPushBack<ContainerType> && + std::same_as<typename ContainerType::value_type, String> +auto split_string(const String &str, Char delimiter) noexcept -> ContainerType; + +#include "string_impl.hpp" diff --git a/src/util/string_impl.hpp b/src/util/string_impl.hpp new file mode 100644 index 0000000..8c85947 --- /dev/null +++ b/src/util/string_impl.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include "string.hpp" + +template <typename ContainerType, typename Char, typename String> +requires Container<ContainerType> && HasPushBack<ContainerType> && + std::same_as<typename ContainerType::value_type, String> +auto split_string(const String &str, Char delimiter) noexcept -> ContainerType +{ + auto result_container = ContainerType(); + + auto str_copy = str; + + size_t pos = 0; + String token; + + while ((pos = str_copy.find(delimiter)) != String::npos) + { + token = str_copy.substr(0, pos); + + result_container.push_back(token); + + str_copy.erase(0, pos + 1U); + } + + result_container.push_back(str_copy); + + return result_container; +} |