From c72e550d8a6a93d0c87227624cdb137408677780 Mon Sep 17 00:00:00 2001 From: tovjemam Date: Sun, 21 Jun 2026 19:52:14 +0200 Subject: [PATCH] Refactor websocket stuff from main and introduce app states --- CMakeLists.txt | 3 + src/client/app.cpp | 297 ++++++++++++++++++--------- src/client/app.hpp | 41 +++- src/client/main.cpp | 164 +-------------- src/client/wsclient.hpp | 27 +++ src/client/wsclient_easywsclient.cpp | 126 ++++++++++++ src/client/wsclient_emscripten.cpp | 130 ++++++++++++ 7 files changed, 523 insertions(+), 265 deletions(-) create mode 100644 src/client/wsclient.hpp create mode 100644 src/client/wsclient_easywsclient.cpp create mode 100644 src/client/wsclient_emscripten.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index be01ce4..a896996 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -80,6 +80,9 @@ set(CLIENT_ONLY_SOURCES "src/client/gl.hpp" "src/client/main.cpp" "src/client/utils.hpp" + "src/client/wsclient.hpp" + "src/client/wsclient_easywsclient.cpp" + "src/client/wsclient_emscripten.cpp" "src/gameview/characterview.hpp" "src/gameview/characterview.cpp" "src/gameview/client_session.hpp" diff --git a/src/client/app.cpp b/src/client/app.cpp index f579a01..a5c3348 100644 --- a/src/client/app.cpp +++ b/src/client/app.cpp @@ -14,105 +14,15 @@ App::App() : std::cout << "Initializing App..." << std::endl; ApplySettings(); - AddChatMessage("Test!"); + // AddChatMessage("Test!"); } void App::Frame() { - delta_time_ = time_ - prev_time_; - prev_time_ = time_; + ws_.Poll(); - if (delta_time_ < 0.0f) - { - delta_time_ = 0.0f; // Prevent negative delta time - } - else if (delta_time_ > 0.1f) - { - delta_time_ = 0.1f; // Cap delta time to avoid large jumps - } - - if (session_) - { - game::view::UpdateInfo updinfo; - updinfo.time = time_; - updinfo.delta_time = delta_time_; - session_->Update(updinfo); - } - - gfx::DrawListParams params{}; - params.screen_width = viewport_size_.x; - params.screen_height = viewport_size_.y; - params.env.clear_color = glm::vec3(0.1f); - - dlist_.Clear(); - gui_.Begin(viewport_size_); - - // draw session - if (session_) - { - session_->Draw(dlist_, params, gui_); - } - - // draw stats - UpdateStats(); - DrawStats(); - - // draw chat - UpdateChat(); - DrawChat(); - - // draw menu - if (menu_) - { - auto menu_size = menu_->MeasureSize(); - menu_->Draw(gui_, (glm::vec2(viewport_size_) - menu_size) * 0.5f); - } - - gui_.Render(); - renderer_.DrawList(dlist_, params); - - ++stat_frames_; -} - -void App::Connected() -{ - std::cout << "WS connected" << std::endl; - AddChatMessagePrefix("WebSocket", "^7f7připojeno"); - - // init session - session_ = std::make_unique(*this); -} - -void App::ProcessMessage(net::InMessage& msg) -{ - if (!session_) - return; - - size_t s = msg.End() - msg.Ptr(); - // AddChatMessage("recvd: ^f00;" + std::to_string(s)); - - // std::cout << "App::ProcessMessage: received message of size " << s << " bytes" << std::endl; - - if (!session_->ProcessMessage(msg)) - { - std::cerr << "FAILED to process message!" << std::endl; - } - - // record stats - ++stat_msgs_; - stat_msglen_total_ += s; - stat_msglen_min_ = std::min(stat_msglen_min_, s); - stat_msglen_max_ = std::max(stat_msglen_max_, s); -} - -void App::Disconnected(const std::string& reason) -{ - std::cout << "WS disconnected" << std::endl; - AddChatMessagePrefix("WebSocket", "^f77spojení je píči"); - - - // close session - session_.reset(); + Update(); + Draw(); } void App::Input(game::PlayerInputType in, bool pressed, bool repeated) @@ -163,6 +73,59 @@ void App::AddChatMessagePrefix(const std::string& prefix, const std::string& tex App::~App() {} +void App::Update() +{ + delta_time_ = time_ - prev_time_; + prev_time_ = time_; + + if (delta_time_ < 0.0f) + { + delta_time_ = 0.0f; // Prevent negative delta time + } + else if (delta_time_ > 0.1f) + { + delta_time_ = 0.1f; // Cap delta time to avoid large jumps + } + + UpdateState(); + UpdateSession(); + UpdateStats(); + UpdateChat(); +} + +void App::Draw() +{ + + gfx::DrawListParams params{}; + params.screen_width = viewport_size_.x; + params.screen_height = viewport_size_.y; + params.env.clear_color = glm::vec3(0.1f); + + dlist_.Clear(); + gui_.Begin(viewport_size_); + + // draw session + if (session_) + { + session_->Draw(dlist_, params, gui_); + } + + DrawStats(); + DrawChat(); + + // draw menu + if (menu_) + { + auto menu_size = menu_->MeasureSize(); + menu_->Draw(gui_, (glm::vec2(viewport_size_) - menu_size) * 0.5f); + } + + gui_.Render(); + renderer_.DrawList(dlist_, params); + + ++stat_frames_; +} + void App::UpdateChat() { // remove expired or over the limit messages @@ -253,6 +216,28 @@ void App::ApplySensitivity() #define COL_LABEL "^ccc" #define COL_VALUE "^5ff" +void App::UpdateSession() +{ + if (!session_) + return; + + game::view::UpdateInfo updinfo; + updinfo.time = time_; + updinfo.delta_time = delta_time_; + session_->Update(updinfo); + + if (connected_) + { + auto msg = session_->GetMsg(); + if (!msg.empty()) + { + ws_.Send(msg); + } + + session_->ResetMsg(); + } +} + void App::UpdateStats() { if (time_ < stats_time_ + 1.0f) @@ -291,3 +276,127 @@ void App::DrawStats() pos.y += 30.0f; gui_.DrawTextAligned(msglen_text_, pos, glm::vec2(-1.0f, 0.0f)); } + +void App::Connect() +{ + ws_.SetOnConnect([this]{ + connected_ = true; + connecting_ = false; + }); + + ws_.SetOnMessage([this](std::span data) { + ProcessWsMessage(data); + }); + + ws_.SetOnDisconnect([this]{ + connected_ = false; + connecting_ = false; + }); + + connecting_ = ws_.Connect(url_); +} + +void App::ProcessWsMessage(std::span data) +{ + if (!session_) + return; + + // record stats + size_t s = data.size(); + ++stat_msgs_; + stat_msglen_total_ += s; + stat_msglen_min_ = std::min(stat_msglen_min_, s); + stat_msglen_max_ = std::max(stat_msglen_max_, s); + + net::InMessage msg(data.data(), data.size()); + if (!session_->ProcessMessage(msg)) + { + std::cerr << "FAILED to process message!" << std::endl; + local_error_ = true; + } +} + +void App::UpdateState() +{ + auto new_state = CheckStateTransition(); + if (new_state == state_) + return; + + EnterState(new_state); +} + +void App::EnterState(AppState state) +{ + state_ = state; + state_time_ = time_; + + switch (state) + { + case APP_STATE_INIT: + break; + + case APP_STATE_LOADING: + break; + + case APP_STATE_IDLE: + break; + + case APP_STATE_CONNECT: + AddChatMessagePrefix("WebSocket", "připojování na " + url_); + Connect(); + break; + + case APP_STATE_CONNECTED: + AddChatMessagePrefix("WebSocket", "^7f7připojeno"); + session_ = std::make_unique(*this); + break; + + case APP_STATE_DISCONNECTED: + AddChatMessagePrefix("WebSocket", "^f77spojení je píči"); + session_.reset(); + AddChatMessagePrefix("WebSocket", "další pokus za 10 s"); + break; + + default: + break; + } +} + +AppState App::CheckStateTransition() +{ + switch (state_) + { + case APP_STATE_INIT: + return APP_STATE_LOADING; + + case APP_STATE_LOADING: + return APP_STATE_IDLE; + + case APP_STATE_IDLE: + return APP_STATE_CONNECT; + + case APP_STATE_CONNECT: + if (connected_) + return APP_STATE_CONNECTED; + + if (!connecting_) + return APP_STATE_DISCONNECTED; + + return APP_STATE_CONNECT; + + case APP_STATE_CONNECTED: + if (!connected_) + return APP_STATE_DISCONNECTED; + + return APP_STATE_CONNECTED; + + case APP_STATE_DISCONNECTED: + if (GetCurrentStateDuration() >= 10.0f) + return APP_STATE_CONNECT; + + return APP_STATE_DISCONNECTED; + + default: + return state_; + } +} diff --git a/src/client/app.hpp b/src/client/app.hpp index 51cc79d..2874277 100644 --- a/src/client/app.hpp +++ b/src/client/app.hpp @@ -12,6 +12,7 @@ #include "net/inmessage.hpp" #include "gui/menu.hpp" #include "gameview/client_session.hpp" +#include "wsclient.hpp" struct ChatMessage { @@ -20,6 +21,16 @@ struct ChatMessage glm::vec4 color = glm::vec4(1.0f); }; +enum AppState +{ + APP_STATE_INIT, + APP_STATE_LOADING, + APP_STATE_IDLE, + APP_STATE_CONNECT, + APP_STATE_CONNECTED, + APP_STATE_DISCONNECTED, +}; + class App { public: @@ -27,13 +38,10 @@ public: void Frame(); - void Connected(); - void ProcessMessage(net::InMessage& msg); - void Disconnected(const std::string& reason); - void SetTime(float time) { time_ = time; } void SetViewportSize(int width, int height) { viewport_size_ = {width, height}; } + void SetUrl(const std::string& url) { url_ = url; } void SetUserName(const std::string& username) { username_ = username; } const std::string& GetUserName() const { return username_; } @@ -43,8 +51,6 @@ public: const float& GetTime() const { return time_; } float GetDeltaTime() const { return delta_time_; } - game::view::ClientSession* GetSession() { return session_.get(); } - audio::Master& GetAudioMaster() { return audiomaster_; } void AddChatMessage(const std::string& text); @@ -53,6 +59,9 @@ public: ~App(); private: + void Update(); + void Draw(); + void UpdateChat(); void DrawChat(); @@ -61,9 +70,18 @@ private: void ApplyVolume(); void ApplySensitivity(); + void UpdateSession(); void UpdateStats(); void DrawStats(); + void Connect(); + void ProcessWsMessage(std::span data); + + void UpdateState(); + void EnterState(AppState state); + AppState CheckStateTransition(); + float GetCurrentStateDuration() const { return time_ - state_time_; } + private: float time_ = 0.0f; glm::ivec2 viewport_size_ = {800, 600}; @@ -74,16 +92,23 @@ private: gfx::Renderer renderer_; gfx::DrawList dlist_; gui::Context gui_; - audio::Master audiomaster_; + WsClient ws_; + std::string url_; std::string username_; + bool connecting_ = false; + bool connected_ = false; + bool local_error_ = false; + std::unique_ptr session_; std::deque chat_; - std::unique_ptr menu_; + AppState state_ = APP_STATE_INIT; + float state_time_ = 0.0f; + // settings int volume_ = 50; int sens_ = 50; diff --git a/src/client/main.cpp b/src/client/main.cpp index f2bdf46..56ab309 100644 --- a/src/client/main.cpp +++ b/src/client/main.cpp @@ -11,15 +11,11 @@ #include #include #include -#else -#include #endif // EMSCRIPTEN #ifdef _WIN32 #define NOMINMAX -#pragma comment(lib, "ws2_32") #pragma comment(lib, "winmm.lib") -#include #include #include #include @@ -239,144 +235,6 @@ static void PollEvents() #endif -static bool s_ws_connected = false; - -#ifdef EMSCRIPTEN - -static EMSCRIPTEN_WEBSOCKET_T s_ws = 0; - -static EM_BOOL OnWSOpen(int type, const EmscriptenWebSocketOpenEvent *ev, void *ud) -{ - s_ws_connected = true; - s_app->Connected(); - return EM_TRUE; -} - -static EM_BOOL OnWSMessage(int type, const EmscriptenWebSocketMessageEvent *ev, void *ud) -{ - if (ev->isText) - return EM_TRUE; - - net::InMessage msg(reinterpret_cast(ev->data), ev->numBytes); - s_app->ProcessMessage(msg); - - return EM_TRUE; -} - -static EM_BOOL OnWSClose(int type, const EmscriptenWebSocketCloseEvent *ev, void *ud) -{ - s_ws_connected = false; - s_app->Disconnected(""); - return EM_TRUE; -} - -static EM_BOOL OnWSError(int type, const EmscriptenWebSocketErrorEvent *ev, void *ud) -{ - s_ws_connected = false; - s_app->Disconnected("error"); - return EM_TRUE; -} - -static bool WSInit(const char* url) -{ - if (!emscripten_websocket_is_supported()) - { - std::cerr << "EMSCRIPTEN WS NOT SUPPORTED" << std::endl; - return false; - } - EmscriptenWebSocketCreateAttributes ws_attrs = { - url, - NULL, - EM_TRUE - }; - - s_ws = emscripten_websocket_new(&ws_attrs); - emscripten_websocket_set_onopen_callback(s_ws, NULL, OnWSOpen); - emscripten_websocket_set_onmessage_callback(s_ws, NULL, OnWSMessage); - emscripten_websocket_set_onclose_callback(s_ws, NULL, OnWSClose); - emscripten_websocket_set_onerror_callback(s_ws, NULL, OnWSError); - - return true; -} - -static void WSSend(std::span data) -{ - emscripten_websocket_send_binary(s_ws, (void*)data.data(), static_cast(data.size())); -} - -static void WSPoll() {} -static void WSClose() {} - -#else /* !EMSCRIPTEN */ - -using namespace easywsclient; - -static std::unique_ptr s_ws; - -static bool WSInit(const char* url) -{ - -#ifdef _WIN32 - INT rc; - WSADATA wsaData; - - rc = WSAStartup(MAKEWORD(2, 2), &wsaData); - if (rc) - { - printf("WSAStartup Failed.\n"); - return false; - } -#endif - - s_ws = std::unique_ptr(WebSocket::from_url(url)); - - if (!s_ws) - return false; - - return true; -} - -static void WSSend(std::span msg) -{ - static std::vector data; - data.resize(msg.size_bytes()); - memcpy(data.data(), msg.data(), msg.size_bytes()); - s_ws->sendBinary(data); -} - -static void WSPoll() -{ - s_ws->poll(); - - auto ws_state = s_ws->getReadyState(); - if (ws_state == WebSocket::OPEN && !s_ws_connected) - { - s_ws_connected = true; - s_app->Connected(); - } - else if (ws_state != WebSocket::OPEN && s_ws_connected) - { - s_ws_connected = false; - s_app->Disconnected("WS closed"); - } - - s_ws->dispatchBinary([&](const std::vector& data) { - net::InMessage msg(reinterpret_cast(data.data()), data.size()); - s_app->ProcessMessage(msg); - }); -} - -static void WSClose() -{ - s_ws.reset(); - -#ifdef _WIN32 - WSACleanup(); -#endif -} - -#endif /* EMSCRIPTEN */ - static bool can_update = false; static Uint32 last_update = 0; @@ -387,7 +245,6 @@ static void Frame() s_app->SetTime(current_time / 1000.0f); // Set time in seconds PollEvents(); - WSPoll(); int width, height; SDL_GetWindowSize(s_window, &width, &height); @@ -395,21 +252,6 @@ static void Frame() s_app->Frame(); - auto session = s_app->GetSession(); - if (session) - { - if (s_ws_connected) - { - auto msg = session->GetMsg(); - if (!msg.empty()) - { - WSSend(msg); - } - } - - session->ResetMsg(); - } - SDL_GL_SwapWindow(s_window); } @@ -435,9 +277,6 @@ static void Main() { if (s_url.empty()) s_url = WS_URL; - if (!WSInit(s_url.c_str())) - return; - InitSDL(); try @@ -454,7 +293,7 @@ static void Main() { s_app = std::make_unique(); s_app->SetUserName(s_username); - s_app->AddChatMessagePrefix("WebSocket", "připojování na " + s_url); + s_app->SetUrl(s_url); can_update = true; @@ -489,7 +328,6 @@ static void Main() { ShutdownGL(); ShutdownSDL(); - WSClose(); #endif // EMSCRIPTEN } diff --git a/src/client/wsclient.hpp b/src/client/wsclient.hpp new file mode 100644 index 0000000..5597184 --- /dev/null +++ b/src/client/wsclient.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include +#include +#include + +using WsConnectCallback = std::function; +using WsMessageCallback = std::function data)>; +using WsDisconnectCallback = std::function; + +class WsClient +{ +public: + WsClient(); + + bool Connect(const std::string& endpoint); + void Send(std::span data); + void Poll(); + void Disconnect(); + + void SetOnConnect(WsConnectCallback cb); + void SetOnMessage(WsMessageCallback cb); + void SetOnDisconnect(WsDisconnectCallback cb); + + ~WsClient(); +}; + diff --git a/src/client/wsclient_easywsclient.cpp b/src/client/wsclient_easywsclient.cpp new file mode 100644 index 0000000..2ab6a1d --- /dev/null +++ b/src/client/wsclient_easywsclient.cpp @@ -0,0 +1,126 @@ +#ifndef EMSCRIPTEN + +#include "wsclient.hpp" + +#include +#include + +#include + +#ifdef _WIN32 +#define NOMINMAX +#pragma comment(lib, "ws2_32") +// #pragma comment(lib, "winmm.lib") +#include +#include +// #include +// #include +#endif + +using namespace easywsclient; + +static std::unique_ptr s_ws; +static bool s_connected = false; + +static WsConnectCallback s_on_connect; +static WsMessageCallback s_on_message; +static WsDisconnectCallback s_on_disconnect; + +WsClient::WsClient() +{ +#ifdef _WIN32 + INT rc; + WSADATA wsaData; + + rc = WSAStartup(MAKEWORD(2, 2), &wsaData); + if (rc) + { + throw std::runtime_error("WSA init failed"); + } +#endif + +} + +bool WsClient::Connect(const std::string& endpoint) +{ + s_ws = std::unique_ptr(WebSocket::from_url(endpoint)); + + if (!s_ws) + return false; + + return true; +} + +void WsClient::Send(std::span data) +{ + static std::vector data_u8; + data_u8.resize(data.size_bytes()); + memcpy(data_u8.data(), data.data(), data.size_bytes()); + s_ws->sendBinary(data_u8); +} + +void WsClient::Poll() +{ + if (!s_ws) + return; + + s_ws->poll(); + + auto ws_state = s_ws->getReadyState(); + if (ws_state == WebSocket::OPEN && !s_connected) + { + s_connected = true; + if (s_on_connect) + s_on_connect(); + } + else if (ws_state != WebSocket::OPEN && s_connected) + { + s_connected = false; + if (s_on_disconnect) + s_on_disconnect(); + } + + s_ws->dispatchBinary([&](const std::vector& data_u8) { + if (s_on_message) + { + std::span data(reinterpret_cast(data_u8.data()), data_u8.size()); + s_on_message(data); + } + }); + + if (!s_connected) + { + s_ws.reset(); + } +} + +void WsClient::Disconnect() +{ + s_ws->close(); +} + +void WsClient::SetOnConnect(WsConnectCallback cb) +{ + s_on_connect = std::move(cb); +} + +void WsClient::SetOnMessage(WsMessageCallback cb) +{ + s_on_message = std::move(cb); +} + +void WsClient::SetOnDisconnect(WsDisconnectCallback cb) +{ + s_on_disconnect = std::move(cb); +} + +WsClient::~WsClient() +{ + s_ws.reset(); + +#ifdef _WIN32 + WSACleanup(); +#endif +} + +#endif // EMSCRIPTEN \ No newline at end of file diff --git a/src/client/wsclient_emscripten.cpp b/src/client/wsclient_emscripten.cpp new file mode 100644 index 0000000..2bdf856 --- /dev/null +++ b/src/client/wsclient_emscripten.cpp @@ -0,0 +1,130 @@ +#ifdef EMSCRIPTEN + +#include "wsclient.hpp" + +#include +#include + +static EMSCRIPTEN_WEBSOCKET_T s_ws = 0; +static bool s_connected = false; + +static WsConnectCallback s_on_connect; +static WsMessageCallback s_on_message; +static WsDisconnectCallback s_on_disconnect; + +static EM_BOOL OnWSOpen(int type, const EmscriptenWebSocketOpenEvent *ev, void *ud) +{ + if (!s_connected) + { + s_connected = true; + if (s_on_connect) + s_on_connect(); + } + + return EM_TRUE; +} + +static EM_BOOL OnWSMessage(int type, const EmscriptenWebSocketMessageEvent *ev, void *ud) +{ + if (ev->isText) + return EM_TRUE; + + if (s_on_message) + { + std::span data(reinterpret_cast(ev->data), ev->numBytes); + s_on_message(data); + } + + return EM_TRUE; +} + +static EM_BOOL OnWSClose(int type, const EmscriptenWebSocketCloseEvent *ev, void *ud) +{ + if (s_connected) + { + s_connected = false; + if (s_on_disconnect) + { + s_on_disconnect(); + } + + } + + return EM_TRUE; +} + +static EM_BOOL OnWSError(int type, const EmscriptenWebSocketErrorEvent *ev, void *ud) +{ + s_connected = false; + if (s_on_disconnect) + { + s_on_disconnect(); + } + + return EM_TRUE; +} + +WsClient::WsClient() +{ + if (!emscripten_websocket_is_supported()) + { + throw std::runtime_error("EMSCRIPTEN WS NOT SUPPORTED"); + } +} + +bool WsClient::Connect(const std::string& endpoint) +{ + if (s_ws) + { + emscripten_websocket_delete(s_ws); + s_ws = 0; + } + + EmscriptenWebSocketCreateAttributes ws_attrs = { + endpoint.c_str(), + NULL, + EM_TRUE + }; + + s_ws = emscripten_websocket_new(&ws_attrs); + emscripten_websocket_set_onopen_callback(s_ws, NULL, OnWSOpen); + emscripten_websocket_set_onmessage_callback(s_ws, NULL, OnWSMessage); + emscripten_websocket_set_onclose_callback(s_ws, NULL, OnWSClose); + emscripten_websocket_set_onerror_callback(s_ws, NULL, OnWSError); + + return true; +} + +void WsClient::Send(std::span data) +{ + emscripten_websocket_send_binary(s_ws, (void*)data.data(), static_cast(data.size())); +} + +void WsClient::Poll() +{ +} + +void WsClient::Disconnect() +{ +} + +void WsClient::SetOnConnect(WsConnectCallback cb) +{ + s_on_connect = std::move(cb); +} + +void WsClient::SetOnMessage(WsMessageCallback cb) +{ + s_on_message = std::move(cb); +} + +void WsClient::SetOnDisconnect(WsDisconnectCallback cb) +{ + s_on_disconnect = std::move(cb); +} + +WsClient::~WsClient() +{ +} + +#endif // EMSCRIPTEN \ No newline at end of file