190 lines
4.3 KiB
C++
190 lines
4.3 KiB
C++
#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
|