Implemented the real server
This commit is contained in:
1 parent
931236bec0
commit
075750442f
22 files changed
+1130
-44
No files matched your search
@@ -0,0 +1,60 @@
|
||||
#pragma once
|
||||
|
||||
#include "IGameServerHost.h"
|
||||
#include "../Location.h"
|
||||
#include <utility>
|
||||
|
||||
namespace FRG {
|
||||
|
||||
class GameServerHostRouter final : public IGameServerHost {
|
||||
public:
|
||||
explicit GameServerHostRouter(IGameServerHost& initial)
|
||||
: active_(&initial)
|
||||
{
|
||||
}
|
||||
|
||||
void use(IGameServerHost& host) { active_ = &host; }
|
||||
void initializeCampaign(
|
||||
const std::string& campaignId, const std::string& questsPath,
|
||||
const std::string& zipFile) override {
|
||||
active_->initializeCampaign(campaignId, questsPath, zipFile);
|
||||
}
|
||||
void clearCampaign() override { active_->clearCampaign(); }
|
||||
void resetCampaign(
|
||||
const std::string& questsPath, const std::string& zipFile) override {
|
||||
active_->resetCampaign(questsPath, zipFile);
|
||||
}
|
||||
bool prepareLocation(const LocationConfig& config) override {
|
||||
return active_->prepareLocation(config);
|
||||
}
|
||||
void setupRuntimeLocation(
|
||||
const std::string& id, const std::shared_ptr<Location>& location,
|
||||
LocationSetup params, AudioPlayerAsync* audioPlayer) override {
|
||||
active_->setupRuntimeLocation(
|
||||
id, location, std::move(params), audioPlayer);
|
||||
}
|
||||
Location* runtimeLocation(const std::string& id) const override {
|
||||
return active_->runtimeLocation(id);
|
||||
}
|
||||
bool hasRuntimeLocation(const std::string& id) const override {
|
||||
return active_->hasRuntimeLocation(id);
|
||||
}
|
||||
std::vector<std::string> loadedRuntimeLocationIds() const override {
|
||||
return active_->loadedRuntimeLocationIds();
|
||||
}
|
||||
void clearRuntimeLocations(bool retainState) override {
|
||||
active_->clearRuntimeLocations(retainState);
|
||||
}
|
||||
void bindRuntimeLocation(
|
||||
const std::string& id, std::shared_ptr<Location> location) override {
|
||||
active_->bindRuntimeLocation(id, std::move(location));
|
||||
}
|
||||
void update(std::int64_t deltaMs) override { active_->update(deltaMs); }
|
||||
void save(nlohmann::json& out) override { active_->save(out); }
|
||||
void load(const nlohmann::json& in) override { active_->load(in); }
|
||||
|
||||
private:
|
||||
IGameServerHost* active_;
|
||||
};
|
||||
|
||||
} // namespace FRG
|
||||
@@ -0,0 +1,36 @@
|
||||
#pragma once
|
||||
|
||||
#include "IGameServer.h"
|
||||
#include <stdexcept>
|
||||
|
||||
namespace FRG {
|
||||
|
||||
// Keeps references held by UI and runtime objects stable while campaigns switch
|
||||
// between the local and remote implementations.
|
||||
class GameServerRouter final : public IGameServer {
|
||||
public:
|
||||
explicit GameServerRouter(IGameServer& initial)
|
||||
: active_(&initial)
|
||||
{
|
||||
}
|
||||
|
||||
void use(IGameServer& server) { active_ = &server; }
|
||||
std::shared_ptr<const GameSnapshot> snapshot() const override {
|
||||
return active_->snapshot();
|
||||
}
|
||||
const std::string& localPlayerId() const override {
|
||||
return active_->localPlayerId();
|
||||
}
|
||||
void submit(const GameCommand& command) override { active_->submit(command); }
|
||||
std::vector<GameServerEvent> drainEvents() override {
|
||||
return active_->drainEvents();
|
||||
}
|
||||
void update(std::int64_t deltaMs) override { active_->update(deltaMs); }
|
||||
void save(nlohmann::json& out) const override { active_->save(out); }
|
||||
void load(const nlohmann::json& in) override { active_->load(in); }
|
||||
|
||||
private:
|
||||
IGameServer* active_;
|
||||
};
|
||||
|
||||
} // namespace FRG
|
||||
@@ -6,6 +6,7 @@
|
||||
namespace FRG {
|
||||
|
||||
class LocalGameServerHost;
|
||||
class RemoteGameServerHost;
|
||||
struct LocationConfig;
|
||||
|
||||
// In-process implementation used by single-player. GameState exists exactly
|
||||
@@ -30,6 +31,7 @@ public:
|
||||
|
||||
private:
|
||||
friend class LocalGameServerHost;
|
||||
friend class RemoteGameServerHost;
|
||||
|
||||
// Host-only lifecycle operations. They never expose authoritative state and
|
||||
// are intentionally absent from IGameServer.
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
#include "NetworkProtocol.h"
|
||||
#include <exception>
|
||||
|
||||
namespace FRG {
|
||||
|
||||
std::string makeStateMessage(const nlohmann::json& state)
|
||||
{
|
||||
return nlohmann::json{
|
||||
{"protocolVersion", kGameProtocolVersion},
|
||||
{"type", "state"},
|
||||
{"payload", state}
|
||||
}.dump();
|
||||
}
|
||||
|
||||
bool parseStateMessage(
|
||||
const std::string& message, nlohmann::json& state,
|
||||
std::string& error)
|
||||
{
|
||||
try {
|
||||
const nlohmann::json envelope = nlohmann::json::parse(message);
|
||||
if (!envelope.is_object()) {
|
||||
error = "message must be a JSON object";
|
||||
return false;
|
||||
}
|
||||
if (envelope.value("protocolVersion", -1) != kGameProtocolVersion) {
|
||||
error = "unsupported protocol version";
|
||||
return false;
|
||||
}
|
||||
if (envelope.value("type", std::string()) != "state") {
|
||||
error = "unsupported message type";
|
||||
return false;
|
||||
}
|
||||
if (!envelope.contains("payload") || !envelope["payload"].is_object()) {
|
||||
error = "state payload must be an object";
|
||||
return false;
|
||||
}
|
||||
state = envelope["payload"];
|
||||
error.clear();
|
||||
return true;
|
||||
} catch (const std::exception& exception) {
|
||||
error = exception.what();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace FRG
|
||||
@@ -0,0 +1,15 @@
|
||||
#pragma once
|
||||
|
||||
#include "../external/nlohmann/json.hpp"
|
||||
#include <string>
|
||||
|
||||
namespace FRG {
|
||||
|
||||
inline constexpr int kGameProtocolVersion = 1;
|
||||
|
||||
std::string makeStateMessage(const nlohmann::json& state);
|
||||
bool parseStateMessage(
|
||||
const std::string& message, nlohmann::json& state,
|
||||
std::string& error);
|
||||
|
||||
} // namespace FRG
|
||||
@@ -0,0 +1,109 @@
|
||||
#include "RemoteGameServerHost.h"
|
||||
#include "WebsocketGameClient.h"
|
||||
#include "../Campaign.h"
|
||||
#include "../Location.h"
|
||||
#include <stdexcept>
|
||||
|
||||
namespace FRG {
|
||||
|
||||
RemoteGameServerHost::RemoteGameServerHost(WebsocketGameClient& client)
|
||||
: client_(client)
|
||||
{
|
||||
}
|
||||
|
||||
void RemoteGameServerHost::initializeCampaign(
|
||||
const std::string& campaignId, const std::string&,
|
||||
const std::string& zipFile)
|
||||
{
|
||||
const CampaignConfig* campaign =
|
||||
CampaignRegistry::instance().find(campaignId);
|
||||
if (!campaign)
|
||||
throw std::runtime_error("Unknown remote campaign '" + campaignId + "'");
|
||||
client_.replica_.startCampaign(*campaign, zipFile, true, false);
|
||||
}
|
||||
|
||||
void RemoteGameServerHost::clearCampaign()
|
||||
{
|
||||
runtimeLocations_.clear();
|
||||
client_.replica_.clearCampaign();
|
||||
}
|
||||
|
||||
void RemoteGameServerHost::resetCampaign(
|
||||
const std::string&, const std::string&)
|
||||
{
|
||||
// The authoritative remote campaign cannot be reset by this client.
|
||||
}
|
||||
|
||||
bool RemoteGameServerHost::prepareLocation(const LocationConfig& config)
|
||||
{
|
||||
return client_.replica_.prepareLocation(config);
|
||||
}
|
||||
|
||||
void RemoteGameServerHost::setupRuntimeLocation(
|
||||
const std::string& id, const std::shared_ptr<Location>& location,
|
||||
LocationSetup params, AudioPlayerAsync* audioPlayer)
|
||||
{
|
||||
auto stored = client_.replica_.state_.locations.find(id);
|
||||
if (stored == client_.replica_.state_.locations.end())
|
||||
throw std::runtime_error("Remote location state was not prepared");
|
||||
params.definition = stored->second.definition;
|
||||
params.gameState = &stored->second;
|
||||
location->setup(params, audioPlayer);
|
||||
}
|
||||
|
||||
Location* RemoteGameServerHost::runtimeLocation(const std::string& id) const
|
||||
{
|
||||
auto found = runtimeLocations_.find(id);
|
||||
return found == runtimeLocations_.end() ? nullptr : found->second.get();
|
||||
}
|
||||
|
||||
bool RemoteGameServerHost::hasRuntimeLocation(const std::string& id) const
|
||||
{
|
||||
return runtimeLocations_.count(id) > 0;
|
||||
}
|
||||
|
||||
std::vector<std::string> RemoteGameServerHost::loadedRuntimeLocationIds() const
|
||||
{
|
||||
std::vector<std::string> result;
|
||||
result.reserve(runtimeLocations_.size());
|
||||
for (const auto& [id, location] : runtimeLocations_) result.push_back(id);
|
||||
return result;
|
||||
}
|
||||
|
||||
void RemoteGameServerHost::clearRuntimeLocations(bool)
|
||||
{
|
||||
runtimeLocations_.clear();
|
||||
}
|
||||
|
||||
void RemoteGameServerHost::bindRuntimeLocation(
|
||||
const std::string& id, std::shared_ptr<Location> location)
|
||||
{
|
||||
runtimeLocations_[id] = std::move(location);
|
||||
auto stored = client_.replica_.state_.locations.find(id);
|
||||
if (stored != client_.replica_.state_.locations.end() &&
|
||||
stored->second.hasPersistentState) {
|
||||
runtimeLocations_[id]->applyAuxiliaryState(stored->second);
|
||||
}
|
||||
}
|
||||
|
||||
void RemoteGameServerHost::update(std::int64_t deltaMs)
|
||||
{
|
||||
client_.update(deltaMs);
|
||||
const auto state = client_.snapshot();
|
||||
if (!state || state->currentLocationId.empty()) return;
|
||||
auto runtime = runtimeLocations_.find(state->currentLocationId);
|
||||
if (runtime == runtimeLocations_.end() || !runtime->second) return;
|
||||
runtime->second->update(deltaMs, LocationSimulationEvents{});
|
||||
}
|
||||
|
||||
void RemoteGameServerHost::save(nlohmann::json&)
|
||||
{
|
||||
// Persistence remains server-side.
|
||||
}
|
||||
|
||||
void RemoteGameServerHost::load(const nlohmann::json&)
|
||||
{
|
||||
// Persistence remains server-side.
|
||||
}
|
||||
|
||||
} // namespace FRG
|
||||
@@ -0,0 +1,39 @@
|
||||
#pragma once
|
||||
|
||||
#include "IGameServerHost.h"
|
||||
#include <unordered_map>
|
||||
|
||||
namespace FRG {
|
||||
|
||||
class WebsocketGameClient;
|
||||
|
||||
class RemoteGameServerHost final : public IGameServerHost {
|
||||
public:
|
||||
explicit RemoteGameServerHost(WebsocketGameClient& client);
|
||||
|
||||
void initializeCampaign(
|
||||
const std::string& campaignId, const std::string& questsPath,
|
||||
const std::string& zipFile) override;
|
||||
void clearCampaign() override;
|
||||
void resetCampaign(
|
||||
const std::string& questsPath, const std::string& zipFile) override;
|
||||
bool prepareLocation(const LocationConfig& config) override;
|
||||
void setupRuntimeLocation(
|
||||
const std::string& id, const std::shared_ptr<Location>& location,
|
||||
LocationSetup params, AudioPlayerAsync* audioPlayer) override;
|
||||
Location* runtimeLocation(const std::string& id) const override;
|
||||
bool hasRuntimeLocation(const std::string& id) const override;
|
||||
std::vector<std::string> loadedRuntimeLocationIds() const override;
|
||||
void clearRuntimeLocations(bool retainState) override;
|
||||
void bindRuntimeLocation(
|
||||
const std::string& id, std::shared_ptr<Location> location) override;
|
||||
void update(std::int64_t deltaMs) override;
|
||||
void save(nlohmann::json& out) override;
|
||||
void load(const nlohmann::json& in) override;
|
||||
|
||||
private:
|
||||
WebsocketGameClient& client_;
|
||||
std::unordered_map<std::string, std::shared_ptr<Location>> runtimeLocations_;
|
||||
};
|
||||
|
||||
} // namespace FRG
|
||||
@@ -0,0 +1,189 @@
|
||||
#include "WebsocketGameClient.h"
|
||||
#include "NetworkProtocol.h"
|
||||
#include <boost/asio.hpp>
|
||||
#include <boost/beast.hpp>
|
||||
#include <atomic>
|
||||
#include <iostream>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace FRG {
|
||||
namespace {
|
||||
|
||||
namespace asio = boost::asio;
|
||||
namespace beast = boost::beast;
|
||||
namespace websocket = beast::websocket;
|
||||
using tcp = asio::ip::tcp;
|
||||
|
||||
} // namespace
|
||||
|
||||
class WebsocketGameClient::Impl {
|
||||
public:
|
||||
Impl(std::string host, std::string port, std::string path)
|
||||
: resolver_(io_)
|
||||
, socket_(io_)
|
||||
, host_(std::move(host))
|
||||
, port_(std::move(port))
|
||||
, path_(std::move(path))
|
||||
{
|
||||
resolve();
|
||||
thread_ = std::thread([this]() { io_.run(); });
|
||||
}
|
||||
|
||||
~Impl()
|
||||
{
|
||||
io_.stop();
|
||||
if (thread_.joinable()) thread_.join();
|
||||
}
|
||||
|
||||
std::vector<std::string> takeMessages()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(messagesMutex_);
|
||||
std::vector<std::string> result;
|
||||
result.swap(messages_);
|
||||
return result;
|
||||
}
|
||||
|
||||
bool connected() const { return connected_.load(); }
|
||||
|
||||
private:
|
||||
void resolve()
|
||||
{
|
||||
resolver_.async_resolve(
|
||||
host_, port_,
|
||||
[this](beast::error_code error, tcp::resolver::results_type results) {
|
||||
if (error) return fail("resolve", error);
|
||||
asio::async_connect(
|
||||
socket_.next_layer(), results,
|
||||
[this](beast::error_code connectError,
|
||||
const tcp::endpoint&) {
|
||||
if (connectError) return fail("connect", connectError);
|
||||
socket_.set_option(
|
||||
websocket::stream_base::timeout::suggested(
|
||||
beast::role_type::client));
|
||||
socket_.async_handshake(
|
||||
host_, path_,
|
||||
[this](beast::error_code handshakeError) {
|
||||
if (handshakeError)
|
||||
return fail("handshake", handshakeError);
|
||||
connected_.store(true);
|
||||
std::cout
|
||||
<< "[network] Connected to ws://"
|
||||
<< host_ << ':' << port_ << path_ << '\n';
|
||||
read();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void read()
|
||||
{
|
||||
socket_.async_read(
|
||||
buffer_,
|
||||
[this](beast::error_code error, std::size_t) {
|
||||
if (error) {
|
||||
connected_.store(false);
|
||||
if (error != websocket::error::closed) fail("read", error);
|
||||
return;
|
||||
}
|
||||
std::string message = beast::buffers_to_string(buffer_.data());
|
||||
buffer_.consume(buffer_.size());
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(messagesMutex_);
|
||||
messages_.push_back(std::move(message));
|
||||
}
|
||||
read();
|
||||
});
|
||||
}
|
||||
|
||||
void fail(const char* operation, beast::error_code error)
|
||||
{
|
||||
connected_.store(false);
|
||||
std::cerr << "[network] WebSocket " << operation << " failed: "
|
||||
<< error.message() << '\n';
|
||||
}
|
||||
|
||||
asio::io_context io_;
|
||||
tcp::resolver resolver_;
|
||||
websocket::stream<tcp::socket> socket_;
|
||||
beast::flat_buffer buffer_;
|
||||
std::thread thread_;
|
||||
std::string host_;
|
||||
std::string port_;
|
||||
std::string path_;
|
||||
std::atomic_bool connected_{false};
|
||||
std::mutex messagesMutex_;
|
||||
std::vector<std::string> messages_;
|
||||
};
|
||||
|
||||
WebsocketGameClient::WebsocketGameClient(
|
||||
std::string host, std::string port, std::string path)
|
||||
: impl_(std::make_unique<Impl>(
|
||||
std::move(host), std::move(port), std::move(path)))
|
||||
{
|
||||
}
|
||||
|
||||
WebsocketGameClient::~WebsocketGameClient() = default;
|
||||
|
||||
std::shared_ptr<const GameSnapshot> WebsocketGameClient::snapshot() const
|
||||
{
|
||||
return replica_.snapshot();
|
||||
}
|
||||
|
||||
const std::string& WebsocketGameClient::localPlayerId() const
|
||||
{
|
||||
return replica_.localPlayerId();
|
||||
}
|
||||
|
||||
void WebsocketGameClient::submit(const GameCommand&)
|
||||
{
|
||||
// Commands intentionally remain local no-ops in the first iteration.
|
||||
}
|
||||
|
||||
std::vector<GameServerEvent> WebsocketGameClient::drainEvents()
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
void WebsocketGameClient::update(std::int64_t)
|
||||
{
|
||||
std::vector<std::string> messages = impl_->takeMessages();
|
||||
if (messages.empty()) return;
|
||||
|
||||
// State messages are complete snapshots; only the newest queued one matters.
|
||||
for (auto it = messages.rbegin(); it != messages.rend(); ++it) {
|
||||
nlohmann::json state;
|
||||
std::string error;
|
||||
if (!parseStateMessage(*it, state, error)) {
|
||||
std::cerr << "[network] Ignoring WebSocket message: "
|
||||
<< error << '\n';
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
replica_.load(state);
|
||||
} catch (const std::exception& exception) {
|
||||
std::cerr << "[network] Ignoring invalid game state: "
|
||||
<< exception.what() << '\n';
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void WebsocketGameClient::save(nlohmann::json&) const
|
||||
{
|
||||
// Persistence remains server-side.
|
||||
}
|
||||
|
||||
void WebsocketGameClient::load(const nlohmann::json&)
|
||||
{
|
||||
// Persistence remains server-side.
|
||||
}
|
||||
|
||||
bool WebsocketGameClient::connected() const
|
||||
{
|
||||
return impl_->connected();
|
||||
}
|
||||
|
||||
} // namespace FRG
|
||||
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
|
||||
#include "IGameServer.h"
|
||||
#include "InternalGameServer.h"
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
namespace FRG {
|
||||
|
||||
class RemoteGameServerHost;
|
||||
|
||||
class WebsocketGameClient final : public IGameServer {
|
||||
public:
|
||||
WebsocketGameClient(
|
||||
std::string host = "127.0.0.1",
|
||||
std::string port = "8080",
|
||||
std::string path = "/");
|
||||
~WebsocketGameClient();
|
||||
|
||||
WebsocketGameClient(const WebsocketGameClient&) = delete;
|
||||
WebsocketGameClient& operator=(const WebsocketGameClient&) = delete;
|
||||
|
||||
std::shared_ptr<const GameSnapshot> snapshot() const override;
|
||||
const std::string& localPlayerId() const override;
|
||||
void submit(const GameCommand& command) override;
|
||||
std::vector<GameServerEvent> drainEvents() override;
|
||||
void update(std::int64_t deltaMs) override;
|
||||
void save(nlohmann::json& out) const override;
|
||||
void load(const nlohmann::json& in) override;
|
||||
|
||||
bool connected() const;
|
||||
|
||||
private:
|
||||
friend class RemoteGameServerHost;
|
||||
class Impl;
|
||||
InternalGameServer replica_;
|
||||
std::unique_ptr<Impl> impl_;
|
||||
};
|
||||
|
||||
} // namespace FRG
|
||||
@@ -0,0 +1,197 @@
|
||||
#include "WebsocketGameServer.h"
|
||||
#include "InternalGameServer.h"
|
||||
#include "NetworkProtocol.h"
|
||||
#include <boost/asio.hpp>
|
||||
#include <boost/beast.hpp>
|
||||
#include <deque>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
namespace FRG {
|
||||
namespace {
|
||||
|
||||
namespace asio = boost::asio;
|
||||
namespace beast = boost::beast;
|
||||
namespace websocket = beast::websocket;
|
||||
using tcp = asio::ip::tcp;
|
||||
|
||||
class ServerSession : public std::enable_shared_from_this<ServerSession> {
|
||||
public:
|
||||
explicit ServerSession(tcp::socket socket)
|
||||
: socket_(std::move(socket))
|
||||
{
|
||||
}
|
||||
|
||||
void start(std::string initialState)
|
||||
{
|
||||
socket_.set_option(
|
||||
websocket::stream_base::timeout::suggested(beast::role_type::server));
|
||||
socket_.async_accept(
|
||||
[self = shared_from_this(), initialState = std::move(initialState)](
|
||||
beast::error_code error) mutable {
|
||||
if (error) {
|
||||
std::cerr << "[network] WebSocket accept failed: "
|
||||
<< error.message() << '\n';
|
||||
return;
|
||||
}
|
||||
self->socket_.text(true);
|
||||
self->send(std::move(initialState));
|
||||
self->read();
|
||||
});
|
||||
}
|
||||
|
||||
void send(std::string message)
|
||||
{
|
||||
const bool writeInProgress = !outbound_.empty();
|
||||
outbound_.push_back(std::move(message));
|
||||
if (!writeInProgress) write();
|
||||
}
|
||||
|
||||
private:
|
||||
void read()
|
||||
{
|
||||
socket_.async_read(
|
||||
inbound_,
|
||||
[self = shared_from_this()](beast::error_code error, std::size_t) {
|
||||
if (error) {
|
||||
if (error != websocket::error::closed)
|
||||
std::cerr << "[network] WebSocket read failed: "
|
||||
<< error.message() << '\n';
|
||||
return;
|
||||
}
|
||||
// Commands are intentionally ignored in the first iteration.
|
||||
self->inbound_.consume(self->inbound_.size());
|
||||
self->read();
|
||||
});
|
||||
}
|
||||
|
||||
void write()
|
||||
{
|
||||
socket_.async_write(
|
||||
asio::buffer(outbound_.front()),
|
||||
[self = shared_from_this()](beast::error_code error, std::size_t) {
|
||||
if (error) {
|
||||
std::cerr << "[network] WebSocket write failed: "
|
||||
<< error.message() << '\n';
|
||||
return;
|
||||
}
|
||||
self->outbound_.pop_front();
|
||||
if (!self->outbound_.empty()) self->write();
|
||||
});
|
||||
}
|
||||
|
||||
websocket::stream<tcp::socket> socket_;
|
||||
beast::flat_buffer inbound_;
|
||||
std::deque<std::string> outbound_;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
class WebsocketGameServer::Impl {
|
||||
public:
|
||||
Impl(const std::string& address, std::uint16_t port, std::string initialState)
|
||||
: acceptor_(io_)
|
||||
, latestState_(std::move(initialState))
|
||||
{
|
||||
const tcp::endpoint endpoint(asio::ip::make_address(address), port);
|
||||
acceptor_.open(endpoint.protocol());
|
||||
acceptor_.set_option(asio::socket_base::reuse_address(true));
|
||||
acceptor_.bind(endpoint);
|
||||
acceptor_.listen(asio::socket_base::max_listen_connections);
|
||||
port_ = acceptor_.local_endpoint().port();
|
||||
accept();
|
||||
thread_ = std::thread([this]() { io_.run(); });
|
||||
}
|
||||
|
||||
~Impl()
|
||||
{
|
||||
asio::post(io_, [this]() {
|
||||
beast::error_code ignored;
|
||||
acceptor_.close(ignored);
|
||||
});
|
||||
io_.stop();
|
||||
if (thread_.joinable()) thread_.join();
|
||||
}
|
||||
|
||||
void broadcast(std::string state)
|
||||
{
|
||||
asio::post(io_, [this, state = std::move(state)]() mutable {
|
||||
latestState_ = std::move(state);
|
||||
auto output = sessions_.begin();
|
||||
for (auto input = sessions_.begin(); input != sessions_.end(); ++input) {
|
||||
if (auto session = input->lock()) {
|
||||
session->send(latestState_);
|
||||
*output++ = *input;
|
||||
}
|
||||
}
|
||||
sessions_.erase(output, sessions_.end());
|
||||
});
|
||||
}
|
||||
|
||||
std::uint16_t port() const { return port_; }
|
||||
|
||||
private:
|
||||
void accept()
|
||||
{
|
||||
acceptor_.async_accept(
|
||||
[this](beast::error_code error, tcp::socket socket) {
|
||||
if (!error) {
|
||||
auto session =
|
||||
std::make_shared<ServerSession>(std::move(socket));
|
||||
sessions_.push_back(session);
|
||||
session->start(latestState_);
|
||||
} else if (error != asio::error::operation_aborted) {
|
||||
std::cerr << "[network] TCP accept failed: "
|
||||
<< error.message() << '\n';
|
||||
}
|
||||
if (acceptor_.is_open()) accept();
|
||||
});
|
||||
}
|
||||
|
||||
asio::io_context io_;
|
||||
tcp::acceptor acceptor_;
|
||||
std::thread thread_;
|
||||
std::vector<std::weak_ptr<ServerSession>> sessions_;
|
||||
std::string latestState_;
|
||||
std::uint16_t port_ = 0;
|
||||
};
|
||||
|
||||
WebsocketGameServer::WebsocketGameServer(
|
||||
InternalGameServer& gameServer,
|
||||
const std::string& address,
|
||||
std::uint16_t port)
|
||||
: gameServer_(gameServer)
|
||||
{
|
||||
nlohmann::json state;
|
||||
gameServer_.save(state);
|
||||
impl_ = std::make_unique<Impl>(
|
||||
address, port, makeStateMessage(state));
|
||||
}
|
||||
|
||||
WebsocketGameServer::~WebsocketGameServer() = default;
|
||||
|
||||
void WebsocketGameServer::update(std::int64_t deltaMs)
|
||||
{
|
||||
gameServer_.update(deltaMs);
|
||||
broadcastElapsedMs_ += deltaMs;
|
||||
if (broadcastElapsedMs_ >= 5000) {
|
||||
broadcastElapsedMs_ %= 5000;
|
||||
broadcastState();
|
||||
}
|
||||
}
|
||||
|
||||
void WebsocketGameServer::broadcastState()
|
||||
{
|
||||
nlohmann::json state;
|
||||
gameServer_.save(state);
|
||||
impl_->broadcast(makeStateMessage(state));
|
||||
}
|
||||
|
||||
std::uint16_t WebsocketGameServer::port() const
|
||||
{
|
||||
return impl_->port();
|
||||
}
|
||||
|
||||
} // namespace FRG
|
||||
@@ -0,0 +1,33 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
namespace FRG {
|
||||
|
||||
class InternalGameServer;
|
||||
|
||||
class WebsocketGameServer {
|
||||
public:
|
||||
WebsocketGameServer(
|
||||
InternalGameServer& gameServer,
|
||||
const std::string& address = "0.0.0.0",
|
||||
std::uint16_t port = 8080);
|
||||
~WebsocketGameServer();
|
||||
|
||||
WebsocketGameServer(const WebsocketGameServer&) = delete;
|
||||
WebsocketGameServer& operator=(const WebsocketGameServer&) = delete;
|
||||
|
||||
void update(std::int64_t deltaMs);
|
||||
void broadcastState();
|
||||
std::uint16_t port() const;
|
||||
|
||||
private:
|
||||
class Impl;
|
||||
InternalGameServer& gameServer_;
|
||||
std::unique_ptr<Impl> impl_;
|
||||
std::int64_t broadcastElapsedMs_ = 0;
|
||||
};
|
||||
|
||||
} // namespace FRG
|
||||
Reference in new issue
Block a user