1
0
Fork 0
mirror of synced 2025-03-06 20:53:27 +01:00

Some progress on the server code

This commit is contained in:
niansa/tuxifan 2022-05-10 14:13:35 +02:00
commit 3b1136e7e0
8 changed files with 654 additions and 0 deletions

74
.gitignore vendored Normal file
View file

@ -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

20
CMakeLists.txt Normal file
View file

@ -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)

74
generic.proto Normal file
View file

@ -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;
}

21
server.cpp Normal file
View file

@ -0,0 +1,21 @@
#include "generic.pb.h"
#include "server/Connection.hpp"
#include "server/World.hpp"
#include <iostream>
#include <boost/asio.hpp>
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;
}
}

127
server/Connection.cpp Normal file
View file

@ -0,0 +1,127 @@
#include "Connection.hpp"
#include <optional>
#include <algorithm>
#include <boost/asio.hpp>
namespace Connection {
asio::awaitable<Packet> Client::receivePacket() {
Packet packet;
// Receive operation
packet.op = co_await receiveProtobuf<Generic::Operation>();
// Receive data
switch (packet.op.code()) {
case Generic::Operation::Error: {
packet.data = std::make_unique<Generic::Error>(co_await receiveProtobuf<Generic::Error>());
} break;
case Generic::Operation::SimpleAuth: {
packet.data = std::make_unique<Generic::SimpleAuth>(co_await receiveProtobuf<Generic::SimpleAuth>(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<void> 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<char> 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<void> Client::sendPacket(const Packet& packet) {
// Send operation
co_await sendProtobuf(packet.op);
// Send data
if (packet.data) {
co_await sendProtobuf(*packet.data);
}
}
asio::awaitable<void> 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<Generic::SimpleAuth*>(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<void> Client::connect() {
// Send initial "OK"
try {
co_await sendOK();
} catch (...) {
good = false;
}
// Run while connection is good
while (good) {
std::optional<Error> 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<void> 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<Client>(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;
}
}
}

111
server/Connection.hpp Normal file
View file

@ -0,0 +1,111 @@
#ifndef _CONNECTION_HPP
#define _CONNECTION_HPP
#include "generic.pb.h"
#include <string>
#include <initializer_list>
#include <vector>
#include <exception>
#include <stdexcept>
#include <memory>
#include <boost/asio/awaitable.hpp>
#include <boost/asio/use_awaitable.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/read.hpp>
namespace Connection {
using namespace boost;
struct Packet {
Generic::Operation op;
std::unique_ptr<google::protobuf::Message> 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<Generic::Error>();
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<Client> {
std::vector<std::shared_ptr<Client>>& clients;
asio::ip::tcp::socket socket;
std::string username;
bool good = true;
public:
Client(asio::io_context& ioc, std::vector<std::shared_ptr<Client>>& clients) : socket(ioc), clients(clients) {}
asio::awaitable<Packet> receivePacket();
asio::awaitable<void> sendProtobuf(const google::protobuf::Message& buffer);
asio::awaitable<void> sendPacket(const Packet& packet);
asio::awaitable<void> handlePacket(const Packet& packet);
asio::awaitable<void> connect();
template<class Buffer>
asio::awaitable<Buffer> 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<char> 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<int>(size));
}
// Return it
co_return protobuf;
}
asio::awaitable<void> 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<std::shared_ptr<Client>> clients;
public:
Server(asio::io_context& ioc) : ioc(ioc) {}
asio::awaitable<void> listen(int port);
};
}
#endif

130
server/World.cpp Normal file
View file

@ -0,0 +1,130 @@
#include "World.hpp"
#include <string>
#include <chrono>
#include <cmath>
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<void> Playfield::reset() {
robots.clear();
const std::vector<std::vector<Vector2>> 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<Generic::PlayfieldSync>(playfieldSync);
co_await client->sendPacket(packet);
}
}
boost::asio::awaitable<void> 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<Generic::Robot>(*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;
}
}

97
server/World.hpp Normal file
View file

@ -0,0 +1,97 @@
#ifndef _WORLD_HPP
#define _WORLD_HPP
#include "generic.pb.h"
#include "Connection.hpp"
#include <vector>
#include <memory>
#include <boost/asio/awaitable.hpp>
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<LocalRobot> robots;
Generic::Settings settings;
uint64_t turn = 0;
public:
std::vector<std::shared_ptr<Connection::Client>> clients;
Playfield(const std::vector<std::shared_ptr<Connection::Client>>& clients, const Generic::Settings& settings)
: clients(clients), settings(settings) {
fillSettings(this->settings);
}
boost::asio::awaitable<void> reset();
boost::asio::awaitable<void> 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