Лобби: комнаты на сервере (C++) и UI на клиенте

Сервер: Lobby/Room/Player — create/join/leave/list_rooms, broadcast обновлений,
проверка авторизации (hello) и состояния комнаты (waiting/full/in_progress).
Клиент: полноценный UI лобби (создание, список, вход в комнату, выход),
блокировка кнопок до установки WS-соединения.
nginx: no-cache для JS/CSS (нет хеширования имён файлов).
This commit is contained in:
2026-07-26 21:22:20 +05:00
parent 238d3d5d26
commit 864868de49
9 changed files with 538 additions and 42 deletions
+227
View File
@@ -0,0 +1,227 @@
#include "Lobby.hpp"
#include <algorithm>
#include <iomanip>
#include <sstream>
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);
}
void Lobby::sendError(WS *ws, const std::string &code, const std::string &message) {
send(ws, "error", {{"code", code}, {"message", message}});
}
std::string Lobby::generateRoomId() {
std::ostringstream oss;
oss << "cactus-room-" << std::setw(2) << std::setfill('0') << next_room_id_++;
return oss.str();
}
json Lobby::roomsSummary() const {
json rooms = json::array();
for (const auto &[id, room] : rooms_) {
rooms.push_back({
{"id", room.id},
{"name", room.id},
{"mode", room.mode},
{"players", room.players.size()},
{"max_players", kMaxPlayersPerRoom},
{"state", room.state},
});
}
return rooms;
}
json Lobby::roomPlayers(const Room &room) const {
json players = json::array();
for (size_t i = 0; i < room.players.size(); i++) {
players.push_back({
{"id", room.players[i].player_id},
{"nickname", room.players[i].nickname},
{"slot", i},
});
}
return players;
}
void Lobby::broadcastRoomsToLobby() {
json rooms = roomsSummary();
for (WS *client : sockets_) {
if (client->getUserData()->room_id.empty()) {
send(client, "lobby.rooms", {{"rooms", rooms}});
}
}
}
void Lobby::broadcastRoomUpdated(const Room &room, WS *exclude) {
json payload = {{"players", roomPlayers(room)}};
for (const auto &p : room.players) {
if (p.ws != exclude) {
send(p.ws, "lobby.room_updated", payload);
}
}
}
void Lobby::onOpen(WS *ws) {
sockets_.insert(ws);
}
void Lobby::onClose(WS *ws) {
removePlayerFromRoom(ws);
sockets_.erase(ws);
}
void Lobby::onMessage(WS *ws, std::string_view message) {
json request;
std::string type;
try {
request = json::parse(message);
type = request.value("type", "");
} catch (const std::exception &e) {
sendError(ws, "bad_json", e.what());
return;
}
json payload = request.value("payload", json::object());
if (type == "hello") {
handleHello(ws, payload);
} else if (type == "lobby.list_rooms") {
handleListRooms(ws);
} else if (type == "lobby.create_room") {
handleCreateRoom(ws, payload);
} else if (type == "lobby.join_room") {
handleJoinRoom(ws, payload);
} else if (type == "lobby.leave_room") {
handleLeaveRoom(ws);
} else {
sendError(ws, "unknown_type", "unknown message type: " + type);
}
}
void Lobby::handleHello(WS *ws, const json &payload) {
auto *data = ws->getUserData();
if (data->player_id.empty()) {
data->player_id = "player-" + std::to_string(next_player_id_++);
}
data->nickname = payload.value("nickname", "anon");
send(ws, "hello.ack", {{"player_id", data->player_id}});
}
void Lobby::handleListRooms(WS *ws) {
send(ws, "lobby.rooms", {{"rooms", roomsSummary()}});
}
void Lobby::handleCreateRoom(WS *ws, const json &payload) {
auto *data = ws->getUserData();
if (data->player_id.empty()) {
sendError(ws, "not_authenticated", "send hello first");
return;
}
if (!data->room_id.empty()) {
sendError(ws, "already_in_room", "leave the current room first");
return;
}
std::string mode = payload.value("mode", "");
if (mode != "coop" && mode != "pvp") {
sendError(ws, "invalid_mode", "mode must be \"coop\" or \"pvp\"");
return;
}
Room room;
room.id = generateRoomId();
room.mode = mode;
room.players.push_back({data->player_id, data->nickname, ws});
data->room_id = room.id;
std::string room_id = room.id;
rooms_[room_id] = std::move(room);
send(ws, "lobby.room_joined", {
{"room_id", room_id},
{"name", room_id},
{"mode", mode},
{"players", roomPlayers(rooms_[room_id])},
});
broadcastRoomsToLobby();
}
void Lobby::handleJoinRoom(WS *ws, const json &payload) {
auto *data = ws->getUserData();
if (data->player_id.empty()) {
sendError(ws, "not_authenticated", "send hello first");
return;
}
if (!data->room_id.empty()) {
sendError(ws, "already_in_room", "leave the current room first");
return;
}
std::string room_id = payload.value("room_id", "");
auto it = rooms_.find(room_id);
if (it == rooms_.end()) {
sendError(ws, "room_not_found", "no such room: " + room_id);
return;
}
Room &room = it->second;
if (room.state != "waiting") {
sendError(ws, "room_in_progress", "room is not accepting new players");
return;
}
if (room.players.size() >= kMaxPlayersPerRoom) {
sendError(ws, "room_full", "room is full");
return;
}
room.players.push_back({data->player_id, data->nickname, ws});
data->room_id = room_id;
send(ws, "lobby.room_joined", {
{"room_id", room_id},
{"name", room_id},
{"mode", room.mode},
{"players", roomPlayers(room)},
});
broadcastRoomUpdated(room, ws);
broadcastRoomsToLobby();
}
void Lobby::handleLeaveRoom(WS *ws) {
if (ws->getUserData()->room_id.empty()) {
sendError(ws, "not_in_room", "you are not in a room");
return;
}
removePlayerFromRoom(ws);
broadcastRoomsToLobby();
}
void Lobby::removePlayerFromRoom(WS *ws) {
auto *data = ws->getUserData();
if (data->room_id.empty()) {
return;
}
auto it = rooms_.find(data->room_id);
data->room_id.clear();
if (it == rooms_.end()) {
return;
}
Room &room = it->second;
room.players.erase(
std::remove_if(room.players.begin(), room.players.end(),
[ws](const RoomPlayer &p) { return p.ws == ws; }),
room.players.end());
if (room.players.empty()) {
rooms_.erase(it);
} else {
broadcastRoomUpdated(room);
}
}