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,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
|
||||
Reference in new issue
Block a user