From 1bee7ec174b591cdf2293477ddad0fced043b917 Mon Sep 17 00:00:00 2001 From: George King <98261225+GeorgeK1ng@users.noreply.github.com> Date: Tue, 21 Apr 2026 14:41:42 +0200 Subject: [PATCH] Start game improvements --- .../Content/config/translations/czech.json | 7 ++ .../Content/config/translations/english.json | 7 ++ client/CServerHandler.cpp | 20 ++++- client/CServerHandler.h | 2 + client/globalLobby/GlobalLobbyClient.cpp | 2 +- client/lobby/CBonusSelection.cpp | 3 + client/lobby/CLobbyScreen.cpp | 81 +++++++++++++++++-- client/lobby/CLobbyScreen.h | 7 ++ client/lobby/SelectionTab.cpp | 72 +++++++++++++++++ client/lobby/SelectionTab.h | 3 + client/mainmenu/CMainMenu.cpp | 23 +++++- client/mainmenu/CMainMenu.h | 5 +- server/CVCMIServer.cpp | 46 ++++++++++- 13 files changed, 261 insertions(+), 17 deletions(-) diff --git a/Mods/vcmi/Content/config/translations/czech.json b/Mods/vcmi/Content/config/translations/czech.json index 46ea64a2f..88b985d83 100644 --- a/Mods/vcmi/Content/config/translations/czech.json +++ b/Mods/vcmi/Content/config/translations/czech.json @@ -795,6 +795,7 @@ "vcmi.lobby.invite.header": "Pozvat hráče", "vcmi.lobby.invite.notification": "Hráč vás pozval do své soukromé místnosti. Nyní se k ní můžete připojit.", "vcmi.lobby.login.as": "Přihlásit se jako %s", + "vcmi.lobby.login.connectionLost": "Spojení s herní lobby bylo ztraceno!", "vcmi.lobby.login.connecting": "Připojování...", "vcmi.lobby.login.create": "Nový účet", "vcmi.lobby.login.error": "Chyba při připojování: %s", @@ -828,6 +829,12 @@ "vcmi.lobby.preview.version": "Verze hry:", "vcmi.lobby.pvp.coin.help": "Hodit mincí", "vcmi.lobby.pvp.coin.hover": "Mince", + "vcmi.lobby.system.waitingForPlayers": "Čekání na připojení hráčů...", + "vcmi.lobby.system.playerJoined": "Hráč %s se připojil", + "vcmi.lobby.system.playerDisconnected": "Hráč %s se odpojil", + "vcmi.lobby.system.hidingIncompatibleMaps": "Skrývám nekompatibilní mapy pro %d hráčů", + "vcmi.lobby.system.unableStartMap": "Mapu nelze spustit!", + "vcmi.lobby.system.reason": "Důvod: %s", "vcmi.lobby.pvp.randomTown.help": "Napsat náhodné město do chatu", "vcmi.lobby.pvp.randomTown.hover": "Náhodné město", "vcmi.lobby.pvp.randomTownVs.help": "Napsat 2 náhodná města do chatu", diff --git a/Mods/vcmi/Content/config/translations/english.json b/Mods/vcmi/Content/config/translations/english.json index 057093970..28b137653 100644 --- a/Mods/vcmi/Content/config/translations/english.json +++ b/Mods/vcmi/Content/config/translations/english.json @@ -795,6 +795,7 @@ "vcmi.lobby.invite.header": "Invite Players", "vcmi.lobby.invite.notification": "Player has invited you to their game room. You can now join their private room.", "vcmi.lobby.login.as": "Login as %s", + "vcmi.lobby.login.connectionLost": "Connection to game lobby was lost!", "vcmi.lobby.login.connecting": "Connecting...", "vcmi.lobby.login.create": "New Account", "vcmi.lobby.login.error": "Connection error: %s", @@ -828,6 +829,12 @@ "vcmi.lobby.preview.version": "Game version:", "vcmi.lobby.pvp.coin.help": "Flips a coin", "vcmi.lobby.pvp.coin.hover": "Coin", + "vcmi.lobby.system.waitingForPlayers": "Waiting for players to join...", + "vcmi.lobby.system.playerJoined": "Player %s joined", + "vcmi.lobby.system.playerDisconnected": "Player %s disconnected", + "vcmi.lobby.system.hidingIncompatibleMaps": "Hiding incompatible maps for %d players", + "vcmi.lobby.system.unableStartMap": "Unable to start map!", + "vcmi.lobby.system.reason": "Reason: %s", "vcmi.lobby.pvp.randomTown.help": "Write a random town in the chat", "vcmi.lobby.pvp.randomTown.hover": "Random town", "vcmi.lobby.pvp.randomTownVs.help": "Write two random towns in the chat", diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp index 2bbdc9eba..bb518dc10 100644 --- a/client/CServerHandler.cpp +++ b/client/CServerHandler.cpp @@ -109,6 +109,7 @@ CServerHandler::CServerHandler() , screenType(ESelectionScreen::unknown) , serverMode(EServerMode::NONE) , loadMode(ELoadMode::NONE) + , hotseatMode(false) , battleMode(false) , client(nullptr) { @@ -139,7 +140,10 @@ void CServerHandler::resetStateForLobby(EStartMode mode, ESelectionScreen screen hostClientId = GameConnectionID::INVALID; setState(EClientState::NONE); serverMode = newServerMode; + loadMode = ELoadMode::NONE; mapToStart = nullptr; + hotseatMode = false; + battleMode = false; th = std::make_unique(); logicConnection.reset(); si = std::make_shared(); @@ -338,6 +342,15 @@ bool CServerHandler::isGuest() const return !logicConnection || hostClientId != logicConnection->connectionID; } +bool CServerHandler::hasRemoteClientInLobby() const +{ + std::set connectedClients; + for(const auto & playerEntry : playerNames) + connectedClients.insert(playerEntry.second.connection); + + return connectedClients.size() > 1; +} + const std::string & CServerHandler::getLocalHostname() const { return settings["server"]["localHostname"].String(); @@ -586,7 +599,12 @@ bool CServerHandler::validateGameStart(bool allowOnlyAI) const catch(std::exception & e) { logGlobal->error("Exception during startScenario: %s", e.what()); - showServerError(std::string("Unable to start map!\nReason: ") + e.what()); + MetaString message; + message.appendTextID("vcmi.lobby.system.unableStartMap"); + message.appendRawString("\n"); + message.appendTextID("vcmi.lobby.system.reason"); + message.replaceRawString(e.what()); + showServerError(message.toString()); return false; } diff --git a/client/CServerHandler.h b/client/CServerHandler.h index a5ce8d83a..ddd15eaad 100644 --- a/client/CServerHandler.h +++ b/client/CServerHandler.h @@ -142,6 +142,7 @@ public: ESelectionScreen screenType; // To create lobby UI only after server is setup EServerMode serverMode; ELoadMode loadMode; // For saves filtering in SelectionTab + bool hotseatMode; bool battleMode; //////////////////// @@ -171,6 +172,7 @@ public: bool isHost() const; bool isGuest() const; + bool hasRemoteClientInLobby() const; bool inLobbyRoom() const; bool inGame() const; diff --git a/client/globalLobby/GlobalLobbyClient.cpp b/client/globalLobby/GlobalLobbyClient.cpp index 6120b718e..29085bbc4 100644 --- a/client/globalLobby/GlobalLobbyClient.cpp +++ b/client/globalLobby/GlobalLobbyClient.cpp @@ -427,7 +427,7 @@ void GlobalLobbyClient::onDisconnected(const std::shared_ptr ENGINE->windows().popWindows(1); } - CInfoWindow::showInfoDialog("Connection to game lobby was lost!", {}); + CInfoWindow::showInfoDialog(LIBRARY->generaltexth->translate("vcmi.lobby.login.connectionLost"), {}); } void GlobalLobbyClient::sendMessage(const JsonNode & data) diff --git a/client/lobby/CBonusSelection.cpp b/client/lobby/CBonusSelection.cpp index 9623836b7..f18d68266 100644 --- a/client/lobby/CBonusSelection.cpp +++ b/client/lobby/CBonusSelection.cpp @@ -153,6 +153,9 @@ CBonusSelection::CBonusSelection() buttonExtraOptions = std::make_shared(Point(643, 431), AnimationPath::builtin("GSPBUT2.DEF"), LIBRARY->generaltexth->zelp[46], [this]{ tabExtraOptions->setEnabled(!tabExtraOptions->isActive()); ENGINE->windows().totalRedraw(); }, EShortcut::LOBBY_EXTRA_OPTIONS); buttonExtraOptions->setTextOverlay(LIBRARY->generaltexth->translate("vcmi.optionsTab.extraOptions.hover"), FONT_SMALL, Colors::WHITE); } + + // Ensure campaign map info is synchronized even if player doesn't click any region manually. + GAME->server().setCampaignMap(GAME->server().campaignMap); } void CBonusSelection::createBonusesIcons() diff --git a/client/lobby/CLobbyScreen.cpp b/client/lobby/CLobbyScreen.cpp index 44faef773..aa3173ba5 100644 --- a/client/lobby/CLobbyScreen.cpp +++ b/client/lobby/CLobbyScreen.cpp @@ -20,6 +20,7 @@ #include "../CServerHandler.h" #include "../GameEngine.h" +#include "../GameChatHandler.h" #include "../GameInstance.h" #include "../gui/Shortcut.h" #include "../widgets/Buttons.h" @@ -40,6 +41,7 @@ CLobbyScreen::CLobbyScreen(ESelectionScreen screenType, bool hideScreen) : CSelectionBase(screenType), bonusSel(nullptr) { OBJECT_CONSTRUCTION; + addUsedEvents(TIME); tabSel = std::make_shared(screenType); curTab = tabSel; @@ -59,7 +61,7 @@ CLobbyScreen::CLobbyScreen(ESelectionScreen screenType, bool hideScreen) if(settings["general"]["enableUiEnhancements"].Bool()) { if(screenType == ESelectionScreen::newGame) - buttonBattleMode = std::make_shared(Point(619, 105), AnimationPath::builtin("GSPButton2Arrow"), CButton::tooltip("", LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyMode.help")), [this](){ + buttonBattleMode = std::make_shared(Point(619, 80), AnimationPath::builtin("GSPButton2Arrow"), CButton::tooltip("", LIBRARY->generaltexth->translate("vcmi.lobby.battleOnlyMode.help")), [this](){ updateAfterStateChange(); // creates tabBattleOnlyMode -> cannot created by init of object because GAME->server().isGuest() isn't valid at that point toggleTab(tabBattleOnlyMode); }, EShortcut::LOBBY_BATTLE_MODE); @@ -67,9 +69,9 @@ CLobbyScreen::CLobbyScreen(ESelectionScreen screenType, bool hideScreen) } }; - if(screenType != ESelectionScreen::campaignList) + if(screenType != ESelectionScreen::campaignList && GAME->server().loadMode == ELoadMode::MULTI && !GAME->server().hotseatMode) { - buttonChat = std::make_shared(Point(619, 80), AnimationPath::builtin("GSPBUT2.DEF"), LIBRARY->generaltexth->zelp[48], std::bind(&CLobbyScreen::toggleChat, this), EShortcut::LOBBY_TOGGLE_CHAT); + buttonChat = std::make_shared(Point(619, 105), AnimationPath::builtin("GSPBUT2.DEF"), LIBRARY->generaltexth->zelp[48], std::bind(&CLobbyScreen::toggleChat, this), EShortcut::LOBBY_TOGGLE_CHAT); buttonChat->setTextOverlay(LIBRARY->generaltexth->allTexts[532], FONT_SMALL, Colors::WHITE); } @@ -128,6 +130,8 @@ CLobbyScreen::CLobbyScreen(ESelectionScreen screenType, bool hideScreen) blackScreen = std::make_shared(Rect(Point(0, 0), pos.dimensions())); blackScreen->addBox(Point(0, 0), pos.dimensions(), Colors::BLACK); } + + updateHostLobbyChatState(); } CLobbyScreen::~CLobbyScreen() @@ -137,6 +141,53 @@ CLobbyScreen::~CLobbyScreen() GAME->server().sendClientDisconnecting(); } +bool CLobbyScreen::canStartLobbyGame() const +{ + if(GAME->server().isGuest() || GAME->server().mi == nullptr) + return false; + + const bool isLanMultiplayerHost = GAME->server().loadMode == ELoadMode::MULTI + && GAME->server().serverMode == EServerMode::LOCAL + && !GAME->server().hotseatMode + && GAME->server().isHost(); + + if(isLanMultiplayerHost && !GAME->server().hasRemoteClientInLobby()) + return false; + + return true; +} + +void CLobbyScreen::updateHostLobbyChatState() +{ + const bool isLanMultiplayerHost = buttonChat + && GAME->server().loadMode == ELoadMode::MULTI + && GAME->server().serverMode == EServerMode::LOCAL + && !GAME->server().hotseatMode + && GAME->server().isHost(); + + if(!isLanMultiplayerHost) + return; + + buttonChat->setTextOverlay(card->showChat ? LIBRARY->generaltexth->allTexts[531] : LIBRARY->generaltexth->allTexts[532], FONT_SMALL, Colors::WHITE); + + if(GAME->server().hasRemoteClientInLobby()) + { + waitingForPlayersMessageShown = false; + return; + } + + if(!waitingForPlayersMessageShown) + { + GAME->server().getGameChat().onNewLobbyMessageReceived("System", LIBRARY->generaltexth->translate("vcmi.lobby.system.waitingForPlayers")); + waitingForPlayersMessageShown = true; + } +} + +void CLobbyScreen::updateStartButtonState() +{ + buttonStart->block(!canStartLobbyGame()); +} + void CLobbyScreen::toggleTab(std::shared_ptr tab) { if(tab == curTab) @@ -161,13 +212,22 @@ void CLobbyScreen::toggleTab(std::shared_ptr tab) } else { - buttonStart->block(GAME->server().mi == nullptr || GAME->server().isGuest()); + updateStartButtonState(); card->changeSelection(); } CSelectionBase::toggleTab(tab); } +void CLobbyScreen::tick(uint32_t msPassed) +{ + CSelectionBase::tick(msPassed); + + updateHostLobbyChatState(); + if(curTab != tabBattleOnlyMode) + updateStartButtonState(); +} + void CLobbyScreen::start(bool campaign) { if(curTab == tabBattleOnlyMode) @@ -221,9 +281,11 @@ void CLobbyScreen::startScenario(bool allowOnlyAI) void CLobbyScreen::toggleMode(bool host) { tabSel->toggleMode(); - buttonStart->block(!host); if(screenType == ESelectionScreen::campaignList) + { + buttonStart->block(!host); return; + } auto buttonColor = host ? Colors::WHITE : Colors::ORANGE; buttonSelect->setTextOverlay(" " + LIBRARY->generaltexth->allTexts[500], FONT_SMALL, buttonColor); @@ -255,6 +317,8 @@ void CLobbyScreen::toggleMode(bool host) tabTurnOptions->recreate(); tabExtraOptions->recreate(); } + + updateStartButtonState(); } void CLobbyScreen::toggleChat() @@ -269,6 +333,9 @@ void CLobbyScreen::toggleChat() void CLobbyScreen::updateAfterStateChange() { OBJECT_CONSTRUCTION; + updateHostLobbyChatState(); + //tabSel->filter(-1); + if(!tabBattleOnlyMode) { tabBattleOnlyMode = std::make_shared(); @@ -298,9 +365,9 @@ void CLobbyScreen::updateAfterStateChange() tabExtraOptions->recreate(); } - if(curTab && curTab != tabBattleOnlyMode) + if(curTab != tabBattleOnlyMode) { - buttonStart->block(GAME->server().mi == nullptr || GAME->server().isGuest()); + updateStartButtonState(); card->changeSelection(); } diff --git a/client/lobby/CLobbyScreen.h b/client/lobby/CLobbyScreen.h index 90d75d44f..298b5c141 100644 --- a/client/lobby/CLobbyScreen.h +++ b/client/lobby/CLobbyScreen.h @@ -16,6 +16,12 @@ class GraphicalPrimitiveCanvas; class CLobbyScreen final : public CSelectionBase { + bool waitingForPlayersMessageShown = false; + + bool canStartLobbyGame() const; + void updateHostLobbyChatState(); + void updateStartButtonState(); + public: std::shared_ptr buttonChat; std::shared_ptr blackScreen; @@ -23,6 +29,7 @@ public: CLobbyScreen(ESelectionScreen type, bool hideScreen = false); ~CLobbyScreen(); void toggleTab(std::shared_ptr tab) final; + void tick(uint32_t msPassed) override; void start(bool campaign); void startCampaign(); void startScenario(bool allowOnlyAI = false); diff --git a/client/lobby/SelectionTab.cpp b/client/lobby/SelectionTab.cpp index 7f83cd501..3c483ac25 100644 --- a/client/lobby/SelectionTab.cpp +++ b/client/lobby/SelectionTab.cpp @@ -16,6 +16,7 @@ #include "../CPlayerInterface.h" #include "../CServerHandler.h" +#include "../GameChatHandler.h" #include "../GameEngine.h" #include "../GameInstance.h" #include "../gui/Shortcut.h" @@ -608,10 +609,19 @@ void SelectionTab::filter(int size, bool selectFirst) if(buttonDeleteMode) buttonDeleteMode->setEnabled(tabType != ESelectionScreen::newGame || showRandom); + size_t hiddenIncompatibleMaps = 0; + size_t requiredHumanPlayers = getRequiredHumanPlayers(); + for(auto elem : allItems) { if((elem->mapHeader && (!size || elem->mapHeader->width == size)) || tabType == ESelectionScreen::campaignList) { + if(!isMapCompatibleWithLobbyPlayerCount(*elem)) + { + ++hiddenIncompatibleMaps; + continue; + } + if(showRandom) curFolder = "RandomMaps/"; @@ -645,6 +655,26 @@ void SelectionTab::filter(int size, bool selectFirst) } } + if(hiddenIncompatibleMaps && tabType == ESelectionScreen::newGame && GAME->server().loadMode == ELoadMode::MULTI) + { + MetaString warningText; + warningText.appendTextID("vcmi.lobby.system.hidingIncompatibleMaps"); + warningText.replaceNumber(requiredHumanPlayers); + const std::string warningTextFormatted = warningText.toString(); + + if(lastCompatibilityNotice != warningTextFormatted) + { + logGlobal->info("%s", warningTextFormatted); + if(!GAME->server().hotseatMode) + GAME->server().getGameChat().onNewLobbyMessageReceived("System", warningTextFormatted); + lastCompatibilityNotice = warningTextFormatted; + } + } + else + { + lastCompatibilityNotice.clear(); + } + if(curItems.size()) { slider->block(false); @@ -660,6 +690,25 @@ void SelectionTab::filter(int size, bool selectFirst) selectAbs(firstPos); } } + else if(tabType == ESelectionScreen::newGame || tabType == ESelectionScreen::campaignList) + { + const std::string selectedMapFileURI = GAME->server().mi ? GAME->server().mi->fileURI : ""; + auto selectedMapIt = boost::range::find_if(curItems, [&selectedMapFileURI](std::shared_ptr e) + { + return !e->isFolder && e->fileURI == selectedMapFileURI; + }); + + if(selectedMapIt == curItems.end()) + { + int firstPos = boost::range::find_if(curItems, [](std::shared_ptr e) { return !e->isFolder; }) - curItems.begin(); + if(firstPos < curItems.size()) + { + slider->scrollTo(firstPos); + callOnSelect(curItems[firstPos]); + selectAbs(firstPos); + } + } + } } else { @@ -943,6 +992,29 @@ bool SelectionTab::isMapSupported(const CMapInfo & info) return false; } +bool SelectionTab::isMapCompatibleWithLobbyPlayerCount(const ElementInfo & info) const +{ + if(tabType != ESelectionScreen::newGame || GAME->server().loadMode != ELoadMode::MULTI || !info.mapHeader) + return true; + + const auto requiredHumanPlayers = getRequiredHumanPlayers(); + size_t supportedHumanPlayers = 0; + + for(const auto & player : info.mapHeader->players) + { + if(player.canHumanPlay) + ++supportedHumanPlayers; + } + + return supportedHumanPlayers >= requiredHumanPlayers; +} + +size_t SelectionTab::getRequiredHumanPlayers() const +{ + const size_t minimumPlayers = (GAME->server().loadMode == ELoadMode::MULTI || GAME->server().hotseatMode) ? 2 : 1; + return std::max(minimumPlayers, GAME->server().playerNames.size()); +} + void SelectionTab::parseMaps(const std::unordered_set & files) { logGlobal->debug("Parsing %d maps", files.size()); diff --git a/client/lobby/SelectionTab.h b/client/lobby/SelectionTab.h index f8d0da0c5..478e7f4a2 100644 --- a/client/lobby/SelectionTab.h +++ b/client/lobby/SelectionTab.h @@ -90,6 +90,7 @@ public: bool sortModeAscending; int currentMapSizeFilter = 0; bool showRandom; + std::string lastCompatibilityNotice; std::shared_ptr inputName; @@ -133,6 +134,8 @@ private: std::shared_ptr buttonCampaignSet; auto checkSubfolder(std::string path); + size_t getRequiredHumanPlayers() const; + bool isMapCompatibleWithLobbyPlayerCount(const ElementInfo & info) const; bool isMapSupported(const CMapInfo & info); void parseMaps(const std::unordered_set & files); diff --git a/client/mainmenu/CMainMenu.cpp b/client/mainmenu/CMainMenu.cpp index bd587fc89..bb1385afe 100644 --- a/client/mainmenu/CMainMenu.cpp +++ b/client/mainmenu/CMainMenu.cpp @@ -398,10 +398,11 @@ void CMainMenu::makeActiveInterface() menu->switchToTab(menu->getActiveTab()); } -void CMainMenu::openLobby(ESelectionScreen screenType, bool host, const std::vector & names, ELoadMode loadMode, bool battleMode, std::string server, ui16 port) +void CMainMenu::openLobby(ESelectionScreen screenType, bool host, const std::vector & names, ELoadMode loadMode, bool battleMode, bool hotseatMode, std::string server, ui16 port) { GAME->server().resetStateForLobby(screenType == ESelectionScreen::newGame ? EStartMode::NEW_GAME : EStartMode::LOAD_GAME, screenType, EServerMode::LOCAL, names); GAME->server().loadMode = loadMode; + GAME->server().hotseatMode = hotseatMode; GAME->server().battleMode = battleMode; ENGINE->windows().createAndPushWindow(host, server, port); @@ -590,7 +591,7 @@ void JoinScreen::onServerDiscovered(const DiscoveredServer & server) auto savedScreenType = screenType; auto savedPlayerNames = playerNames; close(); - CMainMenu::openLobby(savedScreenType, false, savedPlayerNames, ELoadMode::MULTI, false, server.address, server.port); + CMainMenu::openLobby(savedScreenType, false, savedPlayerNames, ELoadMode::MULTI, false, false, server.address, server.port); }); button->setTextOverlay(LIBRARY->generaltexth->translate("vcmi.mainMenu.join"), FONT_SMALL, Colors::WHITE); buttonsJoin.push_back(button); @@ -600,7 +601,7 @@ void JoinScreen::onServerDiscovered(const DiscoveredServer & server) } CMultiPlayers::CMultiPlayers(const std::vector& playerNames, ESelectionScreen ScreenType, bool Host, ELoadMode LoadMode, EShortcut shortcut) - : loadMode(LoadMode), screenType(ScreenType), host(Host) + : host(Host), hotseat(shortcut == EShortcut::MAIN_MENU_HOTSEAT), loadMode(LoadMode), screenType(ScreenType) { OBJECT_CONSTRUCTION; background = std::make_shared(ImagePath::builtin("MUHOTSEA.bmp")); @@ -637,13 +638,24 @@ CMultiPlayers::CMultiPlayers(const std::vector& playerNames, ESelec { inputNames[i]->setText(playerNames[i]); } + + buttonOk->block(hotseat && countEnteredNames() < 2); #ifndef VCMI_MOBILE inputNames[0]->giveFocus(); #endif } +size_t CMultiPlayers::countEnteredNames() const +{ + return std::count_if(inputNames.begin(), inputNames.end(), [](const auto & playerName) + { + return playerName->getText().length(); + }); +} + void CMultiPlayers::onChange(std::string newText) { + buttonOk->block(hotseat && countEnteredNames() < 2); } void CMultiPlayers::enterSelectionScreen() @@ -675,6 +687,9 @@ void CMultiPlayers::enterSelectionScreen() playerName->clear(); } + if(hotseat && playerNames.size() < 2) + return; + if(!host) { auto savedScreenType = screenType; @@ -683,7 +698,7 @@ void CMultiPlayers::enterSelectionScreen() ENGINE->windows().createAndPushWindow(savedScreenType, savedPlayerNames); return; } - CMainMenu::openLobby(screenType, host, playerNames, loadMode, false); + CMainMenu::openLobby(screenType, host, playerNames, loadMode, false, hotseat); } CSimpleJoinScreen::CSimpleJoinScreen(bool host, std::string server, ui16 port) diff --git a/client/mainmenu/CMainMenu.h b/client/mainmenu/CMainMenu.h index 2d197bf78..50fa2f394 100644 --- a/client/mainmenu/CMainMenu.h +++ b/client/mainmenu/CMainMenu.h @@ -131,6 +131,7 @@ public: class CMultiPlayers : public WindowBase { bool host; + bool hotseat; ELoadMode loadMode; ESelectionScreen screenType; std::shared_ptr background; @@ -140,6 +141,7 @@ class CMultiPlayers : public WindowBase std::shared_ptr buttonCancel; std::shared_ptr statusBar; + size_t countEnteredNames() const; void onChange(std::string newText); void enterSelectionScreen(); @@ -178,7 +180,7 @@ public: void activate() override; void onScreenResize() override; void makeActiveInterface(); - static void openLobby(ESelectionScreen screenType, bool host, const std::vector & names, ELoadMode loadMode, bool battleMode, std::string server = {}, ui16 port = 0); + static void openLobby(ESelectionScreen screenType, bool host, const std::vector & names, ELoadMode loadMode, bool battleMode, bool hotseatMode = false, std::string server = {}, ui16 port = 0); static void openCampaignLobby(const std::string & campaignFileName, std::string campaignSet = ""); static void openCampaignLobby(std::shared_ptr campaign); static void startTutorial(); @@ -228,4 +230,3 @@ public: void tick(uint32_t msPassed) override; }; - diff --git a/server/CVCMIServer.cpp b/server/CVCMIServer.cpp index c93f58f1f..f4a601bcd 100644 --- a/server/CVCMIServer.cpp +++ b/server/CVCMIServer.cpp @@ -495,7 +495,11 @@ void CVCMIServer::clientConnected(std::shared_ptr c, std::vector cp.connection = c->connectionID; cp.name = name; playerNames.try_emplace(id, cp); - announceTxt(boost::str(boost::format("%s (pid %d cid %d) joins the game") % name % static_cast(id) % static_cast(c->connectionID))); + logNetwork->info("Player joined lobby: name='%s', playerId=%d, connectionId=%d", name, static_cast(id), static_cast(c->connectionID)); + MetaString joinMessage; + joinMessage.appendTextID("vcmi.lobby.system.playerJoined"); + joinMessage.replaceRawString(name); + announceTxt(joinMessage); //put new player in first slot with AI for(auto & elem : si->playerInfos) @@ -515,6 +519,45 @@ void CVCMIServer::clientDisconnected(std::shared_ptr connection) logGlobal->trace("Received disconnection request"); vstd::erase(activeConnections, connection); + std::vector disconnectedPlayerIds; + std::vector disconnectedPlayerNames; + for(const auto & playerEntry : playerNames) + { + if(playerEntry.second.connection == connection->connectionID) + { + disconnectedPlayerIds.push_back(playerEntry.first); + disconnectedPlayerNames.push_back(playerEntry.second.name); + } + } + + for(const auto & playerName : disconnectedPlayerNames) + { + logNetwork->info("Player disconnected from lobby: name='%s', connectionId=%d", playerName, static_cast(connection->connectionID)); + MetaString disconnectMessage; + disconnectMessage.appendTextID("vcmi.lobby.system.playerDisconnected"); + disconnectMessage.replaceRawString(playerName); + announceTxt(disconnectMessage); + } + + if(disconnectedPlayerNames.empty()) + logNetwork->info("Connection %d disconnected from lobby with no mapped player names", static_cast(connection->connectionID)); + + for(const auto & playerId : disconnectedPlayerIds) + playerNames.erase(playerId); + + if(!disconnectedPlayerIds.empty()) + { + for(auto & playerInfoEntry : si->playerInfos) + { + auto & connectedPlayerIDs = playerInfoEntry.second.connectedPlayerIDs; + for(const auto & playerId : disconnectedPlayerIds) + connectedPlayerIDs.erase(playerId); + + if(connectedPlayerIDs.empty()) + setPlayerConnectedId(playerInfoEntry.second, PlayerConnectionID::PLAYER_AI); + } + } + if(activeConnections.empty() || hostClientId == connection->connectionID) { setState(EServerState::SHUTDOWN); @@ -1196,4 +1239,3 @@ void CVCMIServer::sendPack(CPackForClient & pack, GameConnectionID connectionID) if (c->connectionID == connectionID) c->sendPack(pack); } -