--port <number>
port number to connection to (client) or listen on (server)
+--cmd-port <number>
+ port number to listen on for command connections
+ the default of 0 disables this feature
+
--player-name <name>
use given name to identify with the server (client mode)
default player name is "default"
std::string host = "localhost";
std::uint16_t port = 12354;
+ std::uint16_t cmd_port = 0;
} net;
int port;
in.ReadNumber(port);
net.port = port;
+ } else if (name == "net.cmd_port") {
+ int port;
+ in.ReadNumber(port);
+ net.cmd_port = port;
} else if (name == "player.name") {
in.ReadString(player.name);
} else if (name == "video.dblbuf") {
out << "input.yaw_sensitivity = " << input.yaw_sensitivity << ';' << std::endl;
out << "net.host = \"" << net.host << "\";" << std::endl;
out << "net.port = " << net.port << ';' << std::endl;
+ out << "net.cmd_port = " << net.cmd_port << ';' << std::endl;
out << "player.name = \"" << player.name << "\";" << std::endl;
out << "video.dblbuf = " << (video.dblbuf ? "on" : "off") << ';' << std::endl;
out << "video.vsync = " << (video.vsync ? "on" : "off") << ';' << std::endl;
} else {
config.game.net.port = strtoul(argv[i], nullptr, 10);
}
+ } else if (strcmp(param, "cmd-port") == 0) {
+ ++i;
+ if (i >= argc || argv[i] == nullptr || argv[i][0] == '\0') {
+ cerr << "missing argument to --cmd-port" << endl;
+ error = true;
+ } else {
+ config.game.net.cmd_port = strtoul(argv[i], nullptr, 10);
+ }
} else if (strcmp(param, "player-name") == 0) {
++i;
if (i >= argc || argv[i] == nullptr || argv[i][0] == '\0') {
--- /dev/null
+#include "tcp.hpp"
+
+#include "../app/init.hpp"
+
+#include <stdexcept>
+
+using namespace std;
+
+
+namespace blank {
+namespace tcp {
+
+Socket::Socket()
+: sock(nullptr) {
+
+}
+
+Socket::Socket(unsigned short port)
+: sock(nullptr) {
+ IPaddress ip;
+ if (SDLNet_ResolveHost(&ip, nullptr, port) == -1) {
+ throw NetError("failed to resolve local host");
+ }
+ sock = SDLNet_TCP_Open(&ip);
+ if (!sock) {
+ throw NetError("failed to open local socket");
+ }
+}
+
+Socket::Socket(TCPsocket sock)
+: sock(sock) {
+
+}
+
+Socket::~Socket() noexcept {
+ if (sock) {
+ SDLNet_TCP_Close(sock);
+ }
+}
+
+Socket::Socket(Socket &&other) noexcept
+: sock(other.sock) {
+ other.sock = nullptr;
+}
+
+Socket &Socket::operator =(Socket &&other) noexcept {
+ swap(sock, other.sock);
+ return *this;
+}
+
+
+Socket Socket::Accept() noexcept {
+ return Socket(SDLNet_TCP_Accept(sock));
+}
+
+bool Socket::Ready() const noexcept {
+ return SDLNet_SocketReady(sock);
+}
+
+size_t Socket::Recv(void *buf, std::size_t max_len) {
+ const int len = SDLNet_TCP_Recv(sock, buf, max_len);
+ if (len < 0) {
+ throw NetError("TCP socket recv");
+ }
+ return len;
+}
+
+size_t Socket::Send(const void *buf, size_t max_len) {
+ /// TODO: make TCP send non-blocking
+ const int len = SDLNet_TCP_Send(sock, buf, max_len);
+ if (len < int(max_len)) {
+ throw NetError("TCP socket send");
+ }
+ return len;
+}
+
+
+int Socket::AddTo(SDLNet_SocketSet set) {
+ return SDLNet_TCP_AddSocket(set, sock);
+}
+
+int Socket::RemoveFrom(SDLNet_SocketSet set) {
+ return SDLNet_TCP_DelSocket(set, sock);
+}
+
+
+Pool::Pool(int max_conn, size_t buf_siz)
+: set(SDLNet_AllocSocketSet(max_conn))
+, buffer(buf_siz, '\0')
+, connections()
+, use_conn(0)
+, max_conn(max_conn)
+, buf_siz(buf_siz) {
+ if (!set) {
+ throw runtime_error("failed to allocate socket set");
+ }
+}
+
+Pool::~Pool() noexcept {
+ SDLNet_FreeSocketSet(set);
+}
+
+
+void Pool::AddConnection(Socket sock, IOHandler *handler) {
+ if (FreeSlots() == 0) {
+ Resize(TotalSlots() * 2);
+ }
+ int num = sock.AddTo(set);
+ if (num < 0) {
+ throw NetError("failed to add socket to set");
+ }
+ use_conn = num;
+ connections.emplace_back(move(sock), handler);
+ handler->OnCreate(connections.back().first);
+}
+
+void Pool::Send() {
+ for (auto i = connections.begin(); i != connections.end(); ++i) {
+ if (i->second->Closed()) {
+ continue;
+ }
+
+ try {
+ i->second->OnSend(i->first);
+ } catch (...) {
+ i->second->OnError(i->first);
+ }
+ }
+}
+
+bool Pool::Check(unsigned long timeout) {
+ // SDL_net considers checking an empty set an error, so
+ // we're checking that ourselves
+ if (OccupiedSlots() == 0) {
+ return false;
+ }
+
+ int num = SDLNet_CheckSockets(set, timeout);
+ if (num < 0) {
+ throw NetError("error checking sockets");
+ }
+ return num > 0;
+}
+
+void Pool::Receive() {
+ for (auto i = connections.begin(); i != connections.end(); ++i) {
+ if (!i->first.Ready() || i->second->Closed()) {
+ continue;
+ }
+
+ try {
+ i->second->OnRecv(i->first);
+ } catch (...) {
+ i->second->OnError(i->first);
+ }
+ }
+}
+
+void Pool::Clean() {
+ for (auto i = connections.begin(); i != connections.end();) {
+ if (i->second->Closed()) {
+ int num = i->first.RemoveFrom(set);
+ if (num < 0) {
+ throw NetError("failed to remove socket from set");
+ }
+ use_conn = num;
+ i->second->OnRemove(i->first);
+ i = connections.erase(i);
+ } else {
+ ++i;
+ }
+ }
+}
+
+
+void Pool::Resize(int new_max) {
+ if (new_max < max_conn) {
+ return;
+ }
+
+ int new_size = max(new_max, max_conn * 2);
+ SDLNet_SocketSet new_set(SDLNet_AllocSocketSet(new_size));
+ if (!new_set) {
+ throw NetError("failed to allocate socket set");
+ }
+
+ for (auto &conn : connections) {
+ if (conn.first.AddTo(new_set) == -1) {
+ NetError error("failed to migrate socket to new set");
+ SDLNet_FreeSocketSet(new_set);
+ throw error;
+ }
+ }
+
+ SDLNet_FreeSocketSet(set);
+ set = new_set;
+ max_conn = new_size;
+}
+
+}
+}
--- /dev/null
+#ifndef BLANK_NET_TCP_HPP_
+#define BLANK_NET_TCP_HPP_
+
+#include <algorithm>
+#include <list>
+#include <SDL_net.h>
+#include <string>
+
+
+namespace blank {
+namespace tcp {
+
+/// all failing functions throw NetError
+class Socket {
+
+public:
+ /// create an empty socket that is not connected to anything
+ Socket();
+ /// create TCP socket bound to given port
+ explicit Socket(unsigned short port);
+private:
+ /// wrap given SDLNet TCP socket
+ /// for use with Accept()
+ explicit Socket(TCPsocket sock);
+public:
+ ~Socket() noexcept;
+
+ Socket(const Socket &) = delete;
+ Socket &operator =(const Socket &) = delete;
+
+ Socket(Socket &&) noexcept;
+ Socket &operator =(Socket &&) noexcept;
+
+ explicit operator bool() const noexcept { return sock; }
+
+ bool operator ==(const Socket &other) const noexcept {
+ return sock == other.sock;
+ }
+ bool operator <(const Socket &other) const noexcept {
+ return sock < other.sock;
+ }
+
+public:
+ /// create a socket for an incoming connection
+ /// @return an empty socket if there are none
+ Socket Accept() noexcept;
+ /// check if there is data available to read
+ bool Ready() const noexcept;
+ /// receive data into given buffer
+ /// @return number of bytes read, at most max_len
+ /// non-blocking if Ready() is true
+ std::size_t Recv(void *buf, std::size_t max_len);
+ /// send data from given buffer, at most max_len bytes
+ /// @return number of bytes written
+ /// may be less than len as soon as I get to
+ /// making it non-blocking
+ std::size_t Send(const void *buf, std::size_t max_len);
+
+ int AddTo(SDLNet_SocketSet);
+ int RemoveFrom(SDLNet_SocketSet);
+
+private:
+ TCPsocket sock;
+
+};
+
+
+struct IOHandler {
+
+ virtual ~IOHandler() = default;
+
+ void Close() noexcept { closed = true; }
+ bool Closed() const noexcept { return closed; }
+
+ virtual void OnCreate(Socket &) { }
+ virtual void OnRemove(Socket &) noexcept { }
+
+ virtual void OnSend(Socket &) { };
+ virtual void OnRecv(Socket &) { };
+ virtual void OnError(Socket &) noexcept { Close(); }
+
+private:
+ bool closed = false;
+
+};
+
+
+class Pool {
+
+public:
+ using ConnectionSet = std::list<std::pair<Socket, IOHandler *>>;
+
+public:
+ explicit Pool(int max_conn = 32, std::size_t buf_siz = 1500);
+ ~Pool() noexcept;
+
+ Pool(const Pool &) = delete;
+ Pool &operator =(const Pool &) = delete;
+
+public:
+ void AddConnection(Socket, IOHandler *);
+ void Send();
+ bool Check(unsigned long timeout);
+ void Receive();
+ void Clean();
+
+ int FreeSlots() const noexcept { return max_conn - use_conn; }
+ int OccupiedSlots() const noexcept { return use_conn; }
+ int TotalSlots() const noexcept { return max_conn; }
+ /// reallocate the pool to accomodate at least new_max sockets
+ void Resize(int new_max);
+
+private:
+ SDLNet_SocketSet set;
+ std::string buffer;
+ ConnectionSet connections;
+ int use_conn;
+ int max_conn;
+ std::size_t buf_siz;
+
+};
+
+}
+}
+
+#endif
#include "../world/WorldManipulator.hpp"
#include <cstdint>
+#include <memory>
#include <list>
#include <SDL_net.h>
class ChunkIndex;
class CLIContext;
+class CommandService;
class Model;
class Player;
class WorldSave;
const Model *player_model;
CLI cli;
+ std::unique_ptr<CommandService> cmd_srv;
};
#include "../geometry/distance.hpp"
#include "../io/WorldSave.hpp"
#include "../model/Model.hpp"
+#include "../shared/CommandService.hpp"
#include "../world/ChunkIndex.hpp"
#include "../world/Entity.hpp"
#include "../world/World.hpp"
NetworkCLIFeedback::NetworkCLIFeedback(Player &p, ClientConnection &c)
-: CLIContext(p)
+: CLIContext(&p)
, conn(c) {
}
, spawn_index(world.Chunks().MakeIndex(wc.spawn, 3))
, save(save)
, player_model(nullptr)
-, cli(world) {
+, cli(world)
+, cmd_srv() {
#pragma GCC diagnostic pop
if (!serv_set) {
throw NetError("SDLNet_AllocSocketSet");
serv_pack.data = new Uint8[sizeof(Packet)];
serv_pack.maxlen = sizeof(Packet);
+
+ if (conf.cmd_port) {
+ cmd_srv.reset(new CommandService(cli, conf.cmd_port));
+ }
}
Server::~Server() {
void Server::Wait(int dt) noexcept {
SDLNet_CheckSockets(serv_set, dt);
+ if (cmd_srv) {
+ cmd_srv->Wait(0);
+ }
}
bool Server::Ready() noexcept {
- return SDLNet_CheckSockets(serv_set, 0) > 0;
+ if (SDLNet_CheckSockets(serv_set, 0) > 0) {
+ return true;
+ }
+ return cmd_srv && cmd_srv->Ready();
}
void Server::Handle() {
// a boo boo happened
throw NetError("SDLNet_UDP_Recv");
}
+ if (cmd_srv) {
+ cmd_srv->Handle();
+ }
}
void Server::HandlePacket(const UDPpacket &udp_pack) {
++client;
}
}
+ if (cmd_srv) {
+ cmd_srv->Send();
+ }
}
void Server::SetPlayerModel(const Model &m) noexcept {
class CLIContext {
public:
- explicit CLIContext(Player &p)
+ explicit CLIContext(Player *p = nullptr)
: player(p) { }
+ /// check if this context associates a player
+ bool HasPlayer() { return player; }
/// get the player responsible for all this
- Player &GetPlayer() { return player; }
+ /// only valid if HasPlayer() returns true
+ Player &GetPlayer() { return *player; }
/// an error has happened and the player should be notified
virtual void Error(const std::string &) = 0;
virtual void Broadcast(const std::string &) = 0;
private:
- Player &player;
+ Player *player;
};
--- /dev/null
+#ifndef BLANK_SHARED_COMMANDBUFFER_HPP_
+#define BLANK_SHARED_COMMANDBUFFER_HPP_
+
+#include "CLIContext.hpp"
+#include "../net/tcp.hpp"
+
+#include <string>
+
+
+namespace blank {
+
+/// Turns a tcp stream into commands and writes their
+/// output back to the stream.
+/// Instances delete themselves when OnRemove(tcp::Socket &)
+/// is called, so make sure it was either allocated with new
+/// and isn't dereferenced after removal or OnRemove is never
+/// called.
+class CommandBuffer
+: public CLIContext
+, public tcp::IOHandler {
+
+public:
+ explicit CommandBuffer(CLI &);
+ ~CommandBuffer() override;
+
+ // CLIContext implementation
+ void Error(const std::string &) override;
+ void Message(const std::string &) override;
+ void Broadcast(const std::string &) override;
+
+ /// IOHandler implementation
+ void OnSend(tcp::Socket &) override;
+ void OnRecv(tcp::Socket &) override;
+ void OnRemove(tcp::Socket &) noexcept override;
+
+private:
+ CLI &cli;
+ std::string write_buffer;
+ std::string read_buffer;
+ std::size_t head;
+
+};
+
+}
+
+#endif
--- /dev/null
+#ifndef BLANK_SHARED_COMMANDSERVICE_HPP_
+#define BLANK_SHARED_COMMANDSERVICE_HPP_
+
+#include "../net/tcp.hpp"
+
+
+namespace blank {
+
+class CLI;
+
+class CommandService
+: public tcp::IOHandler {
+
+public:
+ CommandService(CLI &, unsigned short port);
+ ~CommandService();
+
+public:
+ /// wait on incoming data for at most timeout ms
+ void Wait(int timeout) noexcept;
+ /// true if at least one connected socket can read
+ bool Ready() noexcept;
+ /// handle all inbound traffic
+ void Handle();
+ /// send all outbound traffic
+ void Send();
+
+ void OnRecv(tcp::Socket &) override;
+
+private:
+ CLI &cli;
+ tcp::Pool pool;
+
+};
+
+}
+
+#endif
void TeleportCommand::Execute(CLI &, CLIContext &ctx, TokenStreamReader &args) {
+ if (!ctx.HasPlayer()) {
+ ctx.Error("teleport needs player to operate on");
+ return;
+ }
+
glm::vec3 pos(args.GetFloat(), args.GetFloat(), args.GetFloat());
EntityState state = ctx.GetPlayer().GetEntity().GetState();
state.pos = ExactLocation(pos).Sanitize();
--- /dev/null
+#include "CommandService.hpp"
+
+#include "CLI.hpp"
+#include "CommandBuffer.hpp"
+
+#include <algorithm>
+#include <iostream>
+
+using namespace std;
+
+
+namespace blank {
+
+CommandService::CommandService(CLI &cli, unsigned short port)
+: cli(cli)
+, pool() {
+ pool.AddConnection(tcp::Socket(port), this);
+ cout << "listening on TCP port " << port << endl;
+}
+
+CommandService::~CommandService() {
+
+}
+
+
+void CommandService::Wait(int timeout) noexcept {
+ pool.Check(timeout);
+}
+
+bool CommandService::Ready() noexcept {
+ return pool.Check(0);
+}
+
+void CommandService::Handle() {
+ pool.Receive();
+}
+
+void CommandService::Send() {
+ pool.Send();
+}
+
+void CommandService::OnRecv(tcp::Socket &serv) {
+ for (tcp::Socket client = serv.Accept(); client; client = serv.Accept()) {
+ pool.AddConnection(move(client), new CommandBuffer(cli));
+ }
+}
+
+
+CommandBuffer::CommandBuffer(CLI &cli)
+: cli(cli)
+, write_buffer()
+, read_buffer(1440, '\0')
+, head(0) {
+
+}
+
+CommandBuffer::~CommandBuffer() {
+
+}
+
+
+void CommandBuffer::Error(const string &msg) {
+ write_buffer += " ! ";
+ write_buffer += msg;
+ write_buffer += '\n';
+}
+
+void CommandBuffer::Message(const string &msg) {
+ write_buffer += " > ";
+ write_buffer += msg;
+ write_buffer += '\n';
+}
+
+void CommandBuffer::Broadcast(const string &msg) {
+ // TODO: broadcast should be an operation of the
+ // environment, not the singular context
+ write_buffer += " @ ";
+ write_buffer += msg;
+ write_buffer += '\n';
+}
+
+
+void CommandBuffer::OnSend(tcp::Socket &sock) {
+ if (write_buffer.empty()) {
+ return;
+ }
+ size_t len = sock.Send(write_buffer.data(), write_buffer.size());
+ write_buffer.erase(0, len);
+}
+
+void CommandBuffer::OnRecv(tcp::Socket &sock) {
+ size_t len = sock.Recv(&read_buffer[0], read_buffer.size() - head);
+ head += len;
+ // scan for lines
+ string::iterator begin = read_buffer.begin();
+ string::iterator end = begin + head;
+ string::iterator handled = begin;
+ for (
+ string::iterator i = find(handled, end, '\n');
+ i != end;
+ i = find(handled, end, '\n')
+ ) {
+ string line(handled, i);
+ cli.Execute(*this, line);
+ handled = ++i;
+ }
+ if (handled == end) {
+ // guzzled it all
+ head = 0;
+ } else if (handled != begin) {
+ // half a line remaining, move it to the start of the buffer
+ move(handled, end, begin);
+ head = distance(handled, end);
+ }
+}
+
+void CommandBuffer::OnRemove(tcp::Socket &) noexcept {
+ delete this;
+}
+
+}
namespace standalone {
DirectCLIFeedback::DirectCLIFeedback(Player &p, HUD &h)
-: CLIContext(p)
+: CLIContext(&p)
, hud(h) {
}