From 927e065f9829045247be7c0b3296408b6f577c1f Mon Sep 17 00:00:00 2001 From: HampusM Date: Sun, 12 Jun 2022 13:44:58 +0200 Subject: feat: add reading RLE files --- lib/CMakeLists.txt | 8 +++ lib/ctre/CMakeLists.txt | 3 + src/CMakeLists.txt | 5 +- src/bootstrap.cpp | 8 ++- src/errors/RLE_reader.hpp | 19 ++++++ src/errors/io.hpp | 55 +++++++++++++++++ src/game/RLE_reader.cpp | 133 ++++++++++++++++++++++++++++++++++++++++++ src/game/RLE_reader.hpp | 24 ++++++++ src/game/game.cpp | 77 +++++++++++++++++++++--- src/game/game.hpp | 5 +- src/interfaces/RLE_reader.hpp | 19 ++++++ src/util/algorithm.hpp | 7 +++ src/util/algorithm_impl.hpp | 10 ++++ src/util/fs.cpp | 29 +++++++++ src/util/fs.hpp | 22 +++++++ src/util/io.hpp | 14 +++++ src/util/io_impl.hpp | 55 +++++++++++++++++ 17 files changed, 482 insertions(+), 11 deletions(-) create mode 100644 lib/ctre/CMakeLists.txt create mode 100644 src/errors/RLE_reader.hpp create mode 100644 src/errors/io.hpp create mode 100644 src/game/RLE_reader.cpp create mode 100644 src/game/RLE_reader.hpp create mode 100644 src/interfaces/RLE_reader.hpp create mode 100644 src/util/fs.cpp create mode 100644 src/util/fs.hpp create mode 100644 src/util/io.hpp create mode 100644 src/util/io_impl.hpp diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index 84c5a77..9a18dbe 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -24,6 +24,14 @@ FetchContent_Declare( add_subdirectory(yacppdic) +FetchContent_Declare( + ctre + GIT_REPOSITORY "https://github.com/hanickadot/compile-time-regular-expressions" + GIT_TAG v3.7 +) + +add_subdirectory(ctre) + if(test) FetchContent_Declare( gtest diff --git a/lib/ctre/CMakeLists.txt b/lib/ctre/CMakeLists.txt new file mode 100644 index 0000000..3f2aced --- /dev/null +++ b/lib/ctre/CMakeLists.txt @@ -0,0 +1,3 @@ +message(STATUS "Fetching ctre...") + +FetchContent_MakeAvailable(ctre) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 52c5679..f6b27cb 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -11,10 +11,12 @@ endfunction(target_link_libraries_system) file(GLOB SOURCES bootstrap.cpp util/color.cpp + util/fs.cpp game/game.cpp game/status_manager.cpp game/generation_tracker.cpp game/components/statusline.cpp + game/RLE_reader.cpp engine/engine.cpp engine/main.cpp engine/data/vector2.cpp @@ -47,7 +49,7 @@ target_compile_options( -Wsign-conversion -Wsign-promo -Wstrict-overflow=5 -Wswitch-default -Wundef -Werror - -pedantic -fsanitize=address -fno-exceptions -fno-omit-frame-pointer + -pedantic -fsanitize=address -fno-omit-frame-pointer ) if("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU") @@ -65,6 +67,7 @@ target_link_libraries_system( fmt::fmt-header-only GSL yacppdic + ctre ) target_link_options(${PROJECT_NAME} PRIVATE -fsanitize=address) diff --git a/src/bootstrap.cpp b/src/bootstrap.cpp index 3554183..16d38a3 100644 --- a/src/bootstrap.cpp +++ b/src/bootstrap.cpp @@ -1,6 +1,7 @@ #include "bootstrap.hpp" // Interfaces +#include "interfaces/RLE_reader.hpp" #include "interfaces/cell_helper.hpp" #include "interfaces/component.hpp" #include "interfaces/component_renderer.hpp" @@ -23,6 +24,7 @@ #include "engine/graphics/scene.hpp" #include "engine/user/cursor.hpp" #include "engine/user/input.hpp" +#include "game/RLE_reader.hpp" #include "game/cell_helper.hpp" #include "game/components/statusline.hpp" #include "game/game.hpp" @@ -40,6 +42,7 @@ auto bootstrap() noexcept -> yacppdic::Container container.bind().to(); container.bind().to(); container.bind().to(); + container.bind().to(); container.bind().to_factory( [&container]( @@ -57,6 +60,8 @@ auto bootstrap() noexcept -> yacppdic::Container const auto cell_helper_factory = container.get>(); + std::shared_ptr rle_reader = container.get(); + return std::make_unique( statusline_factory, scene, @@ -64,7 +69,8 @@ auto bootstrap() noexcept -> yacppdic::Container generation_tracker, status_manager, user_input_observer, - cell_helper_factory(*(scene->get_matrix()))); + cell_helper_factory(*(scene->get_matrix())), + rle_reader); }); container.bind>().to_factory( diff --git a/src/errors/RLE_reader.hpp b/src/errors/RLE_reader.hpp new file mode 100644 index 0000000..39b4ddb --- /dev/null +++ b/src/errors/RLE_reader.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include + +#include +#include +#include + +class InvalidRLEFileError : public std::runtime_error +{ +public: + explicit InvalidRLEFileError( + const std::filesystem::path &path, + const std::string_view &reason) noexcept + : std::runtime_error( + fmt::format("Invalid RLE file '{}'. {}", path.string(), reason)) + { + } +}; diff --git a/src/errors/io.hpp b/src/errors/io.hpp new file mode 100644 index 0000000..1d0aec3 --- /dev/null +++ b/src/errors/io.hpp @@ -0,0 +1,55 @@ +#pragma once + +#include "util/fs.hpp" + +#include + +#include +#include +#include + +class IOError : public std::runtime_error +{ +public: + IOError() noexcept : std::runtime_error("Unknown IO error") {} + + explicit IOError(const std::string &error_message) noexcept + : std::runtime_error(error_message) + { + } +}; + +class NoSuchFileOrDirectoryError : public IOError +{ +public: + explicit NoSuchFileOrDirectoryError(const std::filesystem::path &path) noexcept + : IOError( + fmt::format("No file or directory exists with path '{}'", path.string())) + { + } +}; + +class WrongFileTypeError : public IOError +{ +public: + explicit WrongFileTypeError( + const std::filesystem::path &path, + std::filesystem::file_type expected_file_type, + std::filesystem::file_type actual_file_type) noexcept + : IOError(fmt::format( + "Wrong file type of file with path '{}'. Expected {}, is {}", + path.string(), + file_type_names.at(expected_file_type), + file_type_names.at(actual_file_type))) + { + } +}; + +class FileNotOpenError : public IOError +{ +public: + explicit FileNotOpenError(const std::filesystem::path &path) noexcept + : IOError(fmt::format("Failed to open file with path '{}'", path.string())) + { + } +}; diff --git a/src/game/RLE_reader.cpp b/src/game/RLE_reader.cpp new file mode 100644 index 0000000..f971966 --- /dev/null +++ b/src/game/RLE_reader.cpp @@ -0,0 +1,133 @@ +#include "RLE_reader.hpp" + +#include "engine/data/bounds.hpp" +#include "engine/data/vector2.hpp" +#include "errors/RLE_reader.hpp" +#include "util/algorithm.hpp" +#include "util/io.hpp" +#include "util/string.hpp" + +#include + +#include +#include +#include +#include + +#include + +RLEReader::RLEReader(const IMatrixFactory &matrix_factory) noexcept + : _matrix_factory(matrix_factory) +{ +} + +auto RLEReader::read_RLE_file(const std::filesystem::path &path) const + -> std::unique_ptr> +{ + auto content_lines = read_file_lines>(path); + + if (content_lines.empty()) + { + throw InvalidRLEFileError(path, "File is empty"); + } + + if (content_lines.back().empty()) + { + content_lines.pop_back(); + } + + const auto header_line_iter = container_find( + content_lines, + [](const std::string &line) + { + return ctre::starts_with<"x = \\d+, y = \\d+">(line); + // return ctre::match<"x = \\d+, y = \\d+(, rule = [a-zA-Z0-9/]+)?">(line); + }); + + if (header_line_iter == content_lines.end()) + { + throw InvalidRLEFileError(path, "No header line"); + } + + const auto &header_line = *header_line_iter; + + const auto header_parts = split_string>(header_line, ','); + + const auto &header_x_part = header_parts[0]; + const auto &header_y_part = header_parts[1].substr(1); + + const auto pattern_width = + static_cast(std::stoul(header_x_part.substr(4))); + + const auto pattern_height = + static_cast(std::stoul(header_y_part.substr(4))); + + const auto pattern_size = Bounds({.width = pattern_width, .height = pattern_height}); + + auto pattern_matrix = _matrix_factory(pattern_size); + + auto pattern_pos = Vector2({.x = 0, .y = 0}); + + const auto first_pattern_line_iter = header_line_iter + 1; + + for (auto pattern_line_iter = first_pattern_line_iter; + pattern_line_iter != content_lines.end(); + ++pattern_line_iter) + { + if (!ctre::match<"[bo$0-9!]+">(*pattern_line_iter)) + { + throw InvalidRLEFileError(path, "Invalid pattern line"); + } + } + + auto pattern = std::string(); + + for (auto pattern_line_iter = first_pattern_line_iter; + pattern_line_iter != content_lines.end(); + ++pattern_line_iter) + { + pattern.append(*pattern_line_iter); + } + + auto run_count_str = std::string(); + + for (const auto &character : pattern) + { + if (static_cast(isdigit(character))) + { + run_count_str += character; + continue; + } + + if (character == '!') + { + break; + } + + const auto run_count = run_count_str.empty() ? 1 : std::stoi(run_count_str); + + run_count_str.clear(); + + if (character == '$') + { + pattern_pos.set_x(0); + pattern_pos += Vector2({.x = 0, .y = run_count}); + continue; + } + + for (auto run_index = 0; run_index < run_count; run_index++) + { + if (pattern_size.validate_coords(pattern_pos) != CoordsValidation::VALID) + { + throw InvalidRLEFileError(path, "Pattern goes out of bounds"); + } + + pattern_matrix->set(pattern_pos, character == 'o' ? 'x' : ' '); + + pattern_pos += Vector2::right(); + } + } + + return pattern_matrix; +} + diff --git a/src/game/RLE_reader.hpp b/src/game/RLE_reader.hpp new file mode 100644 index 0000000..797575e --- /dev/null +++ b/src/game/RLE_reader.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include "interfaces/RLE_reader.hpp" +#include "interfaces/matrix.hpp" + +#include + +#include +#include + +class RLEReader + : public IRLEReader, + public yacppdic:: + AutoWirable> +{ +public: + explicit RLEReader(const IMatrixFactory &matrix_factory) noexcept; + + [[nodiscard]] auto read_RLE_file(const std::filesystem::path &path) const + -> std::unique_ptr> override; + +private: + IMatrixFactory _matrix_factory; +}; diff --git a/src/game/game.cpp b/src/game/game.cpp index 73ab7f5..8d15b86 100644 --- a/src/game/game.cpp +++ b/src/game/game.cpp @@ -3,7 +3,10 @@ #include "engine/data/bounds.hpp" #include "engine/escape.hpp" #include "engine/keycodes.hpp" +#include "errors/RLE_reader.hpp" +#include "errors/io.hpp" #include "util/algorithm.hpp" +#include "util/fs.hpp" #include "util/string.hpp" #include @@ -21,7 +24,8 @@ Game::Game( std::shared_ptr generation_tracker, std::shared_ptr status_manager, std::shared_ptr user_input_observer, - std::shared_ptr cell_helper) noexcept + std::shared_ptr cell_helper, + std::shared_ptr rle_reader) noexcept : _statusline_factory(std::move(statusline_factory)), _scene(std::move(scene)), _cursor_controller(std::move(cursor_controller)), @@ -29,6 +33,7 @@ Game::Game( _status_manager(std::move(status_manager)), _user_input_observer(std::move(user_input_observer)), _cell_helper(std::move(cell_helper)), + _rle_reader(std::move(rle_reader)), _current_mode(Mode::NORMAL), _minimum_cursor_pos_y(0) { @@ -89,18 +94,74 @@ void Game::on_start() noexcept scene_size.get_width(), scene_size.get_height())); - _commands["ping"] = CommandInfo( - {.option_cnt = 0, - .function = [this](CommandInfo::Options /*options*/) + _commands["open"] = CommandInfo( + {.option_cnt = 1U, + .function = [this](CommandInfo::Options options) { - _erase_entire_line(); + const auto rle_file_path = + expand_path_home(std::filesystem::path(options[0])); - const auto position = _cursor_controller->where(); + std::unique_ptr> rle_matrix; - _cursor_controller->move_to(Vector2({.x = 0, .y = position.get_y()})); + try + { + rle_matrix = _rle_reader->read_RLE_file(rle_file_path); + } + catch (const InvalidRLEFileError &error) + { + _show_command_error(fmt::format("Error: {}", error.what())); + return; + } + catch (const IOError &error) + { + _show_command_error(fmt::format("Error: {}", error.what())); + return; + } + + if (rle_matrix == nullptr) + { + _show_command_error(fmt::format( + "A unknown error occurred while reading RLE file with path '{}'", + rle_file_path.string())); + return; + } + + _return_to_normal_mode(); + + const auto previous_pos = _cursor_controller->where(); + + auto scene_matrix = _scene->get_matrix(); + + for (auto row : *rle_matrix) + { + for (auto &col : row) + { + const auto col_pos = _cursor_controller->where(); + + std::cout.put(col); + + _cursor_controller->move_to(col_pos); + + scene_matrix->set(col_pos, col); + + if (!container_has(_living_cell_positions, col_pos)) + { + _living_cell_positions.push_back(col_pos); + } + + _cursor_controller->move(Vector2::right(), 1U); + } + + fmt::print("\n"); + const auto current_pos = _cursor_controller->where(); + + _cursor_controller->move_to( + Vector2({.x = previous_pos.get_x(), .y = current_pos.get_y() - 1})); + } - std::cout << "pong!"; std::cout.flush(); + + _cursor_controller->move_to(previous_pos); }}); } diff --git a/src/game/game.hpp b/src/game/game.hpp index 4d83fe5..abee6ec 100644 --- a/src/game/game.hpp +++ b/src/game/game.hpp @@ -1,5 +1,6 @@ #pragma once +#include "interfaces/RLE_reader.hpp" #include "interfaces/cell_helper.hpp" #include "interfaces/cursor.hpp" #include "interfaces/game.hpp" @@ -59,7 +60,8 @@ public: std::shared_ptr generation_tracker, std::shared_ptr status_manager, std::shared_ptr user_input_observer, - std::shared_ptr cell_helper) noexcept; + std::shared_ptr cell_helper, + std::shared_ptr rle_reader) noexcept; void on_start() noexcept override; @@ -76,6 +78,7 @@ private: std::shared_ptr _status_manager; std::shared_ptr _user_input_observer; std::shared_ptr _cell_helper; + std::shared_ptr _rle_reader; Mode _current_mode; diff --git a/src/interfaces/RLE_reader.hpp b/src/interfaces/RLE_reader.hpp new file mode 100644 index 0000000..a3266a8 --- /dev/null +++ b/src/interfaces/RLE_reader.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include "interfaces/matrix.hpp" + +#include +#include + +// NOLINTNEXTLINE(cppcoreguidelines-special-member-functions) +class IRLEReader +{ +public: + virtual ~IRLEReader() noexcept = default; + + using MatrixElement = char; + + [[nodiscard]] virtual auto read_RLE_file(const std::filesystem::path &path) const + -> std::unique_ptr> = 0; +}; + diff --git a/src/util/algorithm.hpp b/src/util/algorithm.hpp index e3d8b6f..60c68e2 100644 --- a/src/util/algorithm.hpp +++ b/src/util/algorithm.hpp @@ -9,6 +9,13 @@ requires Container constexpr auto container_find(const ContainerType &container, const Value &value) noexcept -> typename ContainerType::const_iterator; +template +requires Container && + std::predicate +constexpr auto +container_find(const ContainerType &container, Predicate predicate) noexcept -> + typename ContainerType::const_iterator; + template requires Container constexpr auto container_has(const ContainerType &container, const Value &value) noexcept diff --git a/src/util/algorithm_impl.hpp b/src/util/algorithm_impl.hpp index 00269ed..d7c5e3b 100644 --- a/src/util/algorithm_impl.hpp +++ b/src/util/algorithm_impl.hpp @@ -12,6 +12,16 @@ constexpr auto container_find(const ContainerType &container, const Value &value return std::find(container.begin(), container.end(), value); } +template +requires Container && + std::predicate +constexpr auto +container_find(const ContainerType &container, Predicate predicate) noexcept -> + typename ContainerType::const_iterator +{ + return std::find_if(container.begin(), container.end(), predicate); +} + template requires Container constexpr auto container_has(const ContainerType &container, const Value &value) noexcept diff --git a/src/util/fs.cpp b/src/util/fs.cpp new file mode 100644 index 0000000..68c3dd9 --- /dev/null +++ b/src/util/fs.cpp @@ -0,0 +1,29 @@ +#include "fs.hpp" + +#include +#include +#include + +auto get_current_user_home_path() noexcept -> std::filesystem::path +{ + const auto *home_path_env = std::getenv("HOME"); + + if (home_path_env == nullptr) + { + return getpwuid(getuid())->pw_dir; + } + + return home_path_env; +} + +auto expand_path_home(const std::filesystem::path &path) noexcept -> std::filesystem::path +{ + const auto path_str = path.string(); + + if (!path_str.starts_with("~/")) + { + return path; + } + + return get_current_user_home_path() / std::filesystem::path(path_str.substr(2)); +} diff --git a/src/util/fs.hpp b/src/util/fs.hpp new file mode 100644 index 0000000..860c055 --- /dev/null +++ b/src/util/fs.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include +#include +#include + +const std::unordered_map file_type_names = { + {std::filesystem::file_type::none, "none"}, + {std::filesystem::file_type::not_found, "not_found"}, + {std::filesystem::file_type::regular, "regular"}, + {std::filesystem::file_type::directory, "directory"}, + {std::filesystem::file_type::symlink, "symlink"}, + {std::filesystem::file_type::block, "block"}, + {std::filesystem::file_type::character, "character"}, + {std::filesystem::file_type::fifo, "fifo"}, + {std::filesystem::file_type::socket, "socket"}, + {std::filesystem::file_type::unknown, "unknown"}}; + +auto get_current_user_home_path() noexcept -> std::filesystem::path; + +auto expand_path_home(const std::filesystem::path &path) noexcept + -> std::filesystem::path; diff --git a/src/util/io.hpp b/src/util/io.hpp new file mode 100644 index 0000000..2c248f7 --- /dev/null +++ b/src/util/io.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include "util/concepts.hpp" + +#include +#include +#include + +template +requires Container && HasPushBack && + std::same_as +auto read_file_lines(const std::filesystem::path &path) -> ContainerType; + +#include "io_impl.hpp" diff --git a/src/util/io_impl.hpp b/src/util/io_impl.hpp new file mode 100644 index 0000000..14f4ded --- /dev/null +++ b/src/util/io_impl.hpp @@ -0,0 +1,55 @@ +#pragma once + +#include "io.hpp" + +#include "errors/io.hpp" + +#include + +template +requires Container && HasPushBack && + std::same_as +auto read_file_lines(const std::filesystem::path &path) -> ContainerType +{ + if (!std::filesystem::exists(path)) + { + throw NoSuchFileOrDirectoryError(path); + } + + const auto file_status = std::filesystem::status(path); + + const auto file_type = file_status.type(); + + if (file_type != std::filesystem::file_type::regular) + { + throw WrongFileTypeError(path, std::filesystem::file_type::regular, file_type); + } + + auto file = std::ifstream(); + + file.open(path); + + if (!file.is_open()) + { + throw FileNotOpenError(path); + } + + auto content_lines = ContainerType(); + + while (file) + { + auto line = std::string(); + std::getline(file, line); + + std::erase(line, '\r'); + + content_lines.push_back(line); + } + + if (!content_lines.empty() && content_lines.back().empty()) + { + content_lines.pop_back(); + } + + return content_lines; +} -- cgit v1.2.3-18-g5258