#include "Lobby.hpp" #include #include #include 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); } }