diff --git a/.clangd b/.clangd index 3c9792ea..8847112d 100644 --- a/.clangd +++ b/.clangd @@ -6,6 +6,8 @@ Diagnostics: Includes: IgnoreHeader: - \.compat\.inc +CompileFlags: + CompilationDatabase: engine/ --- # Header-specific conditions. @@ -20,11 +22,4 @@ CompileFlags: - -Wno-unused-const-variable - -Wno-unused-function - -Wno-unused-variable ---- -# Suppress all third-party warnings. -If: - PathMatch: thirdparty/.* - -Diagnostics: - Suppress: "*" diff --git a/.gitignore b/.gitignore index db142bed..a09ec8ca 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ compile_commands.json *__pycache__ *.blend1 +/.dir-locals.el diff --git a/flatscreen-project/network_test.tscn b/flatscreen-project/network_test.tscn new file mode 100644 index 00000000..744faebe --- /dev/null +++ b/flatscreen-project/network_test.tscn @@ -0,0 +1,72 @@ +[gd_scene load_steps=2 format=3 uid="uid://bbvpj46frv0ho"] + +[sub_resource type="GDScript" id="GDScript_78ugl"] +script/source = "extends ClientNode + +@onready +var client_status := %ClientStatus + +func _ready(): + connect_to_server(\"localhost\") + +func _on_connection_changed(connected: int) -> void: + if (connected == NetworkData.CONNECTION_DISCONNECTED): + client_status.text = \"DISCONNECTED\" + elif (connected == NetworkData.CONNECTION_CONNECTED): + client_status.text = \"CONNECTED\" + elif (connected == NetworkData.CONNECTION_AUTHENTICATED): + client_status.text = \"AUTHENTICATED\" +" + +[node name="Control" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="ServerNode" type="ServerNode" parent="."] + +[node name="ClientNode" type="ClientNode" parent="."] +script = SubResource("GDScript_78ugl") + +[node name="HBoxContainer" type="HBoxContainer" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="ServerInfo" type="PanelContainer" parent="HBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="Header" type="Label" parent="HBoxContainer/ServerInfo"] +layout_mode = 2 +text = "Server: OFF" + +[node name="VBoxContainer" type="VBoxContainer" parent="HBoxContainer/ServerInfo/Header"] +layout_mode = 0 +offset_top = -312.0 +offset_right = 574.0 +offset_bottom = 336.0 + +[node name="ClientInfo" type="PanelContainer" parent="HBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="VBoxContainer" type="VBoxContainer" parent="HBoxContainer/ClientInfo"] +layout_mode = 2 + +[node name="Header" type="Label" parent="HBoxContainer/ClientInfo/VBoxContainer"] +layout_mode = 2 +text = "Client:" + +[node name="ClientStatus" type="Label" parent="HBoxContainer/ClientInfo/VBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +text = "DISCONNECTED" + +[connection signal="connection_changed" from="ClientNode" to="ClientNode" method="_on_connection_changed"] diff --git a/flatscreen-project/project.godot b/flatscreen-project/project.godot index 9acb939c..69c22811 100644 --- a/flatscreen-project/project.godot +++ b/flatscreen-project/project.godot @@ -9,6 +9,8 @@ config_version=5 [application] + config/name="you_done_it" +run/main_scene="uid://bbvpj46frv0ho" config/features=PackedStringArray("4.5", "Forward Plus") config/icon="res://icon.svg" diff --git a/flatscreen-project/scenes/flatscreen_root.tscn b/flatscreen-project/scenes/flatscreen_root.tscn new file mode 100644 index 00000000..1f37582c --- /dev/null +++ b/flatscreen-project/scenes/flatscreen_root.tscn @@ -0,0 +1,3 @@ +[gd_scene format=3 uid="uid://dosb4sb7pvss4"] + +[node name="ServerNode" type="ServerNode"] diff --git a/justfile b/justfile index ce702248..3b1bc6f6 100644 --- a/justfile +++ b/justfile @@ -6,7 +6,7 @@ compiledb := if tree_hash == `cat .tree.hash` { "no" } else { "yes" } build: format # Compiling Editor - cd engine/ && scons target=editor symbols=yes optimization=debug dev_build=yes linker=mold use_llvm=yes compiledb={{compiledb}} custom_modules="../modules" + cd engine/ && scons disable_exceptions=false target=editor symbols=yes optimization=debug dev_build=yes linker=mold use_llvm=yes compiledb={{compiledb}} custom_modules="../modules" echo {{tree_hash}} > .tree.hash run: build @@ -15,7 +15,7 @@ run: build release-linux: build # Compiling Linux Release - cd engine/ && scons platform=linuxbsd target=template_release arch=x86_64 linker=mold use_llvm=yes custom_modules="../modules" + cd engine/ && scons disable_exceptions=false platform=linuxbsd target=template_release arch=x86_64 linker=mold use_llvm=yes custom_modules="../modules" # Preparing Build Environment sed -i "s!templatepath!{{`realpath engine/bin/godot.linuxbsd.template_release.x86_64.llvm`}}!" project/export_presets.cfg rm -rf build && mkdir build @@ -27,7 +27,7 @@ release-linux: build release-windows: build # Compiling Windows Release - cd engine/ && scons platform=windows target=template_release arch=x86_64 linker=mold custom_modules="../modules" + cd engine/ && scons disable_exceptions=false platform=windows target=template_release arch=x86_64 linker=mold custom_modules="../modules" # Preparing Build Environment sed -i "s!templatepath!{{`realpath engine/bin/godot.windows.template_release.x86_64`}}!" project/export_presets.cfg rm -rf build && mkdir build @@ -46,4 +46,5 @@ initialize-template projectname: format: # Formatting Custom Modules - clang-format -i $(find modules/ -iname '*.h' -o -iname '*.c' -o -iname '*.hpp' -o -iname '*.cpp' -o -iname '*.hxx' -o -iname '*.cxx') + clang-format -i modules/you_done_it/*.h modules/you_done_it/*.cpp + # $(find modules/ -iname '*.h' -o -iname '*.c' -o -iname '*.hpp' -o -iname '*.cpp' -o -iname '*.hxx' -o -iname '*.cxx') diff --git a/modules/you_done_it/SCsub b/modules/you_done_it/SCsub index 96fd4e0f..f127e1be 100644 --- a/modules/you_done_it/SCsub +++ b/modules/you_done_it/SCsub @@ -7,9 +7,9 @@ ydi_env = env.Clone() ydi_env.Append(CPPPATH=["libzmq/include", "cppzmq/"]) ydi_env.add_source_files(env.modules_sources, "*.cpp") -if not os.path.isdir(Dir('libzmq/build/lib').abspath): - call(["cmake", "-Slibzmq", "-B" + Dir('libzmq/build').abspath, "-DZMQ_BUILD_TESTS=OFF"]) +call(["cmake", "-Slibzmq", "-B" + Dir('libzmq/build').abspath, "-DZMQ_BUILD_TESTS=OFF", "-DZMQ_BUILD_STATIC=ON", "-DZMQ_BUILD_SHARED=OFF"]) call(["cmake", "--build", Dir('libzmq/build').abspath]) env.Append(LIBPATH=[Dir('libzmq/build/lib/').abspath]) -ydi_env.Append(LIBS=['libzmq']) +ydi_env.Append(LIBPATH=[Dir('libzmq/build/lib/').abspath]) +env.Append(LIBS=['libzmq']) diff --git a/modules/you_done_it/client_node.cpp b/modules/you_done_it/client_node.cpp new file mode 100644 index 00000000..c521840b --- /dev/null +++ b/modules/you_done_it/client_node.cpp @@ -0,0 +1,55 @@ +#include "client_node.h" +#include "ydi_client.h" + +ClientNode *ClientNode::singleton_instance{ nullptr }; +String const ClientNode::sig_connection_changed{ "connection_changed" }; + +void ClientNode::_bind_methods() { + ClassDB::bind_method(D_METHOD("connect_to_server", "server_url"), &self_type::connect_to_server); + ADD_SIGNAL(MethodInfo(sig_connection_changed, PropertyInfo(Variant::INT, "connected", PROPERTY_HINT_ENUM, "Disconnected,Connected,Authenticated"))); +} + +void ClientNode::enter_tree() { + if (singleton_instance) { + print_error("Attempt to create duplicate ClientNode, aborting"); + abort(); + } +} + +void ClientNode::process() { + NetworkData::ConnectionStatus const new_status{ ydi::client::status() }; + if (new_status != this->state) { + this->state = new_status; + emit_signal(sig_connection_changed, new_status); + } +} + +void ClientNode::exit_tree() { + ydi::client::disconnect(); + if (singleton_instance == this) { + singleton_instance = nullptr; + } +} + +void ClientNode::_notification(int what) { + if (Engine::get_singleton()->is_editor_hint()) { + return; + } + switch (what) { + case NOTIFICATION_ENTER_TREE: + set_process(true); + return; + case NOTIFICATION_PROCESS: + process(); + return; + case NOTIFICATION_EXIT_TREE: + exit_tree(); + return; + default: + return; + } +} + +void ClientNode::connect_to_server(String const &server) { + ydi::client::connect(server); +} diff --git a/modules/you_done_it/client_node.h b/modules/you_done_it/client_node.h new file mode 100644 index 00000000..0b1e500a --- /dev/null +++ b/modules/you_done_it/client_node.h @@ -0,0 +1,25 @@ +#pragma once + +#include "ydi_networking.h" +#include + +class ClientNode : Node { + GDCLASS(ClientNode, Node); + static ClientNode *singleton_instance; + static void _bind_methods(); + void enter_tree(); + void process(); + void exit_tree(); + +protected: + void _notification(int what); + +public: + void connect_to_server(String const &url); + +private: + NetworkData::ConnectionStatus state{ NetworkData::CONNECTION_DISCONNECTED }; + +public: + static String const sig_connection_changed; +}; diff --git a/modules/you_done_it/register_types.cpp b/modules/you_done_it/register_types.cpp index 9aa0aef5..74139d10 100644 --- a/modules/you_done_it/register_types.cpp +++ b/modules/you_done_it/register_types.cpp @@ -1,12 +1,17 @@ #include "register_types.h" -#include "core/object/class_db.h" -#include "zmq.hpp" +#include "client_node.h" +#include "server_node.h" +#include "ydi_networking.h" +#include void initialize_you_done_it_module(ModuleInitializationLevel p_level) { if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) { return; } + ClassDB::register_class(); + ClassDB::register_class(); + ClassDB::register_abstract_class(); } void uninitialize_you_done_it_module(ModuleInitializationLevel p_level) { diff --git a/modules/you_done_it/register_types.h b/modules/you_done_it/register_types.h index b42abbe8..fbf2ef28 100644 --- a/modules/you_done_it/register_types.h +++ b/modules/you_done_it/register_types.h @@ -1,9 +1,6 @@ -#ifndef you_done_it_REGISTER_TYPES_H -#define you_done_it_REGISTER_TYPES_H +#pragma once #include "modules/register_module_types.h" void initialize_you_done_it_module(ModuleInitializationLevel p_level); void uninitialize_you_done_it_module(ModuleInitializationLevel p_level); - -#endif // !you_done_it_REGISTER_TYPES_H diff --git a/modules/you_done_it/server_node.cpp b/modules/you_done_it/server_node.cpp new file mode 100644 index 00000000..866c7419 --- /dev/null +++ b/modules/you_done_it/server_node.cpp @@ -0,0 +1,64 @@ +#include "server_node.h" +#include "ydi_server.h" +#include + +ServerNode *ServerNode::singleton_instance{ nullptr }; +String const ServerNode::sig_clue_revealed{ "clue_revealed" }; +String const ServerNode::sig_connection_established{ "connection_established" }; +String const ServerNode::sig_connection_lost{ "connection_lost" }; + +void ServerNode::_bind_methods() { + ADD_SIGNAL(MethodInfo(sig_clue_revealed, PropertyInfo(Variant::INT, "id"))); + ADD_SIGNAL(MethodInfo(sig_connection_established)); + ADD_SIGNAL(MethodInfo(sig_connection_lost)); +} + +void ServerNode::enter_tree() { + if (singleton_instance) { + print_error("Attempt to create duplicate ServerNode, aborting"); + abort(); + } + singleton_instance = this; + ydi::server::open(); +} + +void ServerNode::process(double delta) { + Vector new_clues{}; + if (ydi::server::receive::new_clues(new_clues)) { + for (NetworkData::ClueID clue : new_clues) { + emit_signal(sig_clue_revealed, clue); + } + } + bool new_is_connected{ ydi::server::has_client() }; + if (this->is_connected != new_is_connected) { + this->is_connected = new_is_connected; + emit_signal(this->is_connected ? sig_connection_established : sig_connection_lost); + } +} + +void ServerNode::exit_tree() { + ydi::server::close(); + if (singleton_instance == this) { + singleton_instance = nullptr; + } +} + +void ServerNode::_notification(int what) { + if (Engine::get_singleton()->is_editor_hint()) { + return; + } + switch (what) { + case NOTIFICATION_ENTER_TREE: + set_process(true); + enter_tree(); + return; + case NOTIFICATION_PROCESS: + process(get_process_delta_time()); + return; + case NOTIFICATION_EXIT_TREE: + exit_tree(); + return; + default: + return; + } +} diff --git a/modules/you_done_it/server_node.h b/modules/you_done_it/server_node.h new file mode 100644 index 00000000..b15abe29 --- /dev/null +++ b/modules/you_done_it/server_node.h @@ -0,0 +1,23 @@ +#pragma once + +#include + +class ServerNode : public Node { + GDCLASS(ServerNode, Node); + static ServerNode *singleton_instance; + static void _bind_methods(); + void enter_tree(); + void process(double delta); + void exit_tree(); + +protected: + void _notification(int what); + +public: + static String const sig_clue_revealed; + static String const sig_connection_established; + static String const sig_connection_lost; + +private: + bool is_connected{ false }; +}; diff --git a/modules/you_done_it/ydi_client.cpp b/modules/you_done_it/ydi_client.cpp new file mode 100644 index 00000000..96b9a474 --- /dev/null +++ b/modules/you_done_it/ydi_client.cpp @@ -0,0 +1,144 @@ +#include "ydi_client.h" +#include "core/string/print_string.h" +#include "you_done_it/ydi_networking.h" +#include +#include +#include +#include +#include +#include + +namespace ydi::client { +struct Connection { + std::optional server; + std::optional context{ std::nullopt }; + std::optional socket{ std::nullopt }; + std::recursive_mutex mtx; + std::atomic status; + std::atomic stop_threads{ false }; +}; + +std::optional connection{ std::nullopt }; + +std::optional receive_thread{ std::nullopt }; + +void handle_ok(zmq::multipart_t const &message) { + NetworkData::MessageType type{ to_message_type(message[1]) }; + switch (type) { + case NetworkData::MSG_CONNECT: + connection->status = NetworkData::CONNECTION_AUTHENTICATED; + return; + default: // no need to handle every OK, just some relevant ones + return; + } +} + +void handle_message(zmq::multipart_t const &message) { + print_line("Client handle_message:"); + print_message_contents(message); + NetworkData::MessageType type{ to_message_type(message[0]) }; + switch (type) { + case NetworkData::MSG_OK: + print_line("Client: received OK"); + handle_ok(message); + return; + case NetworkData::MSG_HEART: + print_line("Client: Received HEART, sending BEAT"); + multipart(NetworkData::MSG_BEAT).send(*connection->socket); + return; + default: + print_line("Client: Message not handled"); + return; + } +} + +void receive_thread_entry() { + { + std::scoped_lock lock{ connection->mtx }; + multipart(NetworkData::MSG_CONNECT).send(*connection->socket); + } + zmq::multipart_t message{}; + while (!connection->stop_threads) { + using namespace std::chrono_literals; + std::this_thread::sleep_for(10ms); + std::scoped_lock lock{ connection->mtx }; + if (message.recv(*connection->socket, (int)zmq::recv_flags::dontwait)) { + handle_message(message); + } + } +} + +void connect(String const &url) { + if (connection) { + print_line("Client: Detected attempt to open duplicate client connection, exiting without action"); + return; + } + connection.emplace(); + print_line("Client: Connecting to ", url); + try { + connection->context.emplace(1); + } catch (...) { + connection.reset(); + print_line("Client: Failed to create context"); + return; + } + print_line("Client: Created context"); + try { + connection->socket.emplace(*connection->context, zmq::socket_type::dealer); + } catch (...) { + connection->context->close(); + connection->context.reset(); + connection.reset(); + print_line("Client: Failed to create socket"); + return; + } + print_line("Client: Created socket"); + try { + CharStringT cstrurl{ url.ascii() }; + std::string server{ cstrurl.get_data() }; + server = "tcp://" + server + ":6667"; + connection->socket->connect(server); + connection->server = server; + } catch (...) { + connection->socket->close(); + connection->socket.reset(); + connection->context->close(); + connection->context.reset(); + connection.reset(); + print_line("Client: Failed to connect to server"); + } + print_line("Client: connected to server"); + connection->status = NetworkData::CONNECTION_CONNECTED; + receive_thread.emplace(receive_thread_entry); + print_line("Client: Connection complete!"); +} + +void disconnect() { + if (connection) { + connection->stop_threads = true; + if (receive_thread && receive_thread->joinable()) { + receive_thread->join(); + } + if (connection->socket) { + connection->socket->close(); + connection->socket.reset(); + } + if (connection->context) { + connection->context->shutdown(); + connection->context.reset(); + } + } + connection.reset(); +} + +NetworkData::ConnectionStatus status() { + return connection->status; +} + +namespace send { +void reveal_clue(NetworkData::ClueID id) { + std::scoped_lock lock{ connection->mtx }; + multipart(NetworkData::MSG_REVEAL, id).send(*connection->socket); +} +} //namespace send +} //namespace ydi::client diff --git a/modules/you_done_it/ydi_client.h b/modules/you_done_it/ydi_client.h new file mode 100644 index 00000000..b8f48b72 --- /dev/null +++ b/modules/you_done_it/ydi_client.h @@ -0,0 +1,13 @@ +#pragma once + +#include "ydi_networking.h" +#include + +namespace ydi::client { +void connect(String const &url); +void disconnect(); +NetworkData::ConnectionStatus status(); +namespace send { +void reveal_clue(NetworkData::ClueID id); +} //namespace send +} //namespace ydi::client diff --git a/modules/you_done_it/ydi_networking.cpp b/modules/you_done_it/ydi_networking.cpp new file mode 100644 index 00000000..465bce8d --- /dev/null +++ b/modules/you_done_it/ydi_networking.cpp @@ -0,0 +1,86 @@ +#include "ydi_networking.h" +#include "core/string/print_string.h" +#include +#include + +MAKE_TYPE_INFO(NetworkData::ClueID, Variant::INT); +MAKE_TYPE_INFO(NetworkData::ConnectionStatus, Variant::INT); + +void NetworkData::_bind_methods() { + BIND_ENUM_CONSTANT(CLUE_FIRST); + BIND_ENUM_CONSTANT(CLUE_SECOND); + BIND_ENUM_CONSTANT(CLUE_MAX); + + BIND_ENUM_CONSTANT(CONNECTION_DISCONNECTED); + BIND_ENUM_CONSTANT(CONNECTION_CONNECTED); + BIND_ENUM_CONSTANT(CONNECTION_AUTHENTICATED); +} + +namespace ydi { +int to_int(zmq::message_t const &msg, int failure) { + try { + return std::stoi(msg.to_string()); + } catch (...) { + return failure; + } +} +NetworkData::MessageType to_message_type(zmq::message_t const &msg) { + return (NetworkData::MessageType)to_int(msg, (int)NetworkData::MSG_INVALID); +} + +NetworkData::NOKReason to_nok_reason(zmq::message_t const &msg) { + return (NetworkData::NOKReason)to_int(msg, (int)NetworkData::NOK_INVALID_REASON); +} + +NetworkData::ClueID to_clue_id(zmq::message_t const &msg) { + return (NetworkData::ClueID)to_int(msg, NetworkData::CLUE_MAX); +} + +void print_message_contents(zmq::multipart_t const &mpart) { + print_line("multipart message:"); + for (zmq::message_t const &msg : mpart) { + print_line(" - ", msg.to_string().c_str()); + } +} + +void extend_multipart(zmq::multipart_t &mpart, NetworkData::MessageType type) { + mpart.addstr(std::to_string((int)type)); +} + +void extend_multipart(zmq::multipart_t &mpart, NetworkData::ClueID id) { + mpart.addstr(std::to_string(id)); +} + +void extend_multipart(zmq::multipart_t &mpart, std::string const &string) { + mpart.addstr(string); +} + +void extend_multipart(zmq::multipart_t &mpart, std::string_view const &strv) { + mpart.addmem(strv.data(), strv.size()); +} + +void extend_multipart(zmq::multipart_t &mpart, char const *cstr) { + mpart.addstr(cstr); +} + +void extend_multipart(zmq::multipart_t &mpart, int const &arg) { + std::string as_string{ std::to_string(arg) }; + mpart.addstr(as_string); +} + +void extend_multipart(zmq::multipart_t &mpart, zmq::multipart_t const &right) { + mpart.append(right.clone()); +} + +void extend_multipart(zmq::multipart_t &msg, std::pair range) { + for (; range.first != range.second; ++range.first) { + msg.addstr(range.first->to_string()); + } +} + +void extend_multipart(zmq::multipart_t &msg, std::pair range) { + for (; range.first != range.second; ++range.first) { + msg.addstr(range.first->to_string()); + } +} +} //namespace ydi diff --git a/modules/you_done_it/ydi_networking.h b/modules/you_done_it/ydi_networking.h index ca4ccb93..db8be659 100644 --- a/modules/you_done_it/ydi_networking.h +++ b/modules/you_done_it/ydi_networking.h @@ -1,16 +1,93 @@ -#ifndef YDI_NETWORKING_H -#define YDI_NETWORKING_H +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +class NetworkData : Object { + GDCLASS(NetworkData, Object); + static void _bind_methods(); + +public: + enum MessageType { + // connection management messages + MSG_CONNECT = 1u, + MSG_OK, + MSG_NOK, + MSG_HEART, + MSG_BEAT, + // gameplay messages + MSG_REVEAL, + // end of messages + MSG_INVALID + }; + + enum ClueID { + CLUE_FIRST, + CLUE_SECOND, + CLUE_MAX + }; + + enum ConnectionStatus { + CONNECTION_DISCONNECTED, + CONNECTION_CONNECTED, + CONNECTION_AUTHENTICATED + }; + + enum NOKReason { + NOK_UNAUTHENTICATED, + NOK_UNKNOWN_MSG, + NOK_OUT_OF_CONTEXT, //!< this means a received message is not valid in the recipient's current context + NOK_INVALID_REASON //!< this means the value could not be parsed as a NOK reason, not that the reason is an invalid message. + }; +}; namespace ydi { -enum MessageType { - NONE = 0u, - CONNECTION_REQUEST = 1u, - CONNECTION_RESPONSE = 2u, -}; +int to_int(zmq::message_t const &msg, int failure = 0); +NetworkData::MessageType to_message_type(zmq::message_t const &msg); +NetworkData::NOKReason to_nok_reason(zmq::message_t const &msg); +NetworkData::ClueID to_clue_id(zmq::message_t const &msg); -enum ClueID { - FIRST_CLUE, -}; +void print_message_contents(zmq::multipart_t const &mpart); + +void extend_multipart(zmq::multipart_t &mpart, NetworkData::MessageType type); +void extend_multipart(zmq::multipart_t &mpart, NetworkData::ClueID type); + +void extend_multipart(zmq::multipart_t &mpart, std::string const &string); +void extend_multipart(zmq::multipart_t &mpart, std::string_view const &strv); +void extend_multipart(zmq::multipart_t &mpart, char const *cstr); +void extend_multipart(zmq::multipart_t &mpart, int const &arg); +void extend_multipart(zmq::multipart_t &mpart, zmq::multipart_t const &right); +void extend_multipart(zmq::multipart_t &mpart); +void extend_multipart(zmq::multipart_t &mpart, std::pair range); +void extend_multipart(zmq::multipart_t &mpart, std::pair range); + +template +void extend_multipart_r(zmq::multipart_t &mpart, TArg const &arg) { + extend_multipart(mpart, arg); } -#endif // !YDI_NETWORKING_H +template +void extend_multipart_r(zmq::multipart_t &mpart, TArg const &arg, TArgs const &...args) { + extend_multipart(mpart, arg); + extend_multipart_r(mpart, args...); +} + +template +zmq::multipart_t multipart(TArg const &arg) { + zmq::multipart_t mpart{}; + extend_multipart(mpart, arg); + return mpart; +} + +template +zmq::multipart_t multipart(TArg const &arg, TArgs const &...args) { + zmq::multipart_t mpart{ multipart(arg) }; + extend_multipart_r(mpart, args...); + return mpart; +} +} //namespace ydi diff --git a/modules/you_done_it/ydi_server.cpp b/modules/you_done_it/ydi_server.cpp index c969ed92..1afdf451 100644 --- a/modules/you_done_it/ydi_server.cpp +++ b/modules/you_done_it/ydi_server.cpp @@ -1,9 +1,10 @@ #include "ydi_server.h" +#include "ydi_networking.h" +#include #include +#include #include #include -#include -#include #include #include #include @@ -13,78 +14,179 @@ namespace ydi::server { struct Service { std::optional context{ std::nullopt }; std::optional socket{ std::nullopt }; - std::unordered_set revealedClues{}; + std::unordered_set revealed_clues{}; std::recursive_mutex mtx{}; - std::atomic stop_threads{ false }; std::optional client{ std::nullopt }; + Vector new_clues{}; + double lastHeart{ 0.0 }; + double lastBeat{ 0.0 }; + + std::atomic stop_threads{ false }; }; +std::optional receive_thread{ std::nullopt }; +std::optional ping_thread{ std::nullopt }; std::optional service{ std::nullopt }; -thread_local std::optional receiveThread{ std::nullopt }; -thread_local std::optional pingThread{ std::nullopt }; -void receive_thread_entry() { - using namespace std::chrono_literals; - zmq::multipart_t incoming{}; - while (service->stop_threads) { - std::this_thread::sleep_for(20ms); - std::scoped_lock lock{ service->mtx }; - if (incoming.recv(*service->socket)) { - } +void handle_reveal_clue(zmq::multipart_t const &message) { + service->new_clues.push_back(to_clue_id(message.at(2))); +} + +void handle_authorised_message(std::string_view const &sender, NetworkData::MessageType type, zmq::multipart_t &message) { + switch (type) { + case NetworkData::MSG_BEAT: + print_line("Server: Received beat, storing timestamp"); + service->lastBeat = Time::get_singleton()->get_unix_time_from_system(); + return; + case NetworkData::MSG_REVEAL: + print_line("Server: Received revealed clue, ", (int)to_clue_id(message.at(2))); + handle_reveal_clue(message); + return; + default: + print_line("Server: Encountered unknown message type, sending NOK_UNKNOWN_MSG response"); + multipart(sender, NetworkData::MSG_NOK, NetworkData::NOK_UNKNOWN_MSG, message).send(*service->socket); + return; } } void ping_thread_entry() { using namespace std::chrono_literals; - { std::scoped_lock lock{ service->mtx }; + { + std::scoped_lock lock{ service->mtx }; if (!service->client) { return; } } - static zmq::multipart_t ping{ make_multipart(*service->client, std::string("PING")) }; while (!service->stop_threads) { std::this_thread::sleep_for(1s); std::scoped_lock lock{ service->mtx }; - ping.send(*service->socket); + print_line("Server: Send HEART"); + multipart(*service->client, NetworkData::MSG_HEART).send(*service->socket); + service->lastHeart = Time::get_singleton()->get_unix_time_from_system(); + } +} + +void handle_message(zmq::multipart_t &message) { + print_line("Server handle_message:"); + print_message_contents(message); + std::string_view const sender{ message.at(0).to_string_view() }; + NetworkData::MessageType type{ to_message_type(message.at(1)) }; + std::scoped_lock lock{ service->mtx }; + if (service->client) { + if (sender == service->client) { + handle_authorised_message(sender, type, message); + } else { + multipart(sender, NetworkData::MSG_NOK, NetworkData::NOK_UNAUTHENTICATED, std::pair{ message.begin() + 1, message.end() }).send(*service->socket); + } + } else if (type == NetworkData::MSG_CONNECT) { + service->client.emplace(sender); + multipart(sender, NetworkData::MSG_OK, std::pair{ message.begin() + 1, message.end() }).send(*service->socket); + ping_thread.emplace(ping_thread_entry); + } else { + multipart(sender, NetworkData::MSG_NOK, NetworkData::NOK_OUT_OF_CONTEXT, std::pair{ message.begin() + 1, message.end() }).send(*service->socket); + } +} + +void receive_thread_entry() { + using namespace std::chrono_literals; + zmq::multipart_t incoming{}; + while (!service->stop_threads) { + std::this_thread::sleep_for(20ms); + std::scoped_lock lock{ service->mtx }; + if (incoming.recv(*service->socket, (int)zmq::recv_flags::dontwait)) { + handle_message(incoming); + } } } void open() { + if (service) { + print_error("Server: Detected attempt to open duplicate Server, exiting without action."); + return; + } + print_line("Server: Starting"); service.emplace(); try { service->context.emplace(1); } catch (...) { service.reset(); + print_line("Server: Failed to create context"); return; } + print_line("Server: Created zmq context"); try { service->socket.emplace(*service->context, zmq::socket_type::router); } catch (...) { service->context->close(); service->context.reset(); service.reset(); + print_line("Server: Failed to create socket"); return; } + print_line("Server: Created socket"); try { - service->socket->connect("tcp://*:6667"); + service->socket->bind("tcp://*:6667"); } catch (...) { service->socket->close(); service->socket.reset(); service->context->close(); service->context.reset(); service.reset(); + print_line("Server: Failed to bind socket"); + return; } - receiveThread.emplace(receive_thread_entry); + print_line("Server: Bound socket"); + receive_thread.emplace(receive_thread_entry); + assert(receive_thread->joinable()); + print_line("Server: Startup complete!"); } void close() { if (service) { - std::scoped_lock lock{ service->mtx }; - service->socket->close(); - service->socket.reset(); - service->context->close(); - service->context.reset(); + print_line("Server: Shutting down..."); + service->stop_threads = true; + print_line("Server: Stopping Threads..."); + if (receive_thread && receive_thread->joinable()) { + receive_thread->join(); + } + print_line("Server: Receive thread stopped"); + if (ping_thread && ping_thread->joinable()) { + ping_thread->join(); + } + print_line("Server: Ping thread stopped"); + if (service->socket) { + service->socket->close(); + service->socket.reset(); + } + print_line("Server: Socket closed"); + if (service->context) { + service->context->close(); + service->context.reset(); + } + print_line("Server: Context closed"); service.reset(); + print_line("Server: Shutdown complete!"); } } + +bool has_client() { + if (service) { + std::scoped_lock lock{ service->mtx }; + return service->client.has_value(); + } else { + return false; + } } + +namespace receive { +bool new_clues(Vector &out) { + std::scoped_lock lock{ service->mtx }; + bool const has_new{ !service->new_clues.is_empty() }; + if (has_new) { + out.append_array(service->new_clues); + service->new_clues.clear(); + } + return has_new; +} +} //namespace receive +} //namespace ydi::server diff --git a/modules/you_done_it/ydi_server.h b/modules/you_done_it/ydi_server.h index f5b72142..7e019e9a 100644 --- a/modules/you_done_it/ydi_server.h +++ b/modules/you_done_it/ydi_server.h @@ -1,51 +1,18 @@ -#ifndef YDI_SERVER_H -#define YDI_SERVER_H +#pragma once #include "ydi_networking.h" -#include #include +#include #include #include -#include +#include namespace ydi::server { void open(); void close(); +bool has_client(); + namespace receive { -bool isRevealed(ClueID id); -Ref clueImage(ClueID id); -} -namespace send { -void announceConclusion(ClueID method, ClueID motive, ClueID murderer); -} -} - -template -void extend_multipart(zmq::multipart_t &, TArg const &arg); - -template <> -void extend_multipart(zmq::multipart_t &mpart, std::string const &string) { - mpart.addstr(string); -} - -template -void extend_multipart(zmq::multipart_t &mpart, TArg const &arg) { - std::string as_string{ std::to_string(arg) }; - mpart.addstr(as_string); -} - -template -void extend_multipart(zmq::multipart_t &mpart, TArg const &arg, TArgs const &... args) { - extend_multipart(mpart, arg); - extend_multipart(mpart, arg, args...); -} - -template -zmq::multipart_t make_multipart(std::string const &target, TArg const &arg, TArgs const &... args) { - zmq::multipart_t mpart{}; - mpart.addstr(target); - extend_multipart(mpart, arg, args...); - return mpart; -} - -#endif // !YDI_SERVER_H +bool new_clues(Vector &out); +} //namespace receive +} //namespace ydi::server