diff --git a/base/event.hpp b/base/event.hpp new file mode 100644 index 0000000..f4fcc36 --- /dev/null +++ b/base/event.hpp @@ -0,0 +1,49 @@ +/* + * This file is a part of Pixelbox - Infinite 2D sandbox game + * Evenst for objservers/subscribers patterns + * Copyright (C) 2023-2024 UtoECat + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +#pragma once +#include +#include + +namespace pb { + + // subject for "Observers" + template + class Subject { + protected: + using function_type = std::function; + std::vector handlers; + public: + Subject() {}; + Subject(const Subject&) = default; + Subject(Subject&&) = default; + Subject& operator=(const Subject&) = default; + Subject& operator=(Subject&&) = default; + void subscribe(const function_type& f) { handlers.emplace_back(f); } + void subscribe(function_type&& f) { handlers.emplace_back(std::move(f)); } + void notify_all(Args&&... args) { + for (auto& f : handlers) { + f(std::forward(args)...); + } + } + }; + +}; + + diff --git a/engine/network.cpp b/engine/network.cpp new file mode 100644 index 0000000..a302258 --- /dev/null +++ b/engine/network.cpp @@ -0,0 +1,141 @@ +/* + * This file is a part of Pixelbox - Infinite 2D sandbox game + * Enet wrapper! + * Copyright (C) 2023-2024 UtoECat + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +#pragma once +#include "engine/network.hpp" + +#include +#include +#include +#include +#include +#include + +#include "enet.h" + +/** + * @description ENet Wrapper + */ +namespace pb { + +void ENetBase::on_event_recv(ENetEvent& ev) { + auto conn = ENetConnection(ev.peer); + switch (ev.type) { + case ENET_EVENT_TYPE_CONNECT: + enet_peer_set_data(ev.peer, new ConnectionData()); + h_connect.notify_all(*this, conn); + break; + case ENET_EVENT_TYPE_DISCONNECT: + case ENET_EVENT_TYPE_DISCONNECT_TIMEOUT: + try { + h_disconnect.notify_all(*this, conn); + } catch (std::exception& e) { + std::cerr << "Exception in disconnect handler : " << e.what() << std::endl; + } catch (...) { + std::cerr << "Unknown exception in disconnect handler" << std::endl; + } + { + auto v = (ConnectionData*)enet_peer_get_data(ev.peer); + if (v) delete v; + enet_peer_set_data(ev.peer, nullptr); + } + break; + case ENET_EVENT_TYPE_RECEIVE: + try { + h_recieve.notify_all(*this, conn, ev.channelID, + std::string_view((const char*)enet_packet_get_data(ev.packet), enet_packet_get_length(ev.packet))); + } catch (std::exception& e) { + std::cerr << "Exception in message handler : " << e.what() << std::endl; + } + enet_packet_destroy(ev.packet); // hehe + break; + case ENET_EVENT_TYPE_NONE: + break; + } +} + +void ENetBase::destroy() { + /** request disconnection */ + foreach ([](ENetPeer* peer) { + enet_peer_timeout(peer, 2000, 1000, 5000); + enet_peer_disconnect(peer, -1); // shutdown + }) + ; + + flush(); + + int attempts = host->peerCount * 8; // process as much as *8 events + int dummies = host->peerCount; + + ENetEvent ev; + while (dummies > 0 && attempts > 0 && enet_host_service(host, &ev, 5000)) { + switch (ev.type) { + case ENET_EVENT_TYPE_CONNECT: + enet_peer_reset(ev.peer); // ignore and kill + break; + case ENET_EVENT_TYPE_DISCONNECT: + case ENET_EVENT_TYPE_DISCONNECT_TIMEOUT: + dummies--; + on_event_recv(ev); // default behaviour + break; + case ENET_EVENT_TYPE_RECEIVE: + enet_packet_destroy(ev.packet); // we don't care + break; + case ENET_EVENT_TYPE_NONE: + break; + } + } + + force_destroy(); // shutdown now! +}; + +bool ENetClient::connect(const char* ip, unsigned short port) { + ProtocolInfo info; + info.ip = NULL; + info.port = port; + create_client(); + server = nullptr; + + if (!host) { + std::cerr << "ENetClient: can't create client host! (weird)" << std::endl; + return false; // :( + } + + // connect + const ENetAddress addr = info.getAddress(); + server = enet_host_connect(host, &addr, info.nchannels, 0); + if (!server) { + std::cerr << "ENetClient : can't create peer!" << std::endl; + goto err; + } + + // process events + ENetEvent event; + if (enet_host_service(host, &event, 5000) > 0 && event.type == ENET_EVENT_TYPE_CONNECT) { + on_event_recv(event); // do init + return true; + } + + std::cerr << "ENetClient : can't connect to the server!" << std::endl; +err: + force_destroy(); + return false; +} + +}; // namespace pb diff --git a/engine/network.hpp b/engine/network.hpp index 8bdeae7..1592985 100644 --- a/engine/network.hpp +++ b/engine/network.hpp @@ -19,22 +19,30 @@ #pragma once #include +#include +#include +#include +#include #include #include #include +#include #include "external/enet.h" #include +#include "base/event.hpp" /** * @description ENet Wrapper */ namespace pb { + static constexpr unsigned short DEFAULT_PORT = 4792; + struct ProtocolInfo { - const char* ip; - unsigned short port; - int nchannels; - int nconnections; + const char* ip = "localhost"; + unsigned short port = DEFAULT_PORT; + int nchannels = 5; + int nconnections = 0; public: inline ENetAddress getAddress() { ENetAddress addr; @@ -53,158 +61,286 @@ namespace pb { host->maximumWaitingData = 1024 * 128; } - class ENetClient : public Default { - ENetHost* host; - ENetPeer* server = nullptr; - ProtocolInfo pinfo; + /** represents any data */ + using ConnectionData = std::shared_ptr; + + class ENetConnection { + ENetPeer* peer = nullptr; public: + ENetConnection(const ENetConnection&) = default; + ENetConnection(ENetConnection&&) = default; + ENetConnection& operator=(const ENetConnection&) = default; + ENetConnection& operator=(ENetConnection&&) = default; - ENetClient(ProtocolInfo info) { - pinfo = info; - auto addr = info.getAddress(); - host = enet_host_create(NULL, info.nconnections, info.nchannels, - 0, 0); - if (!host) { - throw std::runtime_error("Can't create client!"); - } - HostDefaultConfig(host); + explicit ENetConnection(ENetPeer* p) { peer = p;} + ~ENetConnection() {} // nothing + public: + + operator bool() { + return peer != nullptr; } void disconnect() { - if (server) { - enet_peer_reset(server); - server = nullptr; - } + if (!peer) return; + enet_peer_disconnect(peer, 0); } - const char* errmsg = nullptr; - bool service(ENetEvent& ev, int timeout = 1) { - return enet_host_service(host, &ev, timeout) > 0; + void disconnect_later() { + if (!peer) return; + enet_peer_disconnect_later(peer, 0); } - bool connect() { - if (server) disconnect(); - auto addr = pinfo.getAddress(); - server = enet_host_connect(host, &addr, pinfo.nchannels, 0); - - if (!server) { - errmsg = "can't create peer!"; - return false; - } - // process events - ENetEvent event; - if (enet_host_service(host, &event, 5000) > 0 && - event.type == ENET_EVENT_TYPE_CONNECT) { - return true; + /** @warning you cannot use object methods after this! */ + void reset() { // forcefully disconnect + if (!peer) return; + { + auto v = (ConnectionData*)enet_peer_get_data(peer); + if (v) delete v; + enet_peer_set_data(peer, nullptr); } + enet_peer_reset(peer); + peer = nullptr; + } - errmsg = "Can't connect to server!"; - enet_peer_reset(server); - server = nullptr; - return false; + bool send(uint8_t channel, ENetPacket* data) { + return enet_peer_send(peer, channel, data) == 0; } - ~ENetClient() { - disconnect(); - enet_host_destroy(host); + bool send(uint8_t channel, std::string_view data) { + ENetPacket* p = enet_packet_create(data.data(), data.size(), 0); + if (!p) return false; + enet_peer_send(peer, channel, p); } - /* must be called after event is processeed */ - bool is_connected() { - return server != nullptr; + ConnectionData& data() { + auto v = (ConnectionData*)enet_peer_get_data(peer); + if (!v) std::runtime_error("Connection has no associated data. Use afetr reset()/release_data() or misuse"); + return *v; + } + }; + + /** @warning NEVER EVER TRY TO DESTROY ENETHOST FROM THE EVENT HANDLER! + * PLEASE set defer_destroy to true if you want to do that instead! + * @warning NEVER try to reconnect/dsconnect or do any other stuff in a shutdown handler too! + * @warning PLEASE DO NOT USE CALLBACKS FOR RESOURCE DEINITIALISATION! Use userdata object destructor for that! + */ + class ENetBase { + protected: + ENetHost* host = nullptr; + ProtocolInfo info; // default + bool defer_destroy = false; + + public: // event handlers + /** handled every time a client connects. @warning does not triggered on a server disconnection process. */ + Subject h_connect; + + /** triggered when asuccesfully connected peer is disconnected. Does not work in force_disconnect() case. + * also it's not triggered on a peers, that does not respond with a succesful disconnection. + */ + Subject h_disconnect; + + /** + * Triggered every time a message is recieved + * @param basic + * @param basic + * @param int specifies channel id where the message was recieved + * @param string_view message data + */ + Subject h_recieve; // channel_id, data + + /** server shutdown/disconnection handler. all connections are still valid. + * @warning does not called on force_disconnect at all! + */ + Subject h_shutdown; + + protected: + // handle events servicing + void on_event_recv(ENetEvent& ev); + + protected: + /** @warning do not use inside event handers! */ + void create_server() { + if (host) std::runtime_error("host is already exists!"); + defer_destroy = false; + auto addr = info.getAddress(); + host = enet_host_create(&addr, info.nconnections, info.nchannels, 0, 0); } - void releaseEvent(ENetEvent& ev) { - if (ev.type == ENET_EVENT_TYPE_RECEIVE && ev.packet) { - enet_packet_destroy(ev.packet); + /** @warning do not use inside event handers! */ + void create_client() { + if (host) std::runtime_error("host is already exists!"); + defer_destroy = false; + host = enet_host_create(NULL, info.nconnections, info.nchannels, 0, 0); + } + + /** calls function at the first argument on all the peers connected to this host.*/ + void foreach(std::function cb) { + if (!host) return; + ENetPeer *currentPeer; + for (currentPeer = host->peers; + currentPeer < &host->peers[host->peerCount]; ++currentPeer) { + cb(currentPeer); } } - /** @warning channel starts FROM ZERO!!!! */ - void send(uint8_t channel, ENetPacket* data) { - if (server) - enet_peer_send(server, channel, data); + /** @warning do not use inside event handers! Use defer_destroy=true; instead! + * Also, this function does not invoke h_shutdown and h_disconnect events AT ALL! + */ + void force_destroy() { + // cleanup all userdata + foreach([](ENetPeer* peer){ + auto v = (ConnectionData*)enet_peer_get_data(peer); + if (v) delete v; + enet_peer_set_data(peer, nullptr); + }); + + if (host) enet_host_destroy(host); + host = nullptr; + } + + /** @warning do not use inside event handers! Use defer_destroy=true; instead! + * Peacefully disconnects clients once, max 5s delay, 8 times more attempts. + * Clients that was not disconnected peacefully will be destroyed without h_desconnect event triggered! + */ + void destroy(); + + /** @warning this does not affect already running Host! You should do destroy() and create()... maybe... */ + void set_address(ProtocolInfo i) { + info = i; + } + + public: + ENetBase() {} + ENetBase(const ENetBase&) = delete; + ENetBase(ENetBase&&) = default; + ENetBase& operator=(const ENetBase&) = delete; + ENetBase& operator=(ENetBase&&) = default; + ENetBase(ProtocolInfo i) : info(i) {} + + /** use this oeprator in order to check if server is terminated! + * You may try to reconnect after that or do delete class instance. + */ + operator bool() const { + return !!host; } + /** + * Same as operator bool(). + */ + bool running() {return !!host;} + + /** does deletes ENet hosts in a way how force_destroy() does, if it wasn't done already. */ + ~ENetBase() { + force_destroy(); + } + + /** flushes all packages to be finally sended! No need to do if you regulary call a service()! */ void flush() { enet_host_flush(host); } - }; - class ConnectionData { - public: - virtual ~ConnectionData() {}; - }; + /** all work is done internally, but you may optional get event, but cna't use any data! + * @param timeout in illiseconds + */ + bool service(ENetEvent* ev = nullptr, uint32_t timeout = 0) { + ENetEvent _dummy; + if (!ev) ev = &_dummy; - class ENetConnection { - ENetPeer* peer; - public: - ENetConnection(ENetPeer* p) { - peer = p; + /** runned in case destruction is deferred. that's right :p */ + if (defer_destroy) destroy(); + if (!host) {*ev = ENetEvent{ENET_EVENT_TYPE_NONE, 0, 0 , 0, 0}; return false;} + + bool stat = enet_host_service(host, ev, timeout) > 0; + on_event_recv(*ev); + return stat; } - ~ENetConnection() {} // nothing + + }; + + class ENetClient : public Default, ENetBase { + ENetPeer* server = nullptr; public: - void disconnect() { - enet_peer_disconnect_later(peer, 0); + + ENetClient() { + h_disconnect.subscribe([this](ENetBase&, ENetConnection& conn) { + defer_destroy = true; // shutdown server + server = nullptr; + std::cerr << "Client: Server is disconnected!" << std::endl; + }); + }; + + ENetClient(ProtocolInfo info) : ENetClient() { + set_address(info); } - void reset() { // forcefully disconnect - release_data(); - enet_peer_reset(peer); + ~ENetClient() { + server = nullptr; + // all the other stuff is done in the base class! } - bool send(uint8_t channel, ENetPacket* data) { - return enet_peer_send(peer, channel, data) == 0; + /** create AND connect a client to the specified server! */ + bool connect(const char* ip, unsigned short port = DEFAULT_PORT); + + /** function for a user :) */ + void disconnect() { + if (server) { + enet_peer_reset(server); + server = nullptr; + } + destroy(); // destroy connection, all user data, etc. } - ConnectionData* data() { - return (ConnectionData*)enet_peer_get_data(peer); + /* same as running() + extra check */ + bool is_connected() { + return running() && server != nullptr; } - void set_data(ConnectionData* dat) { - return enet_peer_set_data(peer, dat); + /** @warning channel starts FROM ZERO!!!! */ + bool send(uint8_t channel, ENetPacket* data) { + if (!server || !host) return false; + return enet_peer_send(server, channel, data); } - void release_data() { - delete data(); - set_data(nullptr); + /** @warning channel starts FROM ZERO!!!! */ + bool send(uint8_t channel, const std::string_view data) { + if (!server || !host) return false; + ENetPacket* p = enet_packet_create(data.data(), data.size(), 0); + if (!p) return false; + return send(channel, p); } }; - class ENetServer : public Default { - ENetHost* host; + class ENetServer : public Default,ENetBase { + bool shutdown_on_leaving = false; public: - ENetServer(ProtocolInfo info) { - ENetAddress addr = info.getAddress(); - host = enet_host_create(&addr, info.nconnections, info.nchannels, - 0, 0); - if (!host) { - throw std::runtime_error("Can't create server!"); - } - HostDefaultConfig(host); + set_address(info); } - bool service(ENetEvent& ev, int timeout = 1) { - bool stat = enet_host_service(host, &ev, timeout) > 0; - if (stat && ev.type == ENET_EVENT_TYPE_CONNECT) { - enet_peer_set_data(ev.peer, nullptr); // we rely on this behaviour! - } - return stat; + bool create() { + ENetBase::create_server(); + return !!host; } - ~ENetServer() { - enet_host_destroy(host); + void shutdown() { + ENetBase::destroy(); // destroy connection, all user data, etc, and fire all callbacks } - bool is_valid() { - return host != nullptr; + void destroy() { + ENetBase::force_destroy(); // immediate destroy! } - void releaseEvent(ENetEvent& ev) { - if (ev.type == ENET_EVENT_TYPE_RECEIVE && ev.packet) { - enet_packet_destroy(ev.packet); - } + ~ENetServer() { + destroy(); + } + + /** all work is done internally, but you may optional get event, but cna't use any data! */ + bool service(ENetEvent* ev = nullptr, int timeout = 1) { + ENetEvent _dummy; + if (!ev) ev = &_dummy; + bool stat = enet_host_service(host, ev, timeout) > 0; + on_event_recv(*ev); + return stat; } void broadcast(uint8_t channel, ENetPacket* data) { @@ -213,52 +349,9 @@ namespace pb { } void foreach(std::function cb) { - if (!cb) return; - ENetPeer *currentPeer; - for (currentPeer = host->peers; - currentPeer < &host->peers[host->peerCount]; ++currentPeer) { - cb(currentPeer); - } - } - - void flush() { - enet_host_flush(host); - } - - /* shutdown sequence. Behaviour after this is not specified! */ - void shutdown() { - // send disconnect - foreach([](ENetConnection c) { - c.disconnect(); - }); - enet_host_flush(host); - - // queue - ENetEvent ev; - while (service(ev, 200)) { - switch (ev.type) { - case ENET_EVENT_TYPE_DISCONNECT: - case ENET_EVENT_TYPE_DISCONNECT_TIMEOUT: { - ENetConnection con(ev.peer); - con.release_data(); - }; break; - case ENET_EVENT_TYPE_CONNECT: - enet_peer_disconnect_now(ev.peer, 0); - break; - case ENET_EVENT_TYPE_RECEIVE: - enet_packet_destroy(ev.packet); - break; - default: - break; - }; - } - - // force disconnect and free all data - foreach([](ENetConnection c) { - c.reset(); + ENetBase::foreach([cb](ENetPeer* p){ + cb(ENetConnection(p)); }); - - // now we can safely call destructor! } }; diff --git a/game/world_view.hpp b/game/world_view.hpp index 4cd3869..0cc92fe 100644 --- a/game/world_view.hpp +++ b/game/world_view.hpp @@ -26,10 +26,10 @@ namespace pb { public: WorldViewer(); ~WorldViewer(); - WorldViewer(const WorldViewer &) = delete; + WorldViewer(const WorldViewer &); WorldViewer(WorldViewer &&) = delete; WorldViewer &operator=(const WorldViewer &) = delete; - WorldViewer &operator=(WorldViewer &&) = delete; + WorldViewer &operator=(WorldViewer &&); }; }; \ No newline at end of file