]> git.tdb.fi Git - libs/game.git/commitdiff
Add basic networking infrastructure
authorMikko Rasa <tdb@tdb.fi>
Tue, 17 Jan 2023 07:52:25 +0000 (09:52 +0200)
committerMikko Rasa <tdb@tdb.fi>
Tue, 17 Jan 2023 07:52:25 +0000 (09:52 +0200)
Currently it only allows running a server, connecting to it and creating
players, but no synchronization yet.

Build
source/game/director.cpp
source/game/director.h
source/game/events.h
source/game/messages.h [new file with mode: 0644]
source/game/networking.cpp [new file with mode: 0644]
source/game/networking.h [new file with mode: 0644]
source/game/protocol.cpp [new file with mode: 0644]
source/game/protocol.h [new file with mode: 0644]

diff --git a/Build b/Build
index f7385bedfb2568deb6abbd0ca20a6a108325bf65..e2a9e8cfee68a3d45480bda96a638f8c556d8a45 100644 (file)
--- a/Build
+++ b/Build
@@ -15,6 +15,7 @@ package "mspgame"
        library "mspgame"
        {
                source "source/game";
+               require "mspnet";
                install true;
                install_map
                {
index 53b2e7f9a3a0c8668580a139355133eebe7b3f61..31d20a947e856f67d6ec3136d930e6f82e52cada 100644 (file)
@@ -11,6 +11,7 @@ namespace Msp::Game {
 
 Director::Director(DataFile::Collection &r):
        resources(r),
+       networking(*this),
        event_source(event_bus)
 {
 #if DEBUG
@@ -42,6 +43,8 @@ void Director::activate_stage(Stage &s)
 
 void Director::tick()
 {
+       io_dispatcher.tick(Time::zero);
+
        Time::TimeStamp now = Time::now();
        Time::TimeDelta dt = (last_tick ? now-last_tick : Time::zero);
        last_tick = now;
@@ -50,6 +53,8 @@ void Director::tick()
        for(unsigned i=0; (i<max_steps_per_frame && backlog>=stepsize); ++i, backlog-=stepsize)
                for(const auto &s: stages)
                        s->tick(stepsize);
+
+       networking.reap_connections();
 }
 
 } // namespace Msp::Game
index 5aeddea932badd73b41d9f5aa56b2e4669c24a44..dc6e483a1cb01e011f49ff945aca928fa5415b2f 100644 (file)
@@ -5,6 +5,7 @@
 #include <optional>
 #include <vector>
 #include <msp/datafile/collection.h>
+#include <msp/io/eventdispatcher.h>
 #include <msp/time/timedelta.h>
 #include <msp/time/timestamp.h>
 #include "accessguard.h"
@@ -12,6 +13,7 @@
 #include "events.h"
 #include "eventsource.h"
 #include "mspgame_api.h"
+#include "networking.h"
 #include "reflection.h"
 
 namespace Msp::Game {
@@ -27,6 +29,8 @@ private:
        std::optional<AccessGuard> access_guard;
        Reflection::Reflector reflector;
        DataFile::Collection &resources;
+       IO::EventDispatcher io_dispatcher;
+       Networking networking;
        EventBus event_bus;
        EventSource event_source;
        std::vector<std::unique_ptr<Stage>> stages;
@@ -42,6 +46,8 @@ public:
        ~Director();
 
        DataFile::Collection &get_resources() const { return resources; }
+       Networking &get_networking() { return networking; }
+       IO::EventDispatcher &get_io_dispatcher() { return io_dispatcher; }
        EventBus &get_event_bus() { return event_bus; }
        EventSource &get_event_source() { return event_source; }
        const std::vector<std::unique_ptr<Stage>> &get_stages() const { return stages; }
index 106d3c4ef5aa4ffb6e0e67e44f8faec0e7d00186..ea903ece54d74d7a0801feff3a699e382e924a9b 100644 (file)
@@ -12,6 +12,20 @@ class Stage;
 
 namespace Events {
 
+// Defined in networking.h
+struct NetworkStateChanged;
+
+struct NetworkPlayerAdded
+{
+       unsigned id;
+       std::string name;
+};
+
+struct NetworkPlayerRemoved
+{
+       unsigned id;
+};
+
 struct EntityCreated
 {
        Handle<Entity> entity;
diff --git a/source/game/messages.h b/source/game/messages.h
new file mode 100644 (file)
index 0000000..3b18c33
--- /dev/null
@@ -0,0 +1,33 @@
+#ifndef MSP_GAME_MESSAGES_H_
+#define MSP_GAME_MESSAGES_H_
+
+namespace Msp::Game {
+namespace Messages {
+
+struct AddPlayerRequest
+{
+       std::uint16_t token;
+       std::string name;
+};
+
+struct AddPlayerResponse
+{
+       std::uint16_t token;
+       std::uint16_t id;
+};
+
+struct PlayerAdded
+{
+       std::uint16_t id;
+       std::string name;
+};
+
+struct PlayerRemoved
+{
+       std::uint16_t id;
+};
+
+} // namespace Messages
+} // namespace Msp::Game
+
+#endif
diff --git a/source/game/networking.cpp b/source/game/networking.cpp
new file mode 100644 (file)
index 0000000..30b5287
--- /dev/null
@@ -0,0 +1,232 @@
+#include "networking.h"
+#include <msp/net/inet6.h>
+#include <msp/net/resolve.h>
+#include "director.h"
+
+using namespace std;
+
+namespace Msp::Game {
+
+Networking::Networking(Director &d):
+       event_source(d.get_event_bus()),
+       io_disp(d.get_io_dispatcher())
+{ }
+
+void Networking::start_server(unsigned port)
+{
+       if(state!=DISABLED)
+               throw invalid_state("Networking::start_server");
+
+       Net::Inet6Addr addr = Net::Inet6Addr::wildcard(port);
+       server_socket = make_unique<Net::StreamServerSocket>(addr.get_family());
+       server_socket->listen(addr);
+       server_socket->signal_data_available.connect(sigc::mem_fun(this, &Networking::incoming_connection));
+       io_disp.add(*server_socket);
+
+       state = SERVER;
+       next_id = 1;
+}
+
+void Networking::connect_to_server(const string &host)
+{
+       if(state!=DISABLED)
+               throw invalid_state("Networking::connect_to_server");
+
+       unique_ptr<Net::SockAddr> addr(Net::resolve(host));
+       connection = make_unique<ServerConnection>(*this, *addr);
+       connection->get_communicator().signal_protocol_ready.connect(sigc::mem_fun(this, &Networking::protocol_ready));
+
+       state = CONNECTING;
+       next_id = ID_PENDING+1;
+}
+
+void Networking::disable()
+{
+       server_socket.reset();
+       connection.reset();
+       set_state(DISABLED);
+}
+
+void Networking::reap_connections()
+{
+       if(connection && connection->is_stale())
+               disable();
+
+       for(auto i=clients.begin(); i!=clients.end(); )
+       {
+               if((*i)->is_stale())
+               {
+                       for(auto j=players.begin(); j!=players.end(); )
+                       {
+                               if(j->owner==i->get())
+                               {
+                                       event_source.emit<Events::NetworkPlayerRemoved>(j->id);
+                                       j = players.erase(j);
+                               }
+                               else
+                                       ++j;
+                       }
+
+                       i = clients.erase(i);
+               }
+               else
+                       ++i;
+       }
+}
+
+void Networking::add_player(const string &name, function<void(unsigned)> callback)
+{
+       if(state!=CONNECTED && state!=SERVER)
+               throw invalid_state("Networking::add_player");
+
+       Player &player = create_player(name);
+
+       if(state==SERVER)
+       {
+               callback(player.id);
+
+               Messages::PlayerAdded added;
+               added.id = player.id;
+               added.name = player.name;
+               for(const auto &c: clients)
+                       c->get_communicator().send(added);
+
+               event_source.emit<Events::NetworkPlayerAdded>(player.id, player.name);
+       }
+       else
+       {
+               player.add_callback = move(callback);
+
+               Messages::AddPlayerRequest add_req;
+               add_req.token = player.id;
+               add_req.name = player.name;
+               connection->get_communicator().send(add_req);
+       }
+}
+
+Networking::Player &Networking::create_player(const string &name, unsigned id)
+{
+       Player &player = players.emplace_back();
+       player.id = (id ? id : next_id++);
+       player.name = name;
+       return player;
+}
+
+void Networking::set_state(State s)
+{
+       if(s==state)
+               return;
+
+       state = s;
+       event_source.emit<Events::NetworkStateChanged>(state);
+}
+
+void Networking::protocol_ready(const Net::Protocol &p)
+{
+       if(state==CONNECTING && &p==&protocol)
+               set_state(CONNECTED);
+}
+
+void Networking::incoming_connection()
+{
+       unique_ptr<Net::StreamSocket> socket(server_socket->accept());
+       clients.emplace_back(make_unique<ClientConnection>(*this, move(socket)));
+}
+
+
+Networking::Connection::Connection(Networking &n, unique_ptr<Net::StreamSocket> s):
+       networking(n),
+       socket(move(s)),
+       communicator(*socket)
+{
+       networking.io_disp.add(*socket);
+       socket->signal_end_of_file.connect(sigc::mem_fun(this, &Connection::disconnected));
+       communicator.signal_error.connect(sigc::mem_fun(this, &Connection::error));
+}
+
+void Networking::Connection::disconnected()
+{
+       stale = true;
+}
+
+void Networking::Connection::error(const exception &)
+{
+       stale = true;
+}
+
+
+Networking::ServerConnection::ServerConnection(Networking &n, const Net::SockAddr &addr):
+       Connection(n, make_unique<Net::StreamSocket>(addr.get_family()))
+{
+       communicator.add_protocol(networking.protocol, *this);
+       socket->set_block(false);
+       socket->signal_connect_finished.connect(sigc::mem_fun(this, &ServerConnection::connect_finished));
+       socket->connect(addr);
+}
+
+void Networking::ServerConnection::connect_finished(const exception *exc)
+{
+       if(exc)
+               stale = true;
+}
+
+void Networking::ServerConnection::receive(const Messages::AddPlayerResponse &add_resp)
+{
+       auto i = find_member(networking.players, static_cast<unsigned>(add_resp.token), &Player::id);
+       if(i==networking.players.end())
+               throw key_error(add_resp.token);
+
+       i->id = add_resp.id;
+
+       if(i->add_callback)
+       {
+               i->add_callback(i->id);
+               i->add_callback = nullptr;
+       }
+       networking.event_source.emit<Events::NetworkPlayerAdded>(i->id, i->name);
+}
+
+void Networking::ServerConnection::receive(const Messages::PlayerAdded &added)
+{
+       Player &player = networking.create_player(added.name, added.id);
+       networking.event_source.emit<Events::NetworkPlayerAdded>(player.id, player.name);
+}
+
+void Networking::ServerConnection::receive(const Messages::PlayerRemoved &removed)
+{
+       auto i = find_member(networking.players, static_cast<unsigned>(removed.id), &Player::id);
+       if(i!=networking.players.end())
+       {
+               networking.event_source.emit<Events::NetworkPlayerRemoved>(i->id);
+               networking.players.erase(i);
+       }
+}
+
+
+Networking::ClientConnection::ClientConnection(Networking &n, unique_ptr<Net::StreamSocket> s):
+       Connection(n, move(s))
+{
+       communicator.add_protocol(networking.protocol, *this);
+}
+
+void Networking::ClientConnection::receive(const Messages::AddPlayerRequest &add_req)
+{
+       Player &player = networking.create_player(add_req.name);
+       player.owner = this;
+
+       Messages::AddPlayerResponse add_resp;
+       add_resp.token = add_req.token;
+       add_resp.id = player.id;
+       communicator.send(add_resp);
+
+       Messages::PlayerAdded added;
+       added.id = player.id;
+       added.name = player.name;
+       for(const auto &c: networking.clients)
+               if(c.get()!=this)
+                       c->get_communicator().send(added);
+
+       networking.event_source.emit<Events::NetworkPlayerAdded>(player.id, player.name);
+}
+
+} // namespace Msp::Game
diff --git a/source/game/networking.h b/source/game/networking.h
new file mode 100644 (file)
index 0000000..478b347
--- /dev/null
@@ -0,0 +1,125 @@
+#ifndef MSP_GAME_NETWORKING_H_
+#define MSP_GAME_NETWORKING_H_
+
+#include <memory>
+#include <msp/io/eventdispatcher.h>
+#include <msp/net/communicator.h>
+#include <msp/net/streamserversocket.h>
+#include "events.h"
+#include "eventsource.h"
+#include "messages.h"
+#include "mspgame_api.h"
+#include "protocol.h"
+
+namespace Msp::Game {
+
+class Director;
+
+class MSPGAME_API Networking
+{
+public:
+       enum State
+       {
+               DISABLED,
+               SERVER,
+               CONNECTING,
+               CONNECTED
+       };
+
+       using EventSource = Game::EventSource<Events::NetworkStateChanged, Events::NetworkPlayerAdded,
+               Events::NetworkPlayerRemoved>;
+
+private:
+       class Connection
+       {
+       protected:
+               Networking &networking;
+               std::unique_ptr<Net::StreamSocket> socket;
+               Net::Communicator communicator;
+               bool stale = false;
+
+               Connection(Networking &, std::unique_ptr<Net::StreamSocket>);
+
+               void disconnected();
+               void error(const std::exception &);
+
+       public:
+               Net::Communicator &get_communicator() { return communicator; }
+               bool is_stale() const { return stale; }
+       };
+
+       class ServerConnection: public Connection,
+               public Net::PacketReceiver<Messages::AddPlayerResponse>,
+               public Net::PacketReceiver<Messages::PlayerAdded>,
+               public Net::PacketReceiver<Messages::PlayerRemoved>
+       {
+       public:
+               ServerConnection(Networking &, const Net::SockAddr &);
+
+       private:
+               void connect_finished(const std::exception *);
+
+       public:
+               void receive(const Messages::AddPlayerResponse &) override;
+               void receive(const Messages::PlayerAdded &) override;
+               void receive(const Messages::PlayerRemoved &) override;
+       };
+
+       class ClientConnection: public Connection,
+               public Net::PacketReceiver<Messages::AddPlayerRequest>
+       {
+       public:
+               ClientConnection(Networking &, std::unique_ptr<Net::StreamSocket>);
+
+               void receive(const Messages::AddPlayerRequest &) override;
+       };
+
+       struct Player
+       {
+               ClientConnection *owner = nullptr;
+               unsigned id = 0;
+               std::string name;
+               std::function<void(unsigned)> add_callback;
+       };
+
+       static constexpr unsigned ID_PENDING = 0x8000;
+
+       EventSource event_source;
+       IO::EventDispatcher &io_disp;
+       Protocol protocol;
+       State state = DISABLED;
+       std::unique_ptr<Net::StreamServerSocket> server_socket;
+       std::unique_ptr<ServerConnection> connection;
+       std::vector<std::unique_ptr<ClientConnection>> clients;
+       std::vector<Player> players;
+       unsigned next_id = 1;
+
+public:
+       Networking(Director &);
+
+       void start_server(unsigned);
+       void connect_to_server(const std::string &);
+       void disable();
+       void reap_connections();
+
+       void add_player(const std::string &, std::function<void(unsigned)>);
+private:
+       Player &create_player(const std::string &, unsigned = 0);
+
+       void set_state(State);
+       void protocol_ready(const Net::Protocol &);
+       void incoming_connection();
+};
+
+
+namespace Events {
+
+struct NetworkStateChanged
+{
+       Networking::State state;
+};
+
+} // namespace Events
+} // namespace Msp::Game
+
+#endif
diff --git a/source/game/protocol.cpp b/source/game/protocol.cpp
new file mode 100644 (file)
index 0000000..1226521
--- /dev/null
@@ -0,0 +1,15 @@
+#include "protocol.h"
+#include "messages.h"
+
+namespace Msp::Game {
+
+Protocol::Protocol()
+{
+       using namespace Messages;
+
+       add<AddPlayerRequest>(&AddPlayerRequest::token, &AddPlayerRequest::name);
+       add<AddPlayerResponse>(&AddPlayerResponse::token, &AddPlayerResponse::id);
+       add<PlayerAdded>(&PlayerAdded::id, &PlayerAdded::name);
+}
+
+} // namespace Msp::Game
diff --git a/source/game/protocol.h b/source/game/protocol.h
new file mode 100644 (file)
index 0000000..efeca1f
--- /dev/null
@@ -0,0 +1,16 @@
+#ifndef MSP_GAME_PROTOCOL_H_
+#define MSP_GAME_PROTOCOL_H_
+
+#include <msp/net/protocol.h>
+
+namespace Msp::Game {
+
+class Protocol: public Net::Protocol
+{
+public:
+       Protocol();
+};
+
+} // namespace Msp::Game
+
+#endif