Refactor websocket stuff from main and introduce app states

This commit is contained in:
tovjemam 2026-06-21 19:52:14 +02:00
parent 352fa9a178
commit c72e550d8a
7 changed files with 523 additions and 265 deletions

View File

@ -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"

View File

@ -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<game::view::ClientSession>(*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<const char> data) {
ProcessWsMessage(data);
});
ws_.SetOnDisconnect([this]{
connected_ = false;
connecting_ = false;
});
connecting_ = ws_.Connect(url_);
}
void App::ProcessWsMessage(std::span<const char> 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<game::view::ClientSession>(*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_;
}
}

View File

@ -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<const char> 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<game::view::ClientSession> session_;
std::deque<ChatMessage> chat_;
std::unique_ptr<gui::Menu> menu_;
AppState state_ = APP_STATE_INIT;
float state_time_ = 0.0f;
// settings
int volume_ = 50;
int sens_ = 50;

View File

@ -11,15 +11,11 @@
#include <emscripten/html5_webgl.h>
#include <emscripten/emscripten.h>
#include <emscripten/websocket.h>
#else
#include <easywsclient.hpp>
#endif // EMSCRIPTEN
#ifdef _WIN32
#define NOMINMAX
#pragma comment(lib, "ws2_32")
#pragma comment(lib, "winmm.lib")
#include <WinSock2.h>
#include <windows.h>
#include <chrono>
#include <thread>
@ -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<char*>(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<const char> data)
{
emscripten_websocket_send_binary(s_ws, (void*)data.data(), static_cast<uint32_t>(data.size()));
}
static void WSPoll() {}
static void WSClose() {}
#else /* !EMSCRIPTEN */
using namespace easywsclient;
static std::unique_ptr<WebSocket> 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>(WebSocket::from_url(url));
if (!s_ws)
return false;
return true;
}
static void WSSend(std::span<const char> msg)
{
static std::vector<uint8_t> 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<uint8_t>& data) {
net::InMessage msg(reinterpret_cast<const char*>(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<App>();
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
}

27
src/client/wsclient.hpp Normal file
View File

@ -0,0 +1,27 @@
#pragma once
#include <string>
#include <span>
#include <functional>
using WsConnectCallback = std::function<void()>;
using WsMessageCallback = std::function<void(std::span<const char> data)>;
using WsDisconnectCallback = std::function<void()>;
class WsClient
{
public:
WsClient();
bool Connect(const std::string& endpoint);
void Send(std::span<const char> data);
void Poll();
void Disconnect();
void SetOnConnect(WsConnectCallback cb);
void SetOnMessage(WsMessageCallback cb);
void SetOnDisconnect(WsDisconnectCallback cb);
~WsClient();
};

View File

@ -0,0 +1,126 @@
#ifndef EMSCRIPTEN
#include "wsclient.hpp"
#include <memory>
#include <stdexcept>
#include <easywsclient.hpp>
#ifdef _WIN32
#define NOMINMAX
#pragma comment(lib, "ws2_32")
// #pragma comment(lib, "winmm.lib")
#include <WinSock2.h>
#include <windows.h>
// #include <chrono>
// #include <thread>
#endif
using namespace easywsclient;
static std::unique_ptr<WebSocket> 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>(WebSocket::from_url(endpoint));
if (!s_ws)
return false;
return true;
}
void WsClient::Send(std::span<const char> data)
{
static std::vector<uint8_t> 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<uint8_t>& data_u8) {
if (s_on_message)
{
std::span<const char> data(reinterpret_cast<const char*>(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

View File

@ -0,0 +1,130 @@
#ifdef EMSCRIPTEN
#include "wsclient.hpp"
#include <emscripten/emscripten.h>
#include <emscripten/websocket.h>
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<const char> data(reinterpret_cast<char*>(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<const char> data)
{
emscripten_websocket_send_binary(s_ws, (void*)data.data(), static_cast<uint32_t>(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