#include "WebsocketGameClient.h" #include "NetworkProtocol.h" #include #include #include #include #include #include #include #include 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 takeMessages() { std::lock_guard lock(messagesMutex_); std::vector 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 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 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 messages_; }; WebsocketGameClient::WebsocketGameClient( std::string host, std::string port, std::string path) : impl_(std::make_unique( std::move(host), std::move(port), std::move(path))) { } WebsocketGameClient::~WebsocketGameClient() = default; std::shared_ptr 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 WebsocketGameClient::drainEvents() { return {}; } void WebsocketGameClient::update(std::int64_t) { std::vector 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