From ac551122d98446b37cfc996df586d5d0ca220eb2 Mon Sep 17 00:00:00 2001 From: Vladislav Khorev Date: Fri, 6 Mar 2026 20:05:55 +0300 Subject: [PATCH] Added force disconnect from server --- proj-web/README.md | 7 +++++- server/server.cpp | 39 +++++++++++++++++++++++++++-- server/server.h | 3 +++ src/Space.cpp | 23 +++++++++++++++++ src/Space.h | 1 + src/network/ClientState.h | 1 + src/network/NetworkInterface.h | 1 + src/network/WebSocketClientBase.cpp | 19 ++++++++++++++ src/network/WebSocketClientBase.h | 2 ++ 9 files changed, 93 insertions(+), 3 deletions(-) diff --git a/proj-web/README.md b/proj-web/README.md index 7969215..d3706c8 100644 --- a/proj-web/README.md +++ b/proj-web/README.md @@ -1,9 +1,14 @@ # how to build +If emsdk is not installed, you need to clone it from here: https://github.com/emscripten-core/emsdk -Activate the environment: +and install: ``` C:\Work\Projects\emsdk\emsdk.bat install latest +``` + +Then activate the environment: +``` C:\Work\Projects\emsdk\emsdk.bat activate latest C:\Work\Projects\emsdk\emsdk_env.bat ``` diff --git a/server/server.cpp b/server/server.cpp index 54168e2..2f1da7d 100644 --- a/server/server.cpp +++ b/server/server.cpp @@ -20,7 +20,19 @@ std::vector split(const std::string& s, char delimiter) { Session::Session(Server& server, tcp::socket&& socket, int id) : server_(server) , ws_(std::move(socket)) - , id_(id) { + , id_(id) + , lastReceivedTime_(std::chrono::system_clock::now()) { +} + +bool Session::is_timed_out(std::chrono::system_clock::time_point now) const { + if (!joined_) return false; + auto elapsed = std::chrono::duration_cast(now - lastReceivedTime_).count(); + return elapsed > PLAYER_TIMEOUT_MS; +} + +void Session::force_disconnect() { + ws_.async_close(websocket::close_code::normal, + [self = shared_from_this()](beast::error_code) {}); } int Session::get_id() const { return id_; } @@ -166,12 +178,16 @@ void Session::doWrite() { void Session::do_read() { ws_.async_read(buffer_, [self = shared_from_this()](beast::error_code ec, std::size_t) { if (ec) { + if (self->joined_) { + self->server_.broadcastToAllExceptId("PLAYER_LEFT:" + std::to_string(self->id_), self->id_); + std::cout << "Client " << self->id_ << " disconnected, broadcasting PLAYER_LEFT\n"; + } std::lock_guard lock(self->server_.g_sessions_mutex); self->server_.g_sessions.erase(std::remove_if(self->server_.g_sessions.begin(), self->server_.g_sessions.end(), [self](const std::shared_ptr& session) { return session.get() == self.get(); }), self->server_.g_sessions.end()); - std::cout << "Client " << self->id_ << " disconnected\n"; + std::cout << "Client " << self->id_ << " removed from session list\n"; return; } @@ -188,6 +204,7 @@ void Session::process_message(const std::string& msg) { std::cout << "[Security] Invalid packet hash. Dropping message: " << msg << std::endl; return; } + lastReceivedTime_ = std::chrono::system_clock::now(); std::string cleanMessage = msg.substr(0, msg.find("#hash:")); std::cout << "Received from player " << id_ << ": " << cleanMessage << std::endl; @@ -480,6 +497,24 @@ void Server::update_world() { uint64_t now_ms = static_cast( std::chrono::duration_cast(now.time_since_epoch()).count()); + // --- Detect and force-disconnect timed-out players --- + { + std::vector> timedOut; + { + std::lock_guard lock(g_sessions_mutex); + for (auto& session : g_sessions) { + if (session->is_timed_out(now)) { + timedOut.push_back(session); + } + } + } + for (auto& session : timedOut) { + std::cout << "Server: Player " << session->get_id() + << " timed out after " << PLAYER_TIMEOUT_MS << "ms, forcing disconnect\n"; + session->force_disconnect(); + } + } + { std::lock_guard lock(g_sessions_mutex); for (auto& sender : g_sessions) { diff --git a/server/server.h b/server/server.h index fbeca04..485f35c 100644 --- a/server/server.h +++ b/server/server.h @@ -72,6 +72,7 @@ class Session : public std::enable_shared_from_this { public: ClientStateInterval timedClientStates; bool joined_ = false; + std::chrono::system_clock::time_point lastReceivedTime_; bool hasReservedSpawn_ = false; Eigen::Vector3f reservedSpawn_ = Eigen::Vector3f(0.0f, 0.0f, kWorldZOffset); @@ -87,6 +88,8 @@ public: void send_message(const std::string& msg); void run(); bool IsMessageValid(const std::string& fullMessage); + bool is_timed_out(std::chrono::system_clock::time_point now) const; + void force_disconnect(); private: void sendBoxesToClient(); diff --git a/src/Space.cpp b/src/Space.cpp index 9b22995..b079041 100644 --- a/src/Space.cpp +++ b/src/Space.cpp @@ -1822,6 +1822,18 @@ namespace ZL void Space::update() { if (networkClient) { + if (networkClient->IsConnected()) { + wasConnectedToServer = true; + } + else if (wasConnectedToServer && shipAlive && !gameOver) { + wasConnectedToServer = false; + shipAlive = false; + gameOver = true; + Environment::shipState.velocity = 0.0f; + std::cout << "Client: Lost connection to server\n"; + menuManager.showGameOver(this->playerScore); + } + auto pending = networkClient->getPendingProjectiles(); if (!pending.empty()) { const float projectileSpeed = PROJECTILE_VELOCITY; @@ -1911,6 +1923,17 @@ namespace ZL } } + auto disconnects = networkClient->getPendingDisconnects(); + for (int pid : disconnects) { + remotePlayerStates.erase(pid); + deadRemotePlayers.erase(pid); + if (trackedTargetId == pid) { + trackedTargetId = -1; + targetAcquireAnim = 0.f; + } + std::cout << "Client: Remote player " << pid << " left the game, removed from scene\n"; + } + auto boxDestructions = networkClient->getPendingBoxDestructions(); if (!boxDestructions.empty()) { std::cout << "Game: Received " << boxDestructions.size() << " box destruction events" << std::endl; diff --git a/src/Space.h b/src/Space.h index 94f50c6..54f54d5 100644 --- a/src/Space.h +++ b/src/Space.h @@ -124,6 +124,7 @@ namespace ZL { std::unordered_set deadRemotePlayers; int playerScore = 0; + bool wasConnectedToServer = false; static constexpr float TARGET_MAX_DIST = 50000.0f; static constexpr float TARGET_MAX_DIST_SQ = TARGET_MAX_DIST * TARGET_MAX_DIST; diff --git a/src/network/ClientState.h b/src/network/ClientState.h index 0812892..d78903d 100644 --- a/src/network/ClientState.h +++ b/src/network/ClientState.h @@ -26,6 +26,7 @@ constexpr float PITCH_LIMIT = static_cast(M_PI) / 9.f;//18.0f; constexpr long long SERVER_DELAY = 0; //ms constexpr long long CLIENT_DELAY = 500; //ms constexpr long long CUTOFF_TIME = 5000; //ms +constexpr long long PLAYER_TIMEOUT_MS = 10000; //ms — disconnect if no UPD received constexpr float PROJECTILE_VELOCITY = 600.f; constexpr float PROJECTILE_LIFE = 15000.f; //ms diff --git a/src/network/NetworkInterface.h b/src/network/NetworkInterface.h index 2c7e704..191445d 100644 --- a/src/network/NetworkInterface.h +++ b/src/network/NetworkInterface.h @@ -50,6 +50,7 @@ namespace ZL { virtual int GetClientId() const { return -1; } virtual std::vector getPendingBoxDestructions() = 0; virtual int64_t getTimeOffset() const { return 0; } + virtual std::vector getPendingDisconnects() { return {}; } }; } diff --git a/src/network/WebSocketClientBase.cpp b/src/network/WebSocketClientBase.cpp index 710bf7d..6be0c77 100644 --- a/src/network/WebSocketClientBase.cpp +++ b/src/network/WebSocketClientBase.cpp @@ -103,6 +103,19 @@ namespace ZL { return; } + if (msg.rfind("PLAYER_LEFT:", 0) == 0) { + if (parts.size() >= 2) { + try { + int pid = std::stoi(parts[1]); + remotePlayers.erase(pid); + pendingDisconnects_.push_back(pid); + std::cout << "Client: Player " << pid << " disconnected (PLAYER_LEFT)\n"; + } + catch (...) {} + } + return; + } + if (msg.rfind("RESPAWN_ACK:", 0) == 0) { //auto parts = split(msg, ':'); if (parts.size() >= 2) { @@ -356,6 +369,12 @@ namespace ZL { return copy; } + std::vector WebSocketClientBase::getPendingDisconnects() { + std::vector copy; + copy.swap(pendingDisconnects_); + return copy; + } + std::vector WebSocketClientBase::getPendingSpawns() { std::vector copy; copy.swap(pendingSpawns_); diff --git a/src/network/WebSocketClientBase.h b/src/network/WebSocketClientBase.h index eb8cc8c..f74c9b5 100644 --- a/src/network/WebSocketClientBase.h +++ b/src/network/WebSocketClientBase.h @@ -20,6 +20,7 @@ namespace ZL { std::vector pendingDeaths_; std::vector pendingRespawns_; std::vector pendingBoxDestructions_; + std::vector pendingDisconnects_; int clientId = -1; int64_t timeOffset = 0; std::vector pendingSpawns_; @@ -49,6 +50,7 @@ namespace ZL { std::vector getPendingDeaths() override; std::vector getPendingRespawns() override; std::vector getPendingBoxDestructions() override; + std::vector getPendingDisconnects() override; std::vector getPendingSpawns(); int getClientId() const { return clientId; } };