commit 3b1136e7e0304e1161e9bd143301e2583d35f2ba Author: niansa Date: Tue May 10 14:13:35 2022 +0200 Some progress on the server code diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4a0b530 --- /dev/null +++ b/.gitignore @@ -0,0 +1,74 @@ +# This file is used to ignore files which are generated +# ---------------------------------------------------------------------------- + +*~ +*.autosave +*.a +*.core +*.moc +*.o +*.obj +*.orig +*.rej +*.so +*.so.* +*_pch.h.cpp +*_resource.rc +*.qm +.#* +*.*# +core +!core/ +tags +.DS_Store +.directory +*.debug +Makefile* +*.prl +*.app +moc_*.cpp +ui_*.h +qrc_*.cpp +Thumbs.db +*.res +*.rc +/.qmake.cache +/.qmake.stash + +# qtcreator generated files +*.pro.user* +CMakeLists.txt.user* + +# xemacs temporary files +*.flc + +# Vim temporary files +.*.swp + +# Visual Studio generated files +*.ib_pdb_index +*.idb +*.ilk +*.pdb +*.sln +*.suo +*.vcproj +*vcproj.*.*.user +*.ncb +*.sdf +*.opensdf +*.vcxproj +*vcxproj.* + +# MinGW generated files +*.Debug +*.Release + +# Python byte code +*.pyc + +# Binaries +# -------- +*.dll +*.exe + diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..9d25b2f --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,20 @@ +cmake_minimum_required(VERSION 3.5) + +project(mislaborate LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +find_package(Protobuf REQUIRED) +include_directories(${Protobuf_INCLUDE_DIRS}) +include_directories(${CMAKE_CURRENT_BINARY_DIR}) +protobuf_generate_cpp(PROTO_SRCS PROTO_HDRS generic.proto) + +add_executable(mislaborate-server + server.cpp + server/Connection.hpp server/World.hpp + server/Connection.cpp server/World.cpp + ${PROTO_SRCS} ${PROTO_HDRS} +) +target_link_libraries(mislaborate-server PRIVATE ${Protobuf_LIBRARIES}) +target_compile_definitions(mislaborate-server PUBLIC BOOST_ASIO_HAS_CO_AWAIT) diff --git a/generic.proto b/generic.proto new file mode 100644 index 0000000..10785d1 --- /dev/null +++ b/generic.proto @@ -0,0 +1,74 @@ +syntax = "proto3"; +package Generic; + + + +message Error { + string description = 1; + enum Code { + Unknown = 0; + BadString = 1; + TooLarge = 2; + BadUsername = 3; + UsernameAlreadyTaken = 4; + InvalidOpcode = 5; + AuthenticationRequired = 6; + IllegalOperation = 7; + OutOfRange = 8; + } + Code code = 2; +} + +message SimpleAuth { + string username = 1; +} + +message Operation { + enum Code { + Disconnect = 0; + Error = 1; + OK = 2; + SimpleAuth = 3; + _MinAuth = 4; + SettingsSync = 5; + PlayfieldSync = 6; + RobotUpdate = 7; + } + Code code = 1; +} + + +message Vector2 { + float x = 1; + float y = 2; +} + + +message Settings { + optional uint32 robotsPerClient = 1; + optional float maxDistancePerTurn = 2; + optional uint32 maxRobotHealth = 3; + optional uint32 maxRobotDamage = 4; + optional uint32 protectCooldown = 5; + optional uint32 healCooldown = 6; + optional uint32 healPerTurn = 7; +} + +message Robot { + uint32 id = 1; + uint32 client = 2; + Vector2 position = 3; + enum State { + Idle = 0; + Attack = 1; + Protect = 2; + Heal = 3; + Dead = 4; + } + State state = 4; + uint32 health = 5; +} + +message PlayfieldSync { + repeated Robot robots = 1; +} diff --git a/server.cpp b/server.cpp new file mode 100644 index 0000000..03a9923 --- /dev/null +++ b/server.cpp @@ -0,0 +1,21 @@ +#include "generic.pb.h" +#include "server/Connection.hpp" +#include "server/World.hpp" + +#include +#include + + + +int main() { + try { + boost::asio::io_context ioc; + Connection::Server server(ioc); + int port = 78432; + boost::asio::co_spawn(ioc, server.listen(port), boost::asio::detached); + std::cout << "Server has initialized and will now start listening on port " << port << '.' << std::endl; + ioc.run(); + } catch (std::exception& e) { + std::cerr << "Failed to launch: " << e.what() << std::endl; + } +} diff --git a/server/Connection.cpp b/server/Connection.cpp new file mode 100644 index 0000000..5ca23e4 --- /dev/null +++ b/server/Connection.cpp @@ -0,0 +1,127 @@ +#include "Connection.hpp" + +#include +#include +#include + + + +namespace Connection { +asio::awaitable Client::receivePacket() { + Packet packet; + // Receive operation + packet.op = co_await receiveProtobuf(); + // Receive data + switch (packet.op.code()) { + case Generic::Operation::Error: { + packet.data = std::make_unique(co_await receiveProtobuf()); + } break; + case Generic::Operation::SimpleAuth: { + packet.data = std::make_unique(co_await receiveProtobuf(30)); + } break; + case Generic::Operation::Disconnect: + case Generic::Operation::OK: + break; + default: throw FatalError("Invalid opcode: "+std::to_string(packet.op.code()), Generic::Error::InvalidOpcode); + } +} + +asio::awaitable Client::sendProtobuf(const google::protobuf::Message& buffer) { + // Send size + uint16_t size = buffer.ByteSizeLong(); + co_await asio::async_write(socket, asio::buffer(&size, sizeof(size)), asio::use_awaitable); + // Send buffer + std::vector rawBuffer(size); + buffer.SerializeToArray(rawBuffer.data(), rawBuffer.size()); + co_await asio::async_write(socket, asio::buffer(rawBuffer.data(), rawBuffer.size()), asio::use_awaitable); +} + +asio::awaitable Client::sendPacket(const Packet& packet) { + // Send operation + co_await sendProtobuf(packet.op); + // Send data + if (packet.data) { + co_await sendProtobuf(*packet.data); + } +} + +asio::awaitable Client::handlePacket(const Packet& packet) { + // Check if authentication is required and completed + if (packet.op.code() >= Generic::Operation::_MinAuth && !isAuthed()) { + throw FatalError("Authentication required", Generic::Error::AuthenticationRequired); + } + // Handle the packet + switch (packet.op.code()) { + case Generic::Operation::Disconnect: { + good = 0; + } break; + case Generic::Operation::SimpleAuth: { + const Generic::SimpleAuth& authData = *static_cast(packet.data.get()); + // Check that username is not already taken + if (std::find_if(clients.begin(), clients.end(), [&, this] (const auto& o) {return o->getUsername() == authData.username();}) != clients.end()) { + throw Error("Username has already been taken", Generic::Error::UsernameAlreadyTaken); + } + // Check that username is "good" (no unprintable characters and stuff like that) + //TODO... + // Set username + username = std::move(authData.username()); + // Send OK + co_await sendOK(); + } break; + default: {} + } + co_return; +} + +asio::awaitable Client::connect() { + // Send initial "OK" + try { + co_await sendOK(); + } catch (...) { + good = false; + } + // Run while connection is good + while (good) { + std::optional error; + try { + co_await handlePacket(co_await receivePacket()); + } catch (FatalError& e) { + error = std::move(e); + good = false; + } catch (Error& e) { + error = std::move(e); + } catch (std::exception& e) { + error = Error("Internal server error: "+std::string(e.what()), Generic::Error::Unknown); + good = false; + } + if (error.has_value()) { + co_await sendPacket(error.value().packet); + } + } + // Try to send disconnect + try { + Packet packet; + packet.op.set_code(Generic::Operation::Disconnect); + co_await sendPacket(packet); + } catch (...) {} + // Close socket + boost::system::error_code ec; + socket.close(ec); +} + +asio::awaitable Server::listen(int port) { + try { + // Create acceptor + asio::ip::tcp::acceptor acceptor(ioc, asio::ip::tcp::endpoint(asio::ip::tcp::v4(), port)); + // Handle all incoming connections + while (true) { + auto client = std::make_shared(ioc, clients); + co_await acceptor.async_accept(client->getSocket(), asio::use_awaitable); + asio::co_spawn(ioc, client->connect(), asio::detached); + clients.push_back(std::move(client)); + } + } catch (std::exception& e) { + std::cerr << "Failed to stay alive: " << e.what() << std::endl; + } +} +} diff --git a/server/Connection.hpp b/server/Connection.hpp new file mode 100644 index 0000000..2ebe024 --- /dev/null +++ b/server/Connection.hpp @@ -0,0 +1,111 @@ +#ifndef _CONNECTION_HPP +#define _CONNECTION_HPP +#include "generic.pb.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + + +namespace Connection { +using namespace boost; + + +struct Packet { + Generic::Operation op; + std::unique_ptr data = nullptr; +}; + +struct Error : public std::runtime_error { + Packet packet; + + Error(const std::string& description, Generic::Error::Code code) : std::runtime_error(description) { + packet.op.set_code(Generic::Operation::Error); + auto error = std::make_unique(); + error->set_description(description); + error->set_code(code); + packet.data = std::move(error); + } +}; + +struct FatalError : public Error { + using Error::Error; +}; + + +class Client : public std::enable_shared_from_this { + std::vector>& clients; + asio::ip::tcp::socket socket; + std::string username; + bool good = true; + +public: + Client(asio::io_context& ioc, std::vector>& clients) : socket(ioc), clients(clients) {} + + asio::awaitable receivePacket(); + asio::awaitable sendProtobuf(const google::protobuf::Message& buffer); + asio::awaitable sendPacket(const Packet& packet); + asio::awaitable handlePacket(const Packet& packet); + asio::awaitable connect(); + + template + asio::awaitable receiveProtobuf(uint16_t maxSize = 1024) { + // Receive size + uint16_t size; + co_await asio::async_read(socket, asio::buffer(&size, sizeof(size)), asio::use_awaitable); + // Check size + if (size > maxSize) { + throw FatalError("Buffer too large ("+std::to_string(size)+" > "+std::to_string(maxSize)+')', Generic::Error::TooLarge); + } + // Receive protobuf + Buffer protobuf; + if (size) { + std::vector rawProtobuf(size); + co_await asio::async_read(socket, asio::buffer(rawProtobuf.data(), rawProtobuf.size()), asio::use_awaitable); + // Parse protobuf + protobuf.ParseFromArray(rawProtobuf.data(), static_cast(size)); + } + // Return it + co_return protobuf; + } + + asio::awaitable sendOK() { + Packet packet; + packet.op.set_code(Generic::Operation::OK); + co_await sendPacket(packet); + } + + auto& getSocket() { + return socket; + } + const auto& getUsername() const { + return username; + } + bool isGood() const { + return good; + } + bool isAuthed() const { + return good && !username.empty(); + } +}; + + +class Server { + asio::io_context& ioc; + std::vector> clients; + +public: + Server(asio::io_context& ioc) : ioc(ioc) {} + + asio::awaitable listen(int port); +}; +} +#endif diff --git a/server/World.cpp b/server/World.cpp new file mode 100644 index 0000000..55376a2 --- /dev/null +++ b/server/World.cpp @@ -0,0 +1,130 @@ +#include "World.hpp" + +#include +#include +#include + + + +namespace World { +void fillSettings(Generic::Settings& settings) { + if (!settings.has_maxdistanceperturn()) { + settings.set_maxdistanceperturn(0.02f); + } + if (!settings.has_robotsperclient()) { + settings.set_robotsperclient(50); + } + if (!settings.has_maxrobothealth()) { + settings.set_maxrobothealth(20); + } + if (!settings.has_maxrobotdamage()) { + settings.set_maxrobotdamage(4); + } + if (!settings.has_protectcooldown()) { + settings.set_protectcooldown(50); + } + if (!settings.has_healcooldown()) { + settings.set_healcooldown(20); + } + if (!settings.has_healperturn()) { + settings.set_healperturn(2); + } +} + +boost::asio::awaitable Playfield::reset() { + robots.clear(); + const std::vector> robotsPlacementMap = { + {{0.5f, 0.5f}}, + {{0.0f, 0.0f}, {1.0f, 1.0f}}, + {{0.0f, 0.0f}, {0.5f, 0.5f}, {1.0f, 1.0f}}, + {{0.0f, 0.0f}, {1.0f, 0.0f}, {0.0f, 1.0f}, {1.0f, 1.0f}} + }; + const auto& robotsPlacement = robotsPlacementMap.at(clients.size()); + for (unsigned client = 0; client != clients.size(); client++) { + const auto genericTeamRobotsPlacement = robotsPlacement[client].toGeneric(); + for (unsigned robot = 0; robot != settings.robotsperclient(); robot++) { + // Create robot + Generic::Robot genericRobot; + genericRobot.set_id(robot); + genericRobot.set_client(client); + genericRobot.set_state(Generic::Robot::Idle); + genericRobot.set_health(20); + *genericRobot.mutable_position() = genericTeamRobotsPlacement; + // Add to list + robots.push_back({genericRobot}); + } + } + // Broadcast new playfield + auto playfieldSync = getPlayfieldSync(); + for (auto& client : clients) { + Connection::Packet packet; + packet.op.set_code(Generic::Operation::RobotUpdate); + packet.data = std::make_unique(playfieldSync); + co_await client->sendPacket(packet); + } +} + +boost::asio::awaitable Playfield::updateRobot(const Connection::Client *sender, Generic::Robot& newRobot) { + turn++; + // Get local robot + auto& localRobot = getLocalRobot(sender, newRobot.id()); + // Get distance travelled + auto distanceTravelled = getDistanceBetween(*localRobot, newRobot); + if (distanceTravelled != 0.0f) { + // Check that robot is able to travel + if (newRobot.state() != Generic::Robot::Idle) { + throw Connection::Error("Can't travel right now", Generic::Error::IllegalOperation); + } + // Check that robot didn't travel too far + if (distanceTravelled > settings.maxdistanceperturn()) { + throw Connection::Error("Can't travel that far ("+std::to_string(distanceTravelled)+" > "+std::to_string(settings.maxdistanceperturn())+')', Generic::Error::IllegalOperation); + } + } + // Check for illegal changes + if (localRobot->health() != newRobot.health()) { + throw Connection::Error("Can't change value", Generic::Error::IllegalOperation); + } + // Apply heal + if (newRobot.state() == Generic::Robot::Heal) { + newRobot.set_health(localRobot->health() + settings.healperturn()); + if (newRobot.health() > settings.maxrobothealth()) { + newRobot.set_health(settings.maxrobothealth()); + newRobot.set_state(Generic::Robot::Idle); + } + } + // Check for state change + if (localRobot->state() != newRobot.state()) { + // Enforce cooldowns + if (newRobot.state() == Generic::Robot::Protect && turn - localRobot.stateTimer < settings.protectcooldown() + || newRobot.state() == Generic::Robot::Heal && turn - localRobot.stateTimer < settings.healcooldown()) { + throw Connection::Error("Can't change state yet (cooldown still in progress)", Generic::Error::IllegalOperation); + } + // Reset timer + localRobot.stateTimer = turn; + } + // Apply changes to local robot + *localRobot = newRobot; + // Broadcast new robot + for (auto& client : clients) { + Connection::Packet packet; + packet.op.set_code(Generic::Operation::RobotUpdate); + packet.data = std::make_unique(*localRobot); + co_await client->sendPacket(packet); + } +} + +LocalRobot& Playfield::getLocalRobot(const Connection::Client *sender, unsigned robot) { + // Check that new robot is in range + if (robot >= robots.size()) { + throw Connection::Error("Robot is out of range ("+std::to_string(robot)+" >= "+std::to_string(robots.size())+')', Generic::Error::OutOfRange); + } + // Get local robot + auto& localRobot = robots[robot]; + // Check that robot belongs to sender + if (clients[localRobot->client()].get() == sender) { + throw Connection::Error("Robot does not belong to you", Generic::Error::IllegalOperation); + } + // Return local robot + return localRobot; +} +} diff --git a/server/World.hpp b/server/World.hpp new file mode 100644 index 0000000..d10a773 --- /dev/null +++ b/server/World.hpp @@ -0,0 +1,97 @@ +#ifndef _WORLD_HPP +#define _WORLD_HPP +#include "generic.pb.h" +#include "Connection.hpp" + +#include +#include +#include + + + +namespace World { +void fillSettings(Generic::Settings& settings); + + +struct Vector2 { + float x, y; + + inline static Vector2 makeFromGeneric(const Generic::Vector2& generic) { + return {generic.x(), generic.y()}; + } + void fromGeneric(const Generic::Vector2& generic) { + x = generic.x(); + y = generic.y(); + } + auto toGeneric() const { + Generic::Vector2 fres; + fres.set_x(x); + fres.set_y(y); + return fres; + } + + float getDistanceTo(const Vector2& o) const { + return fabs(x - o.x) + fabs(y - o.y); + } +}; + + +struct LocalRobot { + Generic::Robot generic; + uint64_t stateTimer = 0; + + auto& operator *() { + return generic; + } + auto operator ->() { + return &generic; + } + const auto& operator *() const { + return generic; + } + const auto operator ->() const { + return &generic; + } +}; + + +class Playfield { + std::vector robots; + Generic::Settings settings; + uint64_t turn = 0; + +public: + std::vector> clients; + + Playfield(const std::vector>& clients, const Generic::Settings& settings) + : clients(clients), settings(settings) { + fillSettings(this->settings); + } + + boost::asio::awaitable reset(); + boost::asio::awaitable updateRobot(const Connection::Client *sender, Generic::Robot& newRobot); + LocalRobot& getLocalRobot(const Connection::Client *sender, unsigned robot); + + float getDistanceBetween(const Generic::Robot& a, const Generic::Robot& b) { + return Vector2::makeFromGeneric(a.position()).getDistanceTo(Vector2::makeFromGeneric(b.position())); + } + + auto getPlayfieldSync() const { + Generic::PlayfieldSync fres; + for (const auto& robot : robots) { + *fres.add_robots() = robot.generic; + } + return fres; + } + bool isMasterClient(const Connection::Client *client) const { + return client == clients.front().get(); + } + const auto& getSettings() const { + return settings; + } + auto getTurn() const { + return turn; + } +}; +} +#endif