diff --git a/proj-web/CMakeLists.txt b/proj-web/CMakeLists.txt index b613506..fd91bc4 100644 --- a/proj-web/CMakeLists.txt +++ b/proj-web/CMakeLists.txt @@ -65,6 +65,10 @@ set(SOURCES ../src/network/ClientState.cpp ../src/network/WebSocketClient.h ../src/network/WebSocketClient.cpp + ../src/network/WebSocketClientBase.h + ../src/network/WebSocketClientBase.cpp + ../src/network/WebSocketClientEmscripten.h + ../src/network/WebSocketClientEmscripten.cpp ../src/render/TextRenderer.h ../src/render/TextRenderer.cpp ) @@ -90,7 +94,7 @@ add_subdirectory("../thirdparty/libzip-1.11.4" libzip-build) # Линковка: # 'zip' берется из add_subdirectory # 'z' - это системный zlib Emscripten-а (флаг -sUSE_ZLIB=1 добавим ниже) -target_link_libraries(space-game001 PRIVATE zip z) +target_link_libraries(space-game001 PRIVATE zip z websocket) # Эмскриптен-флаги set(EMSCRIPTEN_FLAGS @@ -102,6 +106,7 @@ set(EMSCRIPTEN_FLAGS "-pthread" "-sUSE_PTHREADS=1" "-fexceptions" + "-DNETWORK" ) target_compile_options(space-game001 PRIVATE ${EMSCRIPTEN_FLAGS} "-O2") diff --git a/proj-windows/CMakeLists.txt b/proj-windows/CMakeLists.txt index bc1a45f..3d790e0 100644 --- a/proj-windows/CMakeLists.txt +++ b/proj-windows/CMakeLists.txt @@ -57,6 +57,10 @@ add_executable(space-game001 ../src/network/ClientState.cpp ../src/network/WebSocketClient.h ../src/network/WebSocketClient.cpp + ../src/network/WebSocketClientBase.h + ../src/network/WebSocketClientBase.cpp + ../src/network/WebSocketClientEmscripten.h + ../src/network/WebSocketClientEmscripten.cpp ../src/render/TextRenderer.h ../src/render/TextRenderer.cpp ) diff --git a/server/server.cpp b/server/server.cpp index 9f6ef0f..4615a1a 100644 --- a/server/server.cpp +++ b/server/server.cpp @@ -180,7 +180,9 @@ public: timer->expires_after(std::chrono::milliseconds(100)); timer->async_wait([self = shared_from_this(), timer](const boost::system::error_code& ec) { if (!ec) { - self->send_message("ID:" + std::to_string(self->id_)); + int64_t serverNow = std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()).count(); + self->send_message("ID:" + std::to_string(self->id_) + ":" + std::to_string(serverNow)); self->do_read(); } }); @@ -297,6 +299,7 @@ private: } void process_message(const std::string& msg) { + std::cout << "Received from player " << id_ << ": " << msg << std::endl; auto parts = split(msg, ':'); if (parts.empty()) return; @@ -754,11 +757,11 @@ int main() { std::cout << "Generated " << g_serverBoxes.size() << " boxes on server\n"; } net::io_context ioc; - tcp::acceptor acceptor{ ioc, {tcp::v4(), 8080} }; + tcp::acceptor acceptor{ ioc, {tcp::v4(), 8081} }; int next_id = 1000; - std::cout << "Server started on port 8080...\n"; + std::cout << "Server started on port 8081...\n"; auto do_accept = [&](auto& self_fn) -> void { acceptor.async_accept([&, self_fn](beast::error_code ec, tcp::socket socket) { diff --git a/src/Game.cpp b/src/Game.cpp index 5ce7e6c..4d75e39 100644 --- a/src/Game.cpp +++ b/src/Game.cpp @@ -16,7 +16,11 @@ #endif #ifdef NETWORK +#ifdef EMSCRIPTEN +#include "network/WebSocketClientEmscripten.h" +#else #include "network/WebSocketClient.h" +#endif #else #include "network/LocalClient.h" #endif @@ -389,7 +393,8 @@ namespace ZL std::to_string(speedToSend) + ":" + std::to_string(shotCount); - networkClient->Send(fireMsg); + //Temporary disable to avoid de-sync + //networkClient->Send(fireMsg); } }); @@ -486,8 +491,13 @@ namespace ZL planetObject.init(); #ifdef NETWORK +#ifdef EMSCRIPTEN + networkClient = std::make_unique(); + networkClient->Connect("localhost", 8081); +#else networkClient = std::make_unique(taskManager.getIOContext()); - networkClient->Connect("127.0.0.1", 8080); + networkClient->Connect("127.0.0.1", 8081); +#endif #else networkClient = std::make_unique(); networkClient->Connect("", 0); @@ -870,13 +880,22 @@ namespace ZL #endif } + int64_t Game::getSyncTimeMs() { + int64_t localNow = std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()).count(); + + std::cout << "getSyncTimeMs localNow = " << localNow << std::endl; + std::cout << "getSyncTimeMs getTimeOffset = " << networkClient->getTimeOffset() << std::endl; + + // Добавляем смещение, полученное от сервера + return localNow + networkClient->getTimeOffset(); // Нужно добавить геттер в интерфейс + } + void Game::processTickCount() { if (lastTickCount == 0) { //lastTickCount = SDL_GetTicks64(); - lastTickCount = std::chrono::duration_cast( - std::chrono::system_clock::now().time_since_epoch() - ).count(); + lastTickCount = getSyncTimeMs(); lastTickCount = (lastTickCount / 50) * 50; @@ -884,15 +903,13 @@ namespace ZL } //newTickCount = SDL_GetTicks64(); - newTickCount = std::chrono::duration_cast( - std::chrono::system_clock::now().time_since_epoch() - ).count(); + newTickCount = getSyncTimeMs(); newTickCount = (newTickCount / 50) * 50; if (newTickCount - lastTickCount > CONST_TIMER_INTERVAL) { - size_t delta = newTickCount - lastTickCount; + int64_t delta = newTickCount - lastTickCount; if (delta > CONST_MAX_TIME_INTERVAL) { //throw std::runtime_error("Synchronization is lost"); @@ -900,6 +917,8 @@ namespace ZL auto now_ms = newTickCount; + std::cout << "processTickCount = " << now_ms << std::endl; + sparkEmitter.update(static_cast(delta)); planetObject.update(static_cast(delta)); diff --git a/src/Game.h b/src/Game.h index 02711f6..e3eafdf 100644 --- a/src/Game.h +++ b/src/Game.h @@ -44,6 +44,7 @@ namespace ZL { std::unique_ptr networkClient; private: + int64_t getSyncTimeMs(); void processTickCount(); void drawScene(); void drawCubemap(float skyPercent); @@ -66,8 +67,8 @@ namespace ZL { - size_t newTickCount; - size_t lastTickCount; + int64_t newTickCount; + int64_t lastTickCount; std::vector boxCoordsArr; std::vector boxRenderArr; diff --git a/src/network/NetworkInterface.h b/src/network/NetworkInterface.h index 840f765..74dbdad 100644 --- a/src/network/NetworkInterface.h +++ b/src/network/NetworkInterface.h @@ -47,5 +47,7 @@ namespace ZL { virtual std::vector getPendingRespawns() = 0; virtual int GetClientId() const { return -1; } virtual std::vector getPendingBoxDestructions() = 0; + virtual int64_t getTimeOffset() const { return 0; } + }; } diff --git a/src/network/WebSocketClient.cpp b/src/network/WebSocketClient.cpp index 6dc7865..f614dc3 100644 --- a/src/network/WebSocketClient.cpp +++ b/src/network/WebSocketClient.cpp @@ -4,17 +4,6 @@ #include #include -// Вспомогательный split -std::vector split(const std::string& s, char delimiter) { - std::vector tokens; - std::string token; - std::istringstream tokenStream(s); - while (std::getline(tokenStream, token, delimiter)) { - tokens.push_back(token); - } - return tokens; -} - namespace ZL { void WebSocketClient::Connect(const std::string& host, uint16_t port) { @@ -55,43 +44,15 @@ namespace ZL { void WebSocketClient::processIncomingMessage(const std::string& msg) { // Логика парсинга... - if (msg.rfind("ID:", 0) == 0) { + /*if (msg.rfind("ID:", 0) == 0) { clientId = std::stoi(msg.substr(3)); - } + }*/ // Безопасно кладем в очередь для главного потока std::lock_guard lock(queueMutex); messageQueue.push(msg); } - std::vector WebSocketClient::getPendingProjectiles() { - std::lock_guard lock(projMutex_); - auto copy = pendingProjectiles_; - pendingProjectiles_.clear(); - return copy; - } - - std::vector WebSocketClient::getPendingDeaths() { - std::lock_guard lock(deathsMutex_); - auto copy = pendingDeaths_; - pendingDeaths_.clear(); - return copy; - } - - std::vector WebSocketClient::getPendingRespawns() { - std::lock_guard lock(respawnMutex_); - auto copy = pendingRespawns_; - pendingRespawns_.clear(); - return copy; - } - - std::vector WebSocketClient::getPendingBoxDestructions() { - std::lock_guard lock(boxDestructionsMutex_); - auto copy = pendingBoxDestructions_; - pendingBoxDestructions_.clear(); - return copy; - } - void WebSocketClient::Poll() { std::lock_guard lock(queueMutex); @@ -105,224 +66,15 @@ namespace ZL { auto now_ms = std::chrono::duration_cast( nowTime.time_since_epoch() ).count(); - - - std::string msg = messageQueue.front(); messageQueue.pop(); - // Обработка списка коробок от сервера - if (msg.rfind("BOXES:", 0) == 0) { - std::string payload = msg.substr(6); // после "BOXES:" - std::vector> parsedBoxes; - if (!payload.empty()) { - auto items = split(payload, '|'); - for (auto& item : items) { - if (item.empty()) continue; - auto parts = split(item, ':'); - if (parts.size() < 7) continue; - try { - float px = std::stof(parts[0]); - float py = std::stof(parts[1]); - float pz = std::stof(parts[2]); - Eigen::Quaternionf q( - std::stof(parts[3]), - std::stof(parts[4]), - std::stof(parts[5]), - std::stof(parts[6]) - ); - Eigen::Matrix3f rot = q.toRotationMatrix(); - parsedBoxes.emplace_back(Eigen::Vector3f{ px, py, pz }, rot); - } - catch (...) { - // пропускаем некорректную запись - continue; - } - } - } - { - std::lock_guard bLock(boxesMutex); - serverBoxes_ = std::move(parsedBoxes); - } - continue; - } - if (msg.rfind("RESPAWN_ACK:", 0) == 0) { - auto parts = split(msg, ':'); - if (parts.size() >= 2) { - try { - int respawnedPlayerId = std::stoi(parts[1]); - { - std::lock_guard rLock(respawnMutex_); - pendingRespawns_.push_back(respawnedPlayerId); - } - { - std::lock_guard pLock(playersMutex); - remotePlayers.erase(respawnedPlayerId); - } - std::cout << "Client: Received RESPAWN_ACK for player " << respawnedPlayerId << std::endl; - } - catch (...) {} - } - continue; - } - - if (msg.rfind("BOX_DESTROYED:", 0) == 0) { - auto parts = split(msg, ':'); - if (parts.size() >= 7) { - try { - BoxDestroyedInfo destruction; - destruction.boxIndex = std::stoi(parts[1]); - destruction.serverTime = std::stoull(parts[2]); - destruction.position = Eigen::Vector3f( - std::stof(parts[3]), - std::stof(parts[4]), - std::stof(parts[5]) - ); - destruction.destroyedBy = std::stoi(parts[6]); - - { - std::lock_guard lock(boxDestructionsMutex_); - pendingBoxDestructions_.push_back(destruction); - } - - std::cout << "Client: Received BOX_DESTROYED for box " << destruction.boxIndex - << " destroyed by player " << destruction.destroyedBy << std::endl; - } - catch (const std::exception& e) { - std::cerr << "Client: Error parsing BOX_DESTROYED: " << e.what() << std::endl; - } - } - continue; - } - - if (msg.rfind("PROJECTILE:", 0) == 0) { - auto parts = split(msg, ':'); - if (parts.size() >= 10) { - try { - ProjectileInfo pi; - pi.shooterId = std::stoi(parts[1]); - pi.clientTime = std::stoull(parts[2]); - pi.position = Eigen::Vector3f( - std::stof(parts[3]), - std::stof(parts[4]), - std::stof(parts[5]) - ); - Eigen::Quaternionf q( - std::stof(parts[6]), - std::stof(parts[7]), - std::stof(parts[8]), - std::stof(parts[9]) - ); - pi.rotation = q.toRotationMatrix(); - - pi.velocity = std::stof(parts[10]); - std::lock_guard pl(projMutex_); - pendingProjectiles_.push_back(pi); - } - catch (...) {} - } - continue; - } - - if (msg.rfind("DEAD:", 0) == 0) { - auto parts = split(msg, ':'); - if (parts.size() >= 7) { - try { - DeathInfo di; - di.serverTime = std::stoull(parts[1]); - di.targetId = std::stoi(parts[2]); - di.position = Eigen::Vector3f( - std::stof(parts[3]), - std::stof(parts[4]), - std::stof(parts[5]) - ); - di.killerId = std::stoi(parts[6]); - - std::lock_guard dl(deathsMutex_); - pendingDeaths_.push_back(di); - } - catch (...) {} - } - continue; - } - - if (msg.rfind("EVENT:", 0) == 0) { - auto parts = split(msg, ':'); - if (parts.size() < 5) continue; // EVENT:ID:TYPE:TIME:DATA... - - int remoteId = std::stoi(parts[1]); - std::string subType = parts[2]; - uint64_t sentTime = std::stoull(parts[3]); - - ClientState remoteState; - remoteState.id = remoteId; - - std::chrono::system_clock::time_point uptime_timepoint{ std::chrono::duration_cast(std::chrono::milliseconds(sentTime)) }; - remoteState.lastUpdateServerTime = uptime_timepoint; - - if (subType == "UPD") { - int startFrom = 4; - remoteState.position = { std::stof(parts[startFrom]), std::stof(parts[startFrom + 1]), std::stof(parts[startFrom + 2]) }; - Eigen::Quaternionf q( - std::stof(parts[startFrom + 3]), - std::stof(parts[startFrom + 4]), - std::stof(parts[startFrom + 5]), - std::stof(parts[startFrom + 6])); - remoteState.rotation = q.toRotationMatrix(); - - remoteState.currentAngularVelocity = Eigen::Vector3f{ - std::stof(parts[startFrom + 7]), - std::stof(parts[startFrom + 8]), - std::stof(parts[startFrom + 9]) }; - remoteState.velocity = std::stof(parts[startFrom + 10]); - remoteState.selectedVelocity = std::stoi(parts[startFrom + 11]); - remoteState.discreteMag = std::stof(parts[startFrom + 12]); - remoteState.discreteAngle = std::stoi(parts[startFrom + 13]); - } - else - { - throw std::runtime_error("Unknown EVENT subtype: " + subType); - } - - { - std::lock_guard pLock(playersMutex); - auto& rp = remotePlayers[remoteId]; - - rp.add_state(remoteState); - } - } - - if (msg.rfind("SNAPSHOT:", 0) == 0) { - auto mainParts = split(msg.substr(9), '|'); // Отсекаем "SNAPSHOT:" и делим по игрокам - if (mainParts.empty()) continue; - - uint64_t serverTimestamp = std::stoull(mainParts[0]); - std::chrono::system_clock::time_point serverTime{ std::chrono::milliseconds(serverTimestamp) }; - - for (size_t i = 1; i < mainParts.size(); ++i) { - auto playerParts = split(mainParts[i], ':'); - if (playerParts.size() < 15) continue; // ID + 14 полей ClientState - - int rId = std::stoi(playerParts[0]); - if (rId == clientId) continue; // Свое состояние игрок знает лучше всех (Client Side Prediction) - - ClientState remoteState; - remoteState.id = rId; - remoteState.lastUpdateServerTime = serverTime; - - // Используем твой handle_full_sync, начиная со 2-го индекса (пропускаем ID в playerParts) - remoteState.handle_full_sync(playerParts, 1); - - { - std::lock_guard pLock(playersMutex); - remotePlayers[rId].add_state(remoteState); - } - } - continue; - } - + HandlePollMessage(msg); } } + + + void WebSocketClient::Send(const std::string& message) { if (!connected) return; @@ -364,4 +116,4 @@ namespace ZL { } } -#endif +#endif \ No newline at end of file diff --git a/src/network/WebSocketClient.h b/src/network/WebSocketClient.h index e877f50..23a03c1 100644 --- a/src/network/WebSocketClient.h +++ b/src/network/WebSocketClient.h @@ -2,7 +2,7 @@ #ifdef NETWORK -#include "NetworkInterface.h" +#include "WebSocketClientBase.h" #include #include #include @@ -11,7 +11,7 @@ namespace ZL { - class WebSocketClient : public INetworkClient { + class WebSocketClient : public WebSocketClientBase { private: // Переиспользуем io_context из TaskManager boost::asio::io_context& ioc_; @@ -28,26 +28,7 @@ namespace ZL { std::mutex writeMutex_; // Отдельный мьютекс для очереди записи bool connected = false; - int clientId = -1; - std::unordered_map remotePlayers; - std::mutex playersMutex; - - // Серверные коробки - std::vector> serverBoxes_; - std::mutex boxesMutex; - - std::vector pendingProjectiles_; - std::mutex projMutex_; - - std::vector pendingDeaths_; - std::mutex deathsMutex_; - - std::vector pendingRespawns_; - std::mutex respawnMutex_; - - std::vector pendingBoxDestructions_; - std::mutex boxDestructionsMutex_; void startAsyncRead(); void processIncomingMessage(const std::string& msg); @@ -63,22 +44,6 @@ namespace ZL { void doWrite(); bool IsConnected() const override { return connected; } - int GetClientId() const override { return clientId; } - - std::unordered_map getRemotePlayers() override { - std::lock_guard lock(playersMutex); - return remotePlayers; - } - - std::vector> getServerBoxes() override { - std::lock_guard lock(boxesMutex); - return serverBoxes_; - } - - std::vector getPendingProjectiles() override; - std::vector getPendingDeaths() override; - std::vector getPendingRespawns() override; - std::vector getPendingBoxDestructions() override; }; } #endif diff --git a/src/network/WebSocketClientBase.cpp b/src/network/WebSocketClientBase.cpp new file mode 100644 index 0000000..47f6241 --- /dev/null +++ b/src/network/WebSocketClientBase.cpp @@ -0,0 +1,283 @@ +#ifdef NETWORK + +#include "WebSocketClientBase.h" +#include +#include + +// Вспомогательный split +std::vector split(const std::string& s, char delimiter) { + std::vector tokens; + std::string token; + std::istringstream tokenStream(s); + while (std::getline(tokenStream, token, delimiter)) { + tokens.push_back(token); + } + return tokens; +} + +namespace ZL { + + void WebSocketClientBase::HandlePollMessage(const std::string& msg) { + auto parts = split(msg, ':'); + if (parts.empty()) return; + + if (parts[0] == "ID") { + std::cout << "ID Message Received:" << msg << std::endl; + clientId = std::stoi(parts[1]); + if (parts.size() >= 3) { + std::cout << "ID Message Received step 2" << std::endl; + uint64_t serverTime = std::stoull(parts[2]); + uint64_t localTime = std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()).count(); + + std::cout << "ID Message Received localTime = " << localTime << std::endl; + std::cout << "ID Message Received serverTime = " << serverTime << std::endl; + + // Вычисляем смещение + timeOffset = static_cast(serverTime) - static_cast(localTime); + + std::cout << "Time synchronized. Offset: " << timeOffset << " ms" << std::endl; + } + return; + } + + // Обработка списка коробок от сервера + if (msg.rfind("BOXES:", 0) == 0) { + std::string payload = msg.substr(6); // после "BOXES:" + std::vector> parsedBoxes; + if (!payload.empty()) { + auto items = split(payload, '|'); + for (auto& item : items) { + if (item.empty()) return; + auto parts = split(item, ':'); + if (parts.size() < 7) return; + try { + float px = std::stof(parts[0]); + float py = std::stof(parts[1]); + float pz = std::stof(parts[2]); + Eigen::Quaternionf q( + std::stof(parts[3]), + std::stof(parts[4]), + std::stof(parts[5]), + std::stof(parts[6]) + ); + Eigen::Matrix3f rot = q.toRotationMatrix(); + parsedBoxes.emplace_back(Eigen::Vector3f{ px, py, pz }, rot); + } + catch (...) { + // пропускаем некорректную запись + return; + } + } + } + { + std::lock_guard bLock(boxesMutex); + serverBoxes_ = std::move(parsedBoxes); + } + return; + } + if (msg.rfind("RESPAWN_ACK:", 0) == 0) { + //auto parts = split(msg, ':'); + if (parts.size() >= 2) { + try { + int respawnedPlayerId = std::stoi(parts[1]); + { + std::lock_guard rLock(respawnMutex_); + pendingRespawns_.push_back(respawnedPlayerId); + } + { + std::lock_guard pLock(playersMutex); + remotePlayers.erase(respawnedPlayerId); + } + std::cout << "Client: Received RESPAWN_ACK for player " << respawnedPlayerId << std::endl; + } + catch (...) {} + } + return; + } + + if (msg.rfind("BOX_DESTROYED:", 0) == 0) { + //auto parts = split(msg, ':'); + if (parts.size() >= 7) { + try { + BoxDestroyedInfo destruction; + destruction.boxIndex = std::stoi(parts[1]); + destruction.serverTime = std::stoull(parts[2]); + destruction.position = Eigen::Vector3f( + std::stof(parts[3]), + std::stof(parts[4]), + std::stof(parts[5]) + ); + destruction.destroyedBy = std::stoi(parts[6]); + + { + std::lock_guard lock(boxDestructionsMutex_); + pendingBoxDestructions_.push_back(destruction); + } + + std::cout << "Client: Received BOX_DESTROYED for box " << destruction.boxIndex + << " destroyed by player " << destruction.destroyedBy << std::endl; + } + catch (const std::exception& e) { + std::cerr << "Client: Error parsing BOX_DESTROYED: " << e.what() << std::endl; + } + } + return; + } + + if (msg.rfind("PROJECTILE:", 0) == 0) { + //auto parts = split(msg, ':'); + if (parts.size() >= 10) { + try { + ProjectileInfo pi; + pi.shooterId = std::stoi(parts[1]); + pi.clientTime = std::stoull(parts[2]); + pi.position = Eigen::Vector3f( + std::stof(parts[3]), + std::stof(parts[4]), + std::stof(parts[5]) + ); + Eigen::Quaternionf q( + std::stof(parts[6]), + std::stof(parts[7]), + std::stof(parts[8]), + std::stof(parts[9]) + ); + pi.rotation = q.toRotationMatrix(); + + pi.velocity = std::stof(parts[10]); + std::lock_guard pl(projMutex_); + pendingProjectiles_.push_back(pi); + } + catch (...) {} + } + return; + } + + if (msg.rfind("DEAD:", 0) == 0) { + //auto parts = split(msg, ':'); + if (parts.size() >= 7) { + try { + DeathInfo di; + di.serverTime = std::stoull(parts[1]); + di.targetId = std::stoi(parts[2]); + di.position = Eigen::Vector3f( + std::stof(parts[3]), + std::stof(parts[4]), + std::stof(parts[5]) + ); + di.killerId = std::stoi(parts[6]); + + std::lock_guard dl(deathsMutex_); + pendingDeaths_.push_back(di); + } + catch (...) {} + } + return; + } + + if (msg.rfind("EVENT:", 0) == 0) { + //auto parts = split(msg, ':'); + if (parts.size() < 5) return; // EVENT:ID:TYPE:TIME:DATA... + + int remoteId = std::stoi(parts[1]); + std::string subType = parts[2]; + uint64_t sentTime = std::stoull(parts[3]); + + ClientState remoteState; + remoteState.id = remoteId; + + std::chrono::system_clock::time_point uptime_timepoint{ std::chrono::duration_cast(std::chrono::milliseconds(sentTime)) }; + remoteState.lastUpdateServerTime = uptime_timepoint; + + if (subType == "UPD") { + int startFrom = 4; + remoteState.position = { std::stof(parts[startFrom]), std::stof(parts[startFrom + 1]), std::stof(parts[startFrom + 2]) }; + Eigen::Quaternionf q( + std::stof(parts[startFrom + 3]), + std::stof(parts[startFrom + 4]), + std::stof(parts[startFrom + 5]), + std::stof(parts[startFrom + 6])); + remoteState.rotation = q.toRotationMatrix(); + + remoteState.currentAngularVelocity = Eigen::Vector3f{ + std::stof(parts[startFrom + 7]), + std::stof(parts[startFrom + 8]), + std::stof(parts[startFrom + 9]) }; + remoteState.velocity = std::stof(parts[startFrom + 10]); + remoteState.selectedVelocity = std::stoi(parts[startFrom + 11]); + remoteState.discreteMag = std::stof(parts[startFrom + 12]); + remoteState.discreteAngle = std::stoi(parts[startFrom + 13]); + } + else + { + throw std::runtime_error("Unknown EVENT subtype: " + subType); + } + + { + std::lock_guard pLock(playersMutex); + auto& rp = remotePlayers[remoteId]; + + rp.add_state(remoteState); + } + } + + if (msg.rfind("SNAPSHOT:", 0) == 0) { + auto mainParts = split(msg.substr(9), '|'); // Отсекаем "SNAPSHOT:" и делим по игрокам + if (mainParts.empty()) return; + + uint64_t serverTimestamp = std::stoull(mainParts[0]); + std::chrono::system_clock::time_point serverTime{ std::chrono::milliseconds(serverTimestamp) }; + + for (size_t i = 1; i < mainParts.size(); ++i) { + auto playerParts = split(mainParts[i], ':'); + if (playerParts.size() < 15) return; // ID + 14 полей ClientState + + int rId = std::stoi(playerParts[0]); + if (rId == clientId) return; // Свое состояние игрок знает лучше всех (Client Side Prediction) + + ClientState remoteState; + remoteState.id = rId; + remoteState.lastUpdateServerTime = serverTime; + + // Используем твой handle_full_sync, начиная со 2-го индекса (пропускаем ID в playerParts) + remoteState.handle_full_sync(playerParts, 1); + + { + std::lock_guard pLock(playersMutex); + remotePlayers[rId].add_state(remoteState); + } + } + } + } + + std::vector WebSocketClientBase::getPendingProjectiles() { + std::lock_guard lock(projMutex_); + auto copy = pendingProjectiles_; + pendingProjectiles_.clear(); + return copy; + } + + std::vector WebSocketClientBase::getPendingDeaths() { + std::lock_guard lock(deathsMutex_); + auto copy = pendingDeaths_; + pendingDeaths_.clear(); + return copy; + } + + std::vector WebSocketClientBase::getPendingRespawns() { + std::lock_guard lock(respawnMutex_); + auto copy = pendingRespawns_; + pendingRespawns_.clear(); + return copy; + } + + std::vector WebSocketClientBase::getPendingBoxDestructions() { + std::lock_guard lock(boxDestructionsMutex_); + auto copy = pendingBoxDestructions_; + pendingBoxDestructions_.clear(); + return copy; + } +} + +#endif \ No newline at end of file diff --git a/src/network/WebSocketClientBase.h b/src/network/WebSocketClientBase.h new file mode 100644 index 0000000..1aabf0e --- /dev/null +++ b/src/network/WebSocketClientBase.h @@ -0,0 +1,58 @@ +#pragma once + +#include "NetworkInterface.h" +#include +#include + +namespace ZL { + + class WebSocketClientBase : public INetworkClient { + protected: + + + std::unordered_map remotePlayers; + std::mutex playersMutex; + + // Серверные коробки + std::vector> serverBoxes_; + std::mutex boxesMutex; + + std::vector pendingProjectiles_; + std::mutex projMutex_; + + std::vector pendingDeaths_; + std::mutex deathsMutex_; + + std::vector pendingRespawns_; + std::mutex respawnMutex_; + + std::vector pendingBoxDestructions_; + std::mutex boxDestructionsMutex_; + int clientId = -1; + int64_t timeOffset = 0; + + public: + int GetClientId() const override { return clientId; } + + int64_t getTimeOffset() const override { return timeOffset; } + + void HandlePollMessage(const std::string& msg); + + std::unordered_map getRemotePlayers() override { + std::lock_guard lock(playersMutex); + return remotePlayers; + } + + std::vector> getServerBoxes() override { + std::lock_guard lock(boxesMutex); + return serverBoxes_; + } + + std::vector getPendingProjectiles() override; + std::vector getPendingDeaths() override; + std::vector getPendingRespawns() override; + std::vector getPendingBoxDestructions() override; + }; + +} + diff --git a/src/network/WebSocketClientEmscripten.cpp b/src/network/WebSocketClientEmscripten.cpp new file mode 100644 index 0000000..a3418fa --- /dev/null +++ b/src/network/WebSocketClientEmscripten.cpp @@ -0,0 +1,91 @@ +#ifdef NETWORK +#ifdef EMSCRIPTEN +#include "WebSocketClientEmscripten.h" +#include +#include + +namespace ZL { + void WebSocketClientEmscripten::Connect(const std::string& host, uint16_t port) { + // Формируем URL. Обратите внимание, что в Web часто лучше использовать ws://localhost + std::string url = "ws://" + host + ":" + std::to_string(port); + + EmscriptenWebSocketCreateAttributes attr = { + url.c_str(), + nullptr, + EM_TRUE // create_on_main_thread + }; + + socket_ = emscripten_websocket_new(&attr); + + emscripten_websocket_set_onopen_callback(socket_, this, onOpen); + emscripten_websocket_set_onmessage_callback(socket_, this, onMessage); + emscripten_websocket_set_onerror_callback(socket_, this, onError); + emscripten_websocket_set_onclose_callback(socket_, this, onClose); + } + + void WebSocketClientEmscripten::Send(const std::string& message) { + if (connected && socket_ > 0) { + emscripten_websocket_send_utf8_text(socket_, message.c_str()); + } + } + + void WebSocketClientEmscripten::Poll() { + // Локальная очередь для минимизации времени блокировки мьютекса + std::queue localQueue; + + { + std::lock_guard lock(queueMutex); + if (messageQueue.empty()) return; + std::swap(localQueue, messageQueue); + } + + while (!localQueue.empty()) { + const std::string& msg = localQueue.front(); + std::cout << "[WebWS] Processing message: " << msg << std::endl; + + // Передаем в базовый класс для парсинга игровых событий (BOXES, UPD, и т.д.) + HandlePollMessage(msg); + + localQueue.pop(); + } + } + + // --- Колбэки --- + + EM_BOOL WebSocketClientEmscripten::onOpen(int eventType, const EmscriptenWebSocketOpenEvent* e, void* userData) { + auto* self = static_cast(userData); + self->connected = true; + std::cout << "[WebWS] Connection opened" << std::endl; + return EM_TRUE; + } + + EM_BOOL WebSocketClientEmscripten::onMessage(int eventType, const EmscriptenWebSocketMessageEvent* e, void* userData) { + std::cout << "[WebWS] onMessage " << std::endl; + auto* self = static_cast(userData); + if (e->isText && e->data) { + std::string msg(reinterpret_cast(e->data), e->numBytes); + std::lock_guard lock(self->queueMutex); + self->messageQueue.push(msg); + } + return EM_TRUE; + } + + EM_BOOL WebSocketClientEmscripten::onError(int eventType, const EmscriptenWebSocketErrorEvent* e, void* userData) { + auto* self = static_cast(userData); + self->connected = false; + std::cerr << "[WebWS] Error detected" << std::endl; + return EM_TRUE; + } + + EM_BOOL WebSocketClientEmscripten::onClose(int eventType, const EmscriptenWebSocketCloseEvent* e, void* userData) { + auto* self = static_cast(userData); + self->connected = false; + std::cout << "[WebWS] Connection closed" << std::endl; + return EM_TRUE; + } + +} + +#endif +#endif + diff --git a/src/network/WebSocketClientEmscripten.h b/src/network/WebSocketClientEmscripten.h new file mode 100644 index 0000000..2aac059 --- /dev/null +++ b/src/network/WebSocketClientEmscripten.h @@ -0,0 +1,42 @@ +#pragma once + +#ifdef NETWORK +#ifdef EMSCRIPTEN + +#include +#include "WebSocketClientBase.h" +#include +#include +#include + +namespace ZL { + + class WebSocketClientEmscripten : public WebSocketClientBase { + private: + EMSCRIPTEN_WEBSOCKET_T socket_ = 0; + bool connected = false; + + // Очередь для хранения сырых строк от браузера + std::queue messageQueue; + std::mutex queueMutex; + + public: + WebSocketClientEmscripten() = default; + virtual ~WebSocketClientEmscripten() = default; + + void Connect(const std::string& host, uint16_t port) override; + void Send(const std::string& message) override; + void Poll() override; + + bool IsConnected() const override { return connected; } + + // Статические методы-переходники для C-API Emscripten + static EM_BOOL onOpen(int eventType, const EmscriptenWebSocketOpenEvent* e, void* userData); + static EM_BOOL onMessage(int eventType, const EmscriptenWebSocketMessageEvent* e, void* userData); + static EM_BOOL onError(int eventType, const EmscriptenWebSocketErrorEvent* e, void* userData); + static EM_BOOL onClose(int eventType, const EmscriptenWebSocketCloseEvent* e, void* userData); + }; +} +#endif +#endif + diff --git a/src/render/TextRenderer.cpp b/src/render/TextRenderer.cpp index 067086e..0d9d9fc 100644 --- a/src/render/TextRenderer.cpp +++ b/src/render/TextRenderer.cpp @@ -11,21 +11,14 @@ namespace ZL { TextRenderer::~TextRenderer() { - /*for (auto& kv : glyphs) { - if (kv.second.texID) glDeleteTextures(1, &kv.second.texID); - }*/ glyphs.clear(); textMesh.positionVBO.reset(); - /* - if (vbo) glDeleteBuffers(1, &vbo); - // if (vao) glDeleteVertexArrays(1, &vao); - vao = 0; - vbo = 0;*/ } bool TextRenderer::init(Renderer& renderer, const std::string& ttfPath, int pixelSize, const std::string& zipfilename) { + r = &renderer; #ifdef EMSCRIPTEN @@ -47,15 +40,6 @@ bool TextRenderer::init(Renderer& renderer, const std::string& ttfPath, int pixe textMesh.data.PositionData.resize(6, Eigen::Vector3f(0, 0, 0)); textMesh.RefreshVBO(); - // VAO/VBO для 6 вершин (2 треугольника) * (pos.xy + uv.xy) = 4 float - // glGenVertexArrays(1, &vao); - /* glGenBuffers(1, &vbo); - - //glBindVertexArray(vao); - glBindBuffer(GL_ARRAY_BUFFER, vbo); - glBufferData(GL_ARRAY_BUFFER, sizeof(float) * 6 * 4, nullptr, GL_DYNAMIC_DRAW); - glBindBuffer(GL_ARRAY_BUFFER, 0); - */ ZL::CheckGlError(); return true; } @@ -142,6 +126,7 @@ bool TextRenderer::loadGlyphs(const std::string& ttfPath, int pixelSize, const s void TextRenderer::drawText(const std::string& text, float x, float y, float scale, bool centered, std::array color) { + if (!r || text.empty()) return; // 1. Считаем ширину для центрирования