diff --git a/.github/workflows/meson.yml b/.github/workflows/meson.yml index 77ecaee4..5197cae2 100644 --- a/.github/workflows/meson.yml +++ b/.github/workflows/meson.yml @@ -73,7 +73,8 @@ jobs: sudo apt-get update sudo apt-get install ninja-build libsdl2-2.0-0 libsdl2-dev libsdl2-net* libsdl2-ttf* -y - - name: Configure + - name: Configure (Windows) + if: matrix.config.os == 'windows-latest' run: meson setup build -Dbuildtype=${{ matrix.config.buildtype }} - name: Build diff --git a/CMakeLists.txt b/CMakeLists.txt index dc37e276..c445859c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,6 +6,7 @@ set(CMAKE_CXX_STANDARD 23) find_package(argparse CONFIG REQUIRED) find_package(SDL2 CONFIG REQUIRED) find_package(SDL2_ttf CONFIG REQUIRED) +find_package(SDL2_net CONFIG REQUIRED) find_package(spdlog CONFIG REQUIRED) find_package(nlohmann_json CONFIG REQUIRED) find_package(magic_enum CONFIG REQUIRED) @@ -15,56 +16,61 @@ find_package(tl-expected CONFIG REQUIRED) set(TARGET_LIST oopetris SDL2::SDL2-static) add_executable(oopetris - src/main.cpp - src/application.hpp src/application.cpp - src/renderer.cpp - src/renderer.hpp - src/sdl_context.cpp - src/sdl_context.hpp - src/window.cpp - src/window.hpp - src/color.hpp - src/rect.hpp - src/point.hpp - src/tetromino.hpp - src/mino.hpp - src/grid.hpp - src/tetromino_type.hpp - src/tetromino_type.cpp - src/mino.cpp - src/tetrion.cpp - src/tetrion.hpp - src/tetris_application.hpp - src/tetris_application.cpp + src/application.hpp src/bag.cpp src/bag.hpp - src/font.cpp - src/font.hpp - src/text.cpp - src/text.hpp - src/grid.cpp - src/input.hpp + src/color.hpp + src/command_line_arguments.hpp + src/controls.hpp src/event_dispatcher.cpp src/event_dispatcher.hpp src/event_listener.hpp + src/font.cpp + src/font.hpp + src/grid.cpp + src/grid.hpp + src/input_event.hpp src/input.cpp - src/settings.hpp + src/input.hpp src/key_codes.hpp + src/local_multiplayer.cpp src/magic_enum_wrapper.hpp - src/types.hpp + src/main.cpp + src/mino_stack.cpp + src/mino_stack.hpp + src/mino.cpp + src/mino.hpp + src/network/connection_manager.cpp + src/network/network_data.cpp + src/network/network_manager.cpp + src/network/network_transportable.cpp + src/network/online_handler.cpp + src/play_mode.cpp + src/point.hpp src/random.cpp src/random.hpp src/recording.hpp - src/input_event.hpp - src/utils.hpp - src/utils.cpp - src/command_line_arguments.hpp - src/controls.hpp - src/mino_stack.cpp - src/mino_stack.hpp - src/tetrion_snapshot.hpp + src/rect.hpp + src/renderer.cpp + src/renderer.hpp + src/sdl_context.cpp + src/sdl_context.hpp src/tetrion_snapshot.cpp + src/tetrion_snapshot.hpp + src/tetrion.cpp + src/tetrion.hpp + src/tetris_application.cpp + src/tetris_application.hpp + src/tetromino_type.cpp + src/tetromino_type.hpp + src/tetromino.hpp + src/text.cpp + src/text.hpp + src/types.hpp + src/utils.cpp + src/window.cpp + src/window.hpp ) foreach (target ${TARGET_LIST}) @@ -103,9 +109,10 @@ target_link_libraries(oopetris PRIVATE argparse::argparse) target_link_libraries(oopetris PRIVATE spdlog::spdlog spdlog::spdlog_header_only) target_link_libraries(oopetris PRIVATE nlohmann_json::nlohmann_json) target_link_libraries(oopetris PRIVATE magic_enum::magic_enum) -target_link_libraries(oopetris PRIVATE $,SDL2_ttf::SDL2_ttf,SDL2_ttf::SDL2_ttf-static>) target_link_libraries(oopetris PRIVATE tl::optional) target_link_libraries(oopetris PRIVATE tl::expected) +target_link_libraries(oopetris PRIVATE $,SDL2_ttf::SDL2_ttf,SDL2_ttf::SDL2_ttf-static>) +target_link_libraries(oopetris PRIVATE $,SDL2_net::SDL2_net,SDL2_net::SDL2_net-static>) # static runtime library set_property(TARGET ${target} PROPERTY diff --git a/meson.build b/meson.build index c840d99f..5ac1da10 100644 --- a/meson.build +++ b/meson.build @@ -24,7 +24,7 @@ cpp = meson.get_compiler('cpp') if cpp.get_id() == 'msvc' add_project_arguments('/std:c++latest', language: 'cpp') elif cpp.get_id() == 'gcc' - add_project_arguments('-std=c++23', language: 'cpp') + add_project_arguments('-std=c++23', '-Wold-style-cast', language: 'cpp') elif cpp.get_id() == 'clang' add_project_arguments('-std=c++2b', language: 'cpp') else @@ -65,6 +65,7 @@ else endif deps += dependency('sdl2_ttf', 'SDL2_ttf', required: true, native: native) +deps += dependency('sdl2_net', 'SDL2_net', required: true, native: native) deps += dependency('spdlog', required: true, native: native) deps += dependency('nlohmann_json', required: true, native: native) deps += dependency( diff --git a/src/application.cpp b/src/application.cpp index 8ec150dd..040b59b4 100644 --- a/src/application.cpp +++ b/src/application.cpp @@ -72,7 +72,7 @@ void Application::handle_event(const SDL_Event& event) { #if defined(__ANDROID__) if (event.type == SDL_KEYDOWN and event.key.keysym.sym == SDLK_AC_BACK) { //TODO: also catch the resume event from the app (see SDLActivity.java) - // pause() + // pause() } #endif } diff --git a/src/command_line_arguments.hpp b/src/command_line_arguments.hpp index 1325b7fc..cf647304 100644 --- a/src/command_line_arguments.hpp +++ b/src/command_line_arguments.hpp @@ -3,11 +3,19 @@ #include "types.hpp" #include #include +#include #include #include #include #include +struct PlayModeInformation { + bool is_multiplayer; + bool is_server; + u8 player_nums; +}; + + struct CommandLineArguments { private: static inline constexpr int default_target_fps = 60; @@ -17,6 +25,7 @@ struct CommandLineArguments { tl::optional recording_path{}; u64 target_fps{ default_target_fps }; i32 starting_level{ default_starting_level }; + PlayModeInformation play_mode_information; CommandLineArguments(int argc, char** argv) { argparse::ArgumentParser parser{ "oopetris", "0.0.1", argparse::default_arguments::all }; @@ -29,10 +38,16 @@ struct CommandLineArguments { .help("the starting level of the game") .scan<'i', int>() .default_value(default_starting_level); + parser.add_argument("-m", "--multiplayer").help("play in multiplayer mode").scan<'i', int>(); + parser.add_argument("-s", "--server") + .help("is this instance the server of the multiplayer") + .default_value(false) + .implicit_value(true); + try { parser.parse_args(argc, argv); - if (auto path = parser.present("--recording")) { + if (auto path = parser.present("--recording")) { spdlog::info("recording is present"); recording_path = std::filesystem::path{ *path }; } @@ -52,6 +67,17 @@ struct CommandLineArguments { "invalid value for starting level ({}), using default value instead ({})", level, starting_level ); } + + const auto multiplayer = parser.present>("--multiplayer"); + if (multiplayer.has_value()) { + const auto multiplayer_num = parser.get("--multiplayer"); + const auto is_server = parser.get("--server"); + play_mode_information = PlayModeInformation{ true, is_server, static_cast(multiplayer_num) }; + } else { + play_mode_information = PlayModeInformation{ false, false, 0 }; + } + + } catch (const std::exception& err) { spdlog::error("error parsing command line arguments: {}", err.what()); #if defined(__ANDROID__) diff --git a/src/grid.cpp b/src/grid.cpp index fad0aecb..6f2052df 100644 --- a/src/grid.cpp +++ b/src/grid.cpp @@ -1,6 +1,7 @@ #include "grid.hpp" +#include "rect.hpp" -Grid::Grid(Point offset, int tile_size) : m_offset{ offset - Point{ 0, invisible_rows * tile_size }}, m_tile_size{ tile_size } { +Grid::Grid(Point offset, int tile_size) : m_start_point{ offset }, m_offset{ offset - Point{ 0, invisible_rows * tile_size }}, m_tile_size{ tile_size } { } [[nodiscard]] Point Grid::tile_size() const { @@ -42,11 +43,9 @@ void Grid::draw_playing_field_background(const Application& app) const { width * m_tile_size - 1, (height - invisible_rows) * m_tile_size, }; - app.renderer().draw_rect_filled(Rect{ Point::zero(), bottom_right }, background_color); - const Rect outline_rect = Rect{ - Point{m_offset.x, m_offset.y} - + Point{ 0, invisible_rows * m_tile_size}, - bottom_right - }; - app.renderer().draw_rect_outline(outline_rect, border_color); + app.renderer().draw_rect_filled(m_start_point + Rect{ Point::zero(), bottom_right }, background_color); + app.renderer().draw_line( + m_start_point + Point{ bottom_right.x + 1, 0 }, + m_start_point + Point{ bottom_right.x + 1, app.window().size().y - 1 }, border_color + ); } diff --git a/src/grid.hpp b/src/grid.hpp index e6ba2b61..691f0bbd 100644 --- a/src/grid.hpp +++ b/src/grid.hpp @@ -20,6 +20,7 @@ struct Grid final { static constexpr Point hold_tetromino_position = hold_background_position + Point{ 0, 1 }; private: + Point m_start_point; Point m_offset; int m_tile_size; diff --git a/src/input.cpp b/src/input.cpp index 42a6b9b2..ca54781b 100644 --- a/src/input.cpp +++ b/src/input.cpp @@ -1,8 +1,11 @@ #include "input.hpp" #include "application.hpp" +#include "event_dispatcher.hpp" #include "key_codes.hpp" #include "recording.hpp" #include "tetrion.hpp" +#include +#include #if defined(__ANDROID__) #include @@ -346,3 +349,36 @@ tl::optional TouchInput::sdl_event_to_input_event(const SDL_Event& e return tl::nullopt; } #endif + + +namespace utils { + + + [[nodiscard]] std::unique_ptr create_input( + Controls controls, + Tetrion* associated_tetrion, + Input::OnEventCallback on_event_callback, + EventDispatcher* event_dispatcher + ) { + +#if defined(__ANDROID__) + auto input = std::make_unique(associated_tetrion, std::move(on_event_callback)); + event_dispatcher->register_listener(input.get()); + return input; +#else + return std::visit( + overloaded{ + [&](KeyboardControls& keyboard_controls) -> std::unique_ptr { + auto input = std::make_unique( + associated_tetrion, std::move(on_event_callback), keyboard_controls + ); + + event_dispatcher->register_listener(input.get()); + return input; + }, + }, + controls + ); +#endif + } +} // namespace utils \ No newline at end of file diff --git a/src/input.hpp b/src/input.hpp index 65519a8c..af2bf469 100644 --- a/src/input.hpp +++ b/src/input.hpp @@ -1,15 +1,20 @@ #pragma once #include "controls.hpp" +#include "event_dispatcher.hpp" #include "event_listener.hpp" #include "input_event.hpp" +#include "network/connection_manager.hpp" +#include "network/network_data.hpp" #include "random.hpp" #include "settings.hpp" #include "types.hpp" #include #include +#include #include #include +#include struct Tetrion; @@ -52,7 +57,7 @@ struct Input { public: virtual void update(); - virtual void late_update() {}; + virtual void late_update(){}; virtual ~Input() = default; }; @@ -122,3 +127,13 @@ struct TouchInput final : public Input, public EventListener { [[nodiscard]] tl::optional sdl_event_to_input_event(const SDL_Event& event); }; #endif + + +namespace utils { + [[nodiscard]] std::unique_ptr create_input( + Controls controls, + Tetrion* associated_tetrion, + Input::OnEventCallback on_event_callback, + EventDispatcher* event_dispatcher + ); +} \ No newline at end of file diff --git a/src/local_multiplayer.cpp b/src/local_multiplayer.cpp new file mode 100644 index 00000000..2a0dd9b9 --- /dev/null +++ b/src/local_multiplayer.cpp @@ -0,0 +1,353 @@ + +#include "local_multiplayer.hpp" +#include "network/network_data.hpp" +#include "network/network_manager.hpp" +#include "network/online_input.hpp" +#include "play_mode.hpp" +#include "tetrion.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +LocalMultiplayer::LocalMultiplayer(const bool is_replay_mode, const std::size_t num_players, const bool is_server) + : PlayMode{ is_replay_mode }, + m_num_players{ num_players }, + m_is_server{ is_server }, + m_network_manager{ NetworkManager{} }, + m_server{ nullptr }, + m_input_connections{ ConnectionStore{} } {}; + + +tl::expected LocalMultiplayer::init(Settings settings, Random::Seed seed) { + PlayMode::init(settings, seed); + + if (m_is_server) { + + if (m_num_players > 4 || m_num_players < 2) { + return tl::make_unexpected( + "The LocalMultiplayer mode only allows between 2 and 4 players, but got: " + + std::to_string(m_num_players) + ); + } + + if (settings.controls.size() < m_num_players) { + return tl::make_unexpected( + "Not enough controls provided: needed: " + std::to_string(m_num_players) + + " got: " + std::to_string(settings.controls.size()) + ); + } + + auto server = m_network_manager.spawn_server(); + if (!server.has_value()) { + return tl::make_unexpected("Error in initializing the server: " + server.error()); + } + + m_server = server.value(); + m_input_connections.reserve(m_num_players - 1); + + //TODO in here refactor some routines to separate functions! + for (std::size_t i = 0; i < m_num_players - 1; ++i) { + auto client = m_server->get_client(); + if (!client.has_value()) { + throw std::runtime_error{ "Waited to long for new client nr. " + std::to_string(i) }; + } + + const auto connection = client.value(); + + + auto send_initializer = connection->wait_for_data(10 * 1000, 100); + if (!send_initializer.has_value()) { + throw std::runtime_error{ "client nr. " + std::to_string(i) + + " didn't send InitializationData in time: " + send_initializer.error() }; + } + + const auto send_vector = send_initializer.value(); + if (send_vector.size() != 1) { + throw std::runtime_error{ + "client nr. "+ std::to_string(i)+" did send InitializationData in time, but also to much data afterwards, it can't be handled " + "here!" + }; + } + + const auto received_value = send_vector.at(0); + + if (!received_value.is_of_type()) { + throw std::runtime_error{ "client nr. " + std::to_string(i) + + " didn't send InitializationData but another serializable message!" }; + } + + const auto initializer_message = received_value.as_type(); + + if (initializer_message->type == InitializationDataType::Send) { + + if (initializer_message->uuid != 0) { + throw std::runtime_error{ "Wrong InitializationData: first connection must have uuid 0 but has " + + std::to_string(initializer_message->uuid) }; + } + + //this is correct fro online_input, set this client connection as player index, query conenction for info like starting piece and eventual other information + + //check if client game version is compatible with this game, otherwise reject (with a message to teh client and to the server host!) + + // send the information about how many players partecipate in response, await the creation of x-1 sockets to send them the event from them, not that the client doesn't have a static player number, only the server can (later select this from a menu) set this size + + + auto send_data = ClientInitializationData{ static_cast(m_num_players), + static_cast(i + 1), seed }; + const auto send_result = connection->send_data(&send_data); + if (send_result.has_value()) { + throw std::runtime_error{ "ClientInitializationData failed to send" + send_result.value() }; + } + + + auto receive_connections = std::vector>>{}; + receive_connections.reserve(m_num_players - 1); + + for (std::size_t j = 0; j < m_num_players - 1; ++j) { + auto receive_client = m_server->get_client(); + if (!receive_client.has_value()) { + throw std::runtime_error{ "Waited to long for new receive client nr. " + std::to_string(j) }; + } + + const auto receive_connection = receive_client.value(); + + + auto receive_send_initializer = receive_connection->wait_for_data(10 * 1000, 100); + if (!receive_send_initializer.has_value()) { + throw std::runtime_error{ "receive client nr. " + std::to_string(j) + + " didn't send InitializationData in time: " + + receive_send_initializer.error() }; + } + + const auto receive_send_vector = receive_send_initializer.value(); + if (send_vector.size() != 1) { + throw std::runtime_error{ + "receive client nr. "+ std::to_string(j)+" did send InitializationData in time, but also to much data afterwards, it can't be handled " + "here!" + }; + } + + const auto receive_received_value = receive_send_vector.at(0); + + if (!receive_received_value.is_of_type()) { + throw std::runtime_error{ + "receive client nr. " + std::to_string(j) + + " didn't send InitializationData but another serializable message!" + }; + } + + const auto receive_initializer_message = receive_received_value.as_type(); + + if (receive_initializer_message->type == InitializationDataType::Receive) { + + receive_connections.emplace_back(receive_initializer_message->uuid, receive_connection); + + } else { + throw std::runtime_error{ + "receive client didn't send InitializationDataType::Receive as first message!" + }; + } + } + + + m_input_connections.emplace_back( + std::pair>{ 0, connection }, receive_connections + ); + } else { + throw std::runtime_error{ "client didn't send InitializationDataType::Send as first message!" }; + } + } + + + return StartState{ m_num_players, seed }; + } else { + // client start here + + auto connection_result = get_connection_to_server(); + + if (!connection_result.has_value()) { + throw std::runtime_error{ connection_result.error() }; + } + + auto connection = connection_result.value(); + auto send_data = InitializationData{ InitializationDataType::Send, static_cast(0) }; + const auto send_result = connection->send_data(&send_data); + if (send_result.has_value()) { + throw std::runtime_error{ "InitializationData failed to send" + send_result.value() }; + } + + const auto data = await_exact_one(connection); + const auto num_players = data->player_num; + m_num_players = num_players; + + auto receive_connections = std::vector>>{}; + receive_connections.reserve(num_players - 1); + + + const auto my_player_id = data->your_player_id; + + for (std::uint32_t i = 0; i < num_players; ++i) { + if (i == my_player_id) { + continue; + } + + auto receive_connection_result = get_connection_to_server(); + + if (!receive_connection_result.has_value()) { + throw std::runtime_error{ receive_connection_result.error() }; + } + + auto receive_connection = receive_connection_result.value(); + auto receive_send_data = + InitializationData{ InitializationDataType::Receive, static_cast(i) }; + const auto receive_send_result = receive_connection->send_data(&receive_send_data); + if (receive_send_result.has_value()) { + throw std::runtime_error{ "InitializationData failed to send" + receive_send_result.value() }; + } + + receive_connections.emplace_back(i, receive_connection); + } + + m_input_connections.emplace_back( + std::pair>{ static_cast(my_player_id), + connection }, + receive_connections + ); + + + return StartState{ num_players, data->seed }; + } +} + + +std::unique_ptr LocalMultiplayer::get_input( + u8 index, + Tetrion* tetrion, + Input::OnEventCallback event_callback, + EventDispatcher* event_dispatcher +) { + if (index >= m_num_players) { + throw std::range_error{ "LocalMultiplayer mode: error in index of get_input" }; + } + + if (index == 0) { + if (m_is_server) { + + constexpr std::size_t my_id = 0; + auto send_to = std::vector>{}; + for (const auto& client_bundle : m_input_connections) { + + for (const auto& [num, receive_connection] : client_bundle.second) { + if (my_id == num) { + send_to.push_back(receive_connection); + } + } + } + + m_online_handlers.push_back( + std::make_unique(m_server, nullptr, send_to, std::move(event_callback)) + ); + + auto modified_event_callback = m_online_handlers.back()->get_callback_function(); + + tetrion->set_player_num(0); + return utils::create_input( + settings().controls.at(index), tetrion, std::move(modified_event_callback), event_dispatcher + ); + + /* tetrion->set_player_num(0); + return TetrisApplication::create_input( + settings().controls.at(index), tetrion, std::move(event_callback), event_dispatcher + ); + */ + } else { + + auto connection_result = get_connection_to_server(); + + if (m_input_connections.size() != 1) { + throw std::runtime_error{ + "LocalMultiplayer::init was implemented wrong, client only has one value in m_input_connections, " + "but were: " + + std::to_string(m_input_connections.size()) + }; + } + + m_online_handlers.push_back(std::make_unique( + nullptr, m_input_connections.at(0).first.second, std::move(event_callback) + )); + + + auto modified_event_callback = m_online_handlers.back()->get_callback_function(); + + tetrion->set_player_num(m_input_connections.at(0).first.first); + return utils::create_input( + settings().controls.at(index), tetrion, std::move(modified_event_callback), event_dispatcher + ); + } + } else { + + if (m_is_server) { + + auto online_input = std::make_unique(tetrion, m_input_connections.at(index - 1).first.second); + tetrion->set_player_num(index); + return online_input; + } else { + + if (m_input_connections.size() != 1) { + throw std::runtime_error{ + "LocalMultiplayer::init was implemented wrong, client only has one value in m_input_connections, " + "but were: " + + std::to_string(m_input_connections.size()) + }; + } + + std::shared_ptr connection = nullptr; + std::size_t player_num = 0; + const auto my_id = m_input_connections.at(0).first.first; + for (auto [num, receive_connection] : m_input_connections.at(0).second) { + const u8 actual_index = index <= my_id ? index - 1 : index; + if (actual_index == num) { + connection = receive_connection; + player_num = num; + break; + } + } + + if (!connection) { + throw std::runtime_error{ "fatal initialization error, no connection for index " + std::to_string(index) + + " found!" }; + } + + + auto online_input = std::make_unique(tetrion, connection); + + tetrion->set_player_num(player_num); + return online_input; + } + } +} + + +tl::expected, std::string> +LocalMultiplayer::get_connection_to_server(std::uint32_t delay_between_attempts, std::uint32_t connection_attempts) { + + + for (std::uint32_t i = 0; i < connection_attempts; ++i) { + MaybeConnection connection = m_network_manager.try_connect(); + if (connection.has_value()) { + return connection.value(); + } + SDL_Delay(delay_between_attempts); + } + + return tl::make_unexpected( + "Error in getting a connection for LocalMultiplayer: failed after " + std::to_string(connection_attempts) + + " attempts" + ); +} diff --git a/src/local_multiplayer.hpp b/src/local_multiplayer.hpp new file mode 100644 index 00000000..c9d85ee4 --- /dev/null +++ b/src/local_multiplayer.hpp @@ -0,0 +1,76 @@ + + +#pragma once + +#include "application.hpp" +#include "network/network_data.hpp" +#include "network/network_manager.hpp" +#include "network/online_handler.hpp" +#include "play_mode.hpp" +#include "random.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using ConnectionStore = std::vector>, + std::vector>>>>; + +struct LocalMultiplayer : public PlayMode { +private: + std::size_t m_num_players; + bool m_is_server; + NetworkManager m_network_manager; + std::shared_ptr m_server; + ConnectionStore m_input_connections; + std::vector> m_online_handlers; + + tl::expected, std::string> + get_connection_to_server(std::uint32_t delay_between_attempts = 200, std::uint32_t connection_attempts = 10); + +public: + explicit LocalMultiplayer(const bool is_replay_mode, const std::size_t num_players, const bool is_server); + tl::expected init(Settings settings, Random::Seed seed) override; + std::unique_ptr get_input( + u8 index, + Tetrion* tetrion, + Input::OnEventCallback event_callback, + EventDispatcher* event_dispatcher + ) override; +}; + + +// little fast helper, +//TODO refactor better and move in some reasonable cpp, and don't use exceptions to handle this +template +//TODO add again +// requires std::derived_from +std::shared_ptr await_exact_one(std::shared_ptr connection) { + //TODO make this number customizable + auto send_initializer = connection->wait_for_data(10 * 1000, 100); + if (!send_initializer.has_value()) { + throw std::runtime_error{ "didn't receive data in time: " + send_initializer.error() }; + } + + const auto send_vector = send_initializer.value(); + if (send_vector.size() != 1) { + throw std::runtime_error{ + "did receive data in time, but also to much data afterwards, it can't be handled " + "here!" + }; + } + + const auto received_value = send_vector.at(0); + + if (!received_value.is_of_type()) { + throw std::runtime_error{ "didn't receive correct data but another serializable message!" }; + } + + return received_value.as_type(); +} \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index 6dac6555..31fa2828 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,9 +1,18 @@ #include "command_line_arguments.hpp" +#include "local_multiplayer.hpp" +#include "play_mode.hpp" #include "tetris_application.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + int main(int argc, char** argv) { TetrisApplication tetris_app(CommandLineArguments{ argc, argv }); tetris_app.run(); - - return 0; } diff --git a/src/meson.build b/src/meson.build index b4511124..6bf4d6ab 100644 --- a/src/meson.build +++ b/src/meson.build @@ -1,54 +1,56 @@ src_files += files( - 'main.cpp', - 'application.hpp', 'application.cpp', - 'renderer.cpp', - 'renderer.hpp', - 'sdl_context.cpp', - 'sdl_context.hpp', - 'window.cpp', - 'window.hpp', - 'color.hpp', - 'rect.hpp', - 'point.hpp', - 'tetromino.hpp', - 'mino.hpp', - 'grid.hpp', - 'tetromino_type.hpp', - 'tetromino_type.cpp', - 'mino.cpp', - 'tetrion.cpp', - 'tetrion.hpp', - 'tetris_application.hpp', - 'tetris_application.cpp', + 'application.hpp', 'bag.cpp', 'bag.hpp', - 'font.cpp', - 'font.hpp', - 'text.cpp', - 'text.hpp', - 'grid.cpp', - 'input.hpp', + 'color.hpp', + 'color.hpp', + 'command_line_arguments.hpp', + 'controls.hpp', 'event_dispatcher.cpp', 'event_dispatcher.hpp', 'event_listener.hpp', + 'font.cpp', + 'font.hpp', + 'grid.cpp', + 'grid.hpp', 'input.cpp', - 'settings.hpp', + 'input.hpp', + 'input_event.hpp', 'key_codes.hpp', + 'local_multiplayer.cpp', 'magic_enum_wrapper.hpp', - 'types.hpp', + 'main.cpp', + 'mino.cpp', + 'mino.hpp', + 'mino_stack.cpp', + 'play_mode.cpp', 'random.cpp', 'random.hpp', 'recording.hpp', - 'input_event.hpp', - 'utils.hpp', - 'utils.cpp', - 'command_line_arguments.hpp', - 'controls.hpp', - 'mino_stack.hpp', - 'mino_stack.cpp', - 'tetrion_snapshot.hpp', + 'rect.hpp', + 'renderer.cpp', + 'renderer.hpp', + 'sdl_context.cpp', + 'sdl_context.hpp', + 'settings.hpp', + 'tetrion.cpp', + 'tetrion.hpp', 'tetrion_snapshot.cpp', + 'tetrion_snapshot.hpp', + 'tetris_application.cpp', + 'tetris_application.hpp', + 'tetromino.hpp', + 'tetromino_type.cpp', + 'tetromino_type.hpp', + 'text.cpp', + 'text.hpp', + 'types.hpp', + 'utils.cpp', + 'utils.hpp', + 'window.cpp', + 'window.hpp', ) -inc_dirs += include_directories('.') + +subdir('network') diff --git a/src/mino.hpp b/src/mino.hpp index 69ea91a6..41a6a06e 100644 --- a/src/mino.hpp +++ b/src/mino.hpp @@ -34,6 +34,6 @@ struct Mino final { } [[nodiscard]] bool operator!=(const Mino& other) const { - return not (*this == other); + return not(*this == other); } }; diff --git a/src/network/connection_manager.cpp b/src/network/connection_manager.cpp new file mode 100644 index 00000000..84b4c45e --- /dev/null +++ b/src/network/connection_manager.cpp @@ -0,0 +1,175 @@ + +#include "connection_manager.hpp" +#include "network_manager.hpp" +#include "network_transportable.hpp" +#include +#include +#include +#include +#include +#include + + +Connection::Connection(TCPsocket socket) : m_socket{ socket } {}; + +Connection::~Connection() { + SDLNet_TCP_Close(m_socket); +} + +[[nodiscard]] tl::expected Connection::is_data_available(Uint32 timeout_ms) { + + SDLNet_SocketSet socket_set = SDLNet_AllocSocketSet(1); + if (!socket_set) { + return tl::make_unexpected("no more memory for creating a SDLNet_SocketSet"); + } + + + const auto num_sockets = SDLNet_TCP_AddSocket(socket_set, m_socket); + if (num_sockets != 1) { + SDLNet_FreeSocketSet(socket_set); + return tl::make_unexpected("SDLNet_AddSocket failed, this is an implementation error"); + } + + const auto result = SDLNet_CheckSockets(socket_set, timeout_ms); + if (result == -1) { + SDLNet_FreeSocketSet(socket_set); + return tl::make_unexpected("SDLNet_CheckSockets error (select() system call error)"); + } + + SDLNet_FreeSocketSet(socket_set); + return result == 1; +} + +[[nodiscard]] tl::expected Connection::get_all_data_blocking() { + void* memory = std::malloc(Connection::chunk_size); + if (!memory) { + return tl::make_unexpected("error in malloc for receiving a socket message"); + } + std::uint32_t data_size = 0; + while (true) { + const int received_length = SDLNet_TCP_Recv(m_socket, memory, Connection::chunk_size); + if (received_length <= 0) { + free(memory); + return tl::make_unexpected("SDLNet_TCP_Recv: " + std::string{ SDLNet_GetError() }); + } + + if (received_length != Connection::chunk_size) { + + return RawBytes{ static_cast(memory), data_size + received_length }; + } + + data_size += Connection::chunk_size; + void* new_memory = std::realloc(memory, data_size + Connection::chunk_size); + if (!new_memory) { + free(memory); + return tl::make_unexpected("error in realloc for receiving a socket message"); + } + memory = new_memory; + } +} + +[[nodiscard]] tl::expected, std::string> Connection::get_data() { + + const auto data_available = is_data_available(); + if (!data_available.has_value()) { + return tl::make_unexpected("in is_data_available: " + data_available.error()); + } + + if (!data_available.value()) { + return std::vector{}; + } + + const auto data = get_all_data_blocking(); + if (!data.has_value()) { + return tl::make_unexpected("in get_all_data_blocking: " + data.error()); + } + + + const RawBytes raw_bytes = data.value(); + + const auto result = RawTransportData::from_raw_bytes(raw_bytes); + if (!result.has_value()) { + free(raw_bytes.first); + return tl::make_unexpected("in RawTransportData::from_raw_bytes: " + result.error()); + } + + free(raw_bytes.first); + return result.value(); +} + +[[nodiscard]] tl::expected, std::string> +Connection::wait_for_data(std::size_t abort_after, Uint32 ms_delay) { + const auto start_time = SDL_GetTicks64(); + while (true) { + /* try if data is available */ + const auto data_available = is_data_available(); + if (!data_available.has_value()) { + return tl::make_unexpected("In Connection::wait_for_data: " + data_available.error()); + } + + if (data_available.value()) { + const auto result = get_data(); + if (!result.has_value()) { + return tl::make_unexpected("In Connection::wait_for_data: " + result.error()); + } + + const auto data_vector = result.value(); + if (data_vector.size() == 0) { + return tl::make_unexpected( + "In Connection::wait_for_data: waited for data, but now no data is available?? This is an " + "implementation bug!" + ); + } + + return data_vector; + } + + const auto elapsed_time = SDL_GetTicks64() - start_time; + if (elapsed_time >= abort_after) { + return tl::make_unexpected("In Connection::wait_for_data: took to long"); + } + + SDL_Delay(ms_delay); + continue; + } +} + + +Server::Server(TCPsocket socket) : m_socket{ socket }, m_connections{ std::vector>{} } {}; + +Server::~Server() { + SDLNet_TCP_Close(m_socket); +} + +[[nodiscard]] tl::optional> Server::try_get_client() { + + TCPsocket client; + /* try to accept a connection */ + client = SDLNet_TCP_Accept(m_socket); + if (!client) { + return tl::nullopt; + } + + const auto connection = std::make_shared(client); + m_connections.push_back(connection); + return connection; +} + +[[nodiscard]] tl::optional> Server::get_client(Uint32 ms_delay, std::size_t abort_after) { + const auto start_time = SDL_GetTicks64(); + while (true) { + + const auto client = try_get_client(); + if (client.has_value()) { + return client; + } + + const auto elapsed_time = SDL_GetTicks64() - start_time; + if (elapsed_time >= abort_after) { + return tl::nullopt; + } + + SDL_Delay(ms_delay); + continue; + } +} diff --git a/src/network/connection_manager.hpp b/src/network/connection_manager.hpp new file mode 100644 index 00000000..d837fd94 --- /dev/null +++ b/src/network/connection_manager.hpp @@ -0,0 +1,158 @@ + +#pragma once + + +#include "network_transportable.hpp" +#include +#include +#include +#include +#include +#include +#include + + +/** + * @brief This is a wrapper for a client TCPSocket. It has convenience functions to interact with the socket and send serialized data over it, + * note that the type 'TCPsocket' is a typedef for some '_TCPsocket *' so it's a raw pointer. You shouldn't create a connection yourself, + * to get one you have to call 'Server::try_get_client()' instead. That will handle everything for you automatically. + * If you would like to create a Connection manually, here are some notes about that: + * The lifetime of the underlying TCPsocket is managed by SDL functions and therefore it's closed and deallocated on Destruction of this class. + * By passing in a socket you give the connection the ownership of this pointer! + */ +struct Connection { + +private: + TCPsocket m_socket; + + /** + * @brief static variable for the chunk size for receiving data + */ + static constexpr std::size_t chunk_size = 1024; + +public: + explicit Connection(TCPsocket socket); + ~Connection(); + // these have to be deleted, since a Socket can't be copied, since a copied socket would be closed by destruction of the original and vice versa + explicit Connection(Connection& connection) = delete; + [[nodiscard]] Connection& operator=(Connection& other) = delete; + + //TODO write a method to check if the connection was destroyed + + /** + * @brief check if the underlying socket has some data available + * @param timeout_ms an optional paramater how long to wait for new data, default is 3 ms + * @return tl::expected with the expected value being a bool that describes if data is available and error state is the error as string + */ + [[nodiscard]] tl::expected is_data_available(Uint32 timeout_ms = 3); + + /** + * @brief get all data that is available. this is a blocking function, so not calling 'Connection::is_data_available()' + * before this function may block for a long time! + * This isn't supposed to be used, since it returns raw bytes. But you can use it nevertheless, just note, that the underlying data + * is malloced and has to be freed. If you only transfer classes that are children of 'Transportable' don't use this, + * but use 'Connection::get_data()' + * @return tl::expected with the expected value being a pointer and length wrapped in a pair. note that teh data was malloced + * and has to be freed. Note also that the data is continuous and may be multiple Message one after another. + * To avoid this use 'Connection::get_data()' The error state is the error as string + */ + [[nodiscard]] tl::expected get_all_data_blocking(); + + /** + * @brief get the available data of the Connection. + * @return tl::expected with the expected value being a std::vector and the error the error as string + * the vector contains 0 or more elements of the struct RawTransportData. A vector with length 0 means no data was available. + * 'RawTransportData' has some helpful template member function that can get it's real type and get the underlying type. + * Every data is handled in form of shared pointers that wrap malloced data, it's automatically freed + * in the destructor of the std::shared_ptr and all function that are designed to be used with handle this correctly. + */ + tl::expected, std::string> get_data(); + + /** + * @brief get the available data of the Connection. + * @param timeout_ms an optional paramater how long to wait for new data, default is 3 ms + * @return tl::expected with the expected value being a std::vector and the error the error as string + * the vector contains 0 or more elements of the struct RawTransportData. A vectro with length 0 means no data was available, This has some helpful template member function that can get it's real type and get the underlying type. Every dat is handled in form of shared pointers that wrap malloced data, it'S automatically freed in teh destructor of the std::shared_ptr and all function that are designed to be used with it are + */ + [[nodiscard]] tl::expected, std::string> + wait_for_data(std::size_t abort_after = 60 * 1000, Uint32 ms_delay = 100); + + + template + //TODO add again + //requires std::derived_from std::shared_ptr + [[nodiscard]] tl::optional send_data(const T* transportable) { + + + const auto serialized = Transportable::serialize(transportable); + if (!serialized.has_value()) { + return tl::make_optional("In Connection::send_data: " + serialized.error()); + } + + auto [message, length] = serialized.value(); + + const auto result = SDLNet_TCP_Send(m_socket, message, length); + if (result == -1) { + std::free(message); + return tl::make_optional("SDLNet_TCP_Send: invalid socket"); + } + + if (static_cast(result) != length) { + std::free(message); + std::string error = "SDLNet_TCP_Send: " + std::string{ SDLNet_GetError() }; + return tl::make_optional(error); + } + + std::free(message); + + return tl::nullopt; + } +}; + + +struct Server { + + //TODO shutdown server on quit + +private: + TCPsocket m_socket; + std::vector> m_connections; + +public: + explicit Server(TCPsocket socket); + ~Server(); + // these have to be deleted, since a Socket can't be copied, since a copied socket would be closed by destruction of the original and vice versa + explicit Server(Server& server) = delete; + [[nodiscard]] Server& operator=(Server& other) = delete; + + [[nodiscard]] tl::optional> try_get_client(); + + [[nodiscard]] tl::optional> + get_client(Uint32 ms_delay = 100, std::size_t abort_after = 60 * 1000); + + template + //TODO add again + //requires std::derived_from std::shared_ptr + [[nodiscard]] tl::optional + send_all(const T* transportable, std::vector> send_to) { + + for (std::size_t i = 0; i < send_to.size(); ++i) { + auto result = send_to.at(i)->send_data(transportable); + if (result.has_value()) { + return tl::make_optional( + "Error while sending to client: " + std::to_string(i) + " : " + result.value() + ); + } + } + + return tl::nullopt; + } + + template + //TODO add again + //requires std::derived_from std::shared_ptr + [[nodiscard]] tl::optional send_all(const T* transportable) { + + return send_all(transportable, m_connections); + } +}; diff --git a/src/network/crc32.h b/src/network/crc32.h new file mode 100644 index 00000000..251e0fc4 --- /dev/null +++ b/src/network/crc32.h @@ -0,0 +1,39 @@ +// from:https://gist.github.com/timepp/1f678e200d9e0f2a043a9ec6b3690635 + +#pragma once + +#include + +struct crc32 { + static void generate_table(uint32_t (&table)[256]) { + uint32_t polynomial = 0xEDB88320; + for (uint32_t i = 0; i < 256; i++) { + uint32_t c = i; + for (size_t j = 0; j < 8; j++) { + if (c & 1) { + c = polynomial ^ (c >> 1); + } else { + c >>= 1; + } + } + table[i] = c; + } + } + + static uint32_t update(uint32_t (&table)[256], uint32_t initial, const void* buf, size_t len) { + uint32_t c = initial ^ 0xFFFFFFFF; + const uint8_t* u = static_cast(buf); + for (size_t i = 0; i < len; ++i) { + c = table[(c ^ u[i]) & 0xFF] ^ (c >> 8); + } + return c ^ 0xFFFFFFFF; + } +}; + + +// usage: the following code generates crc for 2 pieces of data +// uint32_t table[256]; +// crc32::generate_table(table); +// uint32_t crc = crc32::update(table, 0, data_piece1, len1); +// crc = crc32::update(table, crc, data_piece2, len2); +// output(crc); diff --git a/src/network/meson.build b/src/network/meson.build new file mode 100644 index 00000000..b305ac12 --- /dev/null +++ b/src/network/meson.build @@ -0,0 +1,11 @@ +src_files += files( + 'connection_manager.cpp', + 'network_data.cpp', + 'network_manager.cpp', + 'network_transportable.cpp', + 'online_handler.cpp', + 'online_input.cpp', +) + +inc_dirs += include_directories('.') + diff --git a/src/network/network_data.cpp b/src/network/network_data.cpp new file mode 100644 index 00000000..f6557132 --- /dev/null +++ b/src/network/network_data.cpp @@ -0,0 +1,24 @@ + + +#include "network_data.hpp" +#include "../random.hpp" +#include "network_transportable.hpp" +#include + + +InitializationData::InitializationData(InitializationDataType type, std::uint32_t uuid) : type{ type }, uuid{ uuid } {}; + + +EventData::EventData(InputEvent event, u64 simulation_step_index) + : event{ event }, + simulation_step_index{ simulation_step_index } {}; + + +ClientInitializationData::ClientInitializationData( + std::uint32_t player_num, + std::uint32_t your_player_id, + Random::Seed seed +) + : player_num{ player_num }, + your_player_id{ your_player_id }, + seed{ seed } {}; diff --git a/src/network/network_data.hpp b/src/network/network_data.hpp new file mode 100644 index 00000000..675e9261 --- /dev/null +++ b/src/network/network_data.hpp @@ -0,0 +1,40 @@ + + +#pragma once + +#include "../input.hpp" +#include "../random.hpp" +#include "../types.hpp" +#include "network_transportable.hpp" +#include + +enum class InitializationDataType : uint8_t { Receive, Send }; + +struct InitializationData : public Transportable { +public: + static constexpr std::uint32_t serialUUID = 1; + + InitializationDataType type; + std::uint32_t uuid; + + explicit InitializationData(InitializationDataType type, std::uint32_t uuid); +}; + +struct EventData : public Transportable { + static constexpr std::uint32_t serialUUID = 2; + + InputEvent event; + u64 simulation_step_index; + + explicit EventData(InputEvent event, u64 simulation_step_index); +}; + +struct ClientInitializationData : public Transportable { +public: + std::uint32_t player_num; + std::uint32_t your_player_id; + Random::Seed seed; + + static constexpr std::uint32_t serialUUID = 3; + explicit ClientInitializationData(std::uint32_t player_num, std::uint32_t your_player_id, Random::Seed seed); +}; diff --git a/src/network/network_manager.cpp b/src/network/network_manager.cpp new file mode 100644 index 00000000..48c1cf01 --- /dev/null +++ b/src/network/network_manager.cpp @@ -0,0 +1,48 @@ + +#include "network_manager.hpp" +#include "network_transportable.hpp" +#include +#include +#include +#include +#include + + +NetworkManager::NetworkManager() : m_connections{ std::vector{} } {}; + + +MaybeConnection NetworkManager::try_connect(const char* host, Uint16 port) { + + IPaddress ip; + TCPsocket tcpsock; + + if (SDLNet_ResolveHost(&ip, host, port) == -1) { + std::string error = "SDLNet_ResolveHost: " + std::string{ SDLNet_GetError() }; + return tl::make_unexpected(error); + } + + tcpsock = SDLNet_TCP_Open(&ip); + if (!tcpsock) { + std::string error = "SDLNet_TCP_Open: " + std::string{ SDLNet_GetError() }; + return tl::make_unexpected(error); + } + + return std::make_shared(tcpsock); +} + +MaybeServer NetworkManager::spawn_server(Uint16 port) { + + TCPsocket server; + IPaddress ip; + if (SDLNet_ResolveHost(&ip, NULL, port) == -1) { + std::string error = "SDLNet_ResolveHost: " + std::string{ SDLNet_GetError() }; + return tl::make_unexpected(error); + } + server = SDLNet_TCP_Open(&ip); + if (!server) { + std::string error = "SDLNet_TCP_Open: " + std::string{ SDLNet_GetError() }; + return tl::make_unexpected(error); + } + + return std::make_shared(server); +} diff --git a/src/network/network_manager.hpp b/src/network/network_manager.hpp new file mode 100644 index 00000000..e243040d --- /dev/null +++ b/src/network/network_manager.hpp @@ -0,0 +1,30 @@ + + + +#pragma once + +#include "connection_manager.hpp" +#include "network_transportable.hpp" +#include +#include +#include +#include +#include +#include +#include + + +using MaybeConnection = tl::expected, std::string>; +using MaybeServer = tl::expected, std::string>; + +struct NetworkManager { +private: + std::vector m_connections; + static constexpr const char* ServerHost = "localhost"; + static constexpr const Uint16 Port = 1212; + +public: + explicit NetworkManager(); + MaybeConnection try_connect(const char* host = NetworkManager::ServerHost, Uint16 port = NetworkManager::Port); + MaybeServer spawn_server(Uint16 port = NetworkManager::Port); +}; diff --git a/src/network/network_transportable.cpp b/src/network/network_transportable.cpp new file mode 100644 index 00000000..07dab56a --- /dev/null +++ b/src/network/network_transportable.cpp @@ -0,0 +1,166 @@ + + +#include "network_transportable.hpp" +#include "../util.hpp" +#include "crc32.h" +#include +#include +#include +#include +#include +#include +#include +#include + + +std::uint32_t Transportable::checksum(RawBytes bytes) { + + auto [start, length] = bytes; + + std::uint32_t table[256] = {}; + crc32::generate_table(table); + std::uint32_t CRC = 0; + for (std::uint32_t i = 0; i < length; ++i) { + CRC = crc32::update(table, CRC, start, 1); + start++; + } + + return CRC; +} + + +void Transportable::write_header(RawBytes bytes, std::uint32_t uuid, std::uint32_t data_size) { + auto [start, length] = bytes; + + if (length != Transportable::header_size) { + throw std::runtime_error{ "Wrong call of Transportable::write_header : header size mismatch" }; + } + + std::uint32_t* data_ptr = reinterpret_cast(start); + + data_ptr[0] = Transportable::protocol_version; + + data_ptr[1] = uuid; + data_ptr[2] = data_size; +} + +void Transportable::write_data(RawBytes bytes, const Transportable* transportable) { + + auto [start, length] = bytes; + + const uint8_t* data_ptr = reinterpret_cast(transportable); + std::memcpy(start, data_ptr, length); +} + + +void Transportable::write_checksum(RawBytes bytes) { + + auto [start, length] = bytes; + + std::uint32_t data_size = length - Transportable::checksum_size; + + std::uint32_t checksum = Transportable::checksum(RawBytes{ start, data_size }); + + std::uint32_t* data_ptr = reinterpret_cast(static_cast(start) + data_size); + + data_ptr[0] = checksum; +} + +RawTransportData::RawTransportData(std::uint32_t serialUUID, std::shared_ptr data, uint32_t data_size) + : m_serialUUID{ serialUUID }, + m_data{ data }, + m_data_size{ data_size } {}; + + +tl::expected, std::string> RawTransportData::from_raw_bytes(RawBytes raw_bytes) { + + auto result = std::vector{}; + + auto [start, length] = raw_bytes; + + long remaining_length = length; + auto advance = [&](std::uint32_t size) { + remaining_length -= size; + start += size; + }; + while (remaining_length > 0) { + + auto header = RawTransportData::read_header(RawBytes{ start, remaining_length }); + if (!header.has_value()) { + return tl::make_unexpected("in RawTransportData::from_raw_bytes: " + header.error()); + } + + advance(Transportable::header_size); + + auto [_protocol_version, uuid, data_size] = header.value(); + + if (remaining_length < static_cast(data_size)) { + return tl::make_unexpected( + "in RawTransportData::from_raw_bytes: couldn't read data, since the raw data is to small" + ); + } + + //TODO check if implemented correctly (check with valgrind --tool=memcheck --leak-check=full) + // this malloc get'S freed in the shared ptr destructor later + uint8_t* memory = static_cast(std::malloc(data_size)); + if (!memory) { + return tl::make_unexpected("in RawTransportData::from_raw_bytes: error in malloc for raw data"); + } + std::memcpy(memory, start, data_size); + std::shared_ptr data{ memory, std::free }; + + auto checksum = RawTransportData::read_checksum(RawBytes{ start, remaining_length }, data_size); + + advance(data_size + Transportable::checksum_size); + result.emplace_back(uuid, data, data_size); + } + + return result; +} + +tl::expected, std::string> RawTransportData::read_header( + RawBytes bytes +) { + auto [start, length] = bytes; + if (length < Transportable::header_size) { + return tl::make_unexpected("couldn't read header, since the raw data is to small"); + } + + std::uint32_t* data_ptr = reinterpret_cast(start); + + std::uint32_t protocol_version_number = data_ptr[0]; + if (RawTransportData::protocol_version != protocol_version_number) { + return tl::make_unexpected( + "couldn't parse header, since the protocol version mismatches: parser can parse: " + + std::to_string(RawTransportData::protocol_version) + + "but received: " + std::to_string(protocol_version_number) + ); + } + + std::uint32_t uuid = data_ptr[1]; + std::uint32_t data_size = data_ptr[2]; + return std::tuple{ protocol_version_number, uuid, data_size }; +} + + +tl::expected RawTransportData::read_checksum(RawBytes bytes, std::uint32_t data_size) { + auto [start, length] = bytes; + if (length < data_size + Transportable::checksum_size) { + return tl::make_unexpected("couldn't read checksum, since the raw data is to small"); + } + + std::uint32_t calc_checksum = Transportable::checksum(RawBytes{ start, data_size }); + + std::uint32_t* data_ptr = reinterpret_cast(static_cast(start) + data_size); + + std::uint32_t read_checksum = data_ptr[0]; + + if (read_checksum != calc_checksum) { + return tl::make_unexpected( + "couldn't read data, since the checksum mismatches: read checksum: " + util::to_hex_str(read_checksum) + + "but calculated checksum: " + util::to_hex_str(calc_checksum) + ); + } + + return read_checksum; +} \ No newline at end of file diff --git a/src/network/network_transportable.hpp b/src/network/network_transportable.hpp new file mode 100644 index 00000000..055e958d --- /dev/null +++ b/src/network/network_transportable.hpp @@ -0,0 +1,101 @@ + + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using RawBytes = std::pair; + +/* abstract*/ struct Transportable { +public: + virtual ~Transportable() = default; + // if you change the protocol behaviour in some way, change this number and the number in RawTransportData + static constexpr std::uint32_t protocol_version = 1; + static constexpr std::uint32_t checksum_size = sizeof(std::uint32_t); + static constexpr std::uint32_t header_size = sizeof(std::uint32_t) + sizeof(std::uint32_t) + sizeof(std::uint32_t); + // every class has to have such a serialUUID in some way + //TODO enforce this in some compile time way, that they are unique! + /* virtual */ static constexpr std::uint32_t serialUUID = 0; + + template + //TODO add again + // requires std::derived_from std::shared_ptr + static tl::expected serialize(const T* transportable) { + constexpr std::uint32_t data_size = sizeof(T); + + const std::uint32_t send_size = Transportable::header_size + data_size + Transportable::checksum_size; + + uint8_t* memory = static_cast(std::malloc(send_size)); + if (!memory) { + return tl::make_unexpected("error in malloc for sending a message"); + } + + + Transportable::write_header(RawBytes{ memory, Transportable::header_size }, T::serialUUID, data_size); + + Transportable::write_data(RawBytes{ memory + Transportable::header_size, data_size }, transportable); + Transportable::write_checksum(RawBytes{ memory + Transportable::header_size, + data_size + Transportable::checksum_size }); + + return RawBytes{ memory, send_size }; + } + + static std::uint32_t checksum(RawBytes bytes); + +protected: + explicit Transportable() { } + +private: + static void write_header(RawBytes bytes, std::uint32_t uuid, std::uint32_t data_size); + static void write_data(RawBytes bytes, const Transportable* transportable); + static void write_checksum(RawBytes bytes); +}; + +//TODO fix inconsistencies in std::size_t and std::uint32_t usage + + +struct RawTransportData { +private: + std::uint32_t m_serialUUID; + std::shared_ptr m_data; + uint32_t m_data_size; + +public: + RawTransportData(std::uint32_t serialUUID, std::shared_ptr data, uint32_t data_size); + + // if you change the protocol behaviour in some way, change this number and the number in Transportable (this parses only packets with the same protocol_version) + static constexpr std::uint32_t protocol_version = 1; + + static tl::expected, std::string> from_raw_bytes(RawBytes raw_bytes); + static tl::expected, std::string> read_header(RawBytes bytes + ); + static tl::expected read_checksum(RawBytes bytes, std::uint32_t data_size); + + template + //TODO add again + // requires std::derived_from std::shared_ptr + bool is_of_type() const { + return m_serialUUID == T::serialUUID; + } + + template + //TODO add again + //requires std::derived_from std::shared_ptr + std::shared_ptr as_type() const { + if (!is_of_type()) { + throw std::bad_cast{}; + } + + // using copy constructor, so that this only get's freed, when also the new usage of this raw malloced memory is done + std::shared_ptr data = std::reinterpret_pointer_cast(m_data); + return data; + } +}; \ No newline at end of file diff --git a/src/network/online_handler.cpp b/src/network/online_handler.cpp new file mode 100644 index 00000000..d5f67a81 --- /dev/null +++ b/src/network/online_handler.cpp @@ -0,0 +1,55 @@ + + +#include "online_handler.hpp" +#include "../application.hpp" +#include "../input.hpp" +#include "connection_manager.hpp" +#include "network_data.hpp" +#include +#include + +OnlineHandler::OnlineHandler( + std::shared_ptr server, + std::shared_ptr connection, + Input::OnEventCallback on_event_callback +) + : m_server{ server }, + m_connection{ connection }, + m_send_to{ std::vector>{} }, + m_on_event_callback{ std::move(on_event_callback) } {}; + + +OnlineHandler::OnlineHandler( + std::shared_ptr server, + std::shared_ptr connection, + std::vector> send_to, + Input::OnEventCallback on_event_callback +) + : m_server{ server }, + m_connection{ connection }, + m_send_to{ send_to }, + m_on_event_callback{ std::move(on_event_callback) } {}; + + +void OnlineHandler::handle_event(InputEvent event, u64 simulation_step_index) { + if (m_server) { + auto event_data = EventData{ event, simulation_step_index }; + //TODO handle error with spdlog + [[maybe_unused]] auto temp = m_server->send_all(&event_data, m_send_to); + + } else if (m_connection) { + auto event_data = EventData{ event, simulation_step_index }; + //TODO handle error with spdlog + [[maybe_unused]] auto temp = m_connection->send_data(&event_data); + } else { + throw std::runtime_error{ "OnlineHandler needs either a connection (client mode) or server!" }; + } +} + +Input::OnEventCallback OnlineHandler::get_callback_function() { + + return [this](InputEvent event) { + this->m_on_event_callback(event); + this->handle_event(event, Application::simulation_step_index()); + }; +} \ No newline at end of file diff --git a/src/network/online_handler.hpp b/src/network/online_handler.hpp new file mode 100644 index 00000000..f4599fb7 --- /dev/null +++ b/src/network/online_handler.hpp @@ -0,0 +1,31 @@ + +#pragma once + +#include "../input.hpp" +#include "connection_manager.hpp" +#include +#include + +struct OnlineHandler { +private: + std::shared_ptr m_server; + std::shared_ptr m_connection; + std::vector> m_send_to; + Input::OnEventCallback m_on_event_callback; + + void handle_event(InputEvent event, u64 simulation_step_index); + +public: + OnlineHandler( + std::shared_ptr server, + std::shared_ptr connection, + Input::OnEventCallback on_event_callback + ); + OnlineHandler( + std::shared_ptr server, + std::shared_ptr connection, + std::vector> send_to, + Input::OnEventCallback on_event_callback + ); + Input::OnEventCallback get_callback_function(); +}; \ No newline at end of file diff --git a/src/network/online_input.cpp b/src/network/online_input.cpp new file mode 100644 index 00000000..3d7858cf --- /dev/null +++ b/src/network/online_input.cpp @@ -0,0 +1,63 @@ + +#include "online_input.hpp" +#include "../application.hpp" + + +void OnlineInput::get_data() { + auto data = m_connection->get_data(); + if (!data.has_value()) { + // TODO: print error here (to log e.g.) + // auto error = data.error(); + return; + } + + const auto data_vector = data.value(); + + if (data_vector.size() == 0) { + // no data given + return; + } + + for (const auto& received_data : data_vector) { + + if (received_data.is_of_type()) { + auto data = received_data.as_type(); + //TODO maybe handle return value ? + m_data.push_back(data); + } + } +} + +void OnlineInput::update() { + //TODO the connection should now if it's disconnected + /* if (not m_connection.is_disconnected()) { + break; + } */ + + get_data(); + + const auto current_application_simulation_step = Application::simulation_step_index(); + for (const auto& data : m_data) { + + //TODO: synchronize so that if I receive an event that is 2 frames behind it has to be displayed nontheless, atm only start snychronization is needed + // const auto is_record_for_current_step = (data->simulation_step_index == current_application_simulation_step); + + if (data->simulation_step_index > current_application_simulation_step) { + continue; + } + + Input::handle_event(data->event); + } + // remove all old data + //TODO improve, that it doesn't discard to much, e.g. wait if it doesn't receive data as quickly + m_data.erase( + std::remove_if( + m_data.begin(), m_data.end(), + [&](const auto& data) { return data->simulation_step_index <= current_application_simulation_step; } + ), + m_data.end() + ); + + + Input::update(); +} diff --git a/src/network/online_input.hpp b/src/network/online_input.hpp new file mode 100644 index 00000000..144c9b65 --- /dev/null +++ b/src/network/online_input.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include "../input.hpp" +#include "../input_event.hpp" +#include "../types.hpp" +#include "connection_manager.hpp" +#include "network_data.hpp" +#include +#include + +struct OnlineInput : public Input { +private: + std::shared_ptr m_connection; + std::vector> m_data; + + void get_data(); + +public: + explicit OnlineInput(Tetrion* tetrion, std::shared_ptr connection) + : Input{ tetrion }, + m_connection{ connection } { } + + void update() override; +}; diff --git a/src/play_mode.cpp b/src/play_mode.cpp new file mode 100644 index 00000000..15689176 --- /dev/null +++ b/src/play_mode.cpp @@ -0,0 +1,43 @@ +#include "play_mode.hpp" +#include "random.hpp" +#include "tetrion.hpp" +#include +#include +#include +#include +#include + +PlayMode::PlayMode(const bool is_replay_mode) : m_is_replay_mode{ is_replay_mode } {}; + +tl::expected PlayMode::init(Settings settings, Random::Seed seed) { + m_settings = settings; + return StartState{ 0, seed }; +}; + +Settings PlayMode::settings() { + return m_settings; +} + +SinglePlayer::SinglePlayer(const bool is_replay_mode) : PlayMode{ is_replay_mode } {}; + + +tl::expected SinglePlayer::init(Settings settings, Random::Seed seed) { + PlayMode::init(settings, seed); + return StartState{ 1, seed }; +} + +std::unique_ptr SinglePlayer::get_input( + u8 index, + Tetrion* tetrion, + Input::OnEventCallback event_callback, + EventDispatcher* event_dispatcher +) { + + if (index != 0) { + throw std::range_error{ "SinglePlayer mode: error in index of get_input" }; + } + + tetrion->set_player_num(0); + + return utils::create_input(settings().controls.at(index), tetrion, std::move(event_callback), event_dispatcher); +} diff --git a/src/play_mode.hpp b/src/play_mode.hpp new file mode 100644 index 00000000..412db8b5 --- /dev/null +++ b/src/play_mode.hpp @@ -0,0 +1,49 @@ + + +#pragma once + +#include "application.hpp" +#include "input.hpp" +#include "random.hpp" +#include "settings.hpp" +#include "types.hpp" +#include +#include +#include +#include +#include +#include +struct StartState { + std::size_t num_players; + Random::Seed seed; +}; + + +//TODO: use m_is_replay_mode to not make network connection or keyboardinput!! + +/* abstract */ struct PlayMode { +private: + Settings m_settings; + bool m_is_replay_mode; + +public: + explicit PlayMode(const bool is_replay_mode); + virtual ~PlayMode() = default; + virtual tl::expected init(Settings settings, Random::Seed seed); + virtual std::unique_ptr + get_input(u8 index, Tetrion* tetrion, Input::OnEventCallback event_callback, EventDispatcher* event_dispatcher) = 0; + Settings settings(); +}; + + +struct SinglePlayer : public PlayMode { +public: + explicit SinglePlayer(const bool is_replay_mode); + tl::expected init(Settings settings, Random::Seed seed) override; + std::unique_ptr get_input( + u8 index, + Tetrion* tetrion, + Input::OnEventCallback event_callback, + EventDispatcher* event_dispatcher + ) override; +}; \ No newline at end of file diff --git a/src/rect.hpp b/src/rect.hpp index dc890fd9..54036d49 100644 --- a/src/rect.hpp +++ b/src/rect.hpp @@ -12,3 +12,12 @@ struct Rect final { : top_left{ x, y }, bottom_right{ x + width - 1, y + height - 1 } { } }; + + +inline constexpr Rect operator+(Point lhs, Rect rhs) { + return Rect{ lhs + rhs.top_left, lhs + rhs.bottom_right }; +} + +inline constexpr Rect operator+(Rect lhs, Point rhs) { + return Rect{ lhs.top_left + rhs, lhs.bottom_right + rhs }; +} \ No newline at end of file diff --git a/src/sdl_context.cpp b/src/sdl_context.cpp index ace5ef54..d0891531 100644 --- a/src/sdl_context.cpp +++ b/src/sdl_context.cpp @@ -1,13 +1,26 @@ #include "sdl_context.hpp" #include +#include #include SdlContext::SdlContext() { - SDL_Init(SDL_INIT_VIDEO); - TTF_Init(); + if (SDL_Init(SDL_INIT_VIDEO) != 0) { + printf("SDL_Init: %s\n", SDL_GetError()); + exit(2); + } + if (TTF_Init() != 0) { + printf("TTF_Init: %s\n", SDL_GetError()); + exit(2); + } + //TODO: if we don't need the network, this should be disabled + if (SDLNet_Init() == -1) { + printf("SDLNet_Init: %s\n", SDLNet_GetError()); + exit(2); + } } SdlContext::~SdlContext() { + SDLNet_Quit(); TTF_Quit(); SDL_Quit(); } diff --git a/src/tetrion.cpp b/src/tetrion.cpp index 7c874721..3dffa74f 100644 --- a/src/tetrion.cpp +++ b/src/tetrion.cpp @@ -10,15 +10,18 @@ Tetrion::Tetrion( const u8 tetrion_index, const Random::Seed random_seed, const int starting_level, - tl::optional recording_writer + tl::optional recording_writer, + const bool use_player_text ) : m_tetrion_index{ tetrion_index }, m_random{ random_seed }, - m_grid{ Point::zero(), tile_size }, + m_grid{Point{ + static_cast((tetrion_index * Tetrion::size_per_field) + (tetrion_index * Tetrion::space_between)), 0 + }, tile_size }, m_level{ starting_level }, m_next_gravity_simulation_step_index{ get_gravity_delay_frames() }, m_recording_writer{ recording_writer }, - m_lock_delay_step_index{ Application::simulation_step_index() + lock_delay } { + m_lock_delay_step_index{ Application::simulation_step_index() + lock_delay }, m_use_player_text{use_player_text} { #if defined(__ANDROID__) constexpr auto font_path = "fonts/PressStart2P.ttf"; @@ -28,18 +31,30 @@ Tetrion::Tetrion( constexpr auto font_size = 18; #endif m_fonts.push_back(std::make_shared(font_path, font_size)); - m_score_text = Text{ - Point{ m_grid.to_screen_coords(Grid::preview_tetromino_position + Point{ 0, Grid::preview_extends.y }) }, - Color::white(), "score: 0", m_fonts.front() - }; - m_level_text = Text{ - Point{ m_grid.to_screen_coords(Grid::preview_tetromino_position + Point{ 0, Grid::preview_extends.y + 1 }) }, - Color::white(), "level: 0", m_fonts.front() - }; - m_cleared_lines_text = Text{ - Point{ m_grid.to_screen_coords(Grid::preview_tetromino_position + Point{ 0, Grid::preview_extends.y + 2 }) }, - Color::white(), "lines: 0", m_fonts.front() - }; + m_text_rows.emplace_back( + Point{ m_grid.to_screen_coords(Grid::preview_tetromino_position + Point{ 0, Grid::preview_extends.y }) }, + Color::white(), "score: 0", m_fonts.front() + ); + m_text_rows.emplace_back( + Point{ m_grid.to_screen_coords( + Grid::preview_tetromino_position + Point{ 0, Grid::preview_extends.y + 1 } + ) }, + Color::white(), "level: 0", m_fonts.front() + ); + m_text_rows.emplace_back( + Point{ m_grid.to_screen_coords( + Grid::preview_tetromino_position + Point{ 0, Grid::preview_extends.y + 2 } + ) }, + Color::white(), "lines: 0", m_fonts.front() + ); + if (m_use_player_text) { + m_text_rows.emplace_back( + Point{ m_grid.to_screen_coords( + Grid::preview_tetromino_position + Point{ 0, Grid::preview_extends.y + 3 } + ) }, + Color::white(), "player: 0", m_fonts.front() + ); + } refresh_texts(); } @@ -84,12 +99,14 @@ void Tetrion::render(const Application& app) const { if (m_preview_tetromino) { m_preview_tetromino->render(app, m_grid); } + if (m_tetromino_on_hold) { m_tetromino_on_hold->render(app, m_grid); } - m_score_text.render(app); - m_level_text.render(app); - m_cleared_lines_text.render(app); + + for (const auto& text : m_text_rows) { + text.render(app); + } } bool Tetrion::handle_input_command(const InputCommand command) { @@ -234,6 +251,12 @@ bool Tetrion::drop_tetromino() { return num_movements > 0; } + +void Tetrion::set_player_num(std::size_t player_num) { + m_player_num = player_num; + refresh_texts(); +} + void Tetrion::hold_tetromino() { if (not m_active_tetromino.has_value()) { return; @@ -249,22 +272,30 @@ void Tetrion::hold_tetromino() { } } + void Tetrion::reset_lock_delay() { m_lock_delay_step_index = Application::simulation_step_index() + lock_delay; } - void Tetrion::refresh_texts() { + auto i = 0; std::stringstream stream; stream << "score: " << m_score; - m_score_text.set_text(stream.str()); + m_text_rows.at(i++).set_text(stream.str()); - stream = std::stringstream{}; + stream = {}; stream << "level: " << m_level; - m_level_text.set_text(stream.str()); + m_text_rows.at(i++).set_text(stream.str()); - stream = std::stringstream{}; + stream = {}; stream << "lines: " << m_lines_cleared; - m_cleared_lines_text.set_text(stream.str()); + m_text_rows.at(i++).set_text(stream.str()); + + if (m_use_player_text and m_player_num.has_value()) { + stream = {}; + // for humans it' more readable, when it's 1 indexed + stream << "player " << m_player_num.value() + 1; + m_text_rows.at(i++).set_text(stream.str()); + } } void Tetrion::clear_fully_occupied_lines() { diff --git a/src/tetrion.hpp b/src/tetrion.hpp index 81515509..5472e082 100644 --- a/src/tetrion.hpp +++ b/src/tetrion.hpp @@ -11,6 +11,7 @@ #include "utils.hpp" #include #include +#include #include #include @@ -32,6 +33,9 @@ struct Tetrion final { using WallKickTable = std::array, 8>; static constexpr int tile_size = 30; + static constexpr std::size_t legend_size = Grid::preview_extends.x; + static constexpr std::size_t size_per_field = tile_size * (Grid::width + legend_size); + static constexpr std::size_t space_between = 125; static constexpr u64 lock_delay = 30; static constexpr int num_lock_delays = 15; @@ -56,17 +60,15 @@ struct Tetrion final { tl::optional m_ghost_tetromino; tl::optional m_preview_tetromino; tl::optional m_tetromino_on_hold; - int m_level = 0; // todo: change into u32 + int m_level = 0; // todo: change into u32 u64 m_next_gravity_simulation_step_index; int m_lines_cleared = 0; // todo: change into u32 GameState m_game_state = GameState::Playing; std::array m_sequence_bags{ Bag{ m_random }, Bag{ m_random } }; int m_sequence_index = 0; std::vector> m_fonts; - Text m_score_text; + std::vector m_text_rows; int m_score = 0; - Text m_level_text; - Text m_cleared_lines_text; bool m_down_key_pressed = false; bool m_is_accelerated_down_movement = false; tl::optional m_recording_writer; @@ -74,12 +76,15 @@ struct Tetrion final { u64 m_lock_delay_step_index; bool m_is_in_lock_delay = false; int m_num_executed_lock_delays = 0; + tl::optional m_player_num = tl::nullopt; + bool m_use_player_text; public: Tetrion(u8 tetrion_index, Random::Seed random_seed, int starting_level, - tl::optional recording_writer = tl::nullopt); + tl::optional recording_writer = tl::nullopt, + const bool use_player_text = false); void update(); void render(const Application& app) const; @@ -94,6 +99,7 @@ struct Tetrion final { bool move_tetromino_right(); bool drop_tetromino(); void hold_tetromino(); + void set_player_num(std::size_t player_num); [[nodiscard]] auto tetrion_index() const { return m_tetrion_index; diff --git a/src/tetrion_snapshot.cpp b/src/tetrion_snapshot.cpp index b588838d..0c088fda 100644 --- a/src/tetrion_snapshot.cpp +++ b/src/tetrion_snapshot.cpp @@ -138,8 +138,7 @@ static void compare_values( result = false; } } -bool TetrionSnapshot::compare_to(const TetrionSnapshot& other, const bool log_result) - const { +bool TetrionSnapshot::compare_to(const TetrionSnapshot& other, const bool log_result) const { bool snapshots_are_equal = true; compare_values("tetrion indices", m_tetrion_index, other.m_tetrion_index, log_result, snapshots_are_equal); diff --git a/src/tetris_application.cpp b/src/tetris_application.cpp index 87faac6a..5f4cf3bd 100644 --- a/src/tetris_application.cpp +++ b/src/tetris_application.cpp @@ -1,4 +1,7 @@ #include "tetris_application.hpp" +#include "local_multiplayer.hpp" +#include "play_mode.hpp" +#include TetrisApplication::TetrisApplication(CommandLineArguments command_line_arguments) #if defined(__ANDROID__) @@ -7,25 +10,46 @@ TetrisApplication::TetrisApplication(CommandLineArguments command_line_arguments : Application{ "OOPetris", WindowPosition::Centered, width, height, std::move(command_line_arguments) } { #endif try_load_settings(); - static constexpr auto num_tetrions = u8{ 1 }; if (is_replay_mode()) { m_recording_reader = std::make_unique(*(this->command_line_arguments().recording_path)); } - const auto seeds = create_seeds(num_tetrions); + const auto seed = create_seeds(1); + + const auto& [is_multiplayer, is_server, player_nums] = this->command_line_arguments().play_mode_information; + if (is_multiplayer) { + m_play_mode = std::make_unique(is_replay_mode(), player_nums, is_server); + } else { + m_play_mode = std::make_unique(is_replay_mode()); + } + + auto initial_play_mode_data = m_play_mode->init(m_settings, seed[0]); + if (!initial_play_mode_data.has_value()) { + std::cerr << "Error in initializing PlayMode: " << initial_play_mode_data.error() << "\n"; + std::exit(2); + } + + auto start_state = initial_play_mode_data.value(); + const auto num_tetrions = start_state.num_players; + const auto seed_to_use = start_state.seed; + + + //TODO: atm each tetrion has the same seed with multiple players + const auto seeds = std::vector(num_tetrions, seed_to_use); if (game_is_recorded()) { const auto seeds_span = std::span{ seeds.data(), seeds.size() }; m_recording_writer = create_recording_writer(create_tetrion_headers(seeds_span)); } + for (u8 tetrion_index = 0; tetrion_index < num_tetrions; ++tetrion_index) { const auto starting_level = starting_level_for_tetrion(tetrion_index); spdlog::info("starting level for tetrion {}: {}", tetrion_index, starting_level); m_tetrions.push_back(std::make_unique( - tetrion_index, seeds.at(tetrion_index), starting_level, recording_writer_optional() + tetrion_index, seeds.at(tetrion_index), starting_level, recording_writer_optional(), is_multiplayer )); auto on_event_callback = create_on_event_callback(tetrion_index); @@ -36,16 +60,34 @@ TetrisApplication::TetrisApplication(CommandLineArguments command_line_arguments create_replay_input(tetrion_index, m_recording_reader.get(), tetrion_pointer, [](InputEvent) {}) ); } else { - m_inputs.push_back( - create_input(m_settings.controls.at(tetrion_index), tetrion_pointer, std::move(on_event_callback)) + + auto input = m_play_mode->get_input( + tetrion_index, tetrion_pointer, std::move(on_event_callback), &m_event_dispatcher ); + + m_inputs.push_back(std::move(input)); } } for (const auto& tetrion : m_tetrions) { tetrion->spawn_next_tetromino(); } + + + //TODO if the window is to small to handle num_players, we have to resize in some way , + //TODO: if it's to big it has to be resized into an appropiate width + + //TODO reintroduce + /* [[maybe_unused]] const std::size_t game_field_size = + (GameManager::size_per_field * num_tetrions) + ((num_players - 1) * GameManager::space_between); + + */ + //TODO: resize(), but then game_managers have to updated as well, to repaint or not? + + + //TODO assert that enough settings are here fro example 4 players! (could be more eventually!) } + void TetrisApplication::update_inputs() { for (const auto& input : m_inputs) { input->update(); @@ -82,22 +124,7 @@ void TetrisApplication::render() const { Tetrion* associated_tetrion, Input::OnEventCallback on_event_callback ) { - return std::visit( - overloaded{ - [&]([[maybe_unused]] KeyboardControls& keyboard_controls) -> std::unique_ptr { -#if defined(__ANDROID__) - auto input = std::make_unique(associated_tetrion, std::move(on_event_callback)); -#else - auto input = std::make_unique( - associated_tetrion, std::move(on_event_callback), keyboard_controls - ); -#endif - m_event_dispatcher.register_listener(input.get()); - return input; - }, - }, - controls - ); + return utils::create_input(controls, associated_tetrion, std::move(on_event_callback), &m_event_dispatcher); } [[nodiscard]] std::unique_ptr TetrisApplication::create_replay_input( diff --git a/src/tetris_application.hpp b/src/tetris_application.hpp index 8a786886..9c1c8af8 100644 --- a/src/tetris_application.hpp +++ b/src/tetris_application.hpp @@ -1,18 +1,20 @@ #pragma once #include "application.hpp" +#include "network/network_manager.hpp" +#include "play_mode.hpp" #include "recording.hpp" #include "settings.hpp" #include "tetrion.hpp" #include "tetromino_type.hpp" #include +#include +#include #include #include #include -#include struct TetrisApplication : public Application { -private: using TetrionHeaders = std::vector; static constexpr auto settings_filename = "settings.json"; @@ -22,11 +24,12 @@ struct TetrisApplication : public Application { Settings m_settings; std::unique_ptr m_recording_writer; std::unique_ptr m_recording_reader; + std::unique_ptr m_play_mode; + public: - static constexpr int width = 800; + static constexpr int width = 1200; static constexpr int height = 600; - explicit TetrisApplication(CommandLineArguments command_line_arguments); protected: @@ -40,10 +43,11 @@ struct TetrisApplication : public Application { [[nodiscard]] std::unique_ptr create_input(Controls controls, Tetrion* associated_tetrion, Input::OnEventCallback on_event_callback); + [[nodiscard]] static std::unique_ptr create_replay_input( u8 tetrion_index, RecordingReader* constrecording_reader, - Tetrion *constassociated_tetrion, + Tetrion* constassociated_tetrion, Input::OnEventCallback on_event_callback ); diff --git a/src/util.hpp b/src/util.hpp new file mode 100644 index 00000000..d032e5ca --- /dev/null +++ b/src/util.hpp @@ -0,0 +1,29 @@ + + +#pragma once + +#include +#include +#include +#include + +namespace util { + + + inline std::string to_hex_str(uint8_t number) { + std::ostringstream ss{}; + + ss << "0x" << std::hex << std::setfill('0'); + ss << std::hex << std::setw(2) << static_cast(number); + return ss.str(); + } + + inline std::string to_hex_str(uint32_t number) { + std::ostringstream ss{}; + + ss << "0x" << std::hex << std::setfill('0'); + ss << std::hex << std::setw(8) << static_cast(number); + return ss.str(); + } + +} // namespace util diff --git a/subprojects/sdl2_net.wrap b/subprojects/sdl2_net.wrap new file mode 100644 index 00000000..732be1c6 --- /dev/null +++ b/subprojects/sdl2_net.wrap @@ -0,0 +1,12 @@ +[wrap-file] +directory = SDL2_net-2.2.0 +source_url = https://www.libsdl.org/projects/SDL_net/release/SDL2_net-2.2.0.tar.gz +source_filename = SDL2_net-2.2.0.tar.gz +source_hash = 4e4a891988316271974ff4e9585ed1ef729a123d22c08bd473129179dc857feb +patch_filename = sdl2_net_2.2.0-1_patch.zip +patch_url = https://wrapdb.mesonbuild.com/v2/sdl2_net_2.2.0-1/get_patch +patch_hash = f63aea40e83c2b4d97d63d97d94c76e73fdaf5923b71a319938cbebea21d7834 +wrapdb_version = 2.2.0-1 + +[provide] +sdl2_net = sdl2_net_dep diff --git a/vcpkg.json b/vcpkg.json index 352309b8..03068f46 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -13,11 +13,21 @@ "version>=": "2.20.2", "platform": "(x64 & static)" }, + { + "name" : "sdl2-net", + "version>=" : "2.2.0", + "platform" : "(x64 & static)" + }, { "name": "tl-optional", "version>=": "2021-05-02", "platform": "(x64 & static)" }, + { + "name" : "tl-expected", + "version>=" : "2022-11-24", + "platform" : "(x64 & static)" + }, { "name": "nlohmann-json", "version>=": "3.11.2",