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 1b3cee73..69c22811 100644 --- a/flatscreen-project/project.godot +++ b/flatscreen-project/project.godot @@ -11,6 +11,6 @@ config_version=5 [application] config/name="you_done_it" -run/main_scene="uid://dosb4sb7pvss4" +run/main_scene="uid://bbvpj46frv0ho" config/features=PackedStringArray("4.5", "Forward Plus") config/icon="res://icon.svg" diff --git a/modules/you_done_it/client_node.cpp b/modules/you_done_it/client_node.cpp index 57dc03ba..a1822da5 100644 --- a/modules/you_done_it/client_node.cpp +++ b/modules/you_done_it/client_node.cpp @@ -1,8 +1,19 @@ #include "client_node.h" #include "ydi_client.h" +String const ClientNode::sig_connection_changed{ "connection_changed" }; + void ClientNode::_bind_methods() { - ClassDB::bind_method(D_METHOD("connect_to_server"), &self_type::connect_to_server); + 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::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() { @@ -14,6 +25,12 @@ void ClientNode::_notification(int what) { return; } switch (what) { + case NOTIFICATION_ENTER_TREE: + set_process(true); + return; + case NOTIFICATION_PROCESS: + process(); + return; case NOTIFICATION_EXIT_TREE: exit_tree(); return; diff --git a/modules/you_done_it/client_node.h b/modules/you_done_it/client_node.h index 67ae2abf..efe84ab6 100644 --- a/modules/you_done_it/client_node.h +++ b/modules/you_done_it/client_node.h @@ -1,10 +1,12 @@ #pragma once +#include "ydi_networking.h" #include class ClientNode : Node { GDCLASS(ClientNode, Node); static void _bind_methods(); + void process(); void exit_tree(); protected: @@ -12,4 +14,10 @@ protected: 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/server_node.cpp b/modules/you_done_it/server_node.cpp index ae3383b7..920fe656 100644 --- a/modules/you_done_it/server_node.cpp +++ b/modules/you_done_it/server_node.cpp @@ -2,9 +2,16 @@ #include "ydi_server.h" #include +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("new_clue", PropertyInfo(Variant::INT, "id"))); + 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() { ydi::server::open(); } @@ -13,9 +20,14 @@ void ServerNode::process(double delta) { Vector new_clues{}; if (ydi::server::receive::new_clues(new_clues)) { for (NetworkData::ClueID clue : new_clues) { - emit_signal("new_clue", clue); + 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() { @@ -28,6 +40,7 @@ void ServerNode::_notification(int what) { } switch (what) { case NOTIFICATION_ENTER_TREE: + set_process(true); enter_tree(); return; case NOTIFICATION_PROCESS: diff --git a/modules/you_done_it/server_node.h b/modules/you_done_it/server_node.h index cd717870..05a76597 100644 --- a/modules/you_done_it/server_node.h +++ b/modules/you_done_it/server_node.h @@ -11,4 +11,12 @@ class ServerNode : public Node { 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 index ece99c3c..96b9a474 100644 --- a/modules/you_done_it/ydi_client.cpp +++ b/modules/you_done_it/ydi_client.cpp @@ -15,7 +15,7 @@ struct Connection { std::optional socket{ std::nullopt }; std::recursive_mutex mtx; std::atomic status; - std::atomic stop_threads; + std::atomic stop_threads{ false }; }; std::optional connection{ std::nullopt }; @@ -25,40 +25,54 @@ std::optional receive_thread{ std::nullopt }; void handle_ok(zmq::multipart_t const &message) { NetworkData::MessageType type{ to_message_type(message[1]) }; switch (type) { - default: // no need to handle every OK, just some relevant ones - return; 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_error(vformat("Client: Received unhandled message: ", type, message[0].to_string().c_str())); + 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)) { + 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 { @@ -94,12 +108,14 @@ void connect(String const &url) { 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) { - std::scoped_lock lock{ connection->mtx }; + connection->stop_threads = true; if (receive_thread && receive_thread->joinable()) { receive_thread->join(); } @@ -114,4 +130,15 @@ void disconnect() { } 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 index af7a37b0..b8f48b72 100644 --- a/modules/you_done_it/ydi_client.h +++ b/modules/you_done_it/ydi_client.h @@ -6,6 +6,7 @@ namespace ydi::client { void connect(String const &url); void disconnect(); +NetworkData::ConnectionStatus status(); namespace send { void reveal_clue(NetworkData::ClueID id); } //namespace send diff --git a/modules/you_done_it/ydi_networking.cpp b/modules/you_done_it/ydi_networking.cpp index 4664a40d..465bce8d 100644 --- a/modules/you_done_it/ydi_networking.cpp +++ b/modules/you_done_it/ydi_networking.cpp @@ -1,45 +1,50 @@ #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 { -NetworkData::MessageType to_message_type(zmq::message_t const &msg) { - int as_int{ std::stoi(msg.str()) }; - if (as_int >= 0 && as_int < NetworkData::MSG_INVALID) { - return (NetworkData::MessageType)as_int; - } else { - return NetworkData::MSG_INVALID; +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) { - int as_int{ std::stoi(msg.str()) }; - if (as_int >= 0 && as_int < NetworkData::NOK_REASON_INVALID) { - return (NetworkData::NOKReason)as_int; - } else { - return NetworkData::NOK_REASON_INVALID; - } + return (NetworkData::NOKReason)to_int(msg, (int)NetworkData::NOK_INVALID_REASON); } NetworkData::ClueID to_clue_id(zmq::message_t const &msg) { - int as_int{ std::stoi(msg.str()) }; - if (as_int >= 0 && as_int < NetworkData::CLUE_MAX) { - return (NetworkData::ClueID)as_int; - } else { - return NetworkData::CLUE_MAX; + 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(type)); + mpart.addstr(std::to_string((int)type)); } void extend_multipart(zmq::multipart_t &mpart, NetworkData::ClueID id) { @@ -67,5 +72,15 @@ void extend_multipart(zmq::multipart_t &mpart, zmq::multipart_t const &right) { mpart.append(right.clone()); } -void extend_multipart(zmq::multipart_t &mpart) {} +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 b88e852e..db8be659 100644 --- a/modules/you_done_it/ydi_networking.h +++ b/modules/you_done_it/ydi_networking.h @@ -41,15 +41,19 @@ public: enum NOKReason { NOK_UNAUTHENTICATED, NOK_UNKNOWN_MSG, - NOK_REASON_INVALID //!< this means the value could not be parsed as a NOK reason, not that the reason is an invalid message.INVALID + 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 { +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); +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); @@ -59,11 +63,18 @@ 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); +} template -void extend_multipart(zmq::multipart_t &mpart, TArg const &arg, TArgs const &...args) { +void extend_multipart_r(zmq::multipart_t &mpart, TArg const &arg, TArgs const &...args) { extend_multipart(mpart, arg); - extend_multipart(mpart, args...); + extend_multipart_r(mpart, args...); } template @@ -76,7 +87,7 @@ zmq::multipart_t multipart(TArg const &arg) { template zmq::multipart_t multipart(TArg const &arg, TArgs const &...args) { zmq::multipart_t mpart{ multipart(arg) }; - extend_multipart(mpart, args...); + 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 3f8d4110..61308a96 100644 --- a/modules/you_done_it/ydi_server.cpp +++ b/modules/you_done_it/ydi_server.cpp @@ -33,42 +33,17 @@ void handle_reveal_clue(zmq::multipart_t const &message) { } void handle_authorised_message(std::string_view const &sender, NetworkData::MessageType type, zmq::multipart_t &message) { - if (type == NetworkData::MSG_BEAT) { - service->lastBeat = Time::get_singleton()->get_unix_time_from_system(); - } else if (type == NetworkData::MSG_REVEAL) { - handle_reveal_clue(message); - } else { - multipart(sender, NetworkData::MSG_NOK, NetworkData::NOK_UNKNOWN_MSG, message).send(*service->socket); - } -} - -void handle_message(zmq::multipart_t &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, "NOK", "UNAUTHORIZED_REQUEST").send(*service->socket); - } - } else if (type == NetworkData::MSG_CONNECT) { - service->client.emplace(sender); - multipart(sender, NetworkData::MSG_OK, message).send(*service->socket); - } else { - multipart(sender, NetworkData::MSG_NOK, "UNAUTHORIZED_REQUEST", message).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)) { - handle_message(incoming); - } + switch (type) { + default: + print_line("Server: Encountered unknown message type, sending NOK_UNKNOWN_MSG"); + multipart(sender, NetworkData::MSG_NOK, NetworkData::NOK_UNKNOWN_MSG, message).send(*service->socket); + return; + case NetworkData::MSG_BEAT: + service->lastBeat = Time::get_singleton()->get_unix_time_from_system(); + return; + case NetworkData::MSG_REVEAL: + handle_reveal_clue(message); + return; } } @@ -80,16 +55,53 @@ void ping_thread_entry() { return; } } - static zmq::multipart_t ping{ multipart(*service->client, "HEART") }; 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 { @@ -123,12 +135,13 @@ void open() { } print_line("Server: Bound socket"); receive_thread.emplace(receive_thread_entry); + assert(receive_thread->joinable()); + print_line("Server: Startup complete!"); } void close() { if (service) { print_line("Server: Shutting down..."); - std::scoped_lock lock{ service->mtx }; service->stop_threads = true; print_line("Server: Stopping Threads..."); if (receive_thread && receive_thread->joinable()) { @@ -154,10 +167,19 @@ void close() { } } +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 has_new{ !service->new_clues.is_empty() }; + bool const has_new{ !service->new_clues.is_empty() }; if (has_new) { out.append_array(service->new_clues); service->new_clues.clear(); diff --git a/modules/you_done_it/ydi_server.h b/modules/you_done_it/ydi_server.h index 77acad2e..7e019e9a 100644 --- a/modules/you_done_it/ydi_server.h +++ b/modules/you_done_it/ydi_server.h @@ -10,6 +10,7 @@ namespace ydi::server { void open(); void close(); +bool has_client(); namespace receive { bool new_clues(Vector &out);