Лобби: комнаты на сервере (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:
@@ -39,6 +39,7 @@ target_compile_definitions(usockets PUBLIC LIBUS_NO_SSL LIBUS_USE_EPOLL)
|
||||
|
||||
add_executable(gameserver
|
||||
src/main.cpp
|
||||
src/Lobby.cpp
|
||||
)
|
||||
target_include_directories(gameserver PRIVATE
|
||||
${uwebsockets_SOURCE_DIR}/src
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
#pragma once
|
||||
|
||||
#include <App.h>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
struct PerSocketData {
|
||||
std::string player_id;
|
||||
std::string nickname;
|
||||
std::string room_id; // пусто, если игрок в лобби, а не в комнате
|
||||
};
|
||||
|
||||
using WS = uWS::WebSocket<false, true, PerSocketData>;
|
||||
|
||||
struct RoomPlayer {
|
||||
std::string player_id;
|
||||
std::string nickname;
|
||||
WS *ws;
|
||||
};
|
||||
|
||||
struct Room {
|
||||
std::string id;
|
||||
std::string mode; // "coop" | "pvp"
|
||||
std::string state = "waiting"; // waiting | playing | finished
|
||||
std::vector<RoomPlayer> players;
|
||||
};
|
||||
|
||||
// Лобби и реестр комнат (этап 3 из TZ.md). Игровая логика — этапы 4-5.
|
||||
class Lobby {
|
||||
public:
|
||||
void onOpen(WS *ws);
|
||||
void onMessage(WS *ws, std::string_view message);
|
||||
void onClose(WS *ws);
|
||||
|
||||
private:
|
||||
static constexpr size_t kMaxPlayersPerRoom = 2;
|
||||
|
||||
std::unordered_map<std::string, Room> rooms_;
|
||||
std::unordered_set<WS *> sockets_;
|
||||
int next_player_id_ = 1;
|
||||
int next_room_id_ = 1;
|
||||
|
||||
void handleHello(WS *ws, const json &payload);
|
||||
void handleListRooms(WS *ws);
|
||||
void handleCreateRoom(WS *ws, const json &payload);
|
||||
void handleJoinRoom(WS *ws, const json &payload);
|
||||
void handleLeaveRoom(WS *ws);
|
||||
void removePlayerFromRoom(WS *ws);
|
||||
|
||||
void broadcastRoomsToLobby();
|
||||
void broadcastRoomUpdated(const Room &room, WS *exclude = nullptr);
|
||||
|
||||
json roomsSummary() const;
|
||||
json roomPlayers(const Room &room) const;
|
||||
std::string generateRoomId();
|
||||
|
||||
static void send(WS *ws, const std::string &type, json payload);
|
||||
static void sendError(WS *ws, const std::string &code, const std::string &message);
|
||||
};
|
||||
+10
-28
@@ -1,41 +1,23 @@
|
||||
// Каркас (этап 1 из TZ.md): проверка связки nginx <-ws-> gameserver.
|
||||
// Лобби/комнаты/игровая логика — следующие этапы.
|
||||
// Игровая логика (тик, коллизии) — этапы 4-5 из TZ.md.
|
||||
|
||||
#include <App.h>
|
||||
#include <iostream>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
struct PerSocketData {};
|
||||
#include "Lobby.hpp"
|
||||
|
||||
int main() {
|
||||
Lobby lobby;
|
||||
|
||||
uWS::App()
|
||||
.ws<PerSocketData>("/ws", {
|
||||
.open = [](auto *ws) {
|
||||
std::cout << "client connected\n";
|
||||
.open = [&lobby](auto *ws) {
|
||||
lobby.onOpen(ws);
|
||||
},
|
||||
.message = [](auto *ws, std::string_view message, uWS::OpCode) {
|
||||
json response;
|
||||
try {
|
||||
json request = json::parse(message);
|
||||
std::string type = request.value("type", "");
|
||||
|
||||
if (type == "hello") {
|
||||
response = {{"type", "hello.ack"}, {"payload", {{"player_id", "stub"}}}};
|
||||
} else if (type == "lobby.list_rooms") {
|
||||
response = {{"type", "lobby.rooms"}, {"payload", {{"rooms", json::array()}}}};
|
||||
} else {
|
||||
response = {{"type", "error"},
|
||||
{"payload", {{"code", "unknown_type"}, {"message", "unknown message type: " + type}}}};
|
||||
}
|
||||
} catch (const std::exception &e) {
|
||||
response = {{"type", "error"}, {"payload", {{"code", "bad_json"}, {"message", e.what()}}}};
|
||||
}
|
||||
ws->send(response.dump(), uWS::OpCode::TEXT);
|
||||
.message = [&lobby](auto *ws, std::string_view message, uWS::OpCode) {
|
||||
lobby.onMessage(ws, message);
|
||||
},
|
||||
.close = [](auto *ws, int, std::string_view) {
|
||||
std::cout << "client disconnected\n";
|
||||
.close = [&lobby](auto *ws, int, std::string_view) {
|
||||
lobby.onClose(ws);
|
||||
},
|
||||
})
|
||||
.listen(9001, [](auto *token) {
|
||||
|
||||
Reference in New Issue
Block a user