diff --git a/server/server.cpp b/server/server.cpp index b981e90..afce8e6 100644 --- a/server/server.cpp +++ b/server/server.cpp @@ -23,6 +23,15 @@ namespace http = beast::http; namespace websocket = beast::websocket; namespace net = boost::asio; using tcp = net::ip::tcp; +static constexpr float kWorldZOffset = 45000.0f; +static const Eigen::Vector3f kWorldOffset(0.0f, 0.0f, kWorldZOffset); + +static constexpr float kShipRadius = 15.0f; +static constexpr float kSpawnShipMargin = 25.0f; +static constexpr float kSpawnBoxMargin = 15.0f; +static constexpr float kSpawnZJitter = 60.0f; + +Eigen::Vector3f PickSafeSpawnPos(int forPlayerId); struct DeathInfo { int targetId = -1; @@ -92,6 +101,10 @@ class Session : public std::enable_shared_from_this { public: ClientStateInterval timedClientStates; + bool joined_ = false; + + bool hasReservedSpawn_ = false; + Eigen::Vector3f reservedSpawn_ = Eigen::Vector3f(0.0f, 0.0f, kWorldZOffset); std::string nickname = "Player"; int shipType = 0; @@ -102,6 +115,9 @@ public: int get_id() const { return id_; } + bool hasSpawnReserved() const { return hasReservedSpawn_; } + const Eigen::Vector3f& reservedSpawn() const { return reservedSpawn_; } + bool fetchStateAtTime(std::chrono::system_clock::time_point targetTime, ClientState& outState) const { if (timedClientStates.canFetchClientStateAtTime(targetTime)) { outState = timedClientStates.fetchClientStateAtTime(targetTime); @@ -189,7 +205,11 @@ 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_)); + auto now_tp = std::chrono::system_clock::now(); + uint64_t now_ms = static_cast( + std::chrono::duration_cast(now_tp.time_since_epoch()).count()); + + self->send_message("ID:" + std::to_string(self->id_) + ":" + std::to_string(now_ms)); self->do_read(); } }); @@ -281,7 +301,45 @@ private: this->nickname = nick; this->shipType = sType; + this->joined_ = true; + auto now_tp = std::chrono::system_clock::now(); + uint64_t now_ms = static_cast( + std::chrono::duration_cast(now_tp.time_since_epoch()).count()); + + Eigen::Vector3f spawnPos = PickSafeSpawnPos(id_); + this->hasReservedSpawn_ = true; + this->reservedSpawn_ = spawnPos; + + ClientState st; + st.id = id_; + st.position = spawnPos; + st.rotation = Eigen::Matrix3f::Identity(); + st.currentAngularVelocity = Eigen::Vector3f::Zero(); + st.velocity = 0.0f; + st.selectedVelocity = 0; + st.discreteMag = 0.0f; + st.discreteAngle = -1; + st.lastUpdateServerTime = now_tp; + st.nickname = this->nickname; + st.shipType = this->shipType; + + timedClientStates.add_state(st); + + this->send_message( + "SPAWN:" + std::to_string(id_) + ":" + std::to_string(now_ms) + ":" + st.formPingMessageContent() + ); + + std::string eventMsg = + "EVENT:" + std::to_string(id_) + ":UPD:" + std::to_string(now_ms) + ":" + st.formPingMessageContent(); + + { + std::lock_guard lock(g_sessions_mutex); + for (auto& session : g_sessions) { + if (session->get_id() == id_) continue; + session->send_message(eventMsg); + } + } std::cout << "Server: Player " << id_ << " joined as [" << nick << "] shipType=" << sType << std::endl; std::string info = "PLAYERINFO:" + std::to_string(id_) + ":" + nick + ":" + std::to_string(sType); @@ -298,12 +356,16 @@ private: for (auto& session : g_sessions) { if (session->get_id() == this->id_) continue; std::string otherInfo = "PLAYERINFO:" + std::to_string(session->get_id()) + ":" + session->nickname + ":" + std::to_string(session->shipType); - // Отправляем именно новому клиенту + this->send_message(otherInfo); } } } else if (type == "UPD") { + if (!joined_) { + std::cout << "Server: Ignoring UPD before JOIN from " << id_ << std::endl; + return; + } { std::lock_guard gd(g_dead_mutex); if (g_dead_players.find(id_) != g_dead_players.end()) { @@ -338,7 +400,13 @@ private: ClientState st; st.id = id_; - st.position = Eigen::Vector3f(0.0f, 0.0f, 45000.0f); + + Eigen::Vector3f spawnPos = PickSafeSpawnPos(id_); + st.position = spawnPos; + + this->hasReservedSpawn_ = true; + this->reservedSpawn_ = spawnPos; + st.rotation = Eigen::Matrix3f::Identity(); st.currentAngularVelocity = Eigen::Vector3f::Zero(); st.velocity = 0.0f; @@ -350,7 +418,9 @@ private: st.shipType = this->shipType; timedClientStates.add_state(st); - + this->send_message( + "SPAWN:" + std::to_string(id_) + ":" + std::to_string(now_ms) + ":" + st.formPingMessageContent() + ); std::string respawnMsg = "RESPAWN_ACK:" + std::to_string(id_); broadcastToAll(respawnMsg); @@ -436,6 +506,71 @@ private: }; +Eigen::Vector3f PickSafeSpawnPos(int forPlayerId) +{ + static thread_local std::mt19937 rng{ std::random_device{}() }; + + std::scoped_lock lock(g_boxes_mutex, g_sessions_mutex, g_dead_mutex); + + auto isSafe = [&](const Eigen::Vector3f& pWorld) -> bool + { + for (const auto& box : g_serverBoxes) { + if (box.destroyed) continue; + + Eigen::Vector3f boxWorld = box.position + kWorldOffset; + float minDist = kShipRadius + box.collisionRadius + kSpawnBoxMargin; + + if ((pWorld - boxWorld).squaredNorm() < minDist * minDist) + return false; + } + + for (const auto& s : g_sessions) { + int pid = s->get_id(); + if (pid == forPlayerId) continue; + if (g_dead_players.count(pid)) continue; + + Eigen::Vector3f otherPos; + if (!s->timedClientStates.timedStates.empty()) { + otherPos = s->timedClientStates.timedStates.back().position; + } + else if (s->hasSpawnReserved()) { + otherPos = s->reservedSpawn(); + } + else { + continue; + } + + float minDist = (kShipRadius * 2.0f) + kSpawnShipMargin; + if ((pWorld - otherPos).squaredNorm() < minDist * minDist) + return false; + } + + return true; + }; + + const float radii[] = { 150.f, 250.f, 400.f, 650.f, 1000.f, 1600.f }; + + for (float r : radii) { + std::uniform_real_distribution dxy(-r, r); + std::uniform_real_distribution dz(-kSpawnZJitter, kSpawnZJitter); + + for (int attempt = 0; attempt < 250; ++attempt) { + Eigen::Vector3f cand( + dxy(rng), + dxy(rng), + kWorldZOffset + dz(rng) + ); + + if (isSafe(cand)) + return cand; + } + } + + int a = (forPlayerId % 10); + int b = ((forPlayerId / 10) % 10); + return Eigen::Vector3f(600.0f + a * 100.0f, -600.0f + b * 100.0f, kWorldZOffset); +} + void broadcastToAll(const std::string& message) { std::lock_guard lock(g_sessions_mutex); for (const auto& session : g_sessions) { diff --git a/src/Game.cpp b/src/Game.cpp index e653d94..72a7b8c 100644 --- a/src/Game.cpp +++ b/src/Game.cpp @@ -16,6 +16,7 @@ #endif #ifdef NETWORK +#include "network/WebSocketClientBase.h" #ifdef EMSCRIPTEN #include "network/WebSocketClientEmscripten.h" #else @@ -443,6 +444,31 @@ namespace ZL } #endif networkClient->Poll(); +#ifdef NETWORK + auto* wsBase = dynamic_cast(networkClient.get()); + if (wsBase) { + auto spawns = wsBase->getPendingSpawns(); + for (auto& st : spawns) { + if (st.id == wsBase->getClientId()) { + // применяем к локальному кораблю + ZL::Environment::shipState.position = st.position; + ZL::Environment::shipState.rotation = st.rotation; + + // обнуляем движение чтобы не было рывков + ZL::Environment::shipState.currentAngularVelocity = Eigen::Vector3f::Zero(); + ZL::Environment::shipState.velocity = 0.0f; + ZL::Environment::shipState.selectedVelocity = 0; + ZL::Environment::shipState.discreteMag = 0.0f; + ZL::Environment::shipState.discreteAngle = -1; + + std::cout << "Game: Applied SPAWN at " + << st.position.x() << ", " + << st.position.y() << ", " + << st.position.z() << std::endl; + } + } + } +#endif } mainThreadHandler.processMainThreadTasks(); diff --git a/src/network/WebSocketClientBase.cpp b/src/network/WebSocketClientBase.cpp index 6253f94..4e5ec20 100644 --- a/src/network/WebSocketClientBase.cpp +++ b/src/network/WebSocketClientBase.cpp @@ -190,7 +190,28 @@ namespace ZL { } return; } + if (msg.rfind("SPAWN:", 0) == 0) { + // SPAWN:playerId:serverTime:<14 полей> + if (parts.size() >= 3 + 14) { + try { + int pid = std::stoi(parts[1]); + uint64_t serverTime = std::stoull(parts[2]); + ClientState st; + st.id = pid; + std::chrono::system_clock::time_point tp{ std::chrono::milliseconds(serverTime) }; + st.lastUpdateServerTime = tp; + + // данные начинаются с parts[3] + st.handle_full_sync(parts, 3); + + pendingSpawns_.push_back(st); + std::cout << "Client: SPAWN received for player " << pid << std::endl; + } + catch (...) {} + } + return; + } if (msg.rfind("EVENT:", 0) == 0) { //auto parts = split(msg, ':'); if (parts.size() < 5) return; // EVENT:ID:TYPE:TIME:DATA... @@ -334,6 +355,12 @@ namespace ZL { copy.swap(pendingBoxDestructions_); return copy; } + + std::vector WebSocketClientBase::getPendingSpawns() { + std::vector copy; + copy.swap(pendingSpawns_); + return copy; + } } #endif \ No newline at end of file diff --git a/src/network/WebSocketClientBase.h b/src/network/WebSocketClientBase.h index 3784700..915952a 100644 --- a/src/network/WebSocketClientBase.h +++ b/src/network/WebSocketClientBase.h @@ -22,6 +22,7 @@ namespace ZL { std::vector pendingBoxDestructions_; int clientId = -1; int64_t timeOffset = 0; + std::vector pendingSpawns_; public: int GetClientId() const override { return clientId; } @@ -48,6 +49,8 @@ namespace ZL { std::vector getPendingDeaths() override; std::vector getPendingRespawns() override; std::vector getPendingBoxDestructions() override; + std::vector getPendingSpawns(); + int getClientId() const { return clientId; } }; }