Игровая логика PvP: сервер (C++) + клиент (Quintus.js)
Сервер: Game — authoritative-тик 20 Гц через uSockets timer, движение/коллизии танков и пуль, разрушаемые кирпичные стены, respawn, победа/поражение/ничья, технический проигрыш при дисконнекте. game.ready запускает матч, когда оба игрока в pvp-комнате готовы. Клиент: заведён Quintus.js (MIT, vendored) — Tank/Bullet/Wall спрайты, рендер по снапшотам game.state, keyboard-инпут (WASD/стрелки/space), экран результата матча. Найден и исправлен use-after-free: колбэк onFinished_ синхронно удалял Game изнутри его же метода — перенесено на uWS::Loop::defer.
This commit is contained in:
@@ -1,9 +1,13 @@
|
||||
#include "Lobby.hpp"
|
||||
#include "Game.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
|
||||
Lobby::Lobby() = default;
|
||||
Lobby::~Lobby() = default;
|
||||
|
||||
void Lobby::send(WS *ws, const std::string &type, json payload) {
|
||||
json msg = {{"type", type}, {"payload", std::move(payload)}};
|
||||
ws->send(msg.dump(), uWS::OpCode::TEXT);
|
||||
@@ -41,6 +45,7 @@ json Lobby::roomPlayers(const Room &room) const {
|
||||
{"id", room.players[i].player_id},
|
||||
{"nickname", room.players[i].nickname},
|
||||
{"slot", i},
|
||||
{"ready", room.players[i].ready},
|
||||
});
|
||||
}
|
||||
return players;
|
||||
@@ -69,6 +74,7 @@ void Lobby::onOpen(WS *ws) {
|
||||
}
|
||||
|
||||
void Lobby::onClose(WS *ws) {
|
||||
endActiveGameIfAny(ws->getUserData()->room_id, ws);
|
||||
removePlayerFromRoom(ws);
|
||||
sockets_.erase(ws);
|
||||
}
|
||||
@@ -96,6 +102,10 @@ void Lobby::onMessage(WS *ws, std::string_view message) {
|
||||
handleJoinRoom(ws, payload);
|
||||
} else if (type == "lobby.leave_room") {
|
||||
handleLeaveRoom(ws);
|
||||
} else if (type == "game.ready") {
|
||||
handleGameReady(ws);
|
||||
} else if (type == "game.input") {
|
||||
handleGameInput(ws, payload);
|
||||
} else {
|
||||
sendError(ws, "unknown_type", "unknown message type: " + type);
|
||||
}
|
||||
@@ -197,10 +207,98 @@ void Lobby::handleLeaveRoom(WS *ws) {
|
||||
sendError(ws, "not_in_room", "you are not in a room");
|
||||
return;
|
||||
}
|
||||
endActiveGameIfAny(ws->getUserData()->room_id, ws);
|
||||
removePlayerFromRoom(ws);
|
||||
broadcastRoomsToLobby();
|
||||
}
|
||||
|
||||
void Lobby::handleGameReady(WS *ws) {
|
||||
auto *data = ws->getUserData();
|
||||
if (data->room_id.empty()) {
|
||||
sendError(ws, "not_in_room", "you are not in a room");
|
||||
return;
|
||||
}
|
||||
|
||||
auto it = rooms_.find(data->room_id);
|
||||
if (it == rooms_.end()) {
|
||||
return;
|
||||
}
|
||||
for (auto &p : it->second.players) {
|
||||
if (p.ws == ws) {
|
||||
p.ready = true;
|
||||
}
|
||||
}
|
||||
broadcastRoomUpdated(it->second);
|
||||
maybeStartGame(data->room_id);
|
||||
}
|
||||
|
||||
void Lobby::handleGameInput(WS *ws, const json &payload) {
|
||||
auto *data = ws->getUserData();
|
||||
if (data->room_id.empty()) {
|
||||
return;
|
||||
}
|
||||
auto it = games_.find(data->room_id);
|
||||
if (it == games_.end()) {
|
||||
return;
|
||||
}
|
||||
it->second->handleInput(data->player_id, payload);
|
||||
}
|
||||
|
||||
void Lobby::maybeStartGame(const std::string &room_id) {
|
||||
auto it = rooms_.find(room_id);
|
||||
if (it == rooms_.end()) {
|
||||
return;
|
||||
}
|
||||
Room &room = it->second;
|
||||
if (room.mode != "pvp" || room.state != "waiting" || room.players.size() != kMaxPlayersPerRoom) {
|
||||
return;
|
||||
}
|
||||
bool allReady = std::all_of(room.players.begin(), room.players.end(),
|
||||
[](const RoomPlayer &p) { return p.ready; });
|
||||
if (!allReady) {
|
||||
return;
|
||||
}
|
||||
|
||||
room.state = "playing";
|
||||
|
||||
auto game = std::make_unique<Game>(room, [this, room_id]() {
|
||||
auto rit = rooms_.find(room_id);
|
||||
if (rit != rooms_.end()) {
|
||||
rit->second.state = "waiting";
|
||||
for (auto &p : rit->second.players) {
|
||||
p.ready = false;
|
||||
}
|
||||
broadcastRoomUpdated(rit->second);
|
||||
}
|
||||
broadcastRoomsToLobby();
|
||||
// Мы всё ещё внутри вызова метода Game (tick/handleDisconnect), который
|
||||
// и вызвал этот колбэк — удалять объект прямо сейчас было бы use-after-free
|
||||
// на возврате из этого вызова. Откладываем erase на следующий тик луп'а.
|
||||
uWS::Loop::get()->defer([this, room_id]() { games_.erase(room_id); });
|
||||
});
|
||||
|
||||
json startPayload = game->buildStartPayload(room.mode);
|
||||
for (const auto &p : room.players) {
|
||||
send(p.ws, "game.start", startPayload);
|
||||
}
|
||||
game->start();
|
||||
games_[room_id] = std::move(game);
|
||||
|
||||
broadcastRoomsToLobby();
|
||||
}
|
||||
|
||||
void Lobby::endActiveGameIfAny(const std::string &room_id, WS *leavingWs) {
|
||||
if (room_id.empty()) {
|
||||
return;
|
||||
}
|
||||
auto it = games_.find(room_id);
|
||||
if (it == games_.end()) {
|
||||
return;
|
||||
}
|
||||
it->second->handleDisconnect(leavingWs);
|
||||
games_.erase(it);
|
||||
}
|
||||
|
||||
void Lobby::removePlayerFromRoom(WS *ws) {
|
||||
auto *data = ws->getUserData();
|
||||
if (data->room_id.empty()) {
|
||||
|
||||
Reference in New Issue
Block a user