From f04cbaf539498348ad370f6e7e8dc685ea4c1c17 Mon Sep 17 00:00:00 2001 From: Arseniy Shestakov Date: Sun, 18 Mar 2018 22:29:09 +0700 Subject: [PATCH 1/7] Settings: add lastMap, lastCampaign, lastSave to schema Nice to see last used save game after client restart --- config/schemas/settings.json | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/config/schemas/settings.json b/config/schemas/settings.json index 3c76a383a..30ed8333f 100644 --- a/config/schemas/settings.json +++ b/config/schemas/settings.json @@ -46,6 +46,18 @@ "saveRandomMaps" : { "type" : "boolean", "default" : false + }, + "lastMap" : { + "type":"string", + "default" : "Maps/Arrogance" + }, + "lastSave" : { + "type":"string", + "default" : "NEWGAME" + }, + "lastCampaign" : { + "type":"string", + "default" : "" } } }, From ab2b9086d3c0dc3b932c2bea640f73c30846b771 Mon Sep 17 00:00:00 2001 From: Arseniy Shestakov Date: Tue, 20 Mar 2018 15:19:33 +0700 Subject: [PATCH 2/7] CLabel: add methods getWidth, setColor, setAutoRedraw For somethign like map list we need to change CLabel data without redrawing it. --- client/widgets/TextControls.cpp | 22 ++++++++++++++++++++++ client/widgets/TextControls.h | 3 +++ 2 files changed, 25 insertions(+) diff --git a/client/widgets/TextControls.cpp b/client/widgets/TextControls.cpp index 1a57f1b9c..c2b841efe 100644 --- a/client/widgets/TextControls.cpp +++ b/client/widgets/TextControls.cpp @@ -63,6 +63,11 @@ std::string CLabel::getText() return text; } +void CLabel::setAutoRedraw(bool value) +{ + autoRedraw = value; +} + void CLabel::setText(const std::string &Txt) { text = Txt; @@ -75,6 +80,23 @@ void CLabel::setText(const std::string &Txt) } } +void CLabel::setColor(const SDL_Color & Color) +{ + color = Color; + if(autoRedraw) + { + if(bg || !parent) + redraw(); + else + parent->redraw(); + } +} + +size_t CLabel::getWidth() +{ + return graphics->fonts[font]->getStringWidth(visibleText());; +} + CMultiLineLabel::CMultiLineLabel(Rect position, EFonts Font, EAlignment Align, const SDL_Color &Color, const std::string &Text): CLabel(position.x, position.y, Font, Align, Color, Text), visibleSize(0, 0, position.w, position.h) diff --git a/client/widgets/TextControls.h b/client/widgets/TextControls.h index 205dc184c..e3f2821ac 100644 --- a/client/widgets/TextControls.h +++ b/client/widgets/TextControls.h @@ -47,7 +47,10 @@ public: bool autoRedraw; //whether control will redraw itself on setTxt std::string getText(); + virtual void setAutoRedraw(bool option); virtual void setText(const std::string &Txt); + virtual void setColor(const SDL_Color & Color); + size_t getWidth(); CLabel(int x=0, int y=0, EFonts Font = FONT_SMALL, EAlignment Align = TOPLEFT, const SDL_Color &Color = Colors::WHITE, const std::string &Text = ""); From 14f03e22da7e3e92b84174c22f290501bfc0e892 Mon Sep 17 00:00:00 2001 From: Arseniy Shestakov Date: Fri, 23 Mar 2018 01:23:18 +0700 Subject: [PATCH 3/7] CLabelGroup: add currentSize method Might be someone will suggest me better name. And actually I need method to clean group too. --- client/widgets/TextControls.cpp | 7 ++++++- client/widgets/TextControls.h | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/client/widgets/TextControls.cpp b/client/widgets/TextControls.cpp index c2b841efe..676105426 100644 --- a/client/widgets/TextControls.cpp +++ b/client/widgets/TextControls.cpp @@ -266,7 +266,12 @@ CLabelGroup::CLabelGroup(EFonts Font, EAlignment Align, const SDL_Color &Color): void CLabelGroup::add(int x, int y, const std::string &text) { OBJ_CONSTRUCTION_CAPTURING_ALL; - new CLabel(x, y, font, align, color, text); + labels.push_back(new CLabel(x, y, font, align, color, text)); +} + +size_t CLabelGroup::currentSize() const +{ + return labels.size(); } CTextBox::CTextBox(std::string Text, const Rect &rect, int SliderStyle, EFonts Font, EAlignment Align, const SDL_Color &Color): diff --git a/client/widgets/TextControls.h b/client/widgets/TextControls.h index e3f2821ac..b3b5b1a78 100644 --- a/client/widgets/TextControls.h +++ b/client/widgets/TextControls.h @@ -67,6 +67,7 @@ class CLabelGroup : public CIntObject public: CLabelGroup(EFonts Font = FONT_SMALL, EAlignment Align = TOPLEFT, const SDL_Color &Color = Colors::WHITE); void add(int x=0, int y=0, const std::string &text = ""); + size_t currentSize() const; }; /// Multi-line label that can display multiple lines of text From ac66fc7f42f50ad961cf8a0656be0d9b5415180d Mon Sep 17 00:00:00 2001 From: Arseniy Shestakov Date: Fri, 5 Jan 2018 20:21:07 +0300 Subject: [PATCH 4/7] Full rework of pre-game interface and networking New features for players: * Loading for multiplayer. Any save could be used for multiplayer. * Restart for multiplayer. All clients will restart together. * Loading from single save. * Hotseat mixed with network game. Multiple players per client. * Now connection to server could be cancelled. * Return to menu on disconnections instead of crashes. * Restoring of last selected map, save or campaign on next run. TLDR on important changes in engine code: * UI: work with server separated from UI * UI: all explitic blitting replaced with IntObject's * UI: all new code use smart pointers instead of DISPOSE * Gameplay always start through lobby controlled by server. * Threads receiving netpacks now shared for lobby and gameplay. * Campaigns: heroes for crossover now serialized as JsonNode. --- CCallback.cpp | 8 +- CCallback.h | 5 +- ChangeLog | 5 + client/CGameInfo.cpp | 4 +- client/CGameInfo.h | 2 +- client/CMT.cpp | 302 +- client/CMT.h | 3 - client/CMakeLists.txt | 33 +- client/CPlayerInterface.cpp | 107 +- client/CPlayerInterface.h | 16 +- client/CPreGame.cpp | 4374 ----------------- client/CPreGame.h | 650 --- client/CServerHandler.cpp | 674 +++ client/CServerHandler.h | 145 + client/Client.cpp | 1207 ++--- client/Client.h | 193 +- client/NetPacksClient.cpp | 20 +- client/NetPacksLobbyClient.cpp | 142 + client/gui/CGuiHandler.cpp | 6 +- client/gui/CGuiHandler.h | 20 +- client/gui/SDL_Extensions.cpp | 2 + client/gui/SDL_Extensions.h | 6 + client/lobby/CBonusSelection.cpp | 534 ++ client/lobby/CBonusSelection.h | 93 + client/lobby/CLobbyScreen.cpp | 201 + client/lobby/CLobbyScreen.h | 35 + client/lobby/CSavingScreen.cpp | 97 + client/lobby/CSavingScreen.h | 32 + client/lobby/CScenarioInfoScreen.cpp | 59 + client/lobby/CScenarioInfoScreen.h | 30 + client/lobby/CSelectionBase.cpp | 407 ++ client/lobby/CSelectionBase.h | 146 + client/lobby/OptionsTab.cpp | 537 ++ client/lobby/OptionsTab.h | 127 + client/lobby/RandomMapTab.cpp | 300 ++ client/lobby/RandomMapTab.h | 58 + client/lobby/SelectionTab.cpp | 664 +++ client/lobby/SelectionTab.h | 98 + client/mainmenu/CCampaignScreen.cpp | 156 + client/mainmenu/CCampaignScreen.h | 55 + client/mainmenu/CMainMenu.cpp | 566 +++ client/mainmenu/CMainMenu.h | 183 + client/mainmenu/CPrologEpilogVideo.cpp | 64 + client/mainmenu/CPrologEpilogVideo.h | 31 + client/mainmenu/CreditsScreen.cpp | 59 + client/mainmenu/CreditsScreen.h | 27 + client/widgets/AdventureMapClasses.cpp | 2 +- client/windows/CAdvmapInterface.cpp | 20 +- client/windows/GUIClasses.cpp | 19 +- lib/CGameState.cpp | 78 +- lib/CGameState.h | 7 +- lib/CMakeLists.txt | 4 +- lib/GameConstants.h | 2 + lib/IGameCallback.cpp | 3 + lib/NetPacks.h | 272 +- lib/NetPacksBase.h | 18 +- lib/NetPacksLib.cpp | 73 +- lib/NetPacksLobby.h | 311 ++ lib/StartInfo.cpp | 195 + lib/StartInfo.h | 119 +- lib/mapObjects/CGHeroInstance.h | 2 + lib/mapObjects/CGPandoraBox.cpp | 2 +- lib/mapObjects/CGTownInstance.cpp | 6 + lib/mapObjects/CGTownInstance.h | 1 + lib/mapping/CCampaignHandler.cpp | 120 +- lib/mapping/CCampaignHandler.h | 37 +- lib/mapping/CMap.cpp | 15 +- lib/mapping/CMap.h | 11 +- lib/mapping/CMapInfo.cpp | 195 +- lib/mapping/CMapInfo.h | 27 +- lib/registerTypes/RegisterTypes.cpp | 2 +- lib/registerTypes/RegisterTypes.h | 50 +- ...esPregamePacks.cpp => TypesLobbyPacks.cpp} | 74 +- lib/rmg/CMapGenOptions.cpp | 3 +- lib/serializer/CSerializer.h | 2 +- lib/serializer/Connection.cpp | 102 +- lib/serializer/Connection.h | 30 +- server/CGameHandler.cpp | 294 +- server/CGameHandler.h | 25 +- server/CMakeLists.txt | 1 + server/CVCMIServer.cpp | 1641 ++++--- server/CVCMIServer.h | 217 +- server/NetPacksLobbyServer.cpp | 250 + server/NetPacksServer.cpp | 31 +- test/game/CGameStateTest.cpp | 2 +- 85 files changed, 8808 insertions(+), 7938 deletions(-) delete mode 100644 client/CPreGame.cpp delete mode 100644 client/CPreGame.h create mode 100644 client/CServerHandler.cpp create mode 100644 client/CServerHandler.h create mode 100644 client/NetPacksLobbyClient.cpp create mode 100644 client/lobby/CBonusSelection.cpp create mode 100644 client/lobby/CBonusSelection.h create mode 100644 client/lobby/CLobbyScreen.cpp create mode 100644 client/lobby/CLobbyScreen.h create mode 100644 client/lobby/CSavingScreen.cpp create mode 100644 client/lobby/CSavingScreen.h create mode 100644 client/lobby/CScenarioInfoScreen.cpp create mode 100644 client/lobby/CScenarioInfoScreen.h create mode 100644 client/lobby/CSelectionBase.cpp create mode 100644 client/lobby/CSelectionBase.h create mode 100644 client/lobby/OptionsTab.cpp create mode 100644 client/lobby/OptionsTab.h create mode 100644 client/lobby/RandomMapTab.cpp create mode 100644 client/lobby/RandomMapTab.h create mode 100644 client/lobby/SelectionTab.cpp create mode 100644 client/lobby/SelectionTab.h create mode 100644 client/mainmenu/CCampaignScreen.cpp create mode 100644 client/mainmenu/CCampaignScreen.h create mode 100644 client/mainmenu/CMainMenu.cpp create mode 100644 client/mainmenu/CMainMenu.h create mode 100644 client/mainmenu/CPrologEpilogVideo.cpp create mode 100644 client/mainmenu/CPrologEpilogVideo.h create mode 100644 client/mainmenu/CreditsScreen.cpp create mode 100644 client/mainmenu/CreditsScreen.h create mode 100644 lib/NetPacksLobby.h create mode 100644 lib/StartInfo.cpp rename lib/registerTypes/{TypesPregamePacks.cpp => TypesLobbyPacks.cpp} (73%) create mode 100644 server/NetPacksLobbyServer.cpp diff --git a/CCallback.cpp b/CCallback.cpp index 8e25354a8..167ffa94f 100644 --- a/CCallback.cpp +++ b/CCallback.cpp @@ -95,7 +95,7 @@ void CCallback::endTurn() { logGlobal->trace("Player %d ended his turn.", player.get().getNum()); EndTurn pack; - sendRequest(&pack); //report that we ended turn + sendRequest(&pack); } int CCallback::swapCreatures(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2) { @@ -174,7 +174,7 @@ int CBattleCallback::battleMakeAction(BattleAction* action) return 0; } -int CBattleCallback::sendRequest(const CPack *request) +int CBattleCallback::sendRequest(const CPackForServer * request) { int requestID = cl->sendRequest(request, *player); if(waitTillRealize) @@ -262,8 +262,8 @@ void CCallback::save( const std::string &fname ) void CCallback::sendMessage(const std::string &mess, const CGObjectInstance * currentObject) { ASSERT_IF_CALLED_WITH_PLAYER - PlayerMessage pm(*player, mess, currentObject? currentObject->id : ObjectInstanceID(-1)); - sendRequest(&(CPackForClient&)pm); + PlayerMessage pm(mess, currentObject? currentObject->id : ObjectInstanceID(-1)); + sendRequest(&pm); } void CCallback::buildBoat( const IShipyard *obj ) diff --git a/CCallback.h b/CCallback.h index 6c72ddeb6..85faa1477 100644 --- a/CCallback.h +++ b/CCallback.h @@ -78,12 +78,12 @@ public: virtual void buildBoat(const IShipyard *obj) = 0; }; -struct CPack; +struct CPackForServer; class CBattleCallback : public IBattleCallback, public CPlayerBattleCallback { protected: - int sendRequest(const CPack *request); //returns requestID (that'll be matched to requestID in PackageApplied) + int sendRequest(const CPackForServer * request); //returns requestID (that'll be matched to requestID in PackageApplied) CClient *cl; public: @@ -95,6 +95,7 @@ public: friend class CClient; }; +class CPlayerInterface; class CCallback : public CPlayerSpecificInfoCallback, public IGameActionCallback, public CBattleCallback { public: diff --git a/ChangeLog b/ChangeLog index ac3730e8d..dc6086100 100644 --- a/ChangeLog +++ b/ChangeLog @@ -20,6 +20,11 @@ GENERAL: - BLOCK_MAGIC_BELOW - allows blocking spells below particular spell level. HotA cape artifact can be implemented with this - DESTRUCTION - creature ability for killing extra units after hit, configurable +MULTIPLAYER: +* Loading support. Save from single client could be used to load all clients. +* Restart support. All clients will restart together on same server. +* Hotseat mixed with network game. Multiple colors can be controlled by each client. + SPELLS: * Implemented cumulative effects for spells diff --git a/client/CGameInfo.cpp b/client/CGameInfo.cpp index 128169d09..ac607a8c3 100644 --- a/client/CGameInfo.cpp +++ b/client/CGameInfo.cpp @@ -14,8 +14,10 @@ #include "../lib/VCMI_Lib.h" -const CGameInfo * CGI; //game info for general use +const CGameInfo * CGI; CClientState * CCS = nullptr; +CServerHandler * CSH; + CGameInfo::CGameInfo() { diff --git a/client/CGameInfo.h b/client/CGameInfo.h index c1c37b500..371b487f3 100644 --- a/client/CGameInfo.h +++ b/client/CGameInfo.h @@ -9,7 +9,6 @@ */ #pragma once - #include "../lib/ConstTransitivePtr.h" class CModHandler; @@ -30,6 +29,7 @@ class CConsoleHandler; class CCursorHandler; class CGameState; class IMainVideoPlayer; +class CServerHandler; class CMap; diff --git a/client/CMT.cpp b/client/CMT.cpp index 7ceb7bb4b..15f84fca5 100644 --- a/client/CMT.cpp +++ b/client/CMT.cpp @@ -20,7 +20,8 @@ #include "../lib/filesystem/Filesystem.h" #include "../lib/filesystem/FileStream.h" -#include "CPreGame.h" +#include "mainmenu/CMainMenu.h" +#include "lobby/CSelectionBase.h" #include "windows/CCastleInterface.h" #include "../lib/CConsoleHandler.h" #include "gui/CCursorHandler.h" @@ -54,6 +55,12 @@ #include "../lib/StringConstants.h" #include "../lib/CPlayerState.h" #include "gui/CAnimation.h" +#include "../lib/serializer/Connection.h" +#include "CServerHandler.h" + +#include + +#include "mainmenu/CPrologEpilogVideo.h" #ifdef VCMI_WINDOWS #include "SDL_syswm.h" @@ -75,7 +82,6 @@ namespace bfs = boost::filesystem; std::string NAME_AFFIX = "client"; std::string NAME = GameConstants::VCMI_VERSION + std::string(" (") + NAME_AFFIX + ')'; //application name CGuiHandler GH; -static CClient *client = nullptr; int preferredDriverIndex = -1; SDL_Window * mainWindow = nullptr; @@ -91,7 +97,6 @@ SDL_Surface *screen = nullptr, //main screen surface std::queue events; boost::mutex eventsM; -CondSh serverAlive(false); static po::variables_map vm; //static bool setResolution = false; //set by event handling thread after resolution is adjusted @@ -102,9 +107,6 @@ static void setScreenRes(int w, int h, int bpp, bool fullscreen, int displayInde void dispose(); void playIntro(); static void mainLoop(); -//void requestChangingResolution(); -void startGame(StartInfo * options, CConnection *serv = nullptr); -void endGame(); #ifndef VCMI_WINDOWS #ifndef _GNU_SOURCE @@ -113,54 +115,6 @@ void endGame(); #include #endif -void startTestMap(const std::string &mapname) -{ - StartInfo si; - si.mapname = mapname; - si.mode = StartInfo::NEW_GAME; - for (int i = 0; i < 8; i++) - { - PlayerSettings &pset = si.playerInfos[PlayerColor(i)]; - pset.color = PlayerColor(i); - pset.name = CGI->generaltexth->allTexts[468];//Computer - pset.playerID = PlayerSettings::PLAYER_AI; - pset.compOnly = true; - pset.castle = 0; - pset.hero = -1; - pset.heroPortrait = -1; - pset.handicap = PlayerSettings::NO_HANDICAP; - } - - while(GH.topInt()) - GH.popIntTotally(GH.topInt()); - startGame(&si); -} - -void startGameFromFile(const bfs::path &fname) -{ - StartInfo si; - try //attempt retrieving start info from given file - { - if(fname.empty() || !bfs::exists(fname)) - throw std::runtime_error("Startfile \"" + fname.string() + "\" does not exist!"); - - CLoadFile out(fname); - if (!out.sfile || !*out.sfile) - throw std::runtime_error("Cannot read from startfile \"" + fname.string() +"\"!"); - out >> si; - } - catch(std::exception &e) - { - logGlobal->error("Failed to start from the file: %s. Error: %s. Falling back to main menu.", fname, e.what()); - GH.curInt = CGPreGame::create(); - return; - } - - while(GH.topInt()) - GH.popIntTotally(GH.topInt()); - startGame(&si); -} - void init() { CStopWatch tmh, pomtime; @@ -243,15 +197,15 @@ int main(int argc, char * argv[]) ("version,v", "display version information and exit") ("disable-shm", "force disable shared memory usage") ("enable-shm-uuid", "use UUID for shared memory identifier") - ("start", po::value(), "starts game from saved StartInfo file") ("testmap", po::value(), "") + ("testsave", po::value(), "") ("spectate,s", "enable spectator interface for AI-only games") ("spectate-ignore-hero", "wont follow heroes on adventure map") ("spectate-hero-speed", po::value(), "hero movement speed on adventure map") ("spectate-battle-speed", po::value(), "battle animation speed for spectator") ("spectate-skip-battle", "skip battles in spectator view") ("spectate-skip-battle-result", "skip battle result window") - ("onlyAI", "runs without human player, all players will be default AI") + ("onlyAI", "allow to run without human player, all players will be default AI") ("headless", "runs without GUI, implies --onlyAI") ("ai", po::value>(), "AI to be used for the player, can be specified several times for the consecutive players") ("oneGoodAI", "puts one default AI and the rest will be EmptyAI") @@ -259,12 +213,6 @@ int main(int argc, char * argv[]) ("disable-video", "disable video player") ("nointro,i", "skips intro movies") ("donotstartserver,d","do not attempt to start server and just connect to it instead server") - ("loadserver","specifies we are the multiplayer server for loaded games") - ("loadnumplayers",po::value(),"specifies the number of players connecting to a multiplayer game") - ("loadhumanplayerindices",po::value>(),"Indexes of human players (0=Red, etc.)") - ("loadplayer", po::value(),"specifies which player we are in multiplayer loaded games (0=Red, etc.)") - ("loadserverip",po::value(),"IP for loaded game server") - ("loadserverport",po::value(),"port for loaded game server") ("serverport", po::value(), "override port specified in config file") ("saveprefix", po::value(), "prefix for auto save files") ("savefrequency", po::value(), "limit auto save creation to each N days"); @@ -317,6 +265,17 @@ int main(int argc, char * argv[]) session["headless"].Bool() = true; session["onlyai"].Bool() = true; } + else if(vm.count("spectate")) + { + session["spectate"].Bool() = true; + session["spectate-ignore-hero"].Bool() = vm.count("spectate-ignore-hero"); + session["spectate-skip-battle"].Bool() = vm.count("spectate-skip-battle"); + session["spectate-skip-battle-result"].Bool() = vm.count("spectate-skip-battle-result"); + if(vm.count("spectate-hero-speed")) + session["spectate-hero-speed"].Integer() = vm["spectate-hero-speed"].as(); + if(vm.count("spectate-battle-speed")) + session["spectate-battle-speed"].Float() = vm["spectate-battle-speed"].as(); + } // Server settings session["donotstartserver"].Bool() = vm.count("donotstartserver"); @@ -441,6 +400,7 @@ int main(int argc, char * argv[]) CCS = new CClientState(); CGI = new CGameInfo(); //contains all global informations about game (texts, lodHandlers, map handler etc.) + CSH = new CServerHandler(); // Initialize video #ifdef DISABLE_VIDEO CCS->videoh = new CEmptyVideoPlayer(); @@ -453,15 +413,17 @@ int main(int argc, char * argv[]) logGlobal->info("\tInitializing video: %d ms", pomtime.getDiff()); - //initializing audio - CCS->soundh = new CSoundHandler(); - CCS->soundh->init(); - CCS->soundh->setVolume(settings["general"]["sound"].Float()); - CCS->musich = new CMusicHandler(); - CCS->musich->init(); - CCS->musich->setVolume(settings["general"]["music"].Float()); - logGlobal->info("Initializing screen and sound handling: %d ms", pomtime.getDiff()); - + if(!settings["session"]["headless"].Bool()) + { + //initializing audio + CCS->soundh = new CSoundHandler(); + CCS->soundh->init(); + CCS->soundh->setVolume(settings["general"]["sound"].Float()); + CCS->musich = new CMusicHandler(); + CCS->musich->init(); + CCS->musich->setVolume(settings["general"]["music"].Float()); + logGlobal->info("Initializing screen and sound handling: %d ms", pomtime.getDiff()); + } #ifdef __APPLE__ // Ctrl+click should be treated as a right click on Mac OS X SDL_SetHint(SDL_HINT_MAC_CTRL_CLICK_EMULATE_RIGHT_CLICK, "1"); @@ -500,41 +462,21 @@ int main(int argc, char * argv[]) session["oneGoodAI"].Bool() = vm.count("oneGoodAI"); session["aiSolo"].Bool() = false; - bfs::path fileToStartFrom; //none by default - if(vm.count("start")) - fileToStartFrom = vm["start"].as(); if(vm.count("testmap")) { session["testmap"].String() = vm["testmap"].as(); + session["onlyai"].Bool() = true; + boost::thread(&CServerHandler::debugStartTest, CSH, session["testmap"].String(), false); } - - session["spectate"].Bool() = vm.count("spectate"); - if(session["spectate"].Bool()) + else if(vm.count("testsave")) { - session["spectate-ignore-hero"].Bool() = vm.count("spectate-ignore-hero"); - session["spectate-skip-battle"].Bool() = vm.count("spectate-skip-battle"); - session["spectate-skip-battle-result"].Bool() = vm.count("spectate-skip-battle-result"); - if(vm.count("spectate-hero-speed")) - session["spectate-hero-speed"].Integer() = vm["spectate-hero-speed"].as(); - if(vm.count("spectate-battle-speed")) - session["spectate-battle-speed"].Float() = vm["spectate-battle-speed"].as(); - } - if(!session["testmap"].isNull()) - { - startTestMap(session["testmap"].String()); + session["testsave"].String() = vm["testsave"].as(); + session["onlyai"].Bool() = true; + boost::thread(&CServerHandler::debugStartTest, CSH, session["testsave"].String(), true); } else { - if(!fileToStartFrom.empty() && bfs::exists(fileToStartFrom)) - startGameFromFile(fileToStartFrom); //ommit pregame and start the game using settings from file - else - { - if(!fileToStartFrom.empty()) - { - logGlobal->warn("Warning: cannot find given file to start from (%s). Falling back to main menu.", fileToStartFrom.string()); - } - GH.curInt = CGPreGame::create(); //will set CGP pointer to itself - } + GH.curInt = CMainMenu::create(); } if(!settings["session"]["headless"].Bool()) @@ -614,8 +556,8 @@ void processCommand(const std::string &message) } else { - if(client && client->erm) - client->erm->executeUserCommand(message); + if(CSH->client && CSH->client->erm) + CSH->client->erm->executeUserCommand(message); std::cout << "erm>"; } } @@ -665,21 +607,21 @@ void processCommand(const std::string &message) } else if(cn=="save") { - if(!client) + if(!CSH->client) { std::cout << "Game in not active"; return; } std::string fname; readed >> fname; - client->save(fname); + CSH->client->save(fname); } // else if(cn=="load") // { // // TODO: this code should end the running game and manage to call startGame instead // std::string fname; // readed >> fname; -// client->loadGame(fname); +// CSH->client->loadGame(fname); // } else if(message=="convert txt") { @@ -861,22 +803,6 @@ void processCommand(const std::string &message) logGlobal->info("Option %s disabled!", what); } } - else if(cn == "sinfo") - { - std::string fname; - readed >> fname; - if(fname.size() && SEL) - { - CSaveFile out(fname); - out << SEL->sInfo; - } - } - else if(cn == "start") - { - std::string fname; - readed >> fname; - startGameFromFile(fname); - } else if(cn == "unlock") { std::string mxname; @@ -935,8 +861,8 @@ void processCommand(const std::string &message) { YourTurn yt; yt.player = player; - yt.daysWithoutCastle = client->getPlayer(player)->daysWithoutCastle; - yt.applyCl(client); + yt.daysWithoutCastle = CSH->client->getPlayer(player)->daysWithoutCastle; + yt.applyCl(CSH->client); }; Settings session = settings.write["session"]; @@ -947,7 +873,7 @@ void processCommand(const std::string &message) else if(cn == "gosolo") { boost::unique_lock un(*CPlayerInterface::pim); - if(!client) + if(!CSH->client) { std::cout << "Game in not active"; return; @@ -955,23 +881,23 @@ void processCommand(const std::string &message) PlayerColor color; if(session["aiSolo"].Bool()) { - for(auto & elem : client->gameState()->players) + for(auto & elem : CSH->client->gameState()->players) { if(elem.second.human) - client->installNewPlayerInterface(std::make_shared(elem.first), elem.first); + CSH->client->installNewPlayerInterface(std::make_shared(elem.first), elem.first); } } else { color = LOCPLINT->playerID; removeGUI(); - for(auto & elem : client->gameState()->players) + for(auto & elem : CSH->client->gameState()->players) { if(elem.second.human) { - auto AiToGive = client->aiNameForPlayer(*client->getPlayerSettings(elem.first), false); + auto AiToGive = CSH->client->aiNameForPlayer(*CSH->client->getPlayerSettings(elem.first), false); logNetwork->info("Player %s will be lead by %s", elem.first, AiToGive); - client->installNewPlayerInterface(CDynLibHandler::getNewAI(AiToGive), elem.first); + CSH->client->installNewPlayerInterface(CDynLibHandler::getNewAI(AiToGive), elem.first); } } GH.totalRedraw(); @@ -986,7 +912,7 @@ void processCommand(const std::string &message) boost::to_lower(colorName); boost::unique_lock un(*CPlayerInterface::pim); - if(!client) + if(!CSH->client) { std::cout << "Game in not active"; return; @@ -994,7 +920,7 @@ void processCommand(const std::string &message) PlayerColor color; if(LOCPLINT) color = LOCPLINT->playerID; - for(auto & elem : client->gameState()->players) + for(auto & elem : CSH->client->gameState()->players) { if(elem.second.human || (colorName.length() && elem.first.getNum() != vstd::find_pos(GameConstants::PLAYER_COLOR_NAMES, colorName))) @@ -1003,7 +929,7 @@ void processCommand(const std::string &message) } removeGUI(); - client->installNewPlayerInterface(std::make_shared(elem.first), elem.first); + CSH->client->installNewPlayerInterface(std::make_shared(elem.first), elem.first); } GH.totalRedraw(); if(color != PlayerColor::NEUTRAL) @@ -1290,46 +1216,59 @@ static void handleEvent(SDL_Event & ev) { switch(ev.user.code) { - case FORCE_QUIT: + case EUserEvent::FORCE_QUIT: { handleQuit(false); return; } break; - case RETURN_TO_MAIN_MENU: + case EUserEvent::RETURN_TO_MAIN_MENU: { - endGame(); - GH.curInt = CGPreGame::create(); + CSH->endGameplay(); + GH.curInt = CMainMenu::create(); GH.defActionsDef = 63; } break; - case RESTART_GAME: + case EUserEvent::RESTART_GAME: { - StartInfo si = *client->getStartInfo(true); - si.seedToBeUsed = 0; //server gives new random generator seed if 0 - endGame(); - startGame(&si); + CSH->sendStartGame(); } break; - case PREPARE_RESTART_CAMPAIGN: + case EUserEvent::CAMPAIGN_START_SCENARIO: { - auto si = reinterpret_cast(ev.user.data1); - endGame(); - startGame(si); + CSH->endGameplay(); + GH.curInt = CMainMenu::create(); + auto ourCampaign = std::shared_ptr(reinterpret_cast(ev.user.data1)); + auto & epilogue = ourCampaign->camp->scenarios[ourCampaign->mapsConquered.back()].epilog; + auto finisher = [=]() + { + if(ourCampaign->mapsRemaining.size()) + { + CMM->openCampaignLobby(ourCampaign); + } + }; + if(epilogue.hasPrologEpilog) + { + GH.pushInt(new CPrologEpilogVideo(epilogue, finisher)); + } + else + { + finisher(); + } } break; - case RETURN_TO_MENU_LOAD: - endGame(); - CGPreGame::create(); + case EUserEvent::RETURN_TO_MENU_LOAD: + CSH->endGameplay(); + CMainMenu::create(); GH.defActionsDef = 63; - CGP->update(); - CGP->menu->switchToTab(vstd::find_pos(CGP->menu->menuNameToEntry, "load")); - GH.curInt = CGP; + CMM->update(); + CMM->menu->switchToTab(vstd::find_pos(CMM->menu->menuNameToEntry, "load")); + GH.curInt = CMM; break; - case FULLSCREEN_TOGGLED: + case EUserEvent::FULLSCREEN_TOGGLED: fullScreenChanged(); break; - case INTERFACE_CHANGED: + case EUserEvent::INTERFACE_CHANGED: if(LOCPLINT) LOCPLINT->updateAmbientSounds(); break; @@ -1360,7 +1299,7 @@ static void handleEvent(SDL_Event & ev) static void mainLoop() { SettingsListener resChanged = settings.listen["video"]["fullscreen"]; - resChanged([](const JsonNode &newState){ CGuiHandler::pushSDLEvent(SDL_USEREVENT, FULLSCREEN_TOGGLED); }); + resChanged([](const JsonNode &newState){ CGuiHandler::pushSDLEvent(SDL_USEREVENT, EUserEvent::FULLSCREEN_TOGGLED); }); inGuiThread.reset(new bool(true)); GH.mainFPSmng->init(); @@ -1374,69 +1313,18 @@ static void mainLoop() handleEvent(ev); } + CSH->applyPacksOnLobbyScreen(); GH.renderFrame(); } } -void startGame(StartInfo * options, CConnection *serv) -{ - if(!settings["session"]["donotstartserver"].Bool()) - { - serverAlive.waitWhileTrue(); - serverAlive.setn(true); - } - - if(settings["session"]["onlyai"].Bool()) - { - auto ais = vm.count("ai") ? vm["ai"].as>() : std::vector(); - - int i = 0; - - - for(auto & elem : options->playerInfos) - { - elem.second.playerID = PlayerSettings::PLAYER_AI; - if(i < ais.size()) - elem.second.name = ais[i++]; - } - } - - client = new CClient(); - CPlayerInterface::howManyPeople = 0; - switch(options->mode) //new game - { - case StartInfo::NEW_GAME: - case StartInfo::CAMPAIGN: - client->newGame(serv, options); - break; - case StartInfo::LOAD_GAME: - std::string fname = options->mapname; - boost::algorithm::erase_last(fname,".vlgm1"); - if(!vm.count("loadplayer")) - client->loadGame(fname); - else - client->loadGame(fname,vm.count("loadserver"),vm.count("loadhumanplayerindices") ? vm["loadhumanplayerindices"].as>() : std::vector(),vm.count("loadnumplayers") ? vm["loadnumplayers"].as() : 1,vm["loadplayer"].as(),vm.count("loadserverip") ? vm["loadserverip"].as() : "", vm.count("loadserverport") ? vm["loadserverport"].as() : CServerHandler::getDefaultPort()); - break; - } - { - TLockGuard _(client->connectionHandlerMutex); - client->connectionHandler = make_unique(&CClient::run, client); - } -} - -void endGame() -{ - client->endGame(); - vstd::clear_pointer(client); -} - void handleQuit(bool ask) { auto quitApplication = []() { - if(client) - endGame(); + if(CSH->client) + CSH->endGameplay(); dispose(); vstd::clear_pointer(console); boost::this_thread::sleep(boost::posix_time::milliseconds(750)); @@ -1450,7 +1338,7 @@ void handleQuit(bool ask) exit(0); }; - if(client && LOCPLINT && ask) + if(CSH->client && LOCPLINT && ask) { CCS->curh->changeGraphic(ECursor::ADVENTURE, 0); LOCPLINT->showYesNoDialog(CGI->generaltexth->allTexts[69], quitApplication, 0); diff --git a/client/CMT.h b/client/CMT.h index 93af4710f..c06a684b6 100644 --- a/client/CMT.h +++ b/client/CMT.h @@ -9,7 +9,6 @@ */ #pragma once #include -#include "../lib/CondSh.h" extern SDL_Texture * screenTexture; @@ -20,7 +19,5 @@ extern SDL_Surface *screen; // main screen surface extern SDL_Surface *screen2; // and hlp surface (used to store not-active interfaces layer) extern SDL_Surface *screenBuf; // points to screen (if only advmapint is present) or screen2 (else) - should be used when updating controls which are not regularly redrawed -extern CondSh serverAlive; //used to prevent game start from executing if server is already running - void removeGUI(); void handleQuit(bool ask = true); diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 6f23b36bb..1b119a5dc 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -43,6 +43,20 @@ set(client_SRCS windows/InfoWindows.cpp windows/QuickRecruitmentWindow.cpp + mainmenu/CMainMenu.cpp + mainmenu/CCampaignScreen.cpp + mainmenu/CreditsScreen.cpp + mainmenu/CPrologEpilogVideo.cpp + + lobby/CBonusSelection.cpp + lobby/CSelectionBase.cpp + lobby/CLobbyScreen.cpp + lobby/CSavingScreen.cpp + lobby/CScenarioInfoScreen.cpp + lobby/OptionsTab.cpp + lobby/RandomMapTab.cpp + lobby/SelectionTab.cpp + CBitmapHandler.cpp CreatureCostBox.cpp CGameInfo.cpp @@ -51,11 +65,12 @@ set(client_SRCS CMT.cpp CMusicHandler.cpp CPlayerInterface.cpp - CPreGame.cpp CVideoHandler.cpp + CServerHandler.cpp Graphics.cpp mapHandler.cpp NetPacksClient.cpp + NetPacksLobbyClient.cpp SDLRWwrapper.cpp ) @@ -100,6 +115,20 @@ set(client_HEADERS windows/InfoWindows.h windows/QuickRecruitmentWindow.h + mainmenu/CMainMenu.h + mainmenu/CCampaignScreen.h + mainmenu/CreditsScreen.h + mainmenu/CPrologEpilogVideo.h + + lobby/CBonusSelection.h + lobby/CSelectionBase.h + lobby/CLobbyScreen.h + lobby/CSavingScreen.h + lobby/CScenarioInfoScreen.h + lobby/OptionsTab.h + lobby/RandomMapTab.h + lobby/SelectionTab.h + CBitmapHandler.h CreatureCostBox.h CGameInfo.h @@ -108,8 +137,8 @@ set(client_HEADERS CMT.h CMusicHandler.h CPlayerInterface.h - CPreGame.h CVideoHandler.h + CServerHandler.h Graphics.h mapHandler.h resource.h diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index e8a61b402..90a848742 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -49,13 +49,16 @@ #include "mapHandler.h" #include "../lib/CStopWatch.h" #include "../lib/StartInfo.h" -#include "../lib/CGameState.h" #include "../lib/CPlayerState.h" #include "../lib/GameConstants.h" #include "gui/CGuiHandler.h" #include "windows/InfoWindows.h" #include "../lib/UnlockGuard.h" +#include "../lib/CPathfinder.h" #include +#include "CServerHandler.h" +// FIXME: only needed for CGameState::mutex +#include "../lib/CGameState.h" // The macro below is used to mark functions that are called by client when game state changes. @@ -90,8 +93,6 @@ CBattleInterface * CPlayerInterface::battleInt; enum EMoveState {STOP_MOVE, WAITING_MOVE, CONTINUE_MOVE, DURING_MOVE}; CondSh stillMoveHero(STOP_MOVE); //used during hero movement -int CPlayerInterface::howManyPeople = 0; - static bool objectBlitOrderSorter(const TerrainTileObject & a, const TerrainTileObject & b) { return CMapHandler::compareObjectBlitOrder(a.obj, b.obj); @@ -114,7 +115,6 @@ CPlayerInterface::CPlayerInterface(PlayerColor Player) logGlobal->trace("\tHuman player interface for player %s being constructed", Player.getStr()); destinationTeleport = ObjectInstanceID(); destinationTeleportPos = int3(-1); - howManyPeople++; GH.defActionsDef = 0; LOCPLINT = this; curAction = nullptr; @@ -139,7 +139,6 @@ CPlayerInterface::~CPlayerInterface() { CCS->soundh->ambientStopAllChannels(); logGlobal->trace("\tHuman player interface for player %s being destructed", playerID.getStr()); - //howManyPeople--; delete showingDialog; delete cingconsole; if (LOCPLINT == this) @@ -148,9 +147,7 @@ CPlayerInterface::~CPlayerInterface() void CPlayerInterface::init(std::shared_ptr CB) { cb = CB; - - if (!towns.size() && !wanderingHeroes.size()) - initializeHeroTownList(); + initializeHeroTownList(); // always recreate advmap interface to avoid possible memory-corruption bugs if (adventureInt) @@ -170,7 +167,7 @@ void CPlayerInterface::yourTurn() std::string prefix = settings["session"]["saveprefix"].String(); if (firstCall) { - if (howManyPeople == 1) + if(CSH->howManyPlayerInterfaces() == 1) adventureInt->setPlayer(playerID); autosaveCount = getLastIndex(prefix + "Autosave_"); @@ -192,7 +189,7 @@ void CPlayerInterface::yourTurn() if (adventureInt->player != playerID) adventureInt->setPlayer(playerID); - if (howManyPeople > 1) //hot seat message + if (CSH->howManyPlayerInterfaces() > 1) //hot seat message { adventureInt->startHotSeatWait(playerID); @@ -1581,45 +1578,20 @@ void CPlayerInterface::objectPropertyChanged(const SetObjectProperty * sop) void CPlayerInterface::initializeHeroTownList() { - std::vector allHeroes = cb->getHeroesInfo(); - /* - std::vector newWanderingHeroes; - - //applying current heroes order to new heroes info - int j; - for (int i = 0; i < wanderingHeroes.size(); i++) - if ((j = vstd::find_pos(allHeroes, wanderingHeroes[i])) >= 0) - if (!allHeroes[j]->inTownGarrison) - { - newWanderingHeroes += allHeroes[j]; - allHeroes -= allHeroes[j]; - } - //all the rest of new heroes go the end of the list - wanderingHeroes.clear(); - wanderingHeroes = newWanderingHeroes; - newWanderingHeroes.clear();*/ - - for (auto & allHeroe : allHeroes) - if (!allHeroe->inTownGarrison) - wanderingHeroes.push_back(allHeroe); - - std::vector allTowns = cb->getTownsInfo(); - /* - std::vector newTowns; - for (int i = 0; i < towns.size(); i++) - if ((j = vstd::find_pos(allTowns, towns[i])) >= 0) + if(!wanderingHeroes.size()) + { + std::vector heroes = cb->getHeroesInfo(); + for(auto & hero : heroes) { - newTowns += allTowns[j]; - allTowns -= allTowns[j]; + if(!hero->inTownGarrison) + wanderingHeroes.push_back(hero); } + } - towns.clear(); - towns = newTowns; - newTowns.clear();*/ - for (auto & allTown : allTowns) - towns.push_back(allTown); + if(!towns.size()) + towns = cb->getTownsInfo(); - if (adventureInt) + if(adventureInt) adventureInt->updateNextHero(nullptr); } @@ -1728,7 +1700,7 @@ void CPlayerInterface::update() return; //if there are any waiting dialogs, show them - if ((howManyPeople <= 1 || makingTurn) && !dialogs.empty() && !showingDialog->get()) + if ((CSH->howManyPlayerInterfaces() <= 1 || makingTurn) && !dialogs.empty() && !showingDialog->get()) { showingDialog->set(true); GH.pushInt(dialogs.front()); @@ -2227,37 +2199,27 @@ void CPlayerInterface::gameOver(PlayerColor player, const EVictoryLossCheckResul waitForAllDialogs(); //wait till all dialogs are displayed and closed } - --howManyPeople; - - if(howManyPeople == 0 && !settings["session"]["spectate"].Bool()) //all human players eliminated + if(CSH->howManyPlayerInterfaces() == 1 && !settings["session"]["spectate"].Bool()) //all human players eliminated { - if (adventureInt) + if(adventureInt) { GH.terminate_cond->setn(true); adventureInt->deactivate(); if (GH.topInt() == adventureInt) GH.popInt(adventureInt); - delete adventureInt; - adventureInt = nullptr; + vstd::clear_pointer(adventureInt); } } - if (cb->getStartInfo()->mode == StartInfo::CAMPAIGN) + if (victoryLossCheckResult.victory() && LOCPLINT == this) { - // if you lose the campaign go back to the main menu - // campaign wins are handled in proposeNextMission - if (victoryLossCheckResult.loss()) requestReturningToMainMenu(); + // end game if current human player has won + requestReturningToMainMenu(true); } - else + else if(CSH->howManyPlayerInterfaces() == 1 && !settings["session"]["spectate"].Bool()) { - if(howManyPeople == 0 && !settings["session"]["spectate"].Bool()) //all human players eliminated - { - requestReturningToMainMenu(); - } - else if (victoryLossCheckResult.victory() && LOCPLINT == this) // end game if current human player has won - { - requestReturningToMainMenu(); - } + //all human players eliminated + requestReturningToMainMenu(false); } if (GH.curInt == this) GH.curInt = nullptr; @@ -2378,7 +2340,7 @@ void CPlayerInterface::acceptTurn() } waitWhileDialog(); - if (howManyPeople > 1) + if(CSH->howManyPlayerInterfaces() > 1) adventureInt->startTurn(); adventureInt->heroList.update(); @@ -2573,11 +2535,14 @@ void CPlayerInterface::showShipyardDialogOrProblemPopup(const IShipyard *obj) showShipyardDialog(obj); } -void CPlayerInterface::requestReturningToMainMenu() +void CPlayerInterface::requestReturningToMainMenu(bool won) { - sendCustomEvent(RETURN_TO_MAIN_MENU); + CSH->state = EClientState::DISCONNECTING; CCS->soundh->ambientStopAllChannels(); - cb->unregisterAllInterfaces(); + if(won && cb->getStartInfo()->campState) + CSH->startCampaignScenario(cb->getStartInfo()->campState); + else + sendCustomEvent(EUserEvent::RETURN_TO_MAIN_MENU); } void CPlayerInterface::sendCustomEvent( int code ) @@ -2672,7 +2637,7 @@ void CPlayerInterface::playerStartsTurn(PlayerColor player) GH.popInts(1); } - if (howManyPeople == 1) + if(CSH->howManyPlayerInterfaces() == 1) { GH.curInt = this; adventureInt->startTurn(); @@ -2696,7 +2661,7 @@ void CPlayerInterface::waitForAllDialogs(bool unlockPim) void CPlayerInterface::proposeLoadingGame() { - showYesNoDialog(CGI->generaltexth->allTexts[68], [this](){ sendCustomEvent(RETURN_TO_MENU_LOAD); }, 0, false); + showYesNoDialog(CGI->generaltexth->allTexts[68], [this](){ sendCustomEvent(EUserEvent::RETURN_TO_MENU_LOAD); }, 0, false); } CPlayerInterface::SpellbookLastSetting::SpellbookLastSetting() diff --git a/client/CPlayerInterface.h b/client/CPlayerInterface.h index ea8493891..b0dec6cdc 100644 --- a/client/CPlayerInterface.h +++ b/client/CPlayerInterface.h @@ -59,19 +59,6 @@ namespace boost class recursive_mutex; } -enum -{ - /*CHANGE_SCREEN_RESOLUTION = 1,*/ - RETURN_TO_MAIN_MENU = 2, - //STOP_CLIENT = 3, - RESTART_GAME = 4, - RETURN_TO_MENU_LOAD, - FULLSCREEN_TOGGLED, - PREPARE_RESTART_CAMPAIGN, - FORCE_QUIT, //quit client without question - INTERFACE_CHANGED -}; - /// Central class for managing user interface logic class CPlayerInterface : public CGameInterface, public IUpdateable { @@ -88,7 +75,6 @@ public: int firstCall; // -1 - just loaded game; 1 - just started game; 0 otherwise int autosaveCount; static const int SAVES_COUNT = 5; - static int howManyPeople; CCastleInterface * castleInt; //nullptr if castle window isn't opened static CBattleInterface * battleInt; //nullptr if no battle @@ -247,7 +233,7 @@ public: void acceptTurn(); //used during hot seat after your turn message is close void tryDiggging(const CGHeroInstance *h); void showShipyardDialogOrProblemPopup(const IShipyard *obj); //obj may be town or shipyard; - void requestReturningToMainMenu(); + void requestReturningToMainMenu(bool won); void sendCustomEvent(int code); void proposeLoadingGame(); diff --git a/client/CPreGame.cpp b/client/CPreGame.cpp deleted file mode 100644 index daeb30feb..000000000 --- a/client/CPreGame.cpp +++ /dev/null @@ -1,4374 +0,0 @@ -/* - * CPreGame.cpp, part of VCMI engine - * - * Authors: listed in file AUTHORS in main folder - * - * License: GNU General Public License v2.0 or later - * Full text of license available in license.txt file, in main folder - * - */ -#include "StdInc.h" -#include "CPreGame.h" - -#include "../lib/filesystem/Filesystem.h" -#include "../lib/filesystem/CCompressedStream.h" - -#include "../lib/CStopWatch.h" -#include "gui/SDL_Extensions.h" -#include "CGameInfo.h" -#include "gui/CCursorHandler.h" -#include "../lib/CGeneralTextHandler.h" -#include "../lib/CSkillHandler.h" -#include "../lib/CTownHandler.h" -#include "../lib/CHeroHandler.h" -#include "../lib/mapping/CCampaignHandler.h" -#include "../lib/CCreatureHandler.h" -#include "../lib/JsonNode.h" -#include "CMusicHandler.h" -#include "CVideoHandler.h" -#include "Graphics.h" -#include "../lib/serializer/Connection.h" -#include "../lib/serializer/CTypeList.h" -#include "../lib/VCMIDirs.h" -#include "../lib/mapping/CMap.h" -#include "windows/GUIClasses.h" -#include "CPlayerInterface.h" -#include "../CCallback.h" -#include "CMessage.h" -#include "../lib/spells/CSpellHandler.h" /*for campaign bonuses*/ -#include "../lib/CArtHandler.h" /*for campaign bonuses*/ -#include "../lib/CBuildingHandler.h" /*for campaign bonuses*/ -#include "CBitmapHandler.h" -#include "Client.h" -#include "../lib/NetPacks.h" -#include "../lib/registerTypes//RegisterTypes.h" -#include "../lib/CThreadHelper.h" -#include "../lib/CConfigHandler.h" -#include "../lib/GameConstants.h" -#include "gui/CGuiHandler.h" -#include "gui/CAnimation.h" -#include "widgets/CComponent.h" -#include "widgets/Buttons.h" -#include "widgets/MiscWidgets.h" -#include "widgets/ObjectLists.h" -#include "widgets/TextControls.h" -#include "windows/InfoWindows.h" -#include "../lib/mapping/CMapService.h" -#include "../lib/CRandomGenerator.h" -#include "../lib/CondSh.h" - -namespace fs = boost::filesystem; - -void startGame(StartInfo * options, CConnection *serv = nullptr); -void endGame(); - -CGPreGame * CGP = nullptr; -ISelectionScreenInfo *SEL; - -static PlayerColor playerColor; //if more than one player - applies to the first - -/** - * Stores the current name of the savegame. - * - * TODO better solution for auto-selection when saving already saved games. - * -> CSelectionScreen should be divided into CLoadGameScreen, CSaveGameScreen,... - * The name of the savegame can then be stored non-statically in CGameState and - * passed separately to CSaveGameScreen. - */ -static std::string saveGameName; - -struct EvilHlpStruct -{ - CConnection *serv; - StartInfo *sInfo; - - void reset() - { -// vstd::clear_pointer(serv); - vstd::clear_pointer(sInfo); - } - -} startingInfo; - -static void do_quit() -{ - SDL_Event event; - event.quit.type = SDL_QUIT; - SDL_PushEvent(&event); -} - -static CMapInfo *mapInfoFromGame() -{ - auto ret = new CMapInfo(); - ret->mapHeader = std::unique_ptr(new CMapHeader(*LOCPLINT->cb->getMapHeader())); - return ret; -} - -static void setPlayersFromGame() -{ - playerColor = LOCPLINT->playerID; -} - -static void swapPlayers(PlayerSettings &a, PlayerSettings &b) -{ - std::swap(a.playerID, b.playerID); - std::swap(a.name, b.name); - - if(a.playerID == 1) - playerColor = a.color; - else if(b.playerID == 1) - playerColor = b.color; -} - -void setPlayer(PlayerSettings &pset, ui8 player, const std::map &playerNames) -{ - if(vstd::contains(playerNames, player)) - pset.name = playerNames.find(player)->second; - else - pset.name = CGI->generaltexth->allTexts[468];//Computer - - pset.playerID = player; - if(player == playerNames.begin()->first) - playerColor = pset.color; -} - -void updateStartInfo(std::string filename, StartInfo & sInfo, const std::unique_ptr & mapHeader, const std::map &playerNames) -{ - sInfo.playerInfos.clear(); - if(!mapHeader.get()) - { - return; - } - - sInfo.mapname = filename; - playerColor = PlayerColor::NEUTRAL; - - auto namesIt = playerNames.cbegin(); - - for (int i = 0; i < mapHeader->players.size(); i++) - { - const PlayerInfo &pinfo = mapHeader->players[i]; - - //neither computer nor human can play - no player - if (!(pinfo.canHumanPlay || pinfo.canComputerPlay)) - continue; - - PlayerSettings &pset = sInfo.playerInfos[PlayerColor(i)]; - pset.color = PlayerColor(i); - if(pinfo.canHumanPlay && namesIt != playerNames.cend()) - { - setPlayer(pset, namesIt++->first, playerNames); - } - else - { - setPlayer(pset, 0, playerNames); - if(!pinfo.canHumanPlay) - { - pset.compOnly = true; - } - } - - pset.castle = pinfo.defaultCastle(); - pset.hero = pinfo.defaultHero(); - - if(pset.hero != PlayerSettings::RANDOM && pinfo.hasCustomMainHero()) - { - pset.hero = pinfo.mainCustomHeroId; - pset.heroName = pinfo.mainCustomHeroName; - pset.heroPortrait = pinfo.mainCustomHeroPortrait; - } - - pset.handicap = PlayerSettings::NO_HANDICAP; - } -} - -template class CApplyOnPG; - -class CBaseForPGApply -{ -public: - virtual void applyOnPG(CSelectionScreen *selScr, void *pack) const =0; - virtual ~CBaseForPGApply(){}; - template static CBaseForPGApply *getApplier(const U * t=nullptr) - { - return new CApplyOnPG(); - } -}; - -template class CApplyOnPG : public CBaseForPGApply -{ -public: - void applyOnPG(CSelectionScreen *selScr, void *pack) const override - { - T *ptr = static_cast(pack); - ptr->apply(selScr); - } -}; - -template <> class CApplyOnPG : public CBaseForPGApply -{ -public: - void applyOnPG(CSelectionScreen *selScr, void *pack) const override - { - logGlobal->error("Cannot apply on PG plain CPack!"); - assert(0); - } -}; - -static CApplier *applier = nullptr; - -static CPicture* createPicture(const JsonNode& config) -{ - return new CPicture(config["name"].String(), config["x"].Float(), config["y"].Float()); -} - -CMenuScreen::CMenuScreen(const JsonNode& configNode): - config(configNode) -{ - OBJ_CONSTRUCTION_CAPTURING_ALL; - - background = new CPicture(config["background"].String()); - if (config["scalable"].Bool()) - { - if (background->bg->format->palette) - background->convertToScreenBPP(); - background->scaleTo(Point(screen->w, screen->h)); - } - - pos = background->center(); - - for(const JsonNode& node : config["items"].Vector()) - menuNameToEntry.push_back(node["name"].String()); - - for(const JsonNode& node : config["images"].Vector()) - images.push_back(createPicture(node)); - - //Hardcoded entry - menuNameToEntry.push_back("credits"); - - tabs = new CTabbedInt(std::bind(&CMenuScreen::createTab, this, _1), CTabbedInt::DestroyFunc()); - tabs->type |= REDRAW_PARENT; -} - -CIntObject * CMenuScreen::createTab(size_t index) -{ - if (config["items"].Vector().size() == index) - return new CreditsScreen(); - - return new CMenuEntry(this, config["items"].Vector()[index]); -} - -void CMenuScreen::showAll(SDL_Surface * to) -{ - CIntObject::showAll(to); - - if (pos.h != to->h || pos.w != to->w) - CMessage::drawBorder(PlayerColor(1), to, pos.w+28, pos.h+30, pos.x-14, pos.y-15); - -} - -void CMenuScreen::show(SDL_Surface * to) -{ - if (!config["video"].isNull()) - CCS->videoh->update(config["video"]["x"].Float() + pos.x, config["video"]["y"].Float() + pos.y, to, true, false); - CIntObject::show(to); -} - -void CMenuScreen::activate() -{ - CCS->musich->playMusic("Music/MainMenu", true); - if (!config["video"].isNull()) - CCS->videoh->open(config["video"]["name"].String()); - CIntObject::activate(); -} - -void CMenuScreen::deactivate() -{ - if (!config["video"].isNull()) - CCS->videoh->close(); - - CIntObject::deactivate(); -} - -void CMenuScreen::switchToTab(size_t index) -{ - tabs->setActive(index); -} - -//funciton for std::string -> std::function conversion for main menu -static std::function genCommand(CMenuScreen* menu, std::vector menuType, const std::string &string) -{ - static const std::vector commandType = - {"to", "campaigns", "start", "load", "exit", "highscores"}; - - static const std::vector gameType = - {"single", "multi", "campaign", "tutorial"}; - - std::list commands; - boost::split(commands, string, boost::is_any_of("\t ")); - - if (!commands.empty()) - { - size_t index = std::find(commandType.begin(), commandType.end(), commands.front()) - commandType.begin(); - commands.pop_front(); - if (index > 3 || !commands.empty()) - { - switch (index) - { - break; case 0://to - switch to another tab, if such tab exists - { - size_t index2 = std::find(menuType.begin(), menuType.end(), commands.front()) - menuType.begin(); - if ( index2 != menuType.size()) - return std::bind(&CMenuScreen::switchToTab, menu, index2); - } - break; case 1://open campaign selection window - { - return std::bind(&CGPreGame::openCampaignScreen, CGP, commands.front()); - } - break; case 2://start - { - switch (std::find(gameType.begin(), gameType.end(), commands.front()) - gameType.begin()) - { - case 0: return std::bind(&CGPreGame::openSel, CGP, CMenuScreen::newGame, CMenuScreen::SINGLE_PLAYER); - case 1: return &pushIntT; - case 2: return std::bind(&CGPreGame::openSel, CGP, CMenuScreen::campaignList, CMenuScreen::SINGLE_PLAYER); - //TODO: start tutorial - case 3: return std::bind(CInfoWindow::showInfoDialog, "Sorry, tutorial is not implemented yet\n", (const std::vector*)nullptr, false, PlayerColor(1)); - } - } - break; case 3://load - { - switch (std::find(gameType.begin(), gameType.end(), commands.front()) - gameType.begin()) - { - case 0: return std::bind(&CGPreGame::openSel, CGP, CMenuScreen::loadGame, CMenuScreen::SINGLE_PLAYER); - case 1: return std::bind(&CGPreGame::openSel, CGP, CMenuScreen::loadGame, CMenuScreen::MULTI_HOT_SEAT); - case 2: return std::bind(&CGPreGame::openSel, CGP, CMenuScreen::loadGame, CMenuScreen::SINGLE_CAMPAIGN); - //TODO: load tutorial - case 3: return std::bind(CInfoWindow::showInfoDialog, "Sorry, tutorial is not implemented yet\n", (const std::vector*)nullptr, false, PlayerColor(1)); - } - } - break; case 4://exit - { - return std::bind(CInfoWindow::showYesNoDialog, std::ref(CGI->generaltexth->allTexts[69]), (const std::vector*)nullptr, do_quit, 0, false, PlayerColor(1)); - } - break; case 5://highscores - { - //TODO: high scores - return std::bind(CInfoWindow::showInfoDialog, "Sorry, high scores menu is not implemented yet\n", (const std::vector*)nullptr, false, PlayerColor(1)); - } - } - } - } - logGlobal->error("Failed to parse command: %s", string); - return std::function(); -} - -CButton* CMenuEntry::createButton(CMenuScreen* parent, const JsonNode& button) -{ - std::function command = genCommand(parent, parent->menuNameToEntry, button["command"].String()); - - std::pair help; - if (!button["help"].isNull() && button["help"].Float() > 0) - help = CGI->generaltexth->zelp[button["help"].Float()]; - - int posx = button["x"].Float(); - if (posx < 0) - posx = pos.w + posx; - - int posy = button["y"].Float(); - if (posy < 0) - posy = pos.h + posy; - - return new CButton(Point(posx, posy), button["name"].String(), help, command, button["hotkey"].Float()); -} - -CMenuEntry::CMenuEntry(CMenuScreen* parent, const JsonNode &config) -{ - OBJ_CONSTRUCTION_CAPTURING_ALL; - type |= REDRAW_PARENT; - pos = parent->pos; - - for(const JsonNode& node : config["images"].Vector()) - images.push_back(createPicture(node)); - - for(const JsonNode& node : config["buttons"].Vector()) - { - buttons.push_back(createButton(parent, node)); - buttons.back()->hoverable = true; - buttons.back()->type |= REDRAW_PARENT; - } -} - -CreditsScreen::CreditsScreen(): - positionCounter(0) -{ - addUsedEvents(LCLICK | RCLICK); - type |= REDRAW_PARENT; - OBJ_CONSTRUCTION_CAPTURING_ALL; - pos.w = CGP->menu->pos.w; - pos.h = CGP->menu->pos.h; - auto textFile = CResourceHandler::get()->load(ResourceID("DATA/CREDITS.TXT"))->readAll(); - std::string text((char*)textFile.first.get(), textFile.second); - size_t firstQuote = text.find('\"')+1; - text = text.substr(firstQuote, text.find('\"', firstQuote) - firstQuote ); - credits = new CMultiLineLabel(Rect(pos.w - 350, 0, 350, 600), FONT_CREDITS, CENTER, Colors::WHITE, text); - credits->scrollTextTo(-600); // move all text below the screen -} - -void CreditsScreen::show(SDL_Surface * to) -{ - CIntObject::show(to); - positionCounter++; - if (positionCounter % 2 == 0) - credits->scrollTextBy(1); - - //end of credits, close this screen - if (credits->textSize.y + 600 < positionCounter / 2) - clickRight(false, false); -} - -void CreditsScreen::clickLeft(tribool down, bool previousState) -{ - clickRight(down, previousState); -} - -void CreditsScreen::clickRight(tribool down, bool previousState) -{ - CTabbedInt* menu = dynamic_cast(parent); - assert(menu); - menu->setActive(0); -} - -CGPreGameConfig & CGPreGameConfig::get() -{ - static CGPreGameConfig config; - return config; -} - -const JsonNode & CGPreGameConfig::getConfig() const -{ - return config; -} - -const JsonNode & CGPreGameConfig::getCampaigns() const -{ - return campaignSets; -} - - -CGPreGameConfig::CGPreGameConfig() : - campaignSets(JsonNode(ResourceID("config/campaignSets.json"))), - config(JsonNode(ResourceID("config/mainmenu.json"))) -{ - -} - -CGPreGame::CGPreGame() -{ - pos.w = screen->w; - pos.h = screen->h; - - GH.defActionsDef = 63; - CGP = this; - menu = new CMenuScreen(CGPreGameConfig::get().getConfig()["window"]); - loadGraphics(); -} - -CGPreGame::~CGPreGame() -{ - boost::unique_lock lock(*CPlayerInterface::pim); - disposeGraphics(); - if(CGP == this) - CGP = nullptr; - - if(GH.curInt == this) - GH.curInt = nullptr; -} - -void CGPreGame::openSel(CMenuScreen::EState screenType, CMenuScreen::EGameMode gameMode) -{ - GH.pushInt(new CSelectionScreen(screenType, gameMode)); -} - -void CGPreGame::loadGraphics() -{ - OBJ_CONSTRUCTION_CAPTURING_ALL; - new CFilledTexture("DIBOXBCK", pos); - - victoryIcons = std::make_shared("SCNRVICT.DEF"); - victoryIcons->load(); - lossIcons = std::make_shared("SCNRLOSS.DEF"); - lossIcons->load(); -} - -void CGPreGame::disposeGraphics() -{ - victoryIcons->unload(); - lossIcons->unload(); -} - -void CGPreGame::update() -{ - if(CGP != this) //don't update if you are not a main interface - return; - - if (GH.listInt.empty()) - { - GH.pushInt(this); - GH.pushInt(menu); - menu->switchToTab(0); - } - - if(SEL) - SEL->update(); - - // Handles mouse and key input - GH.updateTime(); - GH.handleEvents(); - - // check for null othervice crash on finishing a campaign - // /FIXME: find out why GH.listInt is empty to begin with - if (GH.topInt() != nullptr) - GH.topInt()->show(screen); -} - -void CGPreGame::openCampaignScreen(std::string name) -{ - if (vstd::contains(CGPreGameConfig::get().getCampaigns().Struct(), name)) - { - GH.pushInt(new CCampaignScreen(CGPreGameConfig::get().getCampaigns()[name])); - return; - } - logGlobal->error("Unknown campaign set: %s", name); -} - -CGPreGame *CGPreGame::create() -{ - if(!CGP) - CGP = new CGPreGame(); - - GH.terminate_cond->set(false); - return CGP; -} - -void CGPreGame::removeFromGui() -{ - //remove everything but main menu and background - GH.popInts(GH.listInt.size() - 2); - GH.popInt(GH.topInt()); //remove main menu - GH.popInt(GH.topInt()); //remove background -} - -CSelectionScreen::CSelectionScreen(CMenuScreen::EState Type, CMenuScreen::EGameMode GameMode, const std::map * Names, const std::string & Address, const ui16 Port) - : ISelectionScreenInfo(Names), serverHandlingThread(nullptr), mx(new boost::recursive_mutex), - serv(nullptr), ongoingClosing(false), myNameID(255) -{ - CGPreGame::create(); //we depend on its graphics - screenType = Type; - gameMode = GameMode; - - OBJ_CONSTRUCTION_CAPTURING_ALL; - - bool network = (isGuest() || isHost()); - - CServerHandler *sh = nullptr; - if(isHost()) - { - sh = new CServerHandler(); - sh->startServer(); - } - - IShowActivatable::type = BLOCK_ADV_HOTKEYS; - pos.w = 762; - pos.h = 584; - if(Type == CMenuScreen::saveGame) - { - bordered = false; - center(pos); - } - else if(Type == CMenuScreen::campaignList) - { - bordered = false; - bg = new CPicture("CamCust.bmp", 0, 0); - pos = bg->center(); - } - else - { - bordered = true; - //load random background - const JsonVector & bgNames = CGPreGameConfig::get().getConfig()["game-select"].Vector(); - bg = new CPicture(RandomGeneratorUtil::nextItem(bgNames, CRandomGenerator::getDefault())->String(), 0, 0); - pos = bg->center(); - } - - sInfo.difficulty = 1; - current = nullptr; - - sInfo.mode = (Type == CMenuScreen::newGame ? StartInfo::NEW_GAME : StartInfo::LOAD_GAME); - sInfo.turnTime = 0; - curTab = nullptr; - - card = new InfoCard(network); //right info card - if (screenType == CMenuScreen::campaignList) - { - opt = nullptr; - randMapTab = nullptr; - } - else - { - opt = new OptionsTab(); //scenario options tab - opt->recActions = DISPOSE; - - randMapTab = new CRandomMapTab(); - randMapTab->getMapInfoChanged() += std::bind(&CSelectionScreen::changeSelection, this, _1); - randMapTab->recActions = DISPOSE; - } - sel = new SelectionTab(screenType, std::bind(&CSelectionScreen::changeSelection, this, _1), gameMode); //scenario selection tab - sel->recActions = DISPOSE; - - switch(screenType) - { - case CMenuScreen::newGame: - { - SDL_Color orange = {232, 184, 32, 0}; - SDL_Color overlayColor = isGuest() ? orange : Colors::WHITE; - - card->difficulty->addCallback(std::bind(&CSelectionScreen::difficultyChange, this, _1)); - card->difficulty->setSelected(1); - CButton * select = new CButton(Point(411, 80), "GSPBUTT.DEF", CGI->generaltexth->zelp[45], 0, SDLK_s); - select->addCallback([&]() - { - toggleTab(sel); - changeSelection(sel->getSelectedMapInfo()); - }); - select->addTextOverlay(CGI->generaltexth->allTexts[500], FONT_SMALL, overlayColor); - - CButton *opts = new CButton(Point(411, 510), "GSPBUTT.DEF", CGI->generaltexth->zelp[46], std::bind(&CSelectionScreen::toggleTab, this, opt), SDLK_a); - opts->addTextOverlay(CGI->generaltexth->allTexts[501], FONT_SMALL, overlayColor); - - CButton * randomBtn = new CButton(Point(411, 105), "GSPBUTT.DEF", CGI->generaltexth->zelp[47], 0, SDLK_r); - randomBtn->addTextOverlay(CGI->generaltexth->allTexts[740], FONT_SMALL, overlayColor); - randomBtn->addCallback([&]() - { - toggleTab(randMapTab); - changeSelection(randMapTab->getMapInfo()); - }); - - start = new CButton(Point(411, 535), "SCNRBEG.DEF", CGI->generaltexth->zelp[103], std::bind(&CSelectionScreen::startScenario, this), SDLK_b); - - if(network) - { - CButton *hideChat = new CButton(Point(619, 83), "GSPBUT2.DEF", CGI->generaltexth->zelp[48], std::bind(&InfoCard::toggleChat, card), SDLK_h); - hideChat->addTextOverlay(CGI->generaltexth->allTexts[531], FONT_SMALL); - - if(isGuest()) - { - select->block(true); - opts->block(true); - randomBtn->block(true); - start->block(true); - } - } - } - break; - case CMenuScreen::loadGame: - sel->recActions = 255; - start = new CButton(Point(411, 535), "SCNRLOD.DEF", CGI->generaltexth->zelp[103], std::bind(&CSelectionScreen::startScenario, this), SDLK_l); - break; - case CMenuScreen::saveGame: - sel->recActions = 255; - start = new CButton(Point(411, 535), "SCNRSAV.DEF", CGI->generaltexth->zelp[103], std::bind(&CSelectionScreen::startScenario, this), SDLK_s); - break; - case CMenuScreen::campaignList: - sel->recActions = 255; - start = new CButton(Point(411, 535), "SCNRLOD.DEF", CButton::tooltip(), std::bind(&CSelectionScreen::startCampaign, this), SDLK_b); - break; - } - - start->assignedKeys.insert(SDLK_RETURN); - - back = new CButton(Point(581, 535), "SCNRBACK.DEF", CGI->generaltexth->zelp[105], std::bind(&CGuiHandler::popIntTotally, &GH, this), SDLK_ESCAPE); - - if(network) - { - if(isHost()) - { - assert(playerNames.size() == 1 && vstd::contains(playerNames, 1)); //TODO hot-seat/network combo - if(settings["session"]["donotstartserver"].Bool()) - serv = CServerHandler::justConnectToServer(Address, Port); - else - serv = sh->connectToServer(); - *serv << (ui8) 4; - myNameID = 1; - } - else - serv = CServerHandler::justConnectToServer(Address, Port); - - serv->enterPregameConnectionMode(); - *serv << playerNames.begin()->second; - - if(isGuest()) - { - const CMapInfo *map; - *serv >> myNameID >> map; - serv->connectionID = myNameID; - changeSelection(map); - } - else if(current) - { - SelectMap sm(*current); - *serv << &sm; - - UpdateStartOptions uso(sInfo); - *serv << &uso; - } - - applier = new CApplier(); - registerTypesPregamePacks(*applier); - serverHandlingThread = new boost::thread(&CSelectionScreen::handleConnection, this); - } - delete sh; -} - -CSelectionScreen::~CSelectionScreen() -{ - ongoingClosing = true; - if(serv) - { - assert(serverHandlingThread); - QuitMenuWithoutStarting qmws; - *serv << &qmws; -// while(!serverHandlingThread->timed_join(boost::posix_time::milliseconds(50))) -// processPacks(); - serverHandlingThread->join(); - delete serverHandlingThread; - } - playerColor = PlayerColor::CANNOT_DETERMINE; - playerNames.clear(); - - assert(!serv); - vstd::clear_pointer(applier); - delete mx; -} - -void CSelectionScreen::toggleTab(CIntObject *tab) -{ - if(isHost() && serv) - { - PregameGuiAction pga; - if(tab == curTab) - pga.action = PregameGuiAction::NO_TAB; - else if(tab == opt) - pga.action = PregameGuiAction::OPEN_OPTIONS; - else if(tab == sel) - pga.action = PregameGuiAction::OPEN_SCENARIO_LIST; - else if(tab == randMapTab) - pga.action = PregameGuiAction::OPEN_RANDOM_MAP_OPTIONS; - - *serv << &pga; - } - - if(curTab && curTab->active) - { - curTab->deactivate(); - curTab->recActions = DISPOSE; - } - if(curTab != tab) - { - tab->recActions = 255; - tab->activate(); - curTab = tab; - } - else - { - curTab = nullptr; - } - GH.totalRedraw(); -} - -void CSelectionScreen::changeSelection(const CMapInfo * to) -{ - if(current == to) return; - if(isGuest()) - vstd::clear_pointer(current); - - current = to; - - if(to && (screenType == CMenuScreen::loadGame || - screenType == CMenuScreen::saveGame)) - SEL->sInfo.difficulty = to->scenarioOpts->difficulty; - if(screenType != CMenuScreen::campaignList) - { - std::unique_ptr emptyHeader; - if(to) - updateStartInfo(to->fileURI, sInfo, to->mapHeader); - else - updateStartInfo("", sInfo, emptyHeader); - - if(screenType == CMenuScreen::newGame) - { - if(to && to->isRandomMap) - { - sInfo.mapGenOptions = std::shared_ptr(new CMapGenOptions(randMapTab->getMapGenOptions())); - } - else - { - sInfo.mapGenOptions.reset(); - } - } - } - card->changeSelection(to); - if(screenType != CMenuScreen::campaignList) - { - opt->recreate(); - } - - if(isHost() && serv) - { - SelectMap sm(*to); - *serv << &sm; - - UpdateStartOptions uso(sInfo); - *serv << &uso; - } -} - -void CSelectionScreen::startCampaign() -{ - if (SEL->current) - GH.pushInt(new CBonusSelection(SEL->current->fileURI)); -} - -void CSelectionScreen::startScenario() -{ - if(screenType == CMenuScreen::newGame) - { - //there must be at least one human player before game can be started - std::map::const_iterator i; - for(i = SEL->sInfo.playerInfos.cbegin(); i != SEL->sInfo.playerInfos.cend(); i++) - if(i->second.playerID != PlayerSettings::PLAYER_AI) - break; - - if(i == SEL->sInfo.playerInfos.cend()) - { - GH.pushInt(CInfoWindow::create(CGI->generaltexth->allTexts[530])); //You must position yourself prior to starting the game. - return; - } - } - - if(isHost()) - { - start->block(true); - StartWithCurrentSettings swcs; - *serv << &swcs; - ongoingClosing = true; - return; - } - - if(screenType != CMenuScreen::saveGame) - { - if(!current) - return; - - if(sInfo.mapGenOptions) - { - //copy settings from interface to actual options. TODO: refactor, it used to have no effect at all -.- - sInfo.mapGenOptions = std::shared_ptr(new CMapGenOptions(randMapTab->getMapGenOptions())); - - // Update player settings for RMG - for(const auto & psetPair : sInfo.playerInfos) - { - const auto & pset = psetPair.second; - sInfo.mapGenOptions->setStartingTownForPlayer(pset.color, pset.castle); - if(pset.playerID != PlayerSettings::PLAYER_AI) - { - sInfo.mapGenOptions->setPlayerTypeForStandardPlayer(pset.color, EPlayerType::HUMAN); - } - } - - if(!sInfo.mapGenOptions->checkOptions()) - { - GH.pushInt(CInfoWindow::create(CGI->generaltexth->allTexts[751])); - return; - } - } - - saveGameName.clear(); - if(screenType == CMenuScreen::loadGame) - { - saveGameName = sInfo.mapname; - } - - auto si = new StartInfo(sInfo); - CGP->removeFromGui(); - CGP->showLoadingScreen(std::bind(&startGame, si, (CConnection *)nullptr)); - } - else - { - if(!(sel && sel->txt && sel->txt->text.size())) - return; - - saveGameName = "Saves/" + sel->txt->text; - - CFunctionList overWrite; - overWrite += std::bind(&CCallback::save, LOCPLINT->cb.get(), saveGameName); - overWrite += std::bind(&CGuiHandler::popIntTotally, &GH, this); - - if(CResourceHandler::get("local")->existsResource(ResourceID(saveGameName, EResType::CLIENT_SAVEGAME))) - { - std::string hlp = CGI->generaltexth->allTexts[493]; //%s exists. Overwrite? - boost::algorithm::replace_first(hlp, "%s", sel->txt->text); - LOCPLINT->showYesNoDialog(hlp, overWrite, 0, false); - } - else - overWrite(); - } -} - -void CSelectionScreen::difficultyChange( int to ) -{ - assert(screenType == CMenuScreen::newGame); - sInfo.difficulty = to; - propagateOptions(); - redraw(); -} - -void CSelectionScreen::handleConnection() -{ - setThreadName("CSelectionScreen::handleConnection"); - try - { - assert(serv); - while(serv) - { - CPackForSelectionScreen *pack = nullptr; - *serv >> pack; - logNetwork->trace("Received a pack of type %s", typeid(*pack).name()); - assert(pack); - if(QuitMenuWithoutStarting *endingPack = dynamic_cast(pack)) - { - endingPack->apply(this); - } - else if(StartWithCurrentSettings *endingPack = dynamic_cast(pack)) - { - endingPack->apply(this); - } - else - { - boost::unique_lock lll(*mx); - upcomingPacks.push_back(pack); - } - } - } - catch(int i) - { - if(i != 666) - throw; - } - catch(...) - { - handleException(); - throw; - } -} - -void CSelectionScreen::setSInfo(const StartInfo &si) -{ - std::map::const_iterator i; - for(i = si.playerInfos.cbegin(); i != si.playerInfos.cend(); i++) - { - if(i->second.playerID == myNameID) - { - playerColor = i->first; - break; - } - } - - if(i == si.playerInfos.cend()) //not found - playerColor = PlayerColor::CANNOT_DETERMINE; - - sInfo = si; - if(current) - opt->recreate(); //will force to recreate using current sInfo - - card->difficulty->setSelected(si.difficulty); - - if(curTab == randMapTab) - randMapTab->setMapGenOptions(si.mapGenOptions); - - GH.totalRedraw(); -} - -void CSelectionScreen::processPacks() -{ - boost::unique_lock lll(*mx); - while(!upcomingPacks.empty()) - { - CPackForSelectionScreen *pack = upcomingPacks.front(); - upcomingPacks.pop_front(); - CBaseForPGApply *apply = applier->getApplier(typeList.getTypeID(pack)); //find the applier - apply->applyOnPG(this, pack); - delete pack; - } -} - -void CSelectionScreen::update() -{ - if(serverHandlingThread) - processPacks(); -} - -void CSelectionScreen::propagateOptions() -{ - if(isHost() && serv) - { - UpdateStartOptions ups(sInfo); - *serv << &ups; - } -} - -void CSelectionScreen::postRequest(ui8 what, ui8 dir) -{ - if(!isGuest() || !serv) - return; - - RequestOptionsChange roc(what, dir, myNameID); - *serv << &roc; -} - -void CSelectionScreen::postChatMessage(const std::string &txt) -{ - assert(serv); - ChatMessage cm; - cm.message = txt; - cm.playerName = sInfo.getPlayersSettings(myNameID)->name; - *serv << &cm; -} - -void CSelectionScreen::propagateNames() -{ - PlayersNames pn; - pn.playerNames = playerNames; - *serv << &pn; -} - -void CSelectionScreen::showAll(SDL_Surface *to) -{ - CIntObject::showAll(to); - if (bordered && (pos.h != to->h || pos.w != to->w)) - CMessage::drawBorder(PlayerColor(1), to, pos.w+28, pos.h+30, pos.x-14, pos.y-15); -} - -// A new size filter (Small, Medium, ...) has been selected. Populate -// selMaps with the relevant data. -void SelectionTab::filter( int size, bool selectFirst ) -{ - curItems.clear(); - - if(tabType == CMenuScreen::campaignList) - { - for (auto & elem : allItems) - curItems.push_back(&elem); - } - else - { - for (auto & elem : allItems) - if( elem.mapHeader && elem.mapHeader->version && (!size || elem.mapHeader->width == size)) - curItems.push_back(&elem); - } - - if(curItems.size()) - { - slider->block(false); - slider->setAmount(curItems.size()); - sort(); - if(selectFirst) - { - slider->moveTo(0); - onSelect(curItems[0]); - selectAbs(0); - } - } - else - { - slider->block(true); - onSelect(nullptr); - } -} - -std::unordered_set SelectionTab::getFiles(std::string dirURI, int resType) -{ - boost::to_upper(dirURI); - CResourceHandler::get()->updateFilteredFiles([&](const std::string & mount) - { - return boost::algorithm::starts_with(mount, dirURI); - }); - - std::unordered_set ret = CResourceHandler::get()->getFilteredFiles([&](const ResourceID & ident) - { - return ident.getType() == resType - && boost::algorithm::starts_with(ident.getName(), dirURI); - }); - - return ret; -} - -void SelectionTab::parseMaps(const std::unordered_set &files) -{ - logGlobal->debug("Parsing %d maps", files.size()); - allItems.clear(); - for(auto & file : files) - { - try - { - CMapInfo mapInfo; - mapInfo.mapInit(file.getName()); - - // ignore unsupported map versions (e.g. WoG maps without WoG) - // but accept VCMI maps - if((mapInfo.mapHeader->version >= EMapFormat::VCMI) || (mapInfo.mapHeader->version <= CGI->modh->settings.data["textData"]["mapVersion"].Float())) - allItems.push_back(std::move(mapInfo)); - } - catch(std::exception & e) - { - logGlobal->error("Map %s is invalid. Message: %s", file.getName(), e.what()); - } - } -} - -void SelectionTab::parseGames(const std::unordered_set &files, CMenuScreen::EGameMode gameMode) -{ - for(auto & file : files) - { - try - { - CLoadFile lf(*CResourceHandler::get()->getResourceName(file), MINIMAL_SERIALIZATION_VERSION); - lf.checkMagicBytes(SAVEGAME_MAGIC); -// ui8 sign[8]; -// lf >> sign; -// if(std::memcmp(sign,"VCMISVG",7)) -// { -// throw std::runtime_error("not a correct savefile!"); -// } - - // Create the map info object - CMapInfo mapInfo; - mapInfo.mapHeader = make_unique(); - mapInfo.scenarioOpts = nullptr;//to be created by serialiser - lf >> *(mapInfo.mapHeader.get()) >> mapInfo.scenarioOpts; - mapInfo.fileURI = file.getName(); - mapInfo.countPlayers(); - std::time_t time = boost::filesystem::last_write_time(*CResourceHandler::get()->getResourceName(file)); - mapInfo.date = std::asctime(std::localtime(&time)); - - // Filter out other game modes - bool isCampaign = mapInfo.scenarioOpts->mode == StartInfo::CAMPAIGN; - bool isMultiplayer = mapInfo.actualHumanPlayers > 1; - switch(gameMode) - { - case CMenuScreen::SINGLE_PLAYER: - if(isMultiplayer || isCampaign) - mapInfo.mapHeader.reset(); - break; - case CMenuScreen::SINGLE_CAMPAIGN: - if(!isCampaign) - mapInfo.mapHeader.reset(); - break; - default: - if(!isMultiplayer) - mapInfo.mapHeader.reset(); - break; - } - - allItems.push_back(std::move(mapInfo)); - } - catch(const std::exception & e) - { - logGlobal->error("Error: Failed to process %s: %s", file.getName(), e.what()); - } - } -} - -void SelectionTab::parseCampaigns(const std::unordered_set &files ) -{ - allItems.reserve(files.size()); - for (auto & file : files) - { - CMapInfo info; - //allItems[i].date = std::asctime(std::localtime(&files[i].date)); - info.fileURI = file.getName(); - info.campaignInit(); - allItems.push_back(std::move(info)); - } -} - -SelectionTab::SelectionTab(CMenuScreen::EState Type, const std::function &OnSelect, CMenuScreen::EGameMode GameMode) - :bg(nullptr), onSelect(OnSelect) -{ - OBJ_CONSTRUCTION; - selectionPos = 0; - addUsedEvents(LCLICK | WHEEL | KEYBOARD | DOUBLECLICK); - slider = nullptr; - txt = nullptr; - tabType = Type; - - if (Type != CMenuScreen::campaignList) - { - bg = new CPicture("SCSELBCK.bmp", 0, 6); - pos = bg->pos; - } - else - { - bg = nullptr; //use background from parent - type |= REDRAW_PARENT; // we use parent background so we need to make sure it's will be redrawn too - pos.w = parent->pos.w; - pos.h = parent->pos.h; - pos.x += 3; pos.y += 6; - } - - if(GameMode == CMenuScreen::MULTI_NETWORK_GUEST) - { - positions = 18; - } - else - { - switch(tabType) - { - case CMenuScreen::newGame: - parseMaps(getFiles("Maps/", EResType::MAP)); - positions = 18; - break; - - case CMenuScreen::loadGame: - case CMenuScreen::saveGame: - parseGames(getFiles("Saves/", EResType::CLIENT_SAVEGAME), GameMode); - if(tabType == CMenuScreen::loadGame) - { - positions = 18; - } - else - { - positions = 16; - } - if(tabType == CMenuScreen::saveGame) - { - txt = new CTextInput(Rect(32, 539, 350, 20), Point(-32, -25), "GSSTRIP.bmp", 0); - txt->filters += CTextInput::filenameFilter; - } - break; - - case CMenuScreen::campaignList: - parseCampaigns(getFiles("Maps/", EResType::CAMPAIGN)); - positions = 18; - break; - - default: - assert(0); - break; - } - } - - generalSortingBy = (tabType == CMenuScreen::loadGame || tabType == CMenuScreen::saveGame) ? _fileName : _name; - - if (tabType != CMenuScreen::campaignList) - { - //size filter buttons - { - int sizes[] = {36, 72, 108, 144, 0}; - const char * names[] = {"SCSMBUT.DEF", "SCMDBUT.DEF", "SCLGBUT.DEF", "SCXLBUT.DEF", "SCALBUT.DEF"}; - for(int i = 0; i < 5; i++) - new CButton(Point(158 + 47*i, 46), names[i], CGI->generaltexth->zelp[54+i], std::bind(&SelectionTab::filter, this, sizes[i], true)); - } - - //sort buttons buttons - { - int xpos[] = {23, 55, 88, 121, 306, 339}; - const char * names[] = {"SCBUTT1.DEF", "SCBUTT2.DEF", "SCBUTCP.DEF", "SCBUTT3.DEF", "SCBUTT4.DEF", "SCBUTT5.DEF"}; - for(int i = 0; i < 6; i++) - { - ESortBy criteria = (ESortBy)i; - - if(criteria == _name) - criteria = generalSortingBy; - - new CButton(Point(xpos[i], 86), names[i], CGI->generaltexth->zelp[107+i], std::bind(&SelectionTab::sortBy, this, criteria)); - } - } - } - else - { - //sort by buttons - new CButton(Point(23, 86), "CamCusM.DEF", CButton::tooltip(), std::bind(&SelectionTab::sortBy, this, _numOfMaps)); //by num of maps - new CButton(Point(55, 86), "CamCusL.DEF", CButton::tooltip(), std::bind(&SelectionTab::sortBy, this, _name)); //by name - } - - slider = new CSlider(Point(372, 86), tabType != CMenuScreen::saveGame ? 480 : 430, std::bind(&SelectionTab::sliderMove, this, _1), positions, curItems.size(), 0, false, CSlider::BLUE); - slider->addUsedEvents(WHEEL); - - formatIcons = std::make_shared("SCSELC.DEF"); - formatIcons->load(); - - sortingBy = _format; - ascending = true; - filter(0); - //select(0); - switch(tabType) - { - case CMenuScreen::newGame: - logGlobal->error(settings["session"]["lastMap"].String()); - if(settings["session"]["lastMap"].isNull()) - selectFName("Maps/Arrogance"); - else - selectFName(settings["session"]["lastMap"].String()); - - break; - case CMenuScreen::campaignList: - select(0); - break; - case CMenuScreen::loadGame: - case CMenuScreen::saveGame: - if(saveGameName.empty()) - { - if(tabType == CMenuScreen::saveGame) - txt->setText("NEWGAME"); - else - select(0); - } - else - { - selectFName(saveGameName); - } - } -} - -SelectionTab::~SelectionTab() -{ - formatIcons->unload(); -} - -void SelectionTab::sortBy( int criteria ) -{ - if(criteria == sortingBy) - { - ascending = !ascending; - } - else - { - sortingBy = (ESortBy)criteria; - ascending = true; - } - sort(); - - selectAbs(0); -} - -void SelectionTab::sort() -{ - if(sortingBy != generalSortingBy) - std::stable_sort(curItems.begin(), curItems.end(), mapSorter(generalSortingBy)); - std::stable_sort(curItems.begin(), curItems.end(), mapSorter(sortingBy)); - - if(!ascending) - std::reverse(curItems.begin(), curItems.end()); - - redraw(); -} - -void SelectionTab::select( int position ) -{ - if(!curItems.size()) return; - - // New selection. py is the index in curItems. - int py = position + slider->getValue(); - vstd::amax(py, 0); - vstd::amin(py, curItems.size()-1); - - selectionPos = py; - - if(position < 0) - slider->moveBy(position); - else if(position >= positions) - slider->moveBy(position - positions + 1); - - if(tabType == CMenuScreen::newGame) - { - Settings lastMap = settings.write["session"]["lastMap"]; - lastMap->String() = getSelectedMapInfo()->fileURI; - } - - if(txt) - { - auto filename = *CResourceHandler::get("local")->getResourceName( - ResourceID(curItems[py]->fileURI, EResType::CLIENT_SAVEGAME)); - txt->setText(filename.stem().string()); - } - - onSelect(curItems[py]); -} - -void SelectionTab::selectAbs( int position ) -{ - select(position - slider->getValue()); -} - -int SelectionTab::getPosition( int x, int y ) -{ - return -1; -} - -void SelectionTab::sliderMove( int slidPos ) -{ - if(!slider) return; //ignore spurious call when slider is being created - redraw(); -} - - -// Display the tab with the scenario names -// -// elemIdx is the index of the maps or saved game to display on line 0 -// slider->capacity contains the number of available screen lines -// slider->positionsAmnt is the number of elements after filtering -void SelectionTab::printMaps(SDL_Surface *to) -{ - - int elemIdx = slider->getValue(); - - // Display all elements if there's enough space - //if(slider->amount < slider->capacity) - // elemIdx = 0; - - - SDL_Color itemColor; - for (int line = 0; line < positions && elemIdx < curItems.size(); elemIdx++, line++) - { - CMapInfo *currentItem = curItems[elemIdx]; - - if(elemIdx == selectionPos) - itemColor=Colors::YELLOW; - else - itemColor=Colors::WHITE; - - if(tabType != CMenuScreen::campaignList) - { - //amount of players - std::ostringstream ostr(std::ostringstream::out); - ostr << currentItem->playerAmnt << "/" << currentItem->humanPlayers; - printAtLoc(ostr.str(), 29, 120 + line * 25, FONT_SMALL, itemColor, to); - - //map size - std::string temp2 = "C"; - switch (currentItem->mapHeader->width) - { - case 36: - temp2="S"; - break; - case 72: - temp2="M"; - break; - case 108: - temp2="L"; - break; - case 144: - temp2="XL"; - break; - } - printAtMiddleLoc(temp2, 70, 128 + line * 25, FONT_SMALL, itemColor, to); - - int frame = -1, group = 0; - switch (currentItem->mapHeader->version) - { - case EMapFormat::ROE: - frame = 0; - break; - case EMapFormat::AB: - frame = 1; - break; - case EMapFormat::SOD: - frame = 2; - break; - case EMapFormat::WOG: - frame = 3; - break; - case EMapFormat::VCMI: - frame = 0; - group = 1; - break; - default: - // Unknown version. Be safe and ignore that map - logGlobal->warn("Warning: %s has wrong version!", currentItem->fileURI); - continue; - } - auto icon = formatIcons->getImage(frame,group); - if(icon) - { - icon->draw(to, pos.x + 88, pos.y + 117 + line * 25); - icon->decreaseRef(); - } - - //victory conditions - icon = CGP->victoryIcons->getImage(currentItem->mapHeader->victoryIconIndex,0); - if(icon) - { - icon->draw(to, pos.x + 306, pos.y + 117 + line * 25); - icon->decreaseRef(); - } - //loss conditions - icon = CGP->lossIcons->getImage(currentItem->mapHeader->defeatIconIndex,0); - if(icon) - { - icon->draw(to, pos.x + 339, pos.y + 117 + line * 25); - icon->decreaseRef(); - } - } - else //if campaign - { - //number of maps in campaign - std::ostringstream ostr(std::ostringstream::out); - ostr << CGI->generaltexth->campaignRegionNames[ currentItem->campaignHeader->mapVersion ].size(); - printAtLoc(ostr.str(), 29, 120 + line * 25, FONT_SMALL, itemColor, to); - } - - std::string name; - if(tabType == CMenuScreen::newGame) - { - if (!currentItem->mapHeader->name.length()) - currentItem->mapHeader->name = "Unnamed"; - name = currentItem->mapHeader->name; - } - else if(tabType == CMenuScreen::campaignList) - { - name = currentItem->campaignHeader->name; - } - else - { - name = CResourceHandler::get("local")->getResourceName( - ResourceID(currentItem->fileURI, EResType::CLIENT_SAVEGAME))->stem().string(); - } - - //print name - printAtMiddleLoc(name, 213, 128 + line * 25, FONT_SMALL, itemColor, to); - } -} - -void SelectionTab::showAll(SDL_Surface * to) -{ - CIntObject::showAll(to); - printMaps(to); - - std::string title; - switch(tabType) { - case CMenuScreen::newGame: - title = CGI->generaltexth->arraytxt[229]; - break; - case CMenuScreen::loadGame: - title = CGI->generaltexth->arraytxt[230]; - break; - case CMenuScreen::saveGame: - title = CGI->generaltexth->arraytxt[231]; - break; - case CMenuScreen::campaignList: - title = CGI->generaltexth->allTexts[726]; - break; - } - - printAtMiddleLoc(title, 205, 28, FONT_MEDIUM, Colors::YELLOW, to); //Select a Scenario to Play - if(tabType != CMenuScreen::campaignList) - { - printAtMiddleLoc(CGI->generaltexth->allTexts[510], 87, 62, FONT_SMALL, Colors::YELLOW, to); //Map sizes - } -} - -void SelectionTab::clickLeft( tribool down, bool previousState ) -{ - if(down) - { - int line = getLine(); - if(line != -1) - select(line); - } -} -void SelectionTab::keyPressed( const SDL_KeyboardEvent & key ) -{ - if(key.state != SDL_PRESSED) return; - - int moveBy = 0; - switch(key.keysym.sym) - { - case SDLK_UP: - moveBy = -1; - break; - case SDLK_DOWN: - moveBy = +1; - break; - case SDLK_PAGEUP: - moveBy = -positions+1; - break; - case SDLK_PAGEDOWN: - moveBy = +positions-1; - break; - case SDLK_HOME: - select(-slider->getValue()); - return; - case SDLK_END: - select(curItems.size() - slider->getValue()); - return; - default: - return; - } - select(selectionPos - slider->getValue() + moveBy); -} - -void SelectionTab::onDoubleClick() -{ - if(getLine() != -1) //double clicked scenarios list - { - //act as if start button was pressed - (static_cast(parent))->start->clickLeft(false, true); - } -} - -int SelectionTab::getLine() -{ - int line = -1; - Point clickPos(GH.current->button.x, GH.current->button.y); - clickPos = clickPos - pos.topLeft(); - - // Ignore clicks on save name area - int maxPosY; - if(tabType == CMenuScreen::saveGame) - maxPosY = 516; - else - maxPosY = 564; - - if(clickPos.y > 115 && clickPos.y < maxPosY && clickPos.x > 22 && clickPos.x < 371) - { - line = (clickPos.y-115) / 25; //which line - } - - return line; -} - -void SelectionTab::selectFName( std::string fname ) -{ - boost::to_upper(fname); - for(int i = curItems.size() - 1; i >= 0; i--) - { - if(curItems[i]->fileURI == fname) - { - slider->moveTo(i); - selectAbs(i); - return; - } - } - - selectAbs(0); -} - -const CMapInfo * SelectionTab::getSelectedMapInfo() const -{ - return curItems.empty() ? nullptr : curItems[selectionPos]; -} - -CRandomMapTab::CRandomMapTab() -{ - OBJ_CONSTRUCTION; - bg = new CPicture("RANMAPBK", 0, 6); - - // Map Size - mapSizeBtnGroup = new CToggleGroup(0); - mapSizeBtnGroup->pos.y += 81; - mapSizeBtnGroup->pos.x += 158; - const std::vector mapSizeBtns = {"RANSIZS", "RANSIZM","RANSIZL","RANSIZX"}; - addButtonsToGroup(mapSizeBtnGroup, mapSizeBtns, 0, 3, 47, 198); - mapSizeBtnGroup->setSelected(1); - mapSizeBtnGroup->addCallback([&](int btnId) - { - auto mapSizeVal = getPossibleMapSizes(); - mapGenOptions.setWidth(mapSizeVal[btnId]); - mapGenOptions.setHeight(mapSizeVal[btnId]); - if(!SEL->isGuest()) - updateMapInfo(); - }); - - // Two levels - twoLevelsBtn = new CToggleButton(Point(346, 81), "RANUNDR", CGI->generaltexth->zelp[202]); - //twoLevelsBtn->select(true); for now, deactivated - twoLevelsBtn->addCallback([&](bool on) - { - mapGenOptions.setHasTwoLevels(on); - if(!SEL->isGuest()) - updateMapInfo(); - }); - - // Create number defs list - std::vector numberDefs; - for(int i = 0; i <= 8; ++i) - { - numberDefs.push_back("RANNUM" + boost::lexical_cast(i)); - } - - const int NUMBERS_WIDTH = 32; - const int BTNS_GROUP_LEFT_MARGIN = 67; - // Amount of players - playersCntGroup = new CToggleGroup(0); - playersCntGroup->pos.y += 153; - playersCntGroup->pos.x += BTNS_GROUP_LEFT_MARGIN; - addButtonsWithRandToGroup(playersCntGroup, numberDefs, 1, 8, NUMBERS_WIDTH, 204, 212); - playersCntGroup->addCallback([&](int btnId) - { - mapGenOptions.setPlayerCount(btnId); - deactivateButtonsFrom(teamsCntGroup, btnId); - deactivateButtonsFrom(compOnlyPlayersCntGroup, btnId); - validatePlayersCnt(btnId); - if(!SEL->isGuest()) - updateMapInfo(); - }); - - // Amount of teams - teamsCntGroup = new CToggleGroup(0); - teamsCntGroup->pos.y += 219; - teamsCntGroup->pos.x += BTNS_GROUP_LEFT_MARGIN; - addButtonsWithRandToGroup(teamsCntGroup, numberDefs, 0, 7, NUMBERS_WIDTH, 214, 222); - teamsCntGroup->addCallback([&](int btnId) - { - mapGenOptions.setTeamCount(btnId); - if(!SEL->isGuest()) - updateMapInfo(); - }); - - // Computer only players - compOnlyPlayersCntGroup = new CToggleGroup(0); - compOnlyPlayersCntGroup->pos.y += 285; - compOnlyPlayersCntGroup->pos.x += BTNS_GROUP_LEFT_MARGIN; - addButtonsWithRandToGroup(compOnlyPlayersCntGroup, numberDefs, 0, 7, NUMBERS_WIDTH, 224, 232); - compOnlyPlayersCntGroup->addCallback([&](int btnId) - { - mapGenOptions.setCompOnlyPlayerCount(btnId); - deactivateButtonsFrom(compOnlyTeamsCntGroup, (btnId == 0 ? 1 : btnId)); - validateCompOnlyPlayersCnt(btnId); - if(!SEL->isGuest()) - updateMapInfo(); - }); - - // Computer only teams - compOnlyTeamsCntGroup = new CToggleGroup(0); - compOnlyTeamsCntGroup->pos.y += 351; - compOnlyTeamsCntGroup->pos.x += BTNS_GROUP_LEFT_MARGIN; - addButtonsWithRandToGroup(compOnlyTeamsCntGroup, numberDefs, 0, 6, NUMBERS_WIDTH, 234, 241); - deactivateButtonsFrom(compOnlyTeamsCntGroup, 1); - compOnlyTeamsCntGroup->addCallback([&](int btnId) - { - mapGenOptions.setCompOnlyTeamCount(btnId); - if(!SEL->isGuest()) - updateMapInfo(); - }); - - const int WIDE_BTN_WIDTH = 85; - // Water content - waterContentGroup = new CToggleGroup(0); - waterContentGroup->pos.y += 419; - waterContentGroup->pos.x += BTNS_GROUP_LEFT_MARGIN; - const std::vector waterContentBtns = {"RANNONE","RANNORM","RANISLD"}; - addButtonsWithRandToGroup(waterContentGroup, waterContentBtns, 0, 2, WIDE_BTN_WIDTH, 243, 246); - waterContentGroup->addCallback([&](int btnId) - { - mapGenOptions.setWaterContent(static_cast(btnId)); - }); - - // Monster strength - monsterStrengthGroup = new CToggleGroup(0); - monsterStrengthGroup->pos.y += 485; - monsterStrengthGroup->pos.x += BTNS_GROUP_LEFT_MARGIN; - const std::vector monsterStrengthBtns = {"RANWEAK","RANNORM","RANSTRG"}; - addButtonsWithRandToGroup(monsterStrengthGroup, monsterStrengthBtns, 0, 2, WIDE_BTN_WIDTH, 248, 251); - monsterStrengthGroup->addCallback([&](int btnId) - { - if (btnId < 0) - mapGenOptions.setMonsterStrength(EMonsterStrength::RANDOM); - else - mapGenOptions.setMonsterStrength(static_cast(btnId + EMonsterStrength::GLOBAL_WEAK)); //value 2 to 4 - }); - - // Show random maps btn - showRandMaps = new CButton(Point(54, 535), "RANSHOW", CGI->generaltexth->zelp[252]); - - // Initialize map info object - if(!SEL->isGuest()) - updateMapInfo(); -} - -void CRandomMapTab::addButtonsWithRandToGroup(CToggleGroup * group, const std::vector & defs, int nStart, int nEnd, int btnWidth, int helpStartIndex, int helpRandIndex) const -{ - addButtonsToGroup(group, defs, nStart, nEnd, btnWidth, helpStartIndex); - - // Buttons are relative to button group, TODO better solution? - SObjectConstruction obj__i(group); - const std::string RANDOM_DEF = "RANRAND"; - group->addToggle(CMapGenOptions::RANDOM_SIZE, new CToggleButton(Point(256, 0), RANDOM_DEF, CGI->generaltexth->zelp[helpRandIndex])); - group->setSelected(CMapGenOptions::RANDOM_SIZE); -} - -void CRandomMapTab::addButtonsToGroup(CToggleGroup * group, const std::vector & defs, int nStart, int nEnd, int btnWidth, int helpStartIndex) const -{ - // Buttons are relative to button group, TODO better solution? - SObjectConstruction obj__i(group); - int cnt = nEnd - nStart + 1; - for(int i = 0; i < cnt; ++i) - { - auto button = new CToggleButton(Point(i * btnWidth, 0), defs[i + nStart], CGI->generaltexth->zelp[helpStartIndex + i]); - // For blocked state we should use pressed image actually - button->setImageOrder(0, 1, 1, 3); - - group->addToggle(i + nStart, button); - } -} - -void CRandomMapTab::deactivateButtonsFrom(CToggleGroup * group, int startId) -{ - logGlobal->debug("Blocking buttons from %d", startId); - for(auto toggle : group->buttons) - { - if (auto button = dynamic_cast(toggle.second)) - { - if(startId == CMapGenOptions::RANDOM_SIZE || toggle.first < startId) - { - button->block(false); - } - else - { - button->block(true); - } - } - } -} - -void CRandomMapTab::validatePlayersCnt(int playersCnt) -{ - if(playersCnt == CMapGenOptions::RANDOM_SIZE) - { - return; - } - - if(mapGenOptions.getTeamCount() >= playersCnt) - { - mapGenOptions.setTeamCount(playersCnt - 1); - teamsCntGroup->setSelected(mapGenOptions.getTeamCount()); - } - if(mapGenOptions.getCompOnlyPlayerCount() >= playersCnt) - { - mapGenOptions.setCompOnlyPlayerCount(playersCnt - 1); - compOnlyPlayersCntGroup->setSelected(mapGenOptions.getCompOnlyPlayerCount()); - } - - validateCompOnlyPlayersCnt(mapGenOptions.getCompOnlyPlayerCount()); -} - -void CRandomMapTab::validateCompOnlyPlayersCnt(int compOnlyPlayersCnt) -{ - if(compOnlyPlayersCnt == CMapGenOptions::RANDOM_SIZE) - { - return; - } - - if(mapGenOptions.getCompOnlyTeamCount() >= compOnlyPlayersCnt) - { - int compOnlyTeamCount = compOnlyPlayersCnt == 0 ? 0 : compOnlyPlayersCnt - 1; - mapGenOptions.setCompOnlyTeamCount(compOnlyTeamCount); - compOnlyTeamsCntGroup->setSelected(compOnlyTeamCount); - } -} - -std::vector CRandomMapTab::getPossibleMapSizes() -{ - return {CMapHeader::MAP_SIZE_SMALL,CMapHeader::MAP_SIZE_MIDDLE,CMapHeader::MAP_SIZE_LARGE,CMapHeader::MAP_SIZE_XLARGE}; -} - -void CRandomMapTab::showAll(SDL_Surface * to) -{ - CIntObject::showAll(to); - - // Headline - printAtMiddleLoc(CGI->generaltexth->allTexts[738], 222, 36, FONT_BIG, Colors::YELLOW, to); - printAtMiddleLoc(CGI->generaltexth->allTexts[739], 222, 56, FONT_SMALL, Colors::WHITE, to); - - // Map size - printAtMiddleLoc(CGI->generaltexth->allTexts[752], 104, 97, FONT_SMALL, Colors::WHITE, to); - - // Players cnt - printAtLoc(CGI->generaltexth->allTexts[753], 68, 133, FONT_SMALL, Colors::WHITE, to); - - // Teams cnt - printAtLoc(CGI->generaltexth->allTexts[754], 68, 199, FONT_SMALL, Colors::WHITE, to); - - // Computer only players cnt - printAtLoc(CGI->generaltexth->allTexts[755], 68, 265, FONT_SMALL, Colors::WHITE, to); - - // Computer only teams cnt - printAtLoc(CGI->generaltexth->allTexts[756], 68, 331, FONT_SMALL, Colors::WHITE, to); - - // Water content - printAtLoc(CGI->generaltexth->allTexts[757], 68, 398, FONT_SMALL, Colors::WHITE, to); - - // Monster strength - printAtLoc(CGI->generaltexth->allTexts[758], 68, 465, FONT_SMALL, Colors::WHITE, to); -} - -void CRandomMapTab::updateMapInfo() -{ - // Generate header info - mapInfo = make_unique(); - mapInfo->isRandomMap = true; - mapInfo->mapHeader = make_unique(); - mapInfo->mapHeader->version = EMapFormat::SOD; - mapInfo->mapHeader->name = CGI->generaltexth->allTexts[740]; - mapInfo->mapHeader->description = CGI->generaltexth->allTexts[741]; - mapInfo->mapHeader->difficulty = 1; // Normal - mapInfo->mapHeader->height = mapGenOptions.getHeight(); - mapInfo->mapHeader->width = mapGenOptions.getWidth(); - mapInfo->mapHeader->twoLevel = mapGenOptions.getHasTwoLevels(); - - // Generate player information - mapInfo->mapHeader->players.clear(); - int playersToGen = PlayerColor::PLAYER_LIMIT_I; - if(mapGenOptions.getPlayerCount() != CMapGenOptions::RANDOM_SIZE) - playersToGen = mapGenOptions.getPlayerCount(); - mapInfo->mapHeader->howManyTeams = playersToGen; - - for(int i = 0; i < playersToGen; ++i) - { - PlayerInfo player; - player.isFactionRandom = true; - player.canComputerPlay = true; - if(mapGenOptions.getCompOnlyPlayerCount() != CMapGenOptions::RANDOM_SIZE && - i >= mapGenOptions.getHumanOnlyPlayerCount()) - { - player.canHumanPlay = false; - } - else - { - player.canHumanPlay = true; - } - player.team = TeamID(i); - player.hasMainTown = true; - player.generateHeroAtMainTown = true; - mapInfo->mapHeader->players.push_back(player); - } - - mapInfoChanged(mapInfo.get()); -} - -CFunctionList & CRandomMapTab::getMapInfoChanged() -{ - return mapInfoChanged; -} - -const CMapInfo * CRandomMapTab::getMapInfo() const -{ - return mapInfo.get(); -} - -const CMapGenOptions & CRandomMapTab::getMapGenOptions() const -{ - return mapGenOptions; -} - -void CRandomMapTab::setMapGenOptions(std::shared_ptr opts) -{ - mapSizeBtnGroup->setSelected(vstd::find_pos(getPossibleMapSizes(), opts->getWidth())); - twoLevelsBtn->setSelected(opts->getHasTwoLevels()); - playersCntGroup->setSelected(opts->getPlayerCount()); - teamsCntGroup->setSelected(opts->getTeamCount()); - compOnlyPlayersCntGroup->setSelected(opts->getCompOnlyPlayerCount()); - compOnlyTeamsCntGroup->setSelected(opts->getCompOnlyTeamCount()); - waterContentGroup->setSelected(opts->getWaterContent()); - monsterStrengthGroup->setSelected(opts->getMonsterStrength()); -} - -CChatBox::CChatBox(const Rect &rect) -{ - OBJ_CONSTRUCTION; - pos += rect; - addUsedEvents(KEYBOARD | TEXTINPUT); - captureAllKeys = true; - type |= REDRAW_PARENT; - - const int height = graphics->fonts[FONT_SMALL]->getLineHeight(); - inputBox = new CTextInput(Rect(0, rect.h - height, rect.w, height)); - inputBox->removeUsedEvents(KEYBOARD); - chatHistory = new CTextBox("", Rect(0, 0, rect.w, rect.h - height), 1); - - chatHistory->label->color = Colors::GREEN; -} - -void CChatBox::keyPressed(const SDL_KeyboardEvent & key) -{ - if(key.keysym.sym == SDLK_RETURN && key.state == SDL_PRESSED && inputBox->text.size()) - { - SEL->postChatMessage(inputBox->text); - inputBox->setText(""); - } - else - inputBox->keyPressed(key); -} - -void CChatBox::addNewMessage(const std::string &text) -{ - CCS->soundh->playSound("CHAT"); - chatHistory->setText(chatHistory->label->text + text + "\n"); - if(chatHistory->slider) - chatHistory->slider->moveToMax(); -} - -InfoCard::InfoCard( bool Network ) - : sizes(nullptr), bg(nullptr), network(Network), chatOn(false), chat(nullptr), playerListBg(nullptr), - difficulty(nullptr) -{ - OBJ_CONSTRUCTION_CAPTURING_ALL; - CIntObject::type |= REDRAW_PARENT; - pos.x += 393; - pos.y += 6; - addUsedEvents(RCLICK); - mapDescription = nullptr; - - Rect descriptionRect(26, 149, 320, 115); - mapDescription = new CTextBox("", descriptionRect, 1); - - if(SEL->screenType == CMenuScreen::campaignList) - { - CSelectionScreen *ss = static_cast(parent); - mapDescription->addChild(new CPicture(*ss->bg, descriptionRect + Point(-393, 0)), true); //move subpicture bg to our description control (by default it's our (Infocard) child) - } - else - { - bg = new CPicture("GSELPOP1.bmp", 0, 0); - parent->addChild(bg); - auto it = vstd::find(parent->children, this); //our position among parent children - parent->children.insert(it, bg); //put BG before us - parent->children.pop_back(); - pos.w = bg->pos.w; - pos.h = bg->pos.h; - sizes = new CAnimImage("SCNRMPSZ", 4, 0, 318, 22);//let it be custom size (frame 4) by default - sizes->recActions &= ~(SHOWALL | UPDATE);//explicit draw - - sFlags = std::make_shared("ITGFLAGS.DEF"); - sFlags->load(); - difficulty = new CToggleGroup(0); - { - static const char *difButns[] = {"GSPBUT3.DEF", "GSPBUT4.DEF", "GSPBUT5.DEF", "GSPBUT6.DEF", "GSPBUT7.DEF"}; - - for(int i = 0; i < 5; i++) - { - auto button = new CToggleButton(Point(110 + i*32, 450), difButns[i], CGI->generaltexth->zelp[24+i]); - - difficulty->addToggle(i, button); - if(SEL->screenType != CMenuScreen::newGame) - button->block(true); - } - } - - if(network) - { - playerListBg = new CPicture("CHATPLUG.bmp", 16, 276); - chat = new CChatBox(Rect(26, 132, 340, 132)); - - chatOn = true; - mapDescription->disable(); - } - } - - victory = new CAnimImage("SCNRVICT",0, 0, 24, 302); - victory->recActions &= ~(SHOWALL | UPDATE);//explicit draw - loss = new CAnimImage("SCNRLOSS", 0, 0, 24, 359); - loss->recActions &= ~(SHOWALL | UPDATE);//explicit draw -} - -InfoCard::~InfoCard() -{ - if(sFlags) - sFlags->unload(); -} - -void InfoCard::showAll(SDL_Surface * to) -{ - CIntObject::showAll(to); - - //blit texts - if(SEL->screenType != CMenuScreen::campaignList) - { - printAtLoc(CGI->generaltexth->allTexts[390] + ":", 24, 400, FONT_SMALL, Colors::WHITE, to); //Allies - printAtLoc(CGI->generaltexth->allTexts[391] + ":", 190, 400, FONT_SMALL, Colors::WHITE, to); //Enemies - printAtLoc(CGI->generaltexth->allTexts[494], 33, 430, FONT_SMALL, Colors::YELLOW, to);//"Map Diff:" - printAtLoc(CGI->generaltexth->allTexts[492] + ":", 133,430, FONT_SMALL, Colors::YELLOW, to); //player difficulty - printAtLoc(CGI->generaltexth->allTexts[218] + ":", 290,430, FONT_SMALL, Colors::YELLOW, to); //"Rating:" - printAtLoc(CGI->generaltexth->allTexts[495], 26, 22, FONT_SMALL, Colors::YELLOW, to); //Scenario Name: - if(!chatOn) - { - printAtLoc(CGI->generaltexth->allTexts[496], 26, 132, FONT_SMALL, Colors::YELLOW, to); //Scenario Description: - printAtLoc(CGI->generaltexth->allTexts[497], 26, 283, FONT_SMALL, Colors::YELLOW, to); //Victory Condition: - printAtLoc(CGI->generaltexth->allTexts[498], 26, 339, FONT_SMALL, Colors::YELLOW, to); //Loss Condition: - } - else //players list - { - std::map playerNames = SEL->playerNames; - int playerSoFar = 0; - for (auto i = SEL->sInfo.playerInfos.cbegin(); i != SEL->sInfo.playerInfos.cend(); i++) - { - if(i->second.playerID != PlayerSettings::PLAYER_AI) - { - printAtLoc(i->second.name, 24, 285 + playerSoFar++ * graphics->fonts[FONT_SMALL]->getLineHeight(), FONT_SMALL, Colors::WHITE, to); - playerNames.erase(i->second.playerID); - } - } - - playerSoFar = 0; - for (auto i = playerNames.cbegin(); i != playerNames.cend(); i++) - { - printAtLoc(i->second, 193, 285 + playerSoFar++ * graphics->fonts[FONT_SMALL]->getLineHeight(), FONT_SMALL, Colors::WHITE, to); - } - - } - } - - if(SEL->current) - { - if(SEL->screenType != CMenuScreen::campaignList) - { - if(!chatOn) - { - CMapHeader * header = SEL->current->mapHeader.get(); - //victory conditions - printAtLoc(header->victoryMessage, 60, 307, FONT_SMALL, Colors::WHITE, to); - victory->setFrame(header->victoryIconIndex); - victory->showAll(to); - //loss conditoins - printAtLoc(header->defeatMessage, 60, 366, FONT_SMALL, Colors::WHITE, to); - loss->setFrame(header->defeatIconIndex); - loss->showAll(to); - } - - //difficulty - assert(SEL->current->mapHeader->difficulty <= 4); - std::string &diff = CGI->generaltexth->arraytxt[142 + SEL->current->mapHeader->difficulty]; - printAtMiddleLoc(diff, 62, 472, FONT_SMALL, Colors::WHITE, to); - - //selecting size icon - switch (SEL->current->mapHeader->width) - { - case 36: - sizes->setFrame(0); - break; - case 72: - sizes->setFrame(1); - break; - case 108: - sizes->setFrame(2); - break; - case 144: - sizes->setFrame(3); - break; - default: - sizes->setFrame(4); - break; - } - sizes->showAll(to); - - if(SEL->screenType == CMenuScreen::loadGame) - printToLoc((static_cast(SEL->current))->date,308,34, FONT_SMALL, Colors::WHITE, to); - - //print flags - int fx = 34 + graphics->fonts[FONT_SMALL]->getStringWidth(CGI->generaltexth->allTexts[390]); - int ex = 200 + graphics->fonts[FONT_SMALL]->getStringWidth(CGI->generaltexth->allTexts[391]); - - TeamID myT; - - if(playerColor < PlayerColor::PLAYER_LIMIT) - myT = SEL->current->mapHeader->players[playerColor.getNum()].team; - else - myT = TeamID::NO_TEAM; - - for (auto i = SEL->sInfo.playerInfos.cbegin(); i != SEL->sInfo.playerInfos.cend(); i++) - { - int *myx = ((i->first == playerColor || SEL->current->mapHeader->players[i->first.getNum()].team == myT) ? &fx : &ex); - auto flag = sFlags->getImage(i->first.getNum(),0); - flag->draw(to, pos.x + *myx, pos.y + 399); - *myx += flag->width(); - flag->decreaseRef(); - } - - std::string tob; - switch (SEL->sInfo.difficulty) - { - case 0: - tob="80%"; - break; - case 1: - tob="100%"; - break; - case 2: - tob="130%"; - break; - case 3: - tob="160%"; - break; - case 4: - tob="200%"; - break; - } - printAtMiddleLoc(tob, 311, 472, FONT_SMALL, Colors::WHITE, to); - } - - //blit description - std::string name; - - if (SEL->screenType == CMenuScreen::campaignList) - { - name = SEL->current->campaignHeader->name; - } - else - { - name = SEL->current->mapHeader->name; - } - - //name - if (name.length()) - printAtLoc(name, 26, 39, FONT_BIG, Colors::YELLOW, to); - else - printAtLoc("Unnamed", 26, 39, FONT_BIG, Colors::YELLOW, to); - } -} - -void InfoCard::changeSelection( const CMapInfo *to ) -{ - if(to && mapDescription) - { - if (SEL->screenType == CMenuScreen::campaignList) - mapDescription->setText(to->campaignHeader->description); - else - mapDescription->setText(to->mapHeader->description); - - mapDescription->label->scrollTextTo(0); - if (mapDescription->slider) - mapDescription->slider->moveToMin(); - - if(SEL->screenType != CMenuScreen::newGame && SEL->screenType != CMenuScreen::campaignList) { - //difficulty->block(true); - difficulty->setSelected(SEL->sInfo.difficulty); - } - } - redraw(); -} - -void InfoCard::clickRight( tribool down, bool previousState ) -{ - static const Rect flagArea(19, 397, 335, 23); - if(SEL->current && down && isItInLoc(flagArea, GH.current->motion.x, GH.current->motion.y)) - showTeamsPopup(); -} - -void InfoCard::showTeamsPopup() -{ - SDL_Surface *bmp = CMessage::drawDialogBox(256, 90 + 50 * SEL->current->mapHeader->howManyTeams); - - graphics->fonts[FONT_MEDIUM]->renderTextCenter(bmp, CGI->generaltexth->allTexts[657], Colors::YELLOW, Point(128, 30)); - - for(int i = 0; i < SEL->current->mapHeader->howManyTeams; i++) - { - std::vector flags; - std::string hlp = CGI->generaltexth->allTexts[656]; //Team %d - hlp.replace(hlp.find("%d"), 2, boost::lexical_cast(i+1)); - - graphics->fonts[FONT_SMALL]->renderTextCenter(bmp, hlp, Colors::WHITE, Point(128, 65 + 50 * i)); - - for(int j = 0; j < PlayerColor::PLAYER_LIMIT_I; j++) - if((SEL->current->mapHeader->players[j].canHumanPlay || SEL->current->mapHeader->players[j].canComputerPlay) - && SEL->current->mapHeader->players[j].team == TeamID(i)) - flags.push_back(j); - - int curx = 128 - 9*flags.size(); - for(auto & flag : flags) - { - auto icon = sFlags->getImage(flag,0); - icon->draw(bmp, curx, 75 + 50*i); - icon->decreaseRef(); - curx += 18; - } - } - - GH.pushInt(new CInfoPopup(bmp, true)); -} - -void InfoCard::toggleChat() -{ - setChat(!chatOn); -} - -void InfoCard::setChat(bool activateChat) -{ - if(chatOn == activateChat) - return; - - assert(active); - - if(activateChat) - { - mapDescription->disable(); - chat->enable(); - playerListBg->enable(); - } - else - { - mapDescription->enable(); - chat->disable(); - playerListBg->disable(); - } - - chatOn = activateChat; - GH.totalRedraw(); -} - -OptionsTab::OptionsTab(): - turnDuration(nullptr) -{ - OBJ_CONSTRUCTION; - bg = new CPicture("ADVOPTBK", 0, 6); - pos = bg->pos; - - if(SEL->screenType == CMenuScreen::newGame) - turnDuration = new CSlider(Point(55, 551), 194, std::bind(&OptionsTab::setTurnLength, this, _1), 1, 11, 11, true, CSlider::BLUE); -} - -OptionsTab::~OptionsTab() -{ - -} - -void OptionsTab::showAll(SDL_Surface * to) -{ - CIntObject::showAll(to); - printAtMiddleLoc(CGI->generaltexth->allTexts[515], 222, 30, FONT_BIG, Colors::YELLOW, to); - printAtMiddleWBLoc(CGI->generaltexth->allTexts[516], 222, 68, FONT_SMALL, 300, Colors::WHITE, to); //Select starting options, handicap, and name for each player in the game. - printAtMiddleWBLoc(CGI->generaltexth->allTexts[517], 107, 110, FONT_SMALL, 100, Colors::YELLOW, to); //Player Name Handicap Type - printAtMiddleWBLoc(CGI->generaltexth->allTexts[518], 197, 110, FONT_SMALL, 70, Colors::YELLOW, to); //Starting Town - printAtMiddleWBLoc(CGI->generaltexth->allTexts[519], 273, 110, FONT_SMALL, 70, Colors::YELLOW, to); //Starting Hero - printAtMiddleWBLoc(CGI->generaltexth->allTexts[520], 349, 110, FONT_SMALL, 70, Colors::YELLOW, to); //Starting Bonus - printAtMiddleLoc(CGI->generaltexth->allTexts[521], 222, 538, FONT_SMALL, Colors::YELLOW, to); // Player Turn Duration - if (turnDuration) - printAtMiddleLoc(CGI->generaltexth->turnDurations[turnDuration->getValue()], 319,559, FONT_SMALL, Colors::WHITE, to);//Turn duration value -} - -void OptionsTab::nextCastle( PlayerColor player, int dir ) -{ - if(SEL->isGuest()) - { - SEL->postRequest(RequestOptionsChange::TOWN, dir); - return; - } - - PlayerSettings &s = SEL->sInfo.playerInfos[player]; - si16 &cur = s.castle; - auto & allowed = SEL->current->mapHeader->players[s.color.getNum()].allowedFactions; - const bool allowRandomTown = SEL->current->mapHeader->players[s.color.getNum()].isFactionRandom; - - if (cur == PlayerSettings::NONE) //no change - return; - - if (cur == PlayerSettings::RANDOM) //first/last available - { - if (dir > 0) - cur = *allowed.begin(); //id of first town - else - cur = *allowed.rbegin(); //id of last town - - } - else // next/previous available - { - if((cur == *allowed.begin() && dir < 0 ) || (cur == *allowed.rbegin() && dir > 0)) - { - if(allowRandomTown) - { - cur = PlayerSettings::RANDOM; - } - else - { - if (dir > 0) - cur = *allowed.begin(); - else - cur = *allowed.rbegin(); - } - } - else - { - assert(dir >= -1 && dir <= 1); //othervice std::advance may go out of range - auto iter = allowed.find(cur); - std::advance(iter, dir); - cur = *iter; - } - } - - if(s.hero >= 0 && !SEL->current->mapHeader->players[s.color.getNum()].hasCustomMainHero()) // remove hero unless it set to fixed one in map editor - { - usedHeroes.erase(s.hero); // restore previously selected hero back to available pool - s.hero = PlayerSettings::RANDOM; - } - if(cur < 0 && s.bonus == PlayerSettings::RESOURCE) - s.bonus = PlayerSettings::RANDOM; - - entries[player]->selectButtons(); - - SEL->propagateOptions(); - entries[player]->update(); - redraw(); -} - -void OptionsTab::nextHero( PlayerColor player, int dir ) -{ - if(SEL->isGuest()) - { - SEL->postRequest(RequestOptionsChange::HERO, dir); - return; - } - - PlayerSettings &s = SEL->sInfo.playerInfos[player]; - int old = s.hero; - if (s.castle < 0 || s.playerID == PlayerSettings::PLAYER_AI || s.hero == PlayerSettings::NONE) - return; - - if (s.hero == PlayerSettings::RANDOM) // first/last available - { - int max = CGI->heroh->heroes.size(), - min = 0; - s.hero = nextAllowedHero(player, min,max,0,dir); - } - else - { - if(dir > 0) - s.hero = nextAllowedHero(player, s.hero, CGI->heroh->heroes.size(), 1, dir); - else - s.hero = nextAllowedHero(player, -1, s.hero, 1, dir); // min needs to be -1 -- hero at index 0 would be skipped otherwise - } - - if(old != s.hero) - { - usedHeroes.erase(old); - usedHeroes.insert(s.hero); - entries[player]->update(); - redraw(); - } - SEL->propagateOptions(); -} - -int OptionsTab::nextAllowedHero( PlayerColor player, int min, int max, int incl, int dir ) -{ - if(dir>0) - { - for(int i=min+incl; i<=max-incl; i++) - if(canUseThisHero(player, i)) - return i; - } - else - { - for(int i=max-incl; i>=min+incl; i--) - if(canUseThisHero(player, i)) - return i; - } - return -1; -} - -bool OptionsTab::canUseThisHero( PlayerColor player, int ID ) -{ - return CGI->heroh->heroes.size() > ID - && SEL->sInfo.playerInfos[player].castle == CGI->heroh->heroes[ID]->heroClass->faction - && !vstd::contains(usedHeroes, ID) - && SEL->current->mapHeader->allowedHeroes[ID]; -} - -void OptionsTab::nextBonus( PlayerColor player, int dir ) -{ - if(SEL->isGuest()) - { - SEL->postRequest(RequestOptionsChange::BONUS, dir); - return; - } - - PlayerSettings &s = SEL->sInfo.playerInfos[player]; - PlayerSettings::Ebonus &ret = s.bonus = static_cast(static_cast(s.bonus) + dir); - - if (s.hero==PlayerSettings::NONE && - !SEL->current->mapHeader->players[s.color.getNum()].heroesNames.size() && - ret==PlayerSettings::ARTIFACT) //no hero - can't be artifact - { - if (dir<0) - ret=PlayerSettings::RANDOM; - else ret=PlayerSettings::GOLD; - } - - if(ret > PlayerSettings::RESOURCE) - ret = PlayerSettings::RANDOM; - if(ret < PlayerSettings::RANDOM) - ret = PlayerSettings::RESOURCE; - - if (s.castle==PlayerSettings::RANDOM && ret==PlayerSettings::RESOURCE) //random castle - can't be resource - { - if (dir<0) - ret=PlayerSettings::GOLD; - else ret=PlayerSettings::RANDOM; - } - - SEL->propagateOptions(); - entries[player]->update(); - redraw(); -} - -void OptionsTab::recreate() -{ - for(auto & elem : entries) - { - delete elem.second; - } - entries.clear(); - usedHeroes.clear(); - - OBJ_CONSTRUCTION_CAPTURING_ALL; - for(auto it = SEL->sInfo.playerInfos.begin(); it != SEL->sInfo.playerInfos.end(); ++it) - { - entries.insert(std::make_pair(it->first, new PlayerOptionsEntry(this, it->second))); - const std::vector &heroes = SEL->current->mapHeader->players[it->first.getNum()].heroesNames; - for(auto & heroe : heroes) - if(heroe.heroId >= 0)//in VCMI map format heroId = -1 means random hero - usedHeroes.insert(heroe.heroId); - } -} - -void OptionsTab::setTurnLength( int npos ) -{ - static const int times[] = {1, 2, 4, 6, 8, 10, 15, 20, 25, 30, 0}; - vstd::amin(npos, ARRAY_COUNT(times) - 1); - - SEL->sInfo.turnTime = times[npos]; - redraw(); -} - -void OptionsTab::flagPressed( PlayerColor color ) -{ - PlayerSettings &clicked = SEL->sInfo.playerInfos[color]; - PlayerSettings *old = nullptr; - - if(SEL->playerNames.size() == 1) //single player -> just swap - { - if(color == playerColor) //that color is already selected, no action needed - return; - - old = &SEL->sInfo.playerInfos[playerColor]; - swapPlayers(*old, clicked); - } - else - { - //identify clicked player - int clickedNameID = clicked.playerID; //human is a number of player, zero means AI - - if(clickedNameID > 0 && playerToRestore.id == clickedNameID) //player to restore is about to being replaced -> put him back to the old place - { - PlayerSettings &restPos = SEL->sInfo.playerInfos[playerToRestore.color]; - SEL->setPlayer(restPos, playerToRestore.id); - playerToRestore.reset(); - } - - int newPlayer; //which player will take clicked position - - //who will be put here? - if(!clickedNameID) //AI player clicked -> if possible replace computer with unallocated player - { - newPlayer = SEL->getIdOfFirstUnallocatedPlayer(); - if(!newPlayer) //no "free" player -> get just first one - newPlayer = SEL->playerNames.begin()->first; - } - else //human clicked -> take next - { - auto i = SEL->playerNames.find(clickedNameID); //clicked one - i++; //player AFTER clicked one - - if(i != SEL->playerNames.end()) - newPlayer = i->first; - else - newPlayer = 0; //AI if we scrolled through all players - } - - SEL->setPlayer(clicked, newPlayer); //put player - - //if that player was somewhere else, we need to replace him with computer - if(newPlayer) //not AI - { - for(auto i = SEL->sInfo.playerInfos.begin(); i != SEL->sInfo.playerInfos.end(); i++) - { - int curNameID = i->second.playerID; - if(i->first != color && curNameID == newPlayer) - { - assert(i->second.playerID); - playerToRestore.color = i->first; - playerToRestore.id = newPlayer; - SEL->setPlayer(i->second, 0); //set computer - old = &i->second; - break; - } - } - } - } - - entries[clicked.color]->selectButtons(); - if(old) - { - entries[old->color]->selectButtons(); - if(old->hero >= 0) - usedHeroes.erase(old->hero); - - old->hero = entries[old->color]->pi.defaultHero(); - entries[old->color]->update(); // update previous frame images in case entries were auto-updated - } - - SEL->propagateOptions(); - GH.totalRedraw(); -} - -OptionsTab::PlayerOptionsEntry::PlayerOptionsEntry( OptionsTab *owner, PlayerSettings &S) - : pi(SEL->current->mapHeader->players[S.color.getNum()]), s(S) -{ - OBJ_CONSTRUCTION; - defActions |= SHARE_POS; - - int serial = 0; - for(int g=0; g < s.color.getNum(); ++g) - { - PlayerInfo &itred = SEL->current->mapHeader->players[g]; - if( itred.canComputerPlay || itred.canHumanPlay) - serial++; - } - - pos.x += 54; - pos.y += 122 + serial*50; - - static const char *flags[] = {"AOFLGBR.DEF", "AOFLGBB.DEF", "AOFLGBY.DEF", "AOFLGBG.DEF", - "AOFLGBO.DEF", "AOFLGBP.DEF", "AOFLGBT.DEF", "AOFLGBS.DEF"}; - static const char *bgs[] = {"ADOPRPNL.bmp", "ADOPBPNL.bmp", "ADOPYPNL.bmp", "ADOPGPNL.bmp", - "ADOPOPNL.bmp", "ADOPPPNL.bmp", "ADOPTPNL.bmp", "ADOPSPNL.bmp"}; - - bg = new CPicture(BitmapHandler::loadBitmap(bgs[s.color.getNum()]), 0, 0, true); - if(SEL->screenType == CMenuScreen::newGame) - { - btns[0] = new CButton(Point(107, 5), "ADOPLFA.DEF", CGI->generaltexth->zelp[132], std::bind(&OptionsTab::nextCastle, owner, s.color, -1)); - btns[1] = new CButton(Point(168, 5), "ADOPRTA.DEF", CGI->generaltexth->zelp[133], std::bind(&OptionsTab::nextCastle, owner, s.color, +1)); - btns[2] = new CButton(Point(183, 5), "ADOPLFA.DEF", CGI->generaltexth->zelp[148], std::bind(&OptionsTab::nextHero, owner, s.color, -1)); - btns[3] = new CButton(Point(244, 5), "ADOPRTA.DEF", CGI->generaltexth->zelp[149], std::bind(&OptionsTab::nextHero, owner, s.color, +1)); - btns[4] = new CButton(Point(259, 5), "ADOPLFA.DEF", CGI->generaltexth->zelp[164], std::bind(&OptionsTab::nextBonus, owner, s.color, -1)); - btns[5] = new CButton(Point(320, 5), "ADOPRTA.DEF", CGI->generaltexth->zelp[165], std::bind(&OptionsTab::nextBonus, owner, s.color, +1)); - } - else - for(auto & elem : btns) - elem = nullptr; - - selectButtons(); - - assert(SEL->current && SEL->current->mapHeader); - const PlayerInfo &p = SEL->current->mapHeader->players[s.color.getNum()]; - assert(p.canComputerPlay || p.canHumanPlay); //someone must be able to control this player - if(p.canHumanPlay && p.canComputerPlay) - whoCanPlay = HUMAN_OR_CPU; - else if(p.canComputerPlay) - whoCanPlay = CPU; - else - whoCanPlay = HUMAN; - - if(SEL->screenType != CMenuScreen::scenarioInfo - && SEL->current->mapHeader->players[s.color.getNum()].canHumanPlay) - { - flag = new CButton(Point(-43, 2), flags[s.color.getNum()], CGI->generaltexth->zelp[180], std::bind(&OptionsTab::flagPressed, owner, s.color)); - flag->hoverable = true; - flag->block(SEL->isGuest()); - } - else - flag = nullptr; - - town = new SelectedBox(Point(119, 2), s, TOWN); - hero = new SelectedBox(Point(195, 2), s, HERO); - bonus = new SelectedBox(Point(271, 2), s, BONUS); -} - -void OptionsTab::PlayerOptionsEntry::showAll(SDL_Surface * to) -{ - CIntObject::showAll(to); - printAtMiddleLoc(s.name, 55, 10, FONT_SMALL, Colors::WHITE, to); - printAtMiddleWBLoc(CGI->generaltexth->arraytxt[206+whoCanPlay], 28, 39, FONT_TINY, 50, Colors::WHITE, to); -} - -void OptionsTab::PlayerOptionsEntry::update() -{ - town->update(); - hero->update(); - bonus->update(); -} - -void OptionsTab::PlayerOptionsEntry::selectButtons() -{ - if(!btns[0]) - return; - - const bool foreignPlayer = SEL->isGuest() && s.color != playerColor; - - if( (pi.allowedFactions.size() < 2 && !pi.isFactionRandom) || foreignPlayer) - { - btns[0]->disable(); - btns[1]->disable(); - } - else - { - btns[0]->enable(); - btns[1]->enable(); - } - - if( (pi.defaultHero() != -1 || !s.playerID || s.castle < 0) //fixed hero - || foreignPlayer)//or not our player - { - btns[2]->disable(); - btns[3]->disable(); - } - else - { - btns[2]->enable(); - btns[3]->enable(); - } - - if(foreignPlayer) - { - btns[4]->disable(); - btns[5]->disable(); - } - else - { - btns[4]->enable(); - btns[5]->enable(); - } -} - -size_t OptionsTab::CPlayerSettingsHelper::getImageIndex() -{ - enum EBonusSelection //frames of bonuses file - { - WOOD_ORE = 0, CRYSTAL = 1, GEM = 2, - MERCURY = 3, SULFUR = 5, GOLD = 8, - ARTIFACT = 9, RANDOM = 10, - WOOD = 0, ORE = 0, MITHRIL = 10, // resources unavailable in bonuses file - - TOWN_RANDOM = 38, TOWN_NONE = 39, // Special frames in ITPA - HERO_RANDOM = 163, HERO_NONE = 164 // Special frames in PortraitsSmall - }; - - switch(type) - { - case TOWN: - switch (settings.castle) - { - case PlayerSettings::NONE: return TOWN_NONE; - case PlayerSettings::RANDOM: return TOWN_RANDOM; - default: return CGI->townh->factions[settings.castle]->town->clientInfo.icons[true][false] + 2; - } - - case HERO: - switch (settings.hero) - { - case PlayerSettings::NONE: return HERO_NONE; - case PlayerSettings::RANDOM: return HERO_RANDOM; - default: - { - if(settings.heroPortrait >= 0) - return settings.heroPortrait; - return CGI->heroh->heroes[settings.hero]->imageIndex; - } - } - - case BONUS: - { - switch(settings.bonus) - { - case PlayerSettings::RANDOM: return RANDOM; - case PlayerSettings::ARTIFACT: return ARTIFACT; - case PlayerSettings::GOLD: return GOLD; - case PlayerSettings::RESOURCE: - { - switch(CGI->townh->factions[settings.castle]->town->primaryRes) - { - case Res::WOOD_AND_ORE : return WOOD_ORE; - case Res::WOOD : return WOOD; - case Res::MERCURY : return MERCURY; - case Res::ORE : return ORE; - case Res::SULFUR : return SULFUR; - case Res::CRYSTAL : return CRYSTAL; - case Res::GEMS : return GEM; - case Res::GOLD : return GOLD; - case Res::MITHRIL : return MITHRIL; - } - } - } - } - } - return 0; -} - -std::string OptionsTab::CPlayerSettingsHelper::getImageName() -{ - switch(type) - { - case OptionsTab::TOWN: return "ITPA"; - case OptionsTab::HERO: return "PortraitsSmall"; - case OptionsTab::BONUS: return "SCNRSTAR"; - } - return ""; -} - -std::string OptionsTab::CPlayerSettingsHelper::getTitle() -{ - switch(type) - { - case OptionsTab::TOWN: return (settings.castle < 0) ? CGI->generaltexth->allTexts[103] : CGI->generaltexth->allTexts[80]; - case OptionsTab::HERO: return (settings.hero < 0) ? CGI->generaltexth->allTexts[101] : CGI->generaltexth->allTexts[77]; - case OptionsTab::BONUS: - { - switch(settings.bonus) - { - case PlayerSettings::RANDOM: return CGI->generaltexth->allTexts[86]; //{Random Bonus} - case PlayerSettings::ARTIFACT: return CGI->generaltexth->allTexts[83]; //{Artifact Bonus} - case PlayerSettings::GOLD: return CGI->generaltexth->allTexts[84]; //{Gold Bonus} - case PlayerSettings::RESOURCE: return CGI->generaltexth->allTexts[85]; //{Resource Bonus} - } - } - } - return ""; -} - -std::string OptionsTab::CPlayerSettingsHelper::getName() -{ - switch(type) - { - case TOWN: - { - switch (settings.castle) - { - case PlayerSettings::NONE : return CGI->generaltexth->allTexts[523]; - case PlayerSettings::RANDOM : return CGI->generaltexth->allTexts[522]; - default : return CGI->townh->factions[settings.castle]->name; - } - } - case HERO: - { - switch (settings.hero) - { - case PlayerSettings::NONE : return CGI->generaltexth->allTexts[523]; - case PlayerSettings::RANDOM : return CGI->generaltexth->allTexts[522]; - default : - { - if (!settings.heroName.empty()) - return settings.heroName; - return CGI->heroh->heroes[settings.hero]->name; - } - } - } - case BONUS: - { - switch (settings.bonus) - { - case PlayerSettings::RANDOM : return CGI->generaltexth->allTexts[522]; - default: return CGI->generaltexth->arraytxt[214 + settings.bonus]; - } - } - } - return ""; -} - -std::string OptionsTab::CPlayerSettingsHelper::getSubtitle() -{ - switch(type) - { - case TOWN: return getName(); - case HERO: - { - if (settings.hero >= 0) - return getName() + " - " + CGI->heroh->heroes[settings.hero]->heroClass->name; - return getName(); - } - - case BONUS: - { - switch(settings.bonus) - { - case PlayerSettings::GOLD: return CGI->generaltexth->allTexts[87]; //500-1000 - case PlayerSettings::RESOURCE: - { - switch(CGI->townh->factions[settings.castle]->town->primaryRes) - { - case Res::MERCURY: return CGI->generaltexth->allTexts[694]; - case Res::SULFUR: return CGI->generaltexth->allTexts[695]; - case Res::CRYSTAL: return CGI->generaltexth->allTexts[692]; - case Res::GEMS: return CGI->generaltexth->allTexts[693]; - case Res::WOOD_AND_ORE: return CGI->generaltexth->allTexts[89]; //At the start of the game, 5-10 wood and 5-10 ore are added to your Kingdom's resource pool - } - } - } - } - } - return ""; -} - -std::string OptionsTab::CPlayerSettingsHelper::getDescription() -{ - switch(type) - { - case TOWN: return CGI->generaltexth->allTexts[104]; - case HERO: return CGI->generaltexth->allTexts[102]; - case BONUS: - { - switch(settings.bonus) - { - case PlayerSettings::RANDOM: return CGI->generaltexth->allTexts[94]; //Gold, wood and ore, or an artifact is randomly chosen as your starting bonus - case PlayerSettings::ARTIFACT: return CGI->generaltexth->allTexts[90]; //An artifact is randomly chosen and equipped to your starting hero - case PlayerSettings::GOLD: return CGI->generaltexth->allTexts[92]; //At the start of the game, 500-1000 gold is added to your Kingdom's resource pool - case PlayerSettings::RESOURCE: - { - switch(CGI->townh->factions[settings.castle]->town->primaryRes) - { - case Res::MERCURY: return CGI->generaltexth->allTexts[690]; - case Res::SULFUR: return CGI->generaltexth->allTexts[691]; - case Res::CRYSTAL: return CGI->generaltexth->allTexts[688]; - case Res::GEMS: return CGI->generaltexth->allTexts[689]; - case Res::WOOD_AND_ORE: return CGI->generaltexth->allTexts[93]; //At the start of the game, 5-10 wood and 5-10 ore are added to your Kingdom's resource pool - } - } - } - } - } - return ""; -} - -OptionsTab::CPregameTooltipBox::CPregameTooltipBox(CPlayerSettingsHelper & helper): - CWindowObject(BORDERED | RCLICK_POPUP), - CPlayerSettingsHelper(helper) -{ - OBJ_CONSTRUCTION_CAPTURING_ALL; - - int value = PlayerSettings::NONE; - - switch(CPlayerSettingsHelper::type) - { - break; case TOWN: - value = settings.castle; - break; case HERO: - value = settings.hero; - break; case BONUS: - value = settings.bonus; - } - - if (value == PlayerSettings::RANDOM) - genBonusWindow(); - else if (CPlayerSettingsHelper::type == BONUS) - genBonusWindow(); - else if (CPlayerSettingsHelper::type == HERO) - genHeroWindow(); - else if (CPlayerSettingsHelper::type == TOWN) - genTownWindow(); - - center(); -} - -void OptionsTab::CPregameTooltipBox::genHeader() -{ - new CFilledTexture("DIBOXBCK", pos); - updateShadow(); - - new CLabel(pos.w / 2 + 8, 21, FONT_MEDIUM, CENTER, Colors::YELLOW, getTitle()); - - new CLabel(pos.w / 2, 88, FONT_SMALL, CENTER, Colors::WHITE, getSubtitle()); - - new CAnimImage(getImageName(), getImageIndex(), 0, pos.w / 2 - 24, 45); -} - -void OptionsTab::CPregameTooltipBox::genTownWindow() -{ - pos = Rect(0, 0, 228, 290); - genHeader(); - - new CLabel(pos.w / 2 + 8, 122, FONT_MEDIUM, CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[79]); - - std::vector components; - const CTown * town = CGI->townh->factions[settings.castle]->town; - - for (auto & elem : town->creatures) - { - if (!elem.empty()) - components.push_back(new CComponent(CComponent::creature, elem.front(), 0, CComponent::tiny)); - } - - new CComponentBox(components, Rect(10, 140, pos.w - 20, 140)); -} - -void OptionsTab::CPregameTooltipBox::genHeroWindow() -{ - pos = Rect(0, 0, 292, 226); - genHeader(); - - // specialty - new CAnimImage("UN44", CGI->heroh->heroes[settings.hero]->imageIndex, 0, pos.w / 2 - 22, 134); - - new CLabel(pos.w / 2 + 4, 117, FONT_MEDIUM, CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[78]); - new CLabel(pos.w / 2, 188, FONT_SMALL, CENTER, Colors::WHITE, CGI->heroh->heroes[settings.hero]->specName); -} - -void OptionsTab::CPregameTooltipBox::genBonusWindow() -{ - pos = Rect(0, 0, 228, 162); - genHeader(); - - new CTextBox(getDescription(), Rect(10, 100, pos.w - 20, 70), 0, FONT_SMALL, CENTER, Colors::WHITE ); -} - -OptionsTab::SelectedBox::SelectedBox(Point position, PlayerSettings & settings, SelType type) - :CIntObject(RCLICK, position), - CPlayerSettingsHelper(settings, type) -{ - OBJ_CONSTRUCTION_CAPTURING_ALL; - - image = new CAnimImage(getImageName(), getImageIndex()); - subtitle = new CLabel(23, 39, FONT_TINY, CENTER, Colors::WHITE, getName()); - - pos = image->pos; -} - -void OptionsTab::SelectedBox::update() -{ - image->setFrame(getImageIndex()); - subtitle->setText(getName()); -} - -void OptionsTab::SelectedBox::clickRight( tribool down, bool previousState ) -{ - if (down) - { - // cases when we do not need to display a message - if (settings.castle == -2 && CPlayerSettingsHelper::type == TOWN ) - return; - if (settings.hero == -2 && !SEL->current->mapHeader->players[settings.color.getNum()].hasCustomMainHero() && CPlayerSettingsHelper::type == HERO) - return; - - GH.pushInt(new CPregameTooltipBox(*this)); - } -} - -CScenarioInfo::CScenarioInfo(const CMapHeader *mapHeader, const StartInfo *startInfo) -{ - OBJ_CONSTRUCTION_CAPTURING_ALL; - - for(auto it = startInfo->playerInfos.cbegin(); it != startInfo->playerInfos.cend(); ++it) - { - if(it->second.playerID) - { - playerColor = it->first; - } - } - - pos.w = 762; - pos.h = 584; - center(pos); - - assert(LOCPLINT); - sInfo = *LOCPLINT->cb->getStartInfo(); - assert(!SEL->current); - current = mapInfoFromGame(); - setPlayersFromGame(); - - screenType = CMenuScreen::scenarioInfo; - - card = new InfoCard(); - opt = new OptionsTab(); - opt->recreate(); - card->changeSelection(current); - - card->difficulty->setSelected(startInfo->difficulty); - back = new CButton(Point(584, 535), "SCNRBACK.DEF", CGI->generaltexth->zelp[105], std::bind(&CGuiHandler::popIntTotally, &GH, this), SDLK_ESCAPE); -} - -CScenarioInfo::~CScenarioInfo() -{ - delete current; -} - -bool mapSorter::operator()(const CMapInfo *aaa, const CMapInfo *bbb) -{ - const CMapHeader * a = aaa->mapHeader.get(), - * b = bbb->mapHeader.get(); - if(a && b) //if we are sorting scenarios - { - switch (sortBy) - { - case _format: //by map format (RoE, WoG, etc) - return (a->versionversion); - break; - case _loscon: //by loss conditions - return (a->defeatMessage < b->defeatMessage); - break; - case _playerAm: //by player amount - int playerAmntB,humenPlayersB,playerAmntA,humenPlayersA; - playerAmntB=humenPlayersB=playerAmntA=humenPlayersA=0; - for (int i=0;i<8;i++) - { - if (a->players[i].canHumanPlay) {playerAmntA++;humenPlayersA++;} - else if (a->players[i].canComputerPlay) {playerAmntA++;} - if (b->players[i].canHumanPlay) {playerAmntB++;humenPlayersB++;} - else if (b->players[i].canComputerPlay) {playerAmntB++;} - } - if (playerAmntB!=playerAmntA) - return (playerAmntAwidthwidth); - break; - case _viccon: //by victory conditions - return (a->victoryMessage < b->victoryMessage); - break; - case _name: //by name - return boost::ilexicographical_compare(a->name, b->name); - case _fileName: //by filename - return boost::ilexicographical_compare(aaa->fileURI, bbb->fileURI); - default: - return boost::ilexicographical_compare(a->name, b->name); - } - } - else //if we are sorting campaigns - { - switch(sortBy) - { - case _numOfMaps: //by number of maps in campaign - return CGI->generaltexth->campaignRegionNames[ aaa->campaignHeader->mapVersion ].size() < - CGI->generaltexth->campaignRegionNames[ bbb->campaignHeader->mapVersion ].size(); - break; - case _name: //by name - return boost::ilexicographical_compare(aaa->campaignHeader->name, bbb->campaignHeader->name); - default: - return boost::ilexicographical_compare(aaa->campaignHeader->name, bbb->campaignHeader->name); - } - } -} - -CMultiMode::CMultiMode() -{ - OBJ_CONSTRUCTION_CAPTURING_ALL; - bg = new CPicture("MUPOPUP.bmp"); - bg->convertToScreenBPP(); //so we could draw without problems - blitAt(CPicture("MUMAP.bmp"), 16, 77, *bg); //blit img - pos = bg->center(); //center, window has size of bg graphic - - bar = new CGStatusBar(new CPicture(Rect(7, 465, 440, 18), 0));//226, 472 - txt = new CTextInput(Rect(19, 436, 334, 16), *bg); - txt->setText(settings["general"]["playerName"].String()); //Player - - btns[0] = new CButton(Point(373, 78), "MUBHOT.DEF", CGI->generaltexth->zelp[266], std::bind(&CMultiMode::openHotseat, this)); - btns[1] = new CButton(Point(373, 78 + 57*1), "MUBHOST.DEF", CButton::tooltip("Host TCP/IP game", ""), std::bind(&CMultiMode::hostTCP, this)); - btns[2] = new CButton(Point(373, 78 + 57*2), "MUBJOIN.DEF", CButton::tooltip("Join TCP/IP game", ""), std::bind(&CMultiMode::joinTCP, this)); - btns[6] = new CButton(Point(373, 424), "MUBCANC.DEF", CGI->generaltexth->zelp[288], [&](){ GH.popIntTotally(this);}, SDLK_ESCAPE); -} - -void CMultiMode::openHotseat() -{ - GH.pushInt(new CHotSeatPlayers(txt->text)); -} - -void CMultiMode::hostTCP() -{ - Settings name = settings.write["general"]["playerName"]; - name->String() = txt->text; - GH.popIntTotally(this); - if(settings["session"]["donotstartserver"].Bool()) - GH.pushInt(new CSimpleJoinScreen(CMenuScreen::MULTI_NETWORK_HOST)); - else - GH.pushInt(new CSelectionScreen(CMenuScreen::newGame, CMenuScreen::MULTI_NETWORK_HOST)); -} - -void CMultiMode::joinTCP() -{ - Settings name = settings.write["general"]["playerName"]; - name->String() = txt->text; - GH.pushInt(new CSimpleJoinScreen(CMenuScreen::MULTI_NETWORK_GUEST)); -} - -CHotSeatPlayers::CHotSeatPlayers(const std::string &firstPlayer) -{ - OBJ_CONSTRUCTION_CAPTURING_ALL; - bg = new CPicture("MUHOTSEA.bmp"); - pos = bg->center(); //center, window has size of bg graphic - - std::string text = CGI->generaltexth->allTexts[446]; - boost::replace_all(text, "\t","\n"); - Rect boxRect(25, 20, 315, 50); - title = new CTextBox(text, boxRect, 0, FONT_BIG, CENTER, Colors::WHITE);//HOTSEAT Please enter names - - for(int i = 0; i < ARRAY_COUNT(txt); i++) - { - txt[i] = new CTextInput(Rect(60, 85 + i*30, 280, 16), *bg); - txt[i]->cb += std::bind(&CHotSeatPlayers::onChange, this, _1); - } - - ok = new CButton(Point(95, 338), "MUBCHCK.DEF", CGI->generaltexth->zelp[560], std::bind(&CHotSeatPlayers::enterSelectionScreen, this), SDLK_RETURN); - cancel = new CButton(Point(205, 338), "MUBCANC.DEF", CGI->generaltexth->zelp[561], std::bind(&CGuiHandler::popIntTotally, std::ref(GH), this), SDLK_ESCAPE); - bar = new CGStatusBar(new CPicture(Rect(7, 381, 348, 18), 0));//226, 472 - - txt[0]->setText(firstPlayer, true); - txt[0]->giveFocus(); -} - -void CHotSeatPlayers::onChange(std::string newText) -{ - size_t namesCount = 0; - - for(auto & elem : txt) - if(!elem->text.empty()) - namesCount++; - - ok->block(namesCount < 2); -} - -void CHotSeatPlayers::enterSelectionScreen() -{ - std::map names; - for(int i = 0, j = 1; i < ARRAY_COUNT(txt); i++) - if(txt[i]->text.length()) - names[j++] = txt[i]->text; - - Settings name = settings.write["general"]["playerName"]; - name->String() = names.begin()->second; - - GH.popInts(2); //pop MP mode window and this - GH.pushInt(new CSelectionScreen(CMenuScreen::newGame, CMenuScreen::MULTI_HOT_SEAT, &names)); -} - -void CBonusSelection::init() -{ - highlightedRegion = nullptr; - ourHeader.reset(); - diffLb = nullptr; - diffRb = nullptr; - bonuses = nullptr; - selectedMap = 0; - - // Initialize start info - startInfo.mapname = ourCampaign->camp->header.filename; - startInfo.mode = StartInfo::CAMPAIGN; - startInfo.campState = ourCampaign; - startInfo.turnTime = 0; - - OBJ_CONSTRUCTION_CAPTURING_ALL; - static const std::string bgNames [] = {"E1_BG.BMP", "G2_BG.BMP", "E2_BG.BMP", "G1_BG.BMP", "G3_BG.BMP", "N1_BG.BMP", - "S1_BG.BMP", "BR_BG.BMP", "IS_BG.BMP", "KR_BG.BMP", "NI_BG.BMP", "TA_BG.BMP", "AR_BG.BMP", "HS_BG.BMP", - "BB_BG.BMP", "NB_BG.BMP", "EL_BG.BMP", "RN_BG.BMP", "UA_BG.BMP", "SP_BG.BMP"}; - - loadPositionsOfGraphics(); - - background = BitmapHandler::loadBitmap(bgNames[ourCampaign->camp->header.mapVersion]); - pos.h = background->h; - pos.w = background->w; - center(); - - SDL_Surface * panel = BitmapHandler::loadBitmap("CAMPBRF.BMP"); - - blitAt(panel, 456, 6, background); - - startB = new CButton(Point(475, 536), "CBBEGIB.DEF", CButton::tooltip(), std::bind(&CBonusSelection::startMap, this), SDLK_RETURN); - restartB = new CButton(Point(475, 536), "CBRESTB.DEF", CButton::tooltip(), std::bind(&CBonusSelection::restartMap, this), SDLK_RETURN); - backB = new CButton(Point(624, 536), "CBCANCB.DEF", CButton::tooltip(), std::bind(&CBonusSelection::goBack, this), SDLK_ESCAPE); - - //campaign name - if (ourCampaign->camp->header.name.length()) - graphics->fonts[FONT_BIG]->renderTextLeft(background, ourCampaign->camp->header.name, Colors::YELLOW, Point(481, 28)); - else - graphics->fonts[FONT_BIG]->renderTextLeft(background, CGI->generaltexth->allTexts[508], Colors::YELLOW, Point(481, 28)); - - //map size icon - sizes = new CAnimImage("SCNRMPSZ",4,0,735, 26); - sizes->recActions &= ~(SHOWALL | UPDATE);//explicit draw - - //campaign description - graphics->fonts[FONT_SMALL]->renderTextLeft(background, CGI->generaltexth->allTexts[38], Colors::YELLOW, Point(481, 63)); - - campaignDescription = new CTextBox(ourCampaign->camp->header.description, Rect(480, 86, 286, 117), 1); - //campaignDescription->showAll(background); - - //map description - mapDescription = new CTextBox("", Rect(480, 280, 286, 117), 1); - - //bonus choosing - graphics->fonts[FONT_MEDIUM]->renderTextLeft(background, CGI->generaltexth->allTexts[71], Colors::WHITE, Point(511, 432)); - bonuses = new CToggleGroup(std::bind(&CBonusSelection::selectBonus, this, _1)); - - //set left part of window - bool isCurrentMapConquerable = ourCampaign->currentMap && ourCampaign->camp->conquerable(*ourCampaign->currentMap); - for(int g = 0; g < ourCampaign->camp->scenarios.size(); ++g) - { - if(ourCampaign->camp->conquerable(g)) - { - regions.push_back(new CRegion(this, true, true, g)); - regions[regions.size()-1]->rclickText = ourCampaign->camp->scenarios[g].regionText; - if(highlightedRegion == nullptr) - { - if(!isCurrentMapConquerable || (isCurrentMapConquerable && g == *ourCampaign->currentMap)) - { - highlightedRegion = regions.back(); - selectMap(g, true); - } - } - } - else if (ourCampaign->camp->scenarios[g].conquered) //display as striped - { - regions.push_back(new CRegion(this, false, false, g)); - regions[regions.size()-1]->rclickText = ourCampaign->camp->scenarios[g].regionText; - } - } - - //allies / enemies - graphics->fonts[FONT_SMALL]->renderTextLeft(background, CGI->generaltexth->allTexts[390] + ":", Colors::WHITE, Point(486, 407)); - graphics->fonts[FONT_SMALL]->renderTextLeft(background, CGI->generaltexth->allTexts[391] + ":", Colors::WHITE, Point(619, 407)); - - SDL_FreeSurface(panel); - - //difficulty - std::vector difficulty; - boost::split(difficulty, CGI->generaltexth->allTexts[492], boost::is_any_of(" ")); - graphics->fonts[FONT_MEDIUM]->renderTextLeft(background, difficulty.back(), Colors::WHITE, Point(689, 432)); - - //difficulty pics - for (size_t b=0; b < diffPics.size(); ++b) - { - diffPics[b] = new CAnimImage("GSPBUT" + boost::lexical_cast(b+3) + ".DEF", 0, 0, 709, 455); - diffPics[b]->recActions &= ~(SHOWALL | UPDATE);//explicit draw - } - - //difficulty selection buttons - if (ourCampaign->camp->header.difficultyChoosenByPlayer) - { - diffLb = new CButton(Point(694, 508), "SCNRBLF.DEF", CButton::tooltip(), std::bind(&CBonusSelection::decreaseDifficulty, this)); - diffRb = new CButton(Point(738, 508), "SCNRBRT.DEF", CButton::tooltip(), std::bind(&CBonusSelection::increaseDifficulty, this)); - } - - //load miniflags - sFlags = std::make_shared("ITGFLAGS.DEF"); - sFlags->load(); -} - -CBonusSelection::CBonusSelection(std::shared_ptr _ourCampaign) : ourCampaign(_ourCampaign) -{ - init(); -} - -CBonusSelection::CBonusSelection(const std::string & campaignFName) -{ - ourCampaign = std::make_shared(CCampaignHandler::getCampaign(campaignFName)); - init(); -} - -CBonusSelection::~CBonusSelection() -{ - SDL_FreeSurface(background); - sFlags->unload(); -} - -void CBonusSelection::goBack() -{ - GH.popIntTotally(this); -} - -void CBonusSelection::showAll(SDL_Surface * to) -{ - blitAt(background, pos.x, pos.y, to); - CIntObject::showAll(to); - - show(to); - if (pos.h != to->h || pos.w != to->w) - CMessage::drawBorder(PlayerColor(1), to, pos.w+28, pos.h+30, pos.x-14, pos.y-15); -} - -void CBonusSelection::loadPositionsOfGraphics() -{ - const JsonNode config(ResourceID("config/campaign_regions.json")); - int idx = 0; - - for(const JsonNode &campaign : config["campaign_regions"].Vector()) - { - SCampPositions sc; - - sc.campPrefix = campaign["prefix"].String(); - sc.colorSuffixLength = campaign["color_suffix_length"].Float(); - - for(const JsonNode &desc : campaign["desc"].Vector()) - { - SCampPositions::SRegionDesc rd; - - rd.infix = desc["infix"].String(); - rd.xpos = desc["x"].Float(); - rd.ypos = desc["y"].Float(); - sc.regions.push_back(rd); - } - - campDescriptions.push_back(sc); - - idx++; - } - - assert(idx == CGI->generaltexth->campaignMapNames.size()); -} - -void CBonusSelection::selectMap(int mapNr, bool initialSelect) -{ - if(initialSelect || selectedMap != mapNr) - { - // initialize restart / start button - if(!ourCampaign->currentMap || *ourCampaign->currentMap != mapNr) - { - // draw start button - restartB->disable(); - startB->enable(); - if (!ourCampaign->mapsConquered.empty()) - backB->block(true); - else - backB->block(false); - } - else - { - // draw restart button - startB->disable(); - restartB->enable(); - backB->block(false); - } - - startInfo.difficulty = ourCampaign->camp->scenarios[mapNr].difficulty; - selectedMap = mapNr; - selectedBonus = boost::none; - - std::string scenarioName = ourCampaign->camp->header.filename.substr(0, ourCampaign->camp->header.filename.find('.')); - boost::to_lower(scenarioName); - scenarioName += ':' + boost::lexical_cast(selectedMap); - - //get header - std::string & headerStr = ourCampaign->camp->mapPieces.find(mapNr)->second; - auto buffer = reinterpret_cast(headerStr.data()); - CMapService mapService; - ourHeader = mapService.loadMapHeader(buffer, headerStr.size(), scenarioName); - - std::map names; - names[1] = settings["general"]["playerName"].String(); - updateStartInfo(ourCampaign->camp->header.filename, startInfo, ourHeader, names); - - mapDescription->setText(ourHeader->description); - - updateBonusSelection(); - - GH.totalRedraw(); - } -} - -void CBonusSelection::show(SDL_Surface * to) -{ - //map name - std::string mapName = ourHeader->name; - - if (mapName.length()) - printAtLoc(mapName, 481, 219, FONT_BIG, Colors::YELLOW, to); - else - printAtLoc("Unnamed", 481, 219, FONT_BIG, Colors::YELLOW, to); - - //map description - printAtLoc(CGI->generaltexth->allTexts[496], 481, 253, FONT_SMALL, Colors::YELLOW, to); - - mapDescription->showAll(to); //showAll because CTextBox has no show() - - //map size icon - int temp; - switch (ourHeader->width) - { - case 36: - temp=0; - break; - case 72: - temp=1; - break; - case 108: - temp=2; - break; - case 144: - temp=3; - break; - default: - temp=4; - break; - } - sizes->setFrame(temp); - sizes->showAll(to); - - //flags - int fx = 496 + graphics->fonts[FONT_SMALL]->getStringWidth(CGI->generaltexth->allTexts[390]); - int ex = 629 + graphics->fonts[FONT_SMALL]->getStringWidth(CGI->generaltexth->allTexts[391]); - TeamID myT; - myT = ourHeader->players[playerColor.getNum()].team; - for (auto i = startInfo.playerInfos.cbegin(); i != startInfo.playerInfos.cend(); i++) - { - int *myx = ((i->first == playerColor || ourHeader->players[i->first.getNum()].team == myT) ? &fx : &ex); - - auto flag = sFlags->getImage(i->first.getNum(), 0); - flag->draw(to, pos.x + *myx, pos.y + 405); - *myx += flag->width(); - flag->decreaseRef(); - } - - //difficulty - diffPics[startInfo.difficulty]->showAll(to); - - CIntObject::show(to); -} - -void CBonusSelection::updateBonusSelection() -{ - OBJ_CONSTRUCTION_CAPTURING_ALL; - //graphics: - //spell - SPELLBON.DEF - //monster - TWCRPORT.DEF - //building - BO*.BMP graphics - //artifact - ARTIFBON.DEF - //spell scroll - SPELLBON.DEF - //prim skill - PSKILBON.DEF - //sec skill - SSKILBON.DEF - //resource - BORES.DEF - //player - CREST58.DEF - //hero - PORTRAITSLARGE (HPL###.BMPs) - const CCampaignScenario &scenario = ourCampaign->camp->scenarios[selectedMap]; - const std::vector & bonDescs = scenario.travelOptions.bonusesToChoose; - - updateStartButtonState(-1); - - delete bonuses; - bonuses = new CToggleGroup(std::bind(&CBonusSelection::selectBonus, this, _1)); - - static const char *bonusPics[] = {"SPELLBON.DEF", "TWCRPORT.DEF", "", "ARTIFBON.DEF", "SPELLBON.DEF", - "PSKILBON.DEF", "SSKILBON.DEF", "BORES.DEF", "PORTRAITSLARGE", "PORTRAITSLARGE"}; - - for(int i = 0; i < bonDescs.size(); i++) - { - std::string picName=bonusPics[bonDescs[i].type]; - size_t picNumber=bonDescs[i].info2; - - std::string desc; - switch(bonDescs[i].type) - { - case CScenarioTravel::STravelBonus::SPELL: - desc = CGI->generaltexth->allTexts[715]; - boost::algorithm::replace_first(desc, "%s", CGI->spellh->objects[bonDescs[i].info2]->name); - break; - case CScenarioTravel::STravelBonus::MONSTER: - picNumber = bonDescs[i].info2 + 2; - desc = CGI->generaltexth->allTexts[717]; - boost::algorithm::replace_first(desc, "%d", boost::lexical_cast(bonDescs[i].info3)); - boost::algorithm::replace_first(desc, "%s", CGI->creh->creatures[bonDescs[i].info2]->namePl); - break; - case CScenarioTravel::STravelBonus::BUILDING: - { - int faction = -1; - for(auto & elem : startInfo.playerInfos) - { - if (elem.second.playerID) - { - faction = elem.second.castle; - break; - } - - } - assert(faction != -1); - - BuildingID buildID = CBuildingHandler::campToERMU(bonDescs[i].info1, faction, std::set()); - picName = graphics->ERMUtoPicture[faction][buildID]; - picNumber = -1; - - if (vstd::contains(CGI->townh->factions[faction]->town->buildings, buildID)) - desc = CGI->townh->factions[faction]->town->buildings.find(buildID)->second->Name(); - } - break; - case CScenarioTravel::STravelBonus::ARTIFACT: - desc = CGI->generaltexth->allTexts[715]; - boost::algorithm::replace_first(desc, "%s", CGI->arth->artifacts[bonDescs[i].info2]->Name()); - break; - case CScenarioTravel::STravelBonus::SPELL_SCROLL: - desc = CGI->generaltexth->allTexts[716]; - boost::algorithm::replace_first(desc, "%s", CGI->spellh->objects[bonDescs[i].info2]->name); - break; - case CScenarioTravel::STravelBonus::PRIMARY_SKILL: - { - int leadingSkill = -1; - std::vector > toPrint; //primary skills to be listed - const ui8* ptr = reinterpret_cast(&bonDescs[i].info2); - for (int g=0; g ptr[leadingSkill]) - { - leadingSkill = g; - } - if (ptr[g] != 0) - { - toPrint.push_back(std::make_pair(g, ptr[g])); - } - } - picNumber = leadingSkill; - desc = CGI->generaltexth->allTexts[715]; - - std::string substitute; //text to be printed instead of %s - for (int v=0; v(toPrint[v].second); - substitute += " " + CGI->generaltexth->primarySkillNames[toPrint[v].first]; - if(v != toPrint.size() - 1) - { - substitute += ", "; - } - } - - boost::algorithm::replace_first(desc, "%s", substitute); - break; - } - case CScenarioTravel::STravelBonus::SECONDARY_SKILL: - desc = CGI->generaltexth->allTexts[718]; - - boost::algorithm::replace_first(desc, "%s", CGI->generaltexth->levels[bonDescs[i].info3 - 1]); //skill level - boost::algorithm::replace_first(desc, "%s", CGI->skillh->skillName(bonDescs[i].info2)); //skill name - picNumber = bonDescs[i].info2 * 3 + bonDescs[i].info3 - 1; - - break; - case CScenarioTravel::STravelBonus::RESOURCE: - { - int serialResID = 0; - switch(bonDescs[i].info1) - { - case 0: case 1: case 2: case 3: case 4: case 5: case 6: - serialResID = bonDescs[i].info1; - break; - case 0xFD: //wood + ore - serialResID = 7; - break; - case 0xFE: //rare resources - serialResID = 8; - break; - } - picNumber = serialResID; - - desc = CGI->generaltexth->allTexts[717]; - boost::algorithm::replace_first(desc, "%d", boost::lexical_cast(bonDescs[i].info2)); - std::string replacement; - if (serialResID <= 6) - { - replacement = CGI->generaltexth->restypes[serialResID]; - } - else - { - replacement = CGI->generaltexth->allTexts[714 + serialResID]; - } - boost::algorithm::replace_first(desc, "%s", replacement); - } - break; - case CScenarioTravel::STravelBonus::HEROES_FROM_PREVIOUS_SCENARIO: - { - auto superhero = ourCampaign->camp->scenarios[bonDescs[i].info2].strongestHero(PlayerColor(bonDescs[i].info1)); - if (!superhero) logGlobal->warn("No superhero! How could it be transferred?"); - picNumber = superhero ? superhero->portrait : 0; - desc = CGI->generaltexth->allTexts[719]; - - boost::algorithm::replace_first(desc, "%s", ourCampaign->camp->scenarios[bonDescs[i].info2].scenarioName); //scenario - } - - break; - case CScenarioTravel::STravelBonus::HERO: - - desc = CGI->generaltexth->allTexts[718]; - boost::algorithm::replace_first(desc, "%s", CGI->generaltexth->capColors[bonDescs[i].info1]); //hero's color - - if (bonDescs[i].info2 == 0xFFFF) - { - boost::algorithm::replace_first(desc, "%s", CGI->generaltexth->allTexts[101]); //hero's name - picNumber = -1; - picName = "CBONN1A3.BMP"; - } - else - { - boost::algorithm::replace_first(desc, "%s", CGI->heroh->heroes[bonDescs[i].info2]->name); //hero's name - } - break; - } - - CToggleButton *bonusButton = new CToggleButton(Point(475 + i*68, 455), "", CButton::tooltip(desc, desc)); - - if (picNumber != -1) - picName += ":" + boost::lexical_cast(picNumber); - - auto anim = std::make_shared(); - anim->setCustom(picName, 0); - bonusButton->setImage(anim); - const SDL_Color brightYellow = { 242, 226, 110, 0 }; - bonusButton->setBorderColor({}, {}, {}, brightYellow); - bonuses->addToggle(i, bonusButton); - } - - // set bonus if already chosen - if(vstd::contains(ourCampaign->chosenCampaignBonuses, selectedMap)) - { - bonuses->setSelected(ourCampaign->chosenCampaignBonuses[selectedMap]); - } -} - -void CBonusSelection::updateCampaignState() -{ - ourCampaign->currentMap = boost::make_optional(selectedMap); - if (selectedBonus) - ourCampaign->chosenCampaignBonuses[selectedMap] = *selectedBonus; -} - -void CBonusSelection::startMap() -{ - auto si = new StartInfo(startInfo); - auto showPrologVideo = [=]() - { - auto exitCb = [=]() - { - logGlobal->info("Starting scenario %d", selectedMap); - CGP->showLoadingScreen(std::bind(&startGame, si, (CConnection *)nullptr)); - }; - - const CCampaignScenario & scenario = ourCampaign->camp->scenarios[selectedMap]; - if (scenario.prolog.hasPrologEpilog) - { - GH.pushInt(new CPrologEpilogVideo(scenario.prolog, exitCb)); - } - else - { - exitCb(); - } - }; - - if(LOCPLINT) // we're currently ingame, so ask for starting new map and end game - { - GH.popInt(this); - LOCPLINT->showYesNoDialog(CGI->generaltexth->allTexts[67], [=]() - { - updateCampaignState(); - endGame(); - GH.curInt = CGPreGame::create(); - showPrologVideo(); - }, 0); - } - else - { - updateCampaignState(); - showPrologVideo(); - } -} - -void CBonusSelection::restartMap() -{ - GH.popInt(this); - LOCPLINT->showYesNoDialog(CGI->generaltexth->allTexts[67], [=]() - { - updateCampaignState(); - auto si = new StartInfo(startInfo); - - SDL_Event event; - event.type = SDL_USEREVENT; - event.user.code = PREPARE_RESTART_CAMPAIGN; - event.user.data1 = si; - SDL_PushEvent(&event); - }, 0); -} - -void CBonusSelection::selectBonus(int id) -{ - // Total redraw is needed because the border around the bonus images - // have to be undrawn/drawn. - if (!selectedBonus || *selectedBonus != id) - { - selectedBonus = boost::make_optional(id); - GH.totalRedraw(); - - updateStartButtonState(id); - } - - const CCampaignScenario &scenario = ourCampaign->camp->scenarios[selectedMap]; - const std::vector & bonDescs = scenario.travelOptions.bonusesToChoose; - if (bonDescs[id].type == CScenarioTravel::STravelBonus::HERO) - { - std::map names; - names[1] = settings["general"]["playerName"].String(); - for(auto & elem : startInfo.playerInfos) - { - if(elem.first == PlayerColor(bonDescs[id].info1)) - ::setPlayer(elem.second, 1, names); - else - ::setPlayer(elem.second, 0, names); - } - } -} - -void CBonusSelection::increaseDifficulty() -{ - startInfo.difficulty = std::min(startInfo.difficulty + 1, 4); -} - -void CBonusSelection::decreaseDifficulty() -{ - startInfo.difficulty = std::max(startInfo.difficulty - 1, 0); -} - -void CBonusSelection::updateStartButtonState(int selected) -{ - if(selected == -1) - { - startB->block(ourCampaign->camp->scenarios[selectedMap].travelOptions.bonusesToChoose.size()); - } - else if(startB->isBlocked()) - { - startB->block(false); - } -} - -CBonusSelection::CRegion::CRegion( CBonusSelection * _owner, bool _accessible, bool _selectable, int _myNumber ) -: owner(_owner), accessible(_accessible), selectable(_selectable), myNumber(_myNumber) -{ - OBJ_CONSTRUCTION; - addUsedEvents(LCLICK | RCLICK); - - static const std::string colors[2][8] = { - {"R", "B", "N", "G", "O", "V", "T", "P"}, - {"Re", "Bl", "Br", "Gr", "Or", "Vi", "Te", "Pi"}}; - - const SCampPositions & campDsc = owner->campDescriptions[owner->ourCampaign->camp->header.mapVersion]; - const SCampPositions::SRegionDesc & desc = campDsc.regions[myNumber]; - pos.x += desc.xpos; - pos.y += desc.ypos; - - //loading of graphics - - std::string prefix = campDsc.campPrefix + desc.infix + "_"; - std::string suffix = colors[campDsc.colorSuffixLength - 1][owner->ourCampaign->camp->scenarios[myNumber].regionColor]; - - static const std::string infix [] = {"En", "Se", "Co"}; - for (int g = 0; g < ARRAY_COUNT(infix); g++) - { - graphics[g] = BitmapHandler::loadBitmap(prefix + infix[g] + suffix + ".BMP"); - } - pos.w = graphics[0]->w; - pos.h = graphics[0]->h; - -} - -CBonusSelection::CRegion::~CRegion() -{ - for (auto & elem : graphics) - { - SDL_FreeSurface(elem); - } -} - -void CBonusSelection::CRegion::clickLeft( tribool down, bool previousState ) -{ - //select if selectable & clicked inside our graphic - if ( indeterminate(down) ) - { - return; - } - if( !down && selectable && !CSDL_Ext::isTransparent(graphics[0], GH.current->motion.x-pos.x, GH.current->motion.y-pos.y) ) - { - owner->selectMap(myNumber, false); - owner->highlightedRegion = this; - parent->showAll(screen); - } -} - -void CBonusSelection::CRegion::clickRight( tribool down, bool previousState ) -{ - //show r-click text - if( down && !CSDL_Ext::isTransparent(graphics[0], GH.current->motion.x-pos.x, GH.current->motion.y-pos.y) && - rclickText.size() ) - { - CRClickPopup::createAndPush(rclickText); - } -} - -void CBonusSelection::CRegion::show(SDL_Surface * to) -{ - //const SCampPositions::SRegionDesc & desc = owner->campDescriptions[owner->ourCampaign->camp->header.mapVersion].regions[myNumber]; - if (!accessible) - { - //show as striped - blitAtLoc(graphics[2], 0, 0, to); - } - else if (this == owner->highlightedRegion) - { - //show as selected - blitAtLoc(graphics[1], 0, 0, to); - } - else - { - //show as not selected selected - blitAtLoc(graphics[0], 0, 0, to); - } -} - -CSavingScreen::CSavingScreen(bool hotseat) - : CSelectionScreen( - CMenuScreen::saveGame, - hotseat ? CMenuScreen::MULTI_HOT_SEAT : (LOCPLINT->cb->getStartInfo()->mode == StartInfo::CAMPAIGN ? CMenuScreen::SINGLE_CAMPAIGN : CMenuScreen::SINGLE_PLAYER) - ) -{ - ourGame = mapInfoFromGame(); - sInfo = *LOCPLINT->cb->getStartInfo(); - setPlayersFromGame(); -} - -CSavingScreen::~CSavingScreen() -{ - -} - -ISelectionScreenInfo::ISelectionScreenInfo(const std::map * Names) -{ - gameMode = CMenuScreen::SINGLE_PLAYER; - screenType = CMenuScreen::mainMenu; - assert(!SEL); - SEL = this; - current = nullptr; - - if(Names && !Names->empty()) //if have custom set of player names - use it - playerNames = *Names; - else - playerNames[1] = settings["general"]["playerName"].String(); //by default we have only one player and his name is "Player" (or whatever the last used name was) -} - -ISelectionScreenInfo::~ISelectionScreenInfo() -{ - assert(SEL == this); - SEL = nullptr; -} - -void ISelectionScreenInfo::updateStartInfo(std::string filename, StartInfo & sInfo, const std::unique_ptr & mapHeader) -{ - ::updateStartInfo(filename, sInfo, mapHeader, playerNames); -} - -void ISelectionScreenInfo::setPlayer(PlayerSettings &pset, ui8 player) -{ - ::setPlayer(pset, player, playerNames); -} - -ui8 ISelectionScreenInfo::getIdOfFirstUnallocatedPlayer() -{ - for(auto i = playerNames.cbegin(); i != playerNames.cend(); i++) - if(!sInfo.getPlayersSettings(i->first)) // - return i->first; - - return 0; -} - -bool ISelectionScreenInfo::isGuest() const -{ - return gameMode == CMenuScreen::MULTI_NETWORK_GUEST; -} - -bool ISelectionScreenInfo::isHost() const -{ - return gameMode == CMenuScreen::MULTI_NETWORK_HOST; -} - -void ChatMessage::apply(CSelectionScreen *selScreen) -{ - selScreen->card->chat->addNewMessage(playerName + ": " + message); - GH.totalRedraw(); -} - -void QuitMenuWithoutStarting::apply(CSelectionScreen *selScreen) -{ - if(!selScreen->ongoingClosing) - { - *selScreen->serv << this; //resend to confirm - GH.popIntTotally(selScreen); //will wait with deleting us before this thread ends - } - - vstd::clear_pointer(selScreen->serv); -} - -void PlayerJoined::apply(CSelectionScreen *selScreen) -{ - //assert(SEL->playerNames.size() == connectionID); //temporary, TODO when player exits - SEL->playerNames[connectionID] = playerName; - - //put new player in first slot with AI - for(auto & elem : SEL->sInfo.playerInfos) - { - if(!elem.second.playerID && !elem.second.compOnly) - { - selScreen->setPlayer(elem.second, connectionID); - selScreen->opt->entries[elem.second.color]->selectButtons(); - break; - } - } - - selScreen->propagateNames(); - selScreen->propagateOptions(); - selScreen->toggleTab(selScreen->curTab); - - GH.totalRedraw(); -} - -void SelectMap::apply(CSelectionScreen *selScreen) -{ - if(selScreen->isGuest()) - { - free = false; - selScreen->changeSelection(mapInfo); - } -} - -void UpdateStartOptions::apply(CSelectionScreen *selScreen) -{ - if(!selScreen->isGuest()) - return; - - selScreen->setSInfo(*options); -} - -void PregameGuiAction::apply(CSelectionScreen *selScreen) -{ - if(!selScreen->isGuest()) - return; - - switch(action) - { - case NO_TAB: - selScreen->toggleTab(selScreen->curTab); - break; - case OPEN_OPTIONS: - selScreen->toggleTab(selScreen->opt); - break; - case OPEN_SCENARIO_LIST: - selScreen->toggleTab(selScreen->sel); - break; - case OPEN_RANDOM_MAP_OPTIONS: - selScreen->toggleTab(selScreen->randMapTab); - break; - } -} - -void RequestOptionsChange::apply(CSelectionScreen *selScreen) -{ - if(!selScreen->isHost()) - return; - - PlayerColor color = selScreen->sInfo.getPlayersSettings(playerID)->color; - - switch(what) - { - case TOWN: - selScreen->opt->nextCastle(color, direction); - break; - case HERO: - selScreen->opt->nextHero(color, direction); - break; - case BONUS: - selScreen->opt->nextBonus(color, direction); - break; - } -} - -void PlayerLeft::apply(CSelectionScreen *selScreen) -{ - if(selScreen->isGuest()) - return; - - SEL->playerNames.erase(playerID); - - if(PlayerSettings *s = selScreen->sInfo.getPlayersSettings(playerID)) //it's possible that player was unallocated - { - selScreen->setPlayer(*s, 0); - selScreen->opt->entries[s->color]->selectButtons(); - } - - selScreen->propagateNames(); - selScreen->propagateOptions(); - GH.totalRedraw(); -} - -void PlayersNames::apply(CSelectionScreen *selScreen) -{ - if(selScreen->isGuest()) - selScreen->playerNames = playerNames; -} - -void StartWithCurrentSettings::apply(CSelectionScreen *selScreen) -{ - startingInfo.reset(); - startingInfo.serv = selScreen->serv; - startingInfo.sInfo = new StartInfo(selScreen->sInfo); - - if(!selScreen->ongoingClosing) - { - *selScreen->serv << this; //resend to confirm - } - - selScreen->serv = nullptr; //hide it so it won't be deleted - vstd::clear_pointer(selScreen->serverHandlingThread); //detach us - saveGameName.clear(); - - CGP->showLoadingScreen(std::bind(&startGame, startingInfo.sInfo, startingInfo.serv)); - throw 666; //EVIL, EVIL, EVIL workaround to kill thread (does "goto catch" outside listening loop) -} - -CCampaignScreen::CCampaignButton::CCampaignButton(const JsonNode &config ) -{ - OBJ_CONSTRUCTION_CAPTURING_ALL; - - pos.x += config["x"].Float(); - pos.y += config["y"].Float(); - pos.w = 200; - pos.h = 116; - - campFile = config["file"].String(); - video = config["video"].String(); - - status = config["open"].Bool() ? CCampaignScreen::ENABLED : CCampaignScreen::DISABLED; - - CCampaignHeader header = CCampaignHandler::getHeader(campFile); - hoverText = header.name; - - hoverLabel = nullptr; - if (status != CCampaignScreen::DISABLED) - { - addUsedEvents(LCLICK | HOVER); - new CPicture(config["image"].String()); - - hoverLabel = new CLabel(pos.w / 2, pos.h + 20, FONT_MEDIUM, CENTER, Colors::YELLOW, ""); - parent->addChild(hoverLabel); - } - - if (status == CCampaignScreen::COMPLETED) - new CPicture("CAMPCHK"); -} - -void CCampaignScreen::CCampaignButton::clickLeft(tribool down, bool previousState) -{ - if (down) - { - // Close running video and open the selected campaign - CCS->videoh->close(); - GH.pushInt( new CBonusSelection(campFile) ); - } -} - -void CCampaignScreen::CCampaignButton::hover(bool on) -{ - if (hoverLabel) - { - if (on) - hoverLabel->setText(hoverText); // Shows the name of the campaign when you get into the bounds of the button - else - hoverLabel->setText(" "); - } -} - -void CCampaignScreen::CCampaignButton::show(SDL_Surface * to) -{ - if (status == CCampaignScreen::DISABLED) - return; - - CIntObject::show(to); - - // Play the campaign button video when the mouse cursor is placed over the button - if (hovered) - { - if (CCS->videoh->fname != video) - CCS->videoh->open(video); - - CCS->videoh->update(pos.x, pos.y, to, true, false); // plays sequentially frame by frame, starts at the beginning when the video is over - } - else if (CCS->videoh->fname == video) // When you got out of the bounds of the button then close the video - { - CCS->videoh->close(); - redraw(); - } -} - -CButton* CCampaignScreen::createExitButton(const JsonNode& button) -{ - std::pair help; - if (!button["help"].isNull() && button["help"].Float() > 0) - help = CGI->generaltexth->zelp[button["help"].Float()]; - - std::function close = std::bind(&CGuiHandler::popIntTotally, &GH, this); - return new CButton(Point(button["x"].Float(), button["y"].Float()), button["name"].String(), help, close, button["hotkey"].Float()); -} - - -CCampaignScreen::CCampaignScreen(const JsonNode &config) -{ - OBJ_CONSTRUCTION_CAPTURING_ALL; - - for(const JsonNode& node : config["images"].Vector()) - images.push_back(createPicture(node)); - - if (!images.empty()) - { - images[0]->center(); // move background to center - moveTo(images[0]->pos.topLeft()); // move everything else to center - images[0]->moveTo(pos.topLeft()); // restore moved twice background - pos = images[0]->pos; // fix height\width of this window - } - - if (!config["exitbutton"].isNull()) - { - CButton * back = createExitButton(config["exitbutton"]); - back->hoverable = true; - } - - for(const JsonNode& node : config["items"].Vector()) - campButtons.push_back(new CCampaignButton(node)); -} - -void CCampaignScreen::showAll(SDL_Surface *to) -{ - CIntObject::showAll(to); - if (pos.h != to->h || pos.w != to->w) - CMessage::drawBorder(PlayerColor(1), to, pos.w+28, pos.h+30, pos.x-14, pos.y-15); -} - -void CGPreGame::showLoadingScreen(std::function loader) -{ - if (GH.listInt.size() && GH.listInt.front() == CGP) //pregame active - CGP->removeFromGui(); - GH.pushInt(new CLoadingScreen(loader)); -} - -std::string CLoadingScreen::getBackground() -{ - const auto & conf = CGPreGameConfig::get().getConfig()["loading"].Vector(); - - if(conf.empty()) - { - return "loadbar"; - } - else - { - return RandomGeneratorUtil::nextItem(conf, CRandomGenerator::getDefault())->String(); - } -} - -CLoadingScreen::CLoadingScreen(std::function loader): - CWindowObject(BORDERED, getBackground()), - loadingThread(loader) -{ - CCS->musich->stopMusic(5000); -} - -CLoadingScreen::~CLoadingScreen() -{ - loadingThread.join(); -} - -void CLoadingScreen::showAll(SDL_Surface *to) -{ - Rect rect(0,0,to->w, to->h); - SDL_FillRect(to, &rect, 0); - - CWindowObject::showAll(to); -} - -CPrologEpilogVideo::CPrologEpilogVideo( CCampaignScenario::SScenarioPrologEpilog _spe, std::function callback ): - CWindowObject(BORDERED), - spe(_spe), - positionCounter(0), - voiceSoundHandle(-1), - exitCb(callback) -{ - OBJ_CONSTRUCTION_CAPTURING_ALL; - addUsedEvents(LCLICK); - pos = center(Rect(0,0, 800, 600)); - updateShadow(); - - CCS->videoh->open(CCampaignHandler::prologVideoName(spe.prologVideo)); - CCS->musich->playMusic("Music/" + CCampaignHandler::prologMusicName(spe.prologMusic), true); - voiceSoundHandle = CCS->soundh->playSound(CCampaignHandler::prologVoiceName(spe.prologVideo)); - - text = new CMultiLineLabel(Rect(100, 500, 600, 100), EFonts::FONT_BIG, CENTER, Colors::METALLIC_GOLD, spe.prologText ); - text->scrollTextTo(-100); -} - -void CPrologEpilogVideo::show( SDL_Surface * to ) -{ - CSDL_Ext::fillRectBlack(to, &pos); - //BUG: some videos are 800x600 in size while some are 800x400 - //VCMI should center them in the middle of the screen. Possible but needs modification - //of video player API which I'd like to avoid until we'll get rid of Windows-specific player - CCS->videoh->update(pos.x, pos.y, to, true, false); - - //move text every 5 calls/frames; seems to be good enough - ++positionCounter; - if(positionCounter % 5 == 0) - { - text->scrollTextBy(1); - } - else - text->showAll(to);// blit text over video, if needed - - if (text->textSize.y + 100 < positionCounter / 5) - clickLeft(false, false); -} - -void CPrologEpilogVideo::clickLeft( tribool down, bool previousState ) -{ - GH.popInt(this); - CCS->soundh->stopSound(voiceSoundHandle); - exitCb(); -} - -CSimpleJoinScreen::CSimpleJoinScreen(CMenuScreen::EGameMode mode) -{ - OBJ_CONSTRUCTION_CAPTURING_ALL; - bg = new CPicture("MUDIALOG.bmp"); // address background - pos = bg->center(); //center, window has size of bg graphic (x,y = 396,278 w=232 h=212) - - Rect boxRect(20, 20, 205, 50); - title = new CTextBox("Enter address:", boxRect, 0, FONT_BIG, CENTER, Colors::WHITE); - - address = new CTextInput(Rect(25, 68, 175, 16), *bg); - address->cb += std::bind(&CSimpleJoinScreen::onChange, this, _1); - - port = new CTextInput(Rect(25, 115, 175, 16), *bg); - port->cb += std::bind(&CSimpleJoinScreen::onChange, this, _1); - port->filters += std::bind(&CTextInput::numberFilter, _1, _2, 0, 65535); - - ok = new CButton(Point( 26, 142), "MUBCHCK.DEF", CGI->generaltexth->zelp[560], std::bind(&CSimpleJoinScreen::enterSelectionScreen, this, mode), SDLK_RETURN); - cancel = new CButton(Point(142, 142), "MUBCANC.DEF", CGI->generaltexth->zelp[561], std::bind(&CGuiHandler::popIntTotally, std::ref(GH), this), SDLK_ESCAPE); - bar = new CGStatusBar(new CPicture(Rect(7, 186, 218, 18), 0)); - - port->setText(CServerHandler::getDefaultPortStr(), true); - address->setText(settings["server"]["server"].String(), true); - address->giveFocus(); -} - -void CSimpleJoinScreen::enterSelectionScreen(CMenuScreen::EGameMode mode) -{ - std::string textAddress = address->text; - std::string textPort = port->text; - - GH.popIntTotally(this); - - GH.pushInt(new CSelectionScreen(CMenuScreen::newGame, mode, nullptr, textAddress, boost::lexical_cast(textPort))); -} -void CSimpleJoinScreen::onChange(const std::string & newText) -{ - ok->block(address->text.empty() || port->text.empty()); -} - diff --git a/client/CPreGame.h b/client/CPreGame.h deleted file mode 100644 index b67e34256..000000000 --- a/client/CPreGame.h +++ /dev/null @@ -1,650 +0,0 @@ -/* - * CPreGame.h, part of VCMI engine - * - * Authors: listed in file AUTHORS in main folder - * - * License: GNU General Public License v2.0 or later - * Full text of license available in license.txt file, in main folder - * - */ -#pragma once - -#include "../lib/StartInfo.h" -#include "../lib/FunctionList.h" -#include "../lib/mapping/CMapInfo.h" -#include "../lib/rmg/CMapGenerator.h" -#include "windows/CWindowObject.h" - -class CMapInfo; -class CMusicHandler; -class CMapHeader; -class CCampaignHeader; -class CTextInput; -class CCampaign; -class CGStatusBar; -class CTextBox; -class CCampaignState; -class CConnection; -class JsonNode; -class CMapGenOptions; -class CRandomMapTab; -struct CPackForSelectionScreen; -struct PlayerInfo; -class CMultiLineLabel; -class CToggleButton; -class CToggleGroup; -class CTabbedInt; -class IImage; -class CAnimation; -class CAnimImage; -class CButton; -class CLabel; -class CSlider; - -namespace boost{ class thread; class recursive_mutex;} - -enum ESortBy{_playerAm, _size, _format, _name, _viccon, _loscon, _numOfMaps, _fileName}; //_numOfMaps is for campaigns - -/// Class which handles map sorting by different criteria -class mapSorter -{ -public: - ESortBy sortBy; - bool operator()(const CMapInfo *aaa, const CMapInfo *bbb); - mapSorter(ESortBy es):sortBy(es){}; -}; - -/// The main menu screens listed in the EState enum -class CMenuScreen : public CIntObject -{ - const JsonNode& config; - - CTabbedInt *tabs; - - CPicture * background; - std::vector images; - - CIntObject *createTab(size_t index); -public: - std::vector menuNameToEntry; - - enum EState { //where are we? - mainMenu, newGame, loadGame, campaignMain, saveGame, scenarioInfo, campaignList - }; - - enum EGameMode { - SINGLE_PLAYER = 0, MULTI_HOT_SEAT, MULTI_NETWORK_HOST, MULTI_NETWORK_GUEST, SINGLE_CAMPAIGN - }; - CMenuScreen(const JsonNode& configNode); - - void showAll(SDL_Surface * to) override; - void show(SDL_Surface * to) override; - void activate() override; - void deactivate() override; - - void switchToTab(size_t index); -}; - -class CMenuEntry : public CIntObject -{ - std::vector images; - std::vector buttons; - - CButton* createButton(CMenuScreen* parent, const JsonNode& button); -public: - CMenuEntry(CMenuScreen* parent, const JsonNode &config); -}; - -class CreditsScreen : public CIntObject -{ - int positionCounter; - CMultiLineLabel* credits; -public: - CreditsScreen(); - - void show(SDL_Surface * to) override; - - void clickLeft(tribool down, bool previousState) override; - void clickRight(tribool down, bool previousState) override; -}; - -/// Implementation of the chat box -class CChatBox : public CIntObject -{ -public: - CTextBox *chatHistory; - CTextInput *inputBox; - - CChatBox(const Rect &rect); - - void keyPressed(const SDL_KeyboardEvent & key) override; - - void addNewMessage(const std::string &text); -}; - -class InfoCard : public CIntObject -{ - CAnimImage * victory, * loss, *sizes; - std::shared_ptr sFlags; -public: - CPicture *bg; - - bool network; - bool chatOn; //if chat is shown, then description is hidden - CTextBox *mapDescription; - CChatBox *chat; - CPicture *playerListBg; - - CToggleGroup *difficulty; - - void changeSelection(const CMapInfo *to); - void showAll(SDL_Surface * to) override; - void clickRight(tribool down, bool previousState) override; - void showTeamsPopup(); - void toggleChat(); - void setChat(bool activateChat); - InfoCard(bool Network = false); - ~InfoCard(); -}; - -/// The selection tab which is shown at the map selection screen -class SelectionTab : public CIntObject -{ -private: - std::shared_ptr formatIcons; - - void parseMaps(const std::unordered_set &files); - void parseGames(const std::unordered_set &files, CMenuScreen::EGameMode gameMode); - std::unordered_set getFiles(std::string dirURI, int resType); - void parseCampaigns(const std::unordered_set & files ); - CMenuScreen::EState tabType; -public: - int positions; //how many entries (games/maps) can be shown - CPicture *bg; //general bg image - CSlider *slider; - std::vector allItems; - std::vector curItems; - size_t selectionPos; - std::function onSelect; - - ESortBy sortingBy; - ESortBy generalSortingBy; - bool ascending; - - CTextInput *txt; - - - void filter(int size, bool selectFirst = false); //0 - all - void select(int position); //position: <0 - positions> position on the screen - void selectAbs(int position); //position: absolute position in curItems vector - int getPosition(int x, int y); //convert mouse coords to entry position; -1 means none - void sliderMove(int slidPos); - void sortBy(int criteria); - void sort(); - void printMaps(SDL_Surface *to); - int getLine(); - void selectFName(std::string fname); - const CMapInfo * getSelectedMapInfo() const; - - void showAll(SDL_Surface * to) override; - void clickLeft(tribool down, bool previousState) override; - void keyPressed(const SDL_KeyboardEvent & key) override; - void onDoubleClick() override; - SelectionTab(CMenuScreen::EState Type, const std::function &OnSelect, CMenuScreen::EGameMode GameMode = CMenuScreen::SINGLE_PLAYER); - ~SelectionTab(); -}; - -/// The options tab which is shown at the map selection phase. -class OptionsTab : public CIntObject -{ - CPicture *bg; -public: - enum SelType {TOWN, HERO, BONUS}; - - struct CPlayerSettingsHelper - { - const PlayerSettings & settings; - const SelType type; - - CPlayerSettingsHelper(const PlayerSettings & settings, SelType type): - settings(settings), - type(type) - {} - - /// visible image settings - size_t getImageIndex(); - std::string getImageName(); - - std::string getName(); /// name visible in options dialog - std::string getTitle(); /// title in popup box - std::string getSubtitle(); /// popup box subtitle - std::string getDescription();/// popup box description, not always present - }; - - class CPregameTooltipBox : public CWindowObject, public CPlayerSettingsHelper - { - void genHeader(); - void genTownWindow(); - void genHeroWindow(); - void genBonusWindow(); - public: - CPregameTooltipBox(CPlayerSettingsHelper & helper); - }; - - struct SelectedBox : public CIntObject, public CPlayerSettingsHelper //img with current town/hero/bonus - { - CAnimImage * image; - CLabel *subtitle; - - SelectedBox(Point position, PlayerSettings & settings, SelType type); - void clickRight(tribool down, bool previousState) override; - - void update(); - }; - - struct PlayerOptionsEntry : public CIntObject - { - PlayerInfo π - PlayerSettings &s; - CPicture *bg; - CButton *btns[6]; //left and right for town, hero, bonus - CButton *flag; - SelectedBox *town; - SelectedBox *hero; - SelectedBox *bonus; - enum {HUMAN_OR_CPU, HUMAN, CPU} whoCanPlay; - - PlayerOptionsEntry(OptionsTab *owner, PlayerSettings &S); - void selectButtons(); //hides unavailable buttons - void showAll(SDL_Surface * to) override; - void update(); - }; - - CSlider *turnDuration; - - std::set usedHeroes; - - struct PlayerToRestore - { - PlayerColor color; - int id; - void reset() { id = -1; color = PlayerColor::CANNOT_DETERMINE; } - PlayerToRestore(){ reset(); } - } playerToRestore; - - - std::map entries; //indexed by color - - void nextCastle(PlayerColor player, int dir); //dir == -1 or +1 - void nextHero(PlayerColor player, int dir); //dir == -1 or +1 - void nextBonus(PlayerColor player, int dir); //dir == -1 or +1 - void setTurnLength(int npos); - void flagPressed(PlayerColor player); - - void recreate(); - OptionsTab(); - ~OptionsTab(); - void showAll(SDL_Surface * to) override; - - int nextAllowedHero(PlayerColor player, int min, int max, int incl, int dir ); - - bool canUseThisHero(PlayerColor player, int ID ); -}; - -/// The random map tab shows options for generating a random map. -class CRandomMapTab : public CIntObject -{ -public: - CRandomMapTab(); - - void showAll(SDL_Surface * to) override; - void updateMapInfo(); - CFunctionList & getMapInfoChanged(); - const CMapInfo * getMapInfo() const; - const CMapGenOptions & getMapGenOptions() const; - void setMapGenOptions(std::shared_ptr opts); - -private: - void addButtonsToGroup(CToggleGroup * group, const std::vector & defs, int startIndex, int endIndex, int btnWidth, int helpStartIndex) const; - void addButtonsWithRandToGroup(CToggleGroup * group, const std::vector & defs, int startIndex, int endIndex, int btnWidth, int helpStartIndex, int helpRandIndex) const; - void deactivateButtonsFrom(CToggleGroup * group, int startId); - void validatePlayersCnt(int playersCnt); - void validateCompOnlyPlayersCnt(int compOnlyPlayersCnt); - std::vector getPossibleMapSizes(); - - CPicture * bg; - CToggleButton * twoLevelsBtn; - CToggleGroup * mapSizeBtnGroup, * playersCntGroup, * teamsCntGroup, * compOnlyPlayersCntGroup, - * compOnlyTeamsCntGroup, * waterContentGroup, * monsterStrengthGroup; - CButton * showRandMaps; - CMapGenOptions mapGenOptions; - std::unique_ptr mapInfo; - CFunctionList mapInfoChanged; -}; - -/// Interface for selecting a map. -class ISelectionScreenInfo -{ -public: - CMenuScreen::EGameMode gameMode; - CMenuScreen::EState screenType; //new/save/load#Game - const CMapInfo *current; - StartInfo sInfo; - std::map playerNames; // id of player <-> player name; 0 is reserved as ID of AI "players" - - ISelectionScreenInfo(const std::map *Names = nullptr); - virtual ~ISelectionScreenInfo(); - virtual void update(){}; - virtual void propagateOptions() {}; - virtual void postRequest(ui8 what, ui8 dir) {}; - virtual void postChatMessage(const std::string &txt){}; - - void setPlayer(PlayerSettings &pset, ui8 player); - void updateStartInfo(std::string filename, StartInfo & sInfo, const std::unique_ptr & mapHeader); - - ui8 getIdOfFirstUnallocatedPlayer(); //returns 0 if none - bool isGuest() const; - bool isHost() const; - -}; - -/// The actual map selection screen which consists of the options and selection tab -class CSelectionScreen : public CIntObject, public ISelectionScreenInfo -{ - bool bordered; -public: - CPicture *bg; //general bg image - InfoCard *card; - OptionsTab *opt; - CRandomMapTab * randMapTab; - CButton *start, *back; - - SelectionTab *sel; - CIntObject *curTab; - - boost::thread *serverHandlingThread; - boost::recursive_mutex *mx; - std::list upcomingPacks; //protected by mx - - CConnection *serv; //connection to server, used in MP mode - bool ongoingClosing; - ui8 myNameID; //used when networking - otherwise all player are "mine" - - CSelectionScreen(CMenuScreen::EState Type, CMenuScreen::EGameMode GameMode = CMenuScreen::SINGLE_PLAYER, const std::map * Names = nullptr, const std::string & Address = "", const ui16 Port = 0); - ~CSelectionScreen(); - void toggleTab(CIntObject *tab); - void changeSelection(const CMapInfo *to); - void startCampaign(); - void startScenario(); - void difficultyChange(int to); - - void handleConnection(); - - void processPacks(); - void setSInfo(const StartInfo &si); - void update() override; - void propagateOptions() override; - void postRequest(ui8 what, ui8 dir) override; - void postChatMessage(const std::string &txt) override; - void propagateNames(); - void showAll(SDL_Surface *to) override; -}; - -/// Save game screen -class CSavingScreen : public CSelectionScreen -{ -public: - const CMapInfo *ourGame; - - - CSavingScreen(bool hotseat = false); - ~CSavingScreen(); -}; - -/// Scenario information screen shown during the game (thus not really a "pre-game" but fits here anyway) -class CScenarioInfo : public CIntObject, public ISelectionScreenInfo -{ -public: - CButton *back; - InfoCard *card; - OptionsTab *opt; - - CScenarioInfo(const CMapHeader *mapHeader, const StartInfo *startInfo); - ~CScenarioInfo(); -}; - -/// Multiplayer mode -class CMultiMode : public CIntObject -{ -public: - CPicture *bg; - CTextInput *txt; - CButton *btns[7]; //0 - hotseat, 6 - cancel - CGStatusBar *bar; - - CMultiMode(); - void openHotseat(); - void hostTCP(); - void joinTCP(); -}; - -/// Hot seat player window -class CHotSeatPlayers : public CIntObject -{ - CPicture *bg; - CTextBox *title; - CTextInput* txt[8]; - CButton *ok, *cancel; - CGStatusBar *bar; - - void onChange(std::string newText); - void enterSelectionScreen(); - -public: - CHotSeatPlayers(const std::string &firstPlayer); -}; - - -class CPrologEpilogVideo : public CWindowObject -{ - CCampaignScenario::SScenarioPrologEpilog spe; - int positionCounter; - int voiceSoundHandle; - std::function exitCb; - - CMultiLineLabel * text; -public: - CPrologEpilogVideo(CCampaignScenario::SScenarioPrologEpilog _spe, std::function callback); - - void clickLeft(tribool down, bool previousState) override; - void show(SDL_Surface * to) override; -}; - -/// Campaign screen where you can choose one out of three starting bonuses -class CBonusSelection : public CIntObject -{ -public: - CBonusSelection(const std::string & campaignFName); - CBonusSelection(std::shared_ptr _ourCampaign); - ~CBonusSelection(); - - void showAll(SDL_Surface * to) override; - void show(SDL_Surface * to) override; - -private: - struct SCampPositions - { - std::string campPrefix; - int colorSuffixLength; - - struct SRegionDesc - { - std::string infix; - int xpos, ypos; - }; - - std::vector regions; - - }; - - class CRegion : public CIntObject - { - CBonusSelection * owner; - SDL_Surface* graphics[3]; //[0] - not selected, [1] - selected, [2] - striped - bool accessible; //false if region should be striped - bool selectable; //true if region should be selectable - int myNumber; //number of region - - public: - std::string rclickText; - CRegion(CBonusSelection * _owner, bool _accessible, bool _selectable, int _myNumber); - ~CRegion(); - - void clickLeft(tribool down, bool previousState) override; - void clickRight(tribool down, bool previousState) override; - void show(SDL_Surface * to) override; - }; - - void init(); - void loadPositionsOfGraphics(); - void updateStartButtonState(int selected = -1); //-1 -- no bonus is selected - void updateBonusSelection(); - void updateCampaignState(); - - // Event handlers - void goBack(); - void startMap(); - void restartMap(); - void selectMap(int mapNr, bool initialSelect); - void selectBonus(int id); - void increaseDifficulty(); - void decreaseDifficulty(); - - // GUI components - SDL_Surface * background; - CButton * startB, * restartB, * backB; - CTextBox * campaignDescription, * mapDescription; - std::vector campDescriptions; - std::vector regions; - CRegion * highlightedRegion; - CToggleGroup * bonuses; - std::array diffPics; //pictures of difficulties, user-selectable (or not if campaign locks this) - CButton * diffLb, * diffRb; //buttons for changing difficulty - CAnimImage * sizes;//icons of map sizes - std::shared_ptr sFlags; - - // Data - std::shared_ptr ourCampaign; - int selectedMap; - boost::optional selectedBonus; - StartInfo startInfo; - std::unique_ptr ourHeader; -}; - -/// Campaign selection screen -class CCampaignScreen : public CIntObject -{ -public: - enum CampaignStatus {DEFAULT = 0, ENABLED, DISABLED, COMPLETED}; // the status of the campaign - -private: - /// A button which plays a video when you move the mouse cursor over it - class CCampaignButton : public CIntObject - { - private: - CLabel *hoverLabel; - CampaignStatus status; - - std::string campFile; // the filename/resourcename of the campaign - std::string video; // the resource name of the video - std::string hoverText; - - void clickLeft(tribool down, bool previousState) override; - void hover(bool on) override; - - public: - CCampaignButton(const JsonNode &config ); - void show(SDL_Surface * to) override; - }; - - std::vector campButtons; - std::vector images; - - CButton* createExitButton(const JsonNode& button); - -public: - enum CampaignSet {ROE, AB, SOD, WOG}; - - CCampaignScreen(const JsonNode &config); - void showAll(SDL_Surface *to) override; -}; - -/// Manages the configuration of pregame GUI elements like campaign screen, main menu, loading screen,... -class CGPreGameConfig -{ -public: - static CGPreGameConfig & get(); - const JsonNode & getConfig() const; - const JsonNode & getCampaigns() const; - -private: - CGPreGameConfig(); - - const JsonNode campaignSets; - const JsonNode config; -}; - -/// Handles background screen, loads graphics for victory/loss condition and random town or hero selection -class CGPreGame : public CIntObject, public IUpdateable -{ - void loadGraphics(); - void disposeGraphics(); - - CGPreGame(); //Use createIfNotPresent - -public: - CMenuScreen * menu; - - std::shared_ptr victoryIcons, lossIcons; - - ~CGPreGame(); - void update() override; - void openSel(CMenuScreen::EState type, CMenuScreen::EGameMode gameMode = CMenuScreen::SINGLE_PLAYER); - - void openCampaignScreen(std::string name); - - static CGPreGame * create(); - void removeFromGui(); - static void showLoadingScreen(std::function loader); -}; - -class CLoadingScreen : public CWindowObject -{ - boost::thread loadingThread; - - std::string getBackground(); -public: - CLoadingScreen(std::function loader); - ~CLoadingScreen(); - - void showAll(SDL_Surface *to) override; -}; - -/// Simple window to enter the server's address. -class CSimpleJoinScreen : public CIntObject -{ - CPicture * bg; - CTextBox * title; - CButton * ok, * cancel; - CGStatusBar * bar; - CTextInput * address; - CTextInput * port; - - void enterSelectionScreen(CMenuScreen::EGameMode mode); - void onChange(const std::string & newText); -public: - CSimpleJoinScreen(CMenuScreen::EGameMode mode); -}; - -extern ISelectionScreenInfo *SEL; -extern CGPreGame *CGP; diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp new file mode 100644 index 000000000..4b596e44c --- /dev/null +++ b/client/CServerHandler.cpp @@ -0,0 +1,674 @@ +/* + * CServerHandler.cpp, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#include "StdInc.h" + +#include "CServerHandler.h" +#include "Client.h" +#include "CGameInfo.h" +#include "CPlayerInterface.h" +#include "gui/CGuiHandler.h" + +#include "lobby/CSelectionBase.h" +#include "lobby/CLobbyScreen.h" + +#include "mainmenu/CMainMenu.h" + +#ifndef VCMI_ANDROID +#include "../lib/Interprocess.h" +#endif +#include "../lib/CConfigHandler.h" +#include "../lib/CGeneralTextHandler.h" +#include "../lib/CThreadHelper.h" +#include "../lib/NetPacks.h" +#include "../lib/StartInfo.h" +#include "../lib/VCMIDirs.h" +#include "../lib/mapping/CCampaignHandler.h" +#include "../lib/mapping/CMap.h" +#include "../lib/mapping/CMapInfo.h" +#include "../lib/mapObjects/MiscObjects.h" +#include "../lib/rmg/CMapGenOptions.h" +#include "../lib/registerTypes/RegisterTypes.h" +#include "../lib/serializer/Connection.h" +#include "../lib/serializer/CMemorySerializer.h" + +#include +#include +#include + +template class CApplyOnLobby; + +class CBaseForLobbyApply +{ +public: + virtual bool applyOnLobbyHandler(CServerHandler * handler, void * pack) const = 0; + virtual void applyOnLobbyScreen(CLobbyScreen * lobby, CServerHandler * handler, void * pack) const = 0; + virtual ~CBaseForLobbyApply(){}; + template static CBaseForLobbyApply * getApplier(const U * t = nullptr) + { + return new CApplyOnLobby(); + } +}; + +template class CApplyOnLobby : public CBaseForLobbyApply +{ +public: + bool applyOnLobbyHandler(CServerHandler * handler, void * pack) const override + { + T * ptr = static_cast(pack); + logNetwork->trace("\tImmidiately apply on lobby: %s", typeList.getTypeInfo(ptr)->name()); + return ptr->applyOnLobbyHandler(handler); + } + + void applyOnLobbyScreen(CLobbyScreen * lobby, CServerHandler * handler, void * pack) const override + { + T * ptr = static_cast(pack); + logNetwork->trace("\tApply on lobby from queue: %s", typeList.getTypeInfo(ptr)->name()); + ptr->applyOnLobbyScreen(lobby, handler); + } +}; + +template<> class CApplyOnLobby: public CBaseForLobbyApply +{ +public: + bool applyOnLobbyHandler(CServerHandler * handler, void * pack) const override + { + logGlobal->error("Cannot apply plain CPack!"); + assert(0); + return false; + } + + void applyOnLobbyScreen(CLobbyScreen * lobby, CServerHandler * handler, void * pack) const override + { + logGlobal->error("Cannot apply plain CPack!"); + assert(0); + } +}; + +extern std::string NAME; + +CServerHandler::CServerHandler() + : state(EClientState::NONE), mx(std::make_shared()), client(nullptr), loadMode(0), campaignStateToSend(nullptr) +{ + uuid = boost::uuids::to_string(boost::uuids::random_generator()()); + applier = std::make_shared>(); + registerTypesLobbyPacks(*applier); +} + +void CServerHandler::resetStateForLobby(const StartInfo::EMode mode, const std::vector * names) +{ + hostClientId = -1; + state = EClientState::NONE; + th = make_unique(); + packsForLobbyScreen.clear(); + c.reset(); + si.reset(new StartInfo()); + playerNames.clear(); + si->difficulty = 1; + si->mode = mode; + myNames.clear(); + if(names && !names->empty()) //if have custom set of player names - use it + myNames = *names; + else + myNames.push_back(settings["general"]["playerName"].String()); + +#ifndef VCMI_ANDROID + shm.reset(); + + if(!settings["session"]["disable-shm"].Bool()) + { + std::string sharedMemoryName = "vcmi_memory"; + if(settings["session"]["enable-shm-uuid"].Bool()) + { + //used or automated testing when multiple clients start simultaneously + sharedMemoryName += "_" + uuid; + } + try + { + shm = std::make_shared(sharedMemoryName, true); + } + catch(...) + { + shm.reset(); + logNetwork->error("Cannot open interprocess memory. Continue without it..."); + } + } +#endif +} + +void CServerHandler::startLocalServerAndConnect() +{ + if(threadRunLocalServer) + threadRunLocalServer->join(); + + th->update(); +#ifdef VCMI_ANDROID + CAndroidVMHelper envHelper; + envHelper.callStaticVoidMethod(CAndroidVMHelper::NATIVE_METHODS_DEFAULT_CLASS, "startServer", true); +#else + threadRunLocalServer = std::make_shared(&CServerHandler::threadRunServer, this); //runs server executable; +#endif + logNetwork->trace("Setting up thread calling server: %d ms", th->getDiff()); + + th->update(); + +#ifndef VCMI_ANDROID + if(shm) + shm->sr->waitTillReady(); +#else + logNetwork->info("waiting for server"); + while(!androidTestServerReadyFlag.load()) + { + logNetwork->info("still waiting..."); + boost::this_thread::sleep(boost::posix_time::milliseconds(1000)); + } + logNetwork->info("waiting for server finished..."); + androidTestServerReadyFlag = false; +#endif + logNetwork->trace("Waiting for server: %d ms", th->getDiff()); + + th->update(); //put breakpoint here to attach to server before it does something stupid + +#ifndef VCMI_ANDROID + justConnectToServer(settings["server"]["server"].String(), shm ? shm->sr->port : 0); +#else + justConnectToServer(settings["server"]["server"].String()); +#endif + + logNetwork->trace("\tConnecting to the server: %d ms", th->getDiff()); +} + +void CServerHandler::justConnectToServer(const std::string & addr, const ui16 port) +{ + state = EClientState::CONNECTING; + while(!c && state != EClientState::CONNECTION_CANCELLED) + { + try + { + logNetwork->info("Establishing connection..."); + c = std::make_shared( + addr.size() ? addr : settings["server"]["server"].String(), + port ? port : getDefaultPort(), + NAME, uuid); + } + catch(...) + { + logNetwork->error("\nCannot establish connection! Retrying within 1 second"); + boost::this_thread::sleep(boost::posix_time::seconds(1)); + } + } + + if(state == EClientState::CONNECTION_CANCELLED) + logNetwork->info("Connection aborted by player!"); + else + c->handler = std::make_shared(&CServerHandler::threadHandleConnection, this); +} + +void CServerHandler::applyPacksOnLobbyScreen() +{ + if(!c || !c->handler) + return; + + boost::unique_lock lock(*mx); + while(!packsForLobbyScreen.empty()) + { + CPackForLobby * pack = packsForLobbyScreen.front(); + packsForLobbyScreen.pop_front(); + CBaseForLobbyApply * apply = applier->getApplier(typeList.getTypeID(pack)); //find the applier + apply->applyOnLobbyScreen(static_cast(SEL), this, pack); + GH.totalRedraw(); + delete pack; + } +} + +void CServerHandler::stopServerConnection() +{ + if(c->handler) + { + while(!c->handler->timed_join(boost::posix_time::milliseconds(50))) + applyPacksOnLobbyScreen(); + c->handler->join(); + } +} + +std::set CServerHandler::getHumanColors() +{ + return clientHumanColors(c->connectionID); +} + + +PlayerColor CServerHandler::myFirstColor() const +{ + return clientFirstColor(c->connectionID); +} + +bool CServerHandler::isMyColor(PlayerColor color) const +{ + return isClientColor(c->connectionID, color); +} + +ui8 CServerHandler::myFirstId() const +{ + return clientFirstId(c->connectionID); +} + +bool CServerHandler::isServerLocal() const +{ + if(threadRunLocalServer) + return true; + + return false; +} + +bool CServerHandler::isHost() const +{ + return c && hostClientId == c->connectionID; +} + +bool CServerHandler::isGuest() const +{ + return !c || hostClientId != c->connectionID; +} + +ui16 CServerHandler::getDefaultPort() +{ + if(settings["session"]["serverport"].Integer()) + return settings["session"]["serverport"].Integer(); + else + return settings["server"]["port"].Integer(); +} + +std::string CServerHandler::getDefaultPortStr() +{ + return boost::lexical_cast(getDefaultPort()); +} + +void CServerHandler::sendClientConnecting() const +{ + LobbyClientConnected lcc; + lcc.uuid = uuid; + lcc.names = myNames; + lcc.mode = si->mode; + sendLobbyPack(lcc); +} + +void CServerHandler::sendClientDisconnecting() +{ + // FIXME: This is workaround needed to make sure client not trying to sent anything to non existed server + if(state == EClientState::DISCONNECTING) + return; + + state = EClientState::DISCONNECTING; + LobbyClientDisconnected lcd; + lcd.clientId = c->connectionID; + logNetwork->info("Connection has been requested to be closed."); + if(isServerLocal()) + { + lcd.shutdownServer = true; + logNetwork->info("Sent closing signal to the server"); + } + else + { + logNetwork->info("Sent leaving signal to the server"); + } + sendLobbyPack(lcd); +} + +void CServerHandler::setCampaignState(std::shared_ptr newCampaign) +{ + state = EClientState::LOBBY_CAMPAIGN; + LobbySetCampaign lsc; + lsc.ourCampaign = newCampaign; + sendLobbyPack(lsc); +} + +void CServerHandler::setCampaignMap(int mapId) const +{ + if(state == EClientState::GAMEPLAY) // FIXME: UI shouldn't sent commands in first place + return; + + LobbySetCampaignMap lscm; + lscm.mapId = mapId; + sendLobbyPack(lscm); +} + +void CServerHandler::setCampaignBonus(int bonusId) const +{ + if(state == EClientState::GAMEPLAY) // FIXME: UI shouldn't sent commands in first place + return; + + LobbySetCampaignBonus lscb; + lscb.bonusId = bonusId; + sendLobbyPack(lscb); +} + +void CServerHandler::setMapInfo(std::shared_ptr to, std::shared_ptr mapGenOpts) const +{ + LobbySetMap lsm; + lsm.mapInfo = to; + lsm.mapGenOpts = mapGenOpts; + sendLobbyPack(lsm); +} + +void CServerHandler::setPlayer(PlayerColor color) const +{ + LobbySetPlayer lsp; + lsp.clickedColor = color; + sendLobbyPack(lsp); +} + +void CServerHandler::setPlayerOption(ui8 what, ui8 dir, PlayerColor player) const +{ + LobbyChangePlayerOption lcpo; + lcpo.what = what; + lcpo.direction = dir; + lcpo.color = player; + sendLobbyPack(lcpo); +} + +void CServerHandler::setDifficulty(int to) const +{ + LobbySetDifficulty lsd; + lsd.difficulty = to; + sendLobbyPack(lsd); +} + +void CServerHandler::setTurnLength(int npos) const +{ + vstd::amin(npos, GameConstants::POSSIBLE_TURNTIME.size() - 1); + LobbySetTurnTime lstt; + lstt.turnTime = GameConstants::POSSIBLE_TURNTIME[npos]; + sendLobbyPack(lstt); +} + +void CServerHandler::sendMessage(const std::string & txt) const +{ + std::istringstream readed; + readed.str(txt); + std::string command; + readed >> command; + if(command == "!passhost") + { + std::string id; + readed >> id; + if(id.length()) + { + LobbyChangeHost lch; + lch.newHostConnectionId = boost::lexical_cast(id); + sendLobbyPack(lch); + } + } + else if(command == "!forcep") + { + std::string connectedId, playerColorId; + readed >> connectedId; + readed >> playerColorId; + if(connectedId.length(), playerColorId.length()) + { + ui8 connected = boost::lexical_cast(connectedId); + auto color = PlayerColor(boost::lexical_cast(playerColorId)); + if(color.isValidPlayer() && playerNames.find(connected) != playerNames.end()) + { + LobbyForceSetPlayer lfsp; + lfsp.targetConnectedPlayer = connected; + lfsp.targetPlayerColor = color; + sendLobbyPack(lfsp); + } + } + } + else + { + LobbyChatMessage lcm; + lcm.message = txt; + lcm.playerName = playerNames.find(myFirstId())->second.name; + sendLobbyPack(lcm); + } +} + +void CServerHandler::sendGuiAction(ui8 action) const +{ + LobbyGuiAction lga; + lga.action = static_cast(action); + sendLobbyPack(lga); +} + +void CServerHandler::sendStartGame(bool allowOnlyAI) const +{ + verifyStateBeforeStart(allowOnlyAI ? true : settings["session"]["onlyai"].Bool()); + LobbyStartGame lsg; + sendLobbyPack(lsg); +} + +void CServerHandler::startGameplay() +{ + client = new CClient(); + + switch(si->mode) + { + case StartInfo::NEW_GAME: + client->newGame(); + break; + case StartInfo::CAMPAIGN: + client->newGame(); + break; + case StartInfo::LOAD_GAME: + client->loadGame(); + break; + default: + throw std::runtime_error("Invalid mode"); + } + // After everything initialized we can accept CPackToClient netpacks + c->enterGameplayConnectionMode(client->gameState()); + state = EClientState::GAMEPLAY; +} + +void CServerHandler::endGameplay(bool closeConnection) +{ + client->endGame(); + vstd::clear_pointer(client); + + // Game is ending + // Tell the network thread to reach a stable state + CSH->sendClientDisconnecting(); + logNetwork->info("Closed connection."); +} + +void CServerHandler::startCampaignScenario(std::shared_ptr cs) +{ + SDL_Event event; + event.type = SDL_USEREVENT; + event.user.code = EUserEvent::CAMPAIGN_START_SCENARIO; + if(cs) + event.user.data1 = CMemorySerializer::deepCopy(*cs.get()).release(); + else + event.user.data1 = CMemorySerializer::deepCopy(*si->campState.get()).release(); + SDL_PushEvent(&event); +} + +int CServerHandler::howManyPlayerInterfaces() +{ + int playerInts = 0; + for(auto pint : client->playerint) + { + if(dynamic_cast(pint.second.get())) + playerInts++; + } + + return playerInts; +} + +ui8 CServerHandler::getLoadMode() +{ + if(state == EClientState::GAMEPLAY) + { + if(si->campState) + return ELoadMode::CAMPAIGN; + for(auto pn : playerNames) + { + if(pn.second.connection != c->connectionID) + return ELoadMode::MULTI; + } + + return ELoadMode::SINGLE; + } + return loadMode; +} + +void CServerHandler::debugStartTest(std::string filename, bool save) +{ + logGlobal->info("Starting debug test with file: %s", filename); + auto mapInfo = std::make_shared(); + if(save) + { + resetStateForLobby(StartInfo::LOAD_GAME); + mapInfo->saveInit(ResourceID(filename, EResType::CLIENT_SAVEGAME)); + screenType = ESelectionScreen::loadGame; + } + else + { + resetStateForLobby(StartInfo::NEW_GAME); + mapInfo->mapInit(filename); + screenType = ESelectionScreen::newGame; + } + if(settings["session"]["donotstartserver"].Bool()) + justConnectToServer("127.0.0.1", 3030); + else + startLocalServerAndConnect(); + + while(!settings["session"]["headless"].Bool() && !dynamic_cast(GH.topInt())) + boost::this_thread::sleep(boost::posix_time::milliseconds(50)); + while(!mi || mapInfo->fileURI != CSH->mi->fileURI) + { + setMapInfo(mapInfo); + boost::this_thread::sleep(boost::posix_time::milliseconds(50)); + } + // "Click" on color to remove us from it + setPlayer(myFirstColor()); + while(myFirstColor() != PlayerColor::CANNOT_DETERMINE) + boost::this_thread::sleep(boost::posix_time::milliseconds(50)); + + while(true) + { + try + { + sendStartGame(); + break; + } + catch(...) + { + + } + boost::this_thread::sleep(boost::posix_time::milliseconds(50)); + } +} + +void CServerHandler::threadHandleConnection() +{ + setThreadName("CServerHandler::threadHandleConnection"); + c->enterLobbyConnectionMode(); + + try + { + sendClientConnecting(); + while(c->connected) + { + while(state == EClientState::STARTING) + boost::this_thread::sleep(boost::posix_time::milliseconds(10)); + + CPack * pack = c->retrievePack(); + if(state == EClientState::DISCONNECTING) + { + // FIXME: server shouldn't really send netpacks after it's tells client to disconnect + // Though currently they'll be delivered and might cause crash. + vstd::clear_pointer(pack); + } + else if(auto lobbyPack = dynamic_cast(pack)) + { + if(applier->getApplier(typeList.getTypeID(pack))->applyOnLobbyHandler(this, pack)) + { + if(!settings["session"]["headless"].Bool()) + { + boost::unique_lock lock(*mx); + packsForLobbyScreen.push_back(lobbyPack); + } + } + } + else if(auto clientPack = dynamic_cast(pack)) + { + client->handlePack(clientPack); + } + } + } + //catch only asio exceptions + catch(const boost::system::system_error & e) + { + if(state == EClientState::DISCONNECTING) + { + logNetwork->info("Successfully closed connection to server, ending listening thread!"); + } + else + { + logNetwork->error("Lost connection to server, ending listening thread!"); + logNetwork->error(e.what()); + if(client) + { + CGuiHandler::pushSDLEvent(SDL_USEREVENT, EUserEvent::RETURN_TO_MAIN_MENU); + } + else + { + auto lcd = new LobbyClientDisconnected(); + lcd->clientId = c->connectionID; + boost::unique_lock lock(*mx); + packsForLobbyScreen.push_back(lcd); + } + } + } + catch(...) + { + handleException(); + throw; + } +} + +void CServerHandler::threadRunServer() +{ +#ifndef VCMI_ANDROID + setThreadName("CServerHandler::threadRunServer"); + const std::string logName = (VCMIDirs::get().userCachePath() / "server_log.txt").string(); + std::string comm = VCMIDirs::get().serverPath().string() + + " --port=" + getDefaultPortStr() + + " --run-by-client" + + " --uuid=" + uuid; + if(shm) + { + comm += " --enable-shm"; + if(settings["session"]["enable-shm-uuid"].Bool()) + comm += " --enable-shm-uuid"; + } + comm += " > \"" + logName + '\"'; + + int result = std::system(comm.c_str()); + if (result == 0) + { + logNetwork->info("Server closed correctly"); + } + else + { + logNetwork->error("Error: server failed to close correctly or crashed!"); + logNetwork->error("Check %s for more info", logName); + } + threadRunLocalServer.reset(); +#endif +} + +void CServerHandler::sendLobbyPack(const CPackForLobby & pack) const +{ + if(state != EClientState::STARTING) + c->sendPack(&pack); +} diff --git a/client/CServerHandler.h b/client/CServerHandler.h new file mode 100644 index 000000000..17fa69f08 --- /dev/null +++ b/client/CServerHandler.h @@ -0,0 +1,145 @@ +/* + * CServerHandler.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +#include "../lib/CStopWatch.h" + +#include "../lib/StartInfo.h" + +struct SharedMemory; +class CConnection; +class PlayerColor; +struct StartInfo; + +class CMapInfo; +struct ClientPlayer; +struct CPack; +struct CPackForLobby; +class CClient; + +template class CApplier; +class CBaseForLobbyApply; + +// TODO: Add mutex so we can't set CONNECTION_CANCELLED if client already connected, but thread not setup yet +enum class EClientState : ui8 +{ + NONE = 0, + CONNECTING, // Trying to connect to server + CONNECTION_CANCELLED, // Connection cancelled by player, stop attempts to connect + LOBBY, // Client is connected to lobby + LOBBY_CAMPAIGN, // Client is on scenario bonus selection screen + STARTING, // Gameplay interfaces being created, we pause netpacks retrieving + GAMEPLAY, // In-game, used by some UI + DISCONNECTING // We disconnecting, drop all netpacks +}; + +class IServerAPI +{ +protected: + virtual void sendLobbyPack(const CPackForLobby & pack) const = 0; + +public: + virtual ~IServerAPI() {} + + virtual void sendClientConnecting() const = 0; + virtual void sendClientDisconnecting() = 0; + virtual void setCampaignState(std::shared_ptr newCampaign) = 0; + virtual void setCampaignMap(int mapId) const = 0; + virtual void setCampaignBonus(int bonusId) const = 0; + virtual void setMapInfo(std::shared_ptr to, std::shared_ptr mapGenOpts = {}) const = 0; + virtual void setPlayer(PlayerColor color) const = 0; + virtual void setPlayerOption(ui8 what, ui8 dir, PlayerColor player) const = 0; + virtual void setDifficulty(int to) const = 0; + virtual void setTurnLength(int npos) const = 0; + virtual void sendMessage(const std::string & txt) const = 0; + virtual void sendGuiAction(ui8 action) const = 0; // TODO: possibly get rid of it? + virtual void sendStartGame(bool allowOnlyAI = false) const = 0; +}; + +/// structure to handle running server and connecting to it +class CServerHandler : public IServerAPI, public LobbyInfo +{ + std::shared_ptr> applier; + + std::shared_ptr mx; + std::list packsForLobbyScreen; //protected by mx + + std::vector myNames; + + void threadHandleConnection(); + void threadRunServer(); + void sendLobbyPack(const CPackForLobby & pack) const override; + +public: + std::atomic state; + //////////////////// + // FIXME: Bunch of crutches to glue it all together + + // For starting non-custom campaign and continue to next mission + std::shared_ptr campaignStateToSend; + + ui8 screenType; // To create lobby UI only after server is setup + ui8 loadMode; // For saves filtering in SelectionTab + //////////////////// + + std::unique_ptr th; + std::shared_ptr threadRunLocalServer; + + std::shared_ptr c; + CClient * client; + + CServerHandler(); + + void resetStateForLobby(const StartInfo::EMode mode, const std::vector * names = nullptr); + void startLocalServerAndConnect(); + void justConnectToServer(const std::string &addr = "", const ui16 port = 0); + void applyPacksOnLobbyScreen(); + void stopServerConnection(); + + // Helpers for lobby state access + std::set getHumanColors(); + PlayerColor myFirstColor() const; + bool isMyColor(PlayerColor color) const; + ui8 myFirstId() const; // Used by chat only! + + bool isServerLocal() const; + bool isHost() const; + bool isGuest() const; + + static ui16 getDefaultPort(); + static std::string getDefaultPortStr(); + + // Lobby server API for UI + void sendClientConnecting() const override; + void sendClientDisconnecting() override; + void setCampaignState(std::shared_ptr newCampaign) override; + void setCampaignMap(int mapId) const override; + void setCampaignBonus(int bonusId) const override; + void setMapInfo(std::shared_ptr to, std::shared_ptr mapGenOpts = {}) const override; + void setPlayer(PlayerColor color) const override; + void setPlayerOption(ui8 what, ui8 dir, PlayerColor player) const override; + void setDifficulty(int to) const override; + void setTurnLength(int npos) const override; + void sendMessage(const std::string & txt) const override; + void sendGuiAction(ui8 action) const override; + void sendStartGame(bool allowOnlyAI = false) const override; + + void startGameplay(); + void endGameplay(bool closeConnection = true); + void startCampaignScenario(std::shared_ptr cs = {}); + + // TODO: LobbyState must be updated within game so we should always know how many player interfaces our client handle + int howManyPlayerInterfaces(); + ui8 getLoadMode(); + + void debugStartTest(std::string filename, bool save = false); +}; + +extern CServerHandler * CSH; diff --git a/client/Client.cpp b/client/Client.cpp index 519cf0b99..101e93ab6 100644 --- a/client/Client.cpp +++ b/client/Client.cpp @@ -11,9 +11,6 @@ #include "Client.h" #include -#include -#include -#include #include "CMusicHandler.h" #include "../lib/mapping/CCampaignHandler.h" @@ -34,9 +31,6 @@ #include "../lib/serializer/CTypeList.h" #include "../lib/serializer/Connection.h" #include "../lib/serializer/CLoadIntegrityValidator.h" -#ifndef VCMI_ANDROID -#include "../lib/Interprocess.h" -#endif #include "../lib/NetPacks.h" #include "../lib/VCMI_Lib.h" #include "../lib/VCMIDirs.h" @@ -45,15 +39,17 @@ #include "../lib/JsonNode.h" #include "mapHandler.h" #include "../lib/CConfigHandler.h" -#include "CPreGame.h" +#include "mainmenu/CMainMenu.h" +#include "mainmenu/CCampaignScreen.h" +#include "lobby/CBonusSelection.h" #include "battle/CBattleInterface.h" #include "../lib/CThreadHelper.h" #include "../lib/CScriptingModule.h" #include "../lib/registerTypes/RegisterTypes.h" #include "gui/CGuiHandler.h" #include "CMT.h" +#include "CServerHandler.h" -extern std::string NAME; #ifdef VCMI_ANDROID #include "lib/CAndroidVMHelper.h" #endif @@ -64,45 +60,45 @@ std::atomic_bool androidTestServerReadyFlag; ThreadSafeVector CClient::waitingRequest; -template class CApplyOnCL; +template class CApplyOnCL; class CBaseForCLApply { public: - virtual void applyOnClAfter(CClient *cl, void *pack) const =0; - virtual void applyOnClBefore(CClient *cl, void *pack) const =0; + virtual void applyOnClAfter(CClient * cl, void * pack) const =0; + virtual void applyOnClBefore(CClient * cl, void * pack) const =0; virtual ~CBaseForCLApply(){} - template static CBaseForCLApply *getApplier(const U * t=nullptr) + template static CBaseForCLApply * getApplier(const U * t = nullptr) { return new CApplyOnCL(); } }; -template class CApplyOnCL : public CBaseForCLApply +template class CApplyOnCL : public CBaseForCLApply { public: - void applyOnClAfter(CClient *cl, void *pack) const override + void applyOnClAfter(CClient * cl, void * pack) const override { - T *ptr = static_cast(pack); + T * ptr = static_cast(pack); ptr->applyCl(cl); } - void applyOnClBefore(CClient *cl, void *pack) const override + void applyOnClBefore(CClient * cl, void * pack) const override { - T *ptr = static_cast(pack); + T * ptr = static_cast(pack); ptr->applyFirstCl(cl); } }; -template <> class CApplyOnCL : public CBaseForCLApply +template<> class CApplyOnCL: public CBaseForCLApply { public: - void applyOnClAfter(CClient *cl, void *pack) const override + void applyOnClAfter(CClient * cl, void * pack) const override { logGlobal->error("Cannot apply on CL plain CPack!"); assert(0); } - void applyOnClBefore(CClient *cl, void *pack) const override + void applyOnClBefore(CClient * cl, void * pack) const override { logGlobal->error("Cannot apply on CL plain CPack!"); assert(0); @@ -110,96 +106,155 @@ public: }; -static CApplier *applier = nullptr; - -void CClient::init() +CClient::CClient() { waitingRequest.clear(); - hotSeat = false; - { - TLockGuard _(connectionHandlerMutex); - connectionHandler.reset(); - } pathInfo = nullptr; - applier = new CApplier(); + applier = std::make_shared>(); registerTypesClientPacks1(*applier); registerTypesClientPacks2(*applier); IObjectInterface::cb = this; - serv = nullptr; gs = nullptr; erm = nullptr; - terminate = false; } -CClient::CClient() +void CClient::newGame() { - init(); + CSH->th->update(); + CMapService mapService; + gs = new CGameState(); + logNetwork->trace("\tCreating gamestate: %i", CSH->th->getDiff()); + gs->init(&mapService, CSH->si.get(), settings["general"]["saveRandomMaps"].Bool()); + logNetwork->trace("Initializing GameState (together): %d ms", CSH->th->getDiff()); + + initMapHandler(); + initPlayerInterfaces(); } -CClient::CClient(CConnection *con, StartInfo *si) +void CClient::loadGame() { - init(); - newGame(con,si); -} + logNetwork->info("Loading procedure started!"); -CClient::~CClient() -{ - delete applier; -} - -void CClient::waitForMoveAndSend(PlayerColor color) -{ + std::unique_ptr loader; try { - setThreadName("CClient::waitForMoveAndSend"); - assert(vstd::contains(battleints, color)); - BattleAction ba = battleints[color]->activeStack(gs->curB->battleGetStackByID(gs->curB->activeStack, false)); - if(ba.actionType != EActionType::CANCEL) + boost::filesystem::path clientSaveName = *CResourceHandler::get("local")->getResourceName(ResourceID(CSH->si->mapname, EResType::CLIENT_SAVEGAME)); + boost::filesystem::path controlServerSaveName; + + if(CResourceHandler::get("local")->existsResource(ResourceID(CSH->si->mapname, EResType::SERVER_SAVEGAME))) { - logNetwork->trace("Send battle action to server: %s", ba.toString()); - MakeAction temp_action(ba); - sendRequest(&temp_action, color); + controlServerSaveName = *CResourceHandler::get("local")->getResourceName(ResourceID(CSH->si->mapname, EResType::SERVER_SAVEGAME)); } + else // create entry for server savegame. Triggered if save was made after launch and not yet present in res handler + { + controlServerSaveName = boost::filesystem::path(clientSaveName).replace_extension(".vsgm1"); + CResourceHandler::get("local")->createResource(controlServerSaveName.string(), true); + } + + if(clientSaveName.empty()) + throw std::runtime_error("Cannot open client part of " + CSH->si->mapname); + if(controlServerSaveName.empty() || !boost::filesystem::exists(controlServerSaveName)) + throw std::runtime_error("Cannot open server part of " + CSH->si->mapname); + + { + CLoadIntegrityValidator checkingLoader(clientSaveName, controlServerSaveName, MINIMAL_SERIALIZATION_VERSION); + loadCommonState(checkingLoader); + loader = checkingLoader.decay(); + } + } - catch(boost::thread_interrupted&) + catch(std::exception & e) { - logNetwork->debug("Wait for move thread was interrupted and no action will be send. Was a battle ended by spell?"); + logGlobal->error("Cannot load game %s. Error: %s", CSH->si->mapname, e.what()); + throw; //obviously we cannot continue here } - catch(...) + logNetwork->trace("Loaded common part of save %d ms", CSH->th->getDiff()); + + gs->updateOnLoad(CSH->si.get()); + initMapHandler(); + serialize(loader->serializer, loader->serializer.fileVersion); + initPlayerInterfaces(); +} + +void CClient::serialize(BinarySerializer & h, const int version) +{ + assert(h.saving); + ui8 players = playerint.size(); + h & players; + + for(auto i = playerint.begin(); i != playerint.end(); i++) { - handleException(); + logGlobal->trace("Saving player %s interface", i->first); + assert(i->first == i->second->playerID); + h & i->first; + h & i->second->dllName; + h & i->second->human; + i->second->saveGame(h, version); } } -void CClient::run() +void CClient::serialize(BinaryDeserializer & h, const int version) { - setThreadName("CClient::run"); - try + assert(!h.saving); + if(version < 787) { - while(!terminate) - { - CPack * pack = serv->retrievePack(); //get the package from the server - - if (terminate) - { - vstd::clear_pointer(pack); - break; - } - - handlePack(pack); - } + bool hotSeat = false; + h & hotSeat; } - //catch only asio exceptions - catch (const boost::system::system_error& e) + + ui8 players = 0; + h & players; + + for(int i = 0; i < players; i++) { - logNetwork->error("Lost connection to server, ending listening thread!"); - logNetwork->error(e.what()); - if(!terminate) //rethrow (-> boom!) only if closing connection was unexpected + std::string dllname; + PlayerColor pid; + bool isHuman = false; + + h & pid; + h & dllname; + h & isHuman; + assert(dllname.length() == 0 || !isHuman); + if(pid == PlayerColor::NEUTRAL) { - logNetwork->error("Something wrong, lost connection while game is still ongoing..."); - throw; + logGlobal->trace("Neutral battle interfaces are not serialized."); + continue; } + + logGlobal->trace("Loading player %s interface", pid); + std::shared_ptr nInt; + if(dllname.length()) + nInt = CDynLibHandler::getNewAI(dllname); + else + nInt = std::make_shared(pid); + + nInt->dllName = dllname; + nInt->human = isHuman; + nInt->playerID = pid; + + nInt->loadGame(h, version); + + // Client no longer handle this player at all + if(!vstd::contains(CSH->getAllClientPlayers(CSH->c->connectionID), pid)) + { + logGlobal->trace("Player %s is not belong to this client. Destroying interface", pid); + } + else if(isHuman && !vstd::contains(CSH->getHumanColors(), pid)) + { + logGlobal->trace("Player %s is no longer controlled by human. Destroying interface", pid); + } + else if(!isHuman && vstd::contains(CSH->getHumanColors(), pid)) + { + logGlobal->trace("Player %s is no longer controlled by AI. Destroying interface", pid); + } + else + { + installNewPlayerInterface(nInt, pid); + continue; + } + nInt.reset(); } + logNetwork->trace("Loaded client part of save %d ms", CSH->th->getDiff()); } void CClient::save(const std::string & fname) @@ -211,21 +266,15 @@ void CClient::save(const std::string & fname) } SaveGame save_game(fname); - sendRequest((CPackForClient*)&save_game, PlayerColor::NEUTRAL); + sendRequest(&save_game, PlayerColor::NEUTRAL); } -void CClient::endGame(bool closeConnection) +void CClient::endGame() { //suggest interfaces to finish their stuff (AI should interrupt any bg working threads) - for (auto& i : playerint) + for(auto & i : playerint) i.second->finish(); - // Game is ending - // Tell the network thread to reach a stable state - if (closeConnection) - stopConnection(); - logNetwork->info("Closed connection."); - GH.curInt = nullptr; { boost::unique_lock un(*CPlayerInterface::pim); @@ -239,7 +288,7 @@ void CClient::endGame(bool closeConnection) GH.statusbar = nullptr; logNetwork->info("Removed GUI."); - vstd::clear_pointer(const_cast(CGI)->mh); + vstd::clear_pointer(const_cast(CGI)->mh); vstd::clear_pointer(gs); logNetwork->info("Deleted mapHandler and gameState."); @@ -250,217 +299,39 @@ void CClient::endGame(bool closeConnection) battleints.clear(); callbacks.clear(); battleCallbacks.clear(); - CGKeys::reset(); - CGMagi::reset(); - CGObelisk::reset(); logNetwork->info("Deleted playerInts."); logNetwork->info("Client stopped."); } -#if 1 -void CClient::loadGame(const std::string & fname, const bool server, const std::vector& humanplayerindices, const int loadNumPlayers, int player_, const std::string & ipaddr, const ui16 port) +void CClient::initMapHandler() { - PlayerColor player(player_); //intentional shadowing - logNetwork->info("Loading procedure started!"); - - CServerHandler sh; - if(server) - sh.startServer(); - else - serv = sh.justConnectToServer(ipaddr, port); - - CStopWatch tmh; - std::unique_ptr loader; - try + // TODO: CMapHandler initialization can probably go somewhere else + // It's can't be before initialization of interfaces + // During loading CPlayerInterface from serialized state it's depend on MH + if(!settings["session"]["headless"].Bool()) { - boost::filesystem::path clientSaveName = *CResourceHandler::get("local")->getResourceName(ResourceID(fname, EResType::CLIENT_SAVEGAME)); - boost::filesystem::path controlServerSaveName; - - if (CResourceHandler::get("local")->existsResource(ResourceID(fname, EResType::SERVER_SAVEGAME))) - { - controlServerSaveName = *CResourceHandler::get("local")->getResourceName(ResourceID(fname, EResType::SERVER_SAVEGAME)); - } - else// create entry for server savegame. Triggered if save was made after launch and not yet present in res handler - { - controlServerSaveName = boost::filesystem::path(clientSaveName).replace_extension(".vsgm1"); - CResourceHandler::get("local")->createResource(controlServerSaveName.string(), true); - } - - if(clientSaveName.empty()) - throw std::runtime_error("Cannot open client part of " + fname); - if(controlServerSaveName.empty() || !boost::filesystem::exists(controlServerSaveName)) - throw std::runtime_error("Cannot open server part of " + fname); - - { - CLoadIntegrityValidator checkingLoader(clientSaveName, controlServerSaveName, MINIMAL_SERIALIZATION_VERSION); - loadCommonState(checkingLoader); - loader = checkingLoader.decay(); - } - logNetwork->info("Loaded common part of save %d ms", tmh.getDiff()); - const_cast(CGI)->mh = new CMapHandler(); - const_cast(CGI)->mh->map = gs->map; - pathInfo = make_unique(getMapSize()); + const_cast(CGI)->mh = new CMapHandler(); + CGI->mh->map = gs->map; + logNetwork->trace("Creating mapHandler: %d ms", CSH->th->getDiff()); CGI->mh->init(); - logNetwork->info("Initing maphandler: %d ms", tmh.getDiff()); + logNetwork->trace("Initializing mapHandler (together): %d ms", CSH->th->getDiff()); } - catch(std::exception &e) - { - logGlobal->error("Cannot load game %s. Error: %s", fname, e.what()); - throw; //obviously we cannot continue here - } - -/* - if(!server) - player = PlayerColor(player_); -*/ - - std::set clientPlayers; - if(server) - serv = sh.connectToServer(); - //*loader >> *this; - - if(server) - { - tmh.update(); - ui8 pom8; - *serv << ui8(3) << ui8(loadNumPlayers); //load game; one client if single-player - *serv << fname; - *serv >> pom8; - if(pom8) - throw std::runtime_error("Server cannot open the savegame!"); - else - logNetwork->info("Server opened savegame properly."); - } - - if(server) - { - for(auto & elem : gs->scenarioOps->playerInfos) - { - if(!std::count(humanplayerindices.begin(),humanplayerindices.end(),elem.first.getNum()) || elem.first==player) - clientPlayers.insert(elem.first); - } - clientPlayers.insert(PlayerColor::NEUTRAL); - } - else - { - clientPlayers.insert(player); - } - - std::cout << "CLIENTPLAYERS:\n"; - for(auto x : clientPlayers) - std::cout << x << std::endl; - std::cout << "ENDCLIENTPLAYERS\n"; - - serialize(loader->serializer, loader->serializer.fileVersion, clientPlayers); - *serv << ui32(clientPlayers.size()); - for(auto & elem : clientPlayers) - *serv << ui8(elem.getNum()); - serv->addStdVecItems(gs); /*why is this here?*/ - - //*loader >> *this; - logNetwork->info("Loaded client part of save %d ms", tmh.getDiff()); - - logNetwork->info("Sent info to server: %d ms", tmh.getDiff()); - - //*serv << clientPlayers; - serv->enableStackSendingByID(); - serv->disableSmartPointerSerialization(); - -// logGlobal->trace("Objects:"); -// for(int i = 0; i < gs->map->objects.size(); i++) -// { -// auto o = gs->map->objects[i]; -// if(o) -// logGlobal->trace("\tindex=%5d, id=%5d; address=%5d, pos=%s, name=%s", i, o->id, (int)o.get(), o->pos, o->getHoverText()); -// else -// logGlobal->trace("\tindex=%5d --- nullptr", i); -// } + pathInfo = make_unique(getMapSize()); } -#endif -void CClient::newGame( CConnection *con, StartInfo *si ) +void CClient::initPlayerInterfaces() { - enum {SINGLE, HOST, GUEST} networkMode = SINGLE; - - if (con == nullptr) - { - CServerHandler sh; - serv = sh.connectToServer(); - } - else - { - serv = con; - networkMode = con->isHost() ? HOST : GUEST; - } - - CConnection &c = *serv; - //////////////////////////////////////////////////// - - logNetwork->info("\tWill send info to server..."); - CStopWatch tmh; - - if(networkMode == SINGLE) - { - ui8 pom8; - c << ui8(2) << ui8(1); //new game; one client - c << *si; - c >> pom8; - if(pom8) throw std::runtime_error("Server cannot open the map!"); - } - - c >> si; - logNetwork->info("\tSending/Getting info to/from the server: %d ms", tmh.getDiff()); - c.enableStackSendingByID(); - c.disableSmartPointerSerialization(); - - // Initialize game state - CMapService mapService; - gs = new CGameState(); - logNetwork->info("\tCreating gamestate: %i",tmh.getDiff()); - - gs->init(&mapService, si, settings["general"]["saveRandomMaps"].Bool()); - logNetwork->info("Initializing GameState (together): %d ms", tmh.getDiff()); - - // Now after possible random map gen, we know exact player count. - // Inform server about how many players client handles - std::set myPlayers; - for(auto & elem : gs->scenarioOps->playerInfos) - { - if((networkMode == SINGLE) //single - one client has all player - || (networkMode != SINGLE && serv->connectionID == elem.second.playerID) //multi - client has only "its players" - || (networkMode == HOST && elem.second.playerID == PlayerSettings::PLAYER_AI))//multi - host has all AI players - { - myPlayers.insert(elem.first); //add player - } - } - if(networkMode != GUEST) - myPlayers.insert(PlayerColor::NEUTRAL); - c << myPlayers; - - // Init map handler - if(gs->map) - { - if(!settings["session"]["headless"].Bool()) - { - const_cast(CGI)->mh = new CMapHandler(); - CGI->mh->map = gs->map; - logNetwork->info("Creating mapHandler: %d ms", tmh.getDiff()); - CGI->mh->init(); - } - pathInfo = make_unique(getMapSize()); - logNetwork->info("Initializing mapHandler (together): %d ms", tmh.getDiff()); - } - - int humanPlayers = 0; - for(auto & elem : gs->scenarioOps->playerInfos)//initializing interfaces for players + for(auto & elem : CSH->si->playerInfos) { PlayerColor color = elem.first; - gs->currentPlayer = color; - if(!vstd::contains(myPlayers, color)) + if(!vstd::contains(CSH->getAllClientPlayers(CSH->c->connectionID), color)) + continue; + + if(vstd::contains(playerint, color)) continue; logNetwork->trace("Preparing interface for player %s", color.getStr()); - if(elem.second.playerID == PlayerSettings::PLAYER_AI) + if(elem.second.isControlledByAI()) { auto AiToGive = aiNameForPlayer(elem.second, false); logNetwork->info("Player %s will be lead by %s", color, AiToGive); @@ -469,7 +340,6 @@ void CClient::newGame( CConnection *con, StartInfo *si ) else { installNewPlayerInterface(std::make_shared(color), color); - humanPlayers++; } } @@ -477,448 +347,36 @@ void CClient::newGame( CConnection *con, StartInfo *si ) { installNewPlayerInterface(std::make_shared(PlayerColor::SPECTATOR), PlayerColor::SPECTATOR, true); } - loadNeutralBattleAI(); - serv->addStdVecItems(gs); - hotSeat = (humanPlayers > 1); + if(CSH->getAllClientPlayers(CSH->c->connectionID).count(PlayerColor::NEUTRAL)) + installNewBattleInterface(CDynLibHandler::getNewBattleAI(settings["server"]["neutralAI"].String()), PlayerColor::NEUTRAL); -// std::vector scriptModules; -// CFileUtility::getFilesWithExt(scriptModules, LIB_DIR "/scripting", "." LIB_EXT); -// for(FileInfo &m : scriptModules) -// { -// CScriptingModule * nm = CDynLibHandler::getNewScriptingModule(m.name); -// privilegedGameEventReceivers.push_back(nm); -// privilegedBattleEventReceivers.push_back(nm); -// nm->giveActionCB(this); -// nm->giveInfoCB(this); -// nm->init(); -// -// erm = nm; //something tells me that there'll at most one module and it'll be ERM -// } + logNetwork->trace("Initialized player interfaces %d ms", CSH->th->getDiff()); } -void CClient::serialize(BinarySerializer & h, const int version) +std::string CClient::aiNameForPlayer(const PlayerSettings & ps, bool battleAI) { - assert(h.saving); - h & hotSeat; + if(ps.name.size()) { - ui8 players = playerint.size(); - h & players; - - for(auto i = playerint.begin(); i != playerint.end(); i++) - { - LOG_TRACE_PARAMS(logGlobal, "Saving player %s interface", i->first); - assert(i->first == i->second->playerID); - h & i->first; - h & i->second->dllName; - h & i->second->human; - i->second->saveGame(h, version); - } + const boost::filesystem::path aiPath = VCMIDirs::get().fullLibraryPath("AI", ps.name); + if(boost::filesystem::exists(aiPath)) + return ps.name; } + + return aiNameForPlayer(battleAI); } -void CClient::serialize(BinaryDeserializer & h, const int version) +std::string CClient::aiNameForPlayer(bool battleAI) { - assert(!h.saving); - h & hotSeat; - { - ui8 players = 0; //fix for uninitialized warning - h & players; + const int sensibleAILimit = settings["session"]["oneGoodAI"].Bool() ? 1 : PlayerColor::PLAYER_LIMIT_I; + std::string goodAI = battleAI ? settings["server"]["neutralAI"].String() : settings["server"]["playerAI"].String(); + std::string badAI = battleAI ? "StupidAI" : "EmptyAI"; - for(int i=0; i < players; i++) - { - std::string dllname; - PlayerColor pid; - bool isHuman = false; + //TODO what about human players + if(battleints.size() >= sensibleAILimit) + return badAI; - h & pid; - h & dllname; - h & isHuman; - LOG_TRACE_PARAMS(logGlobal, "Loading player %s interface", pid); - - std::shared_ptr nInt; - if(dllname.length()) - { - if(pid == PlayerColor::NEUTRAL) - { - installNewBattleInterface(CDynLibHandler::getNewBattleAI(dllname), pid); - //TODO? consider serialization - continue; - } - else - { - assert(!isHuman); - nInt = CDynLibHandler::getNewAI(dllname); - } - } - else - { - assert(isHuman); - nInt = std::make_shared(pid); - } - - nInt->dllName = dllname; - nInt->human = isHuman; - nInt->playerID = pid; - - installNewPlayerInterface(nInt, pid); - nInt->loadGame(h, version); //another evil cast, check above - } - - if(!vstd::contains(battleints, PlayerColor::NEUTRAL)) - loadNeutralBattleAI(); - } -} - -void CClient::serialize(BinarySerializer & h, const int version, const std::set & playerIDs) -{ - assert(h.saving); - h & hotSeat; - { - ui8 players = playerint.size(); - h & players; - - for(auto i = playerint.begin(); i != playerint.end(); i++) - { - LOG_TRACE_PARAMS(logGlobal, "Saving player %s interface", i->first); - assert(i->first == i->second->playerID); - h & i->first; - h & i->second->dllName; - h & i->second->human; - i->second->saveGame(h, version); - } - } -} - -void CClient::serialize(BinaryDeserializer & h, const int version, const std::set & playerIDs) -{ - assert(!h.saving); - h & hotSeat; - { - ui8 players = 0; //fix for uninitialized warning - h & players; - - for(int i=0; i < players; i++) - { - std::string dllname; - PlayerColor pid; - bool isHuman = false; - - h & pid; - h & dllname; - h & isHuman; - LOG_TRACE_PARAMS(logGlobal, "Loading player %s interface", pid); - - std::shared_ptr nInt; - if(dllname.length()) - { - if(pid == PlayerColor::NEUTRAL) - { - if(playerIDs.count(pid)) - installNewBattleInterface(CDynLibHandler::getNewBattleAI(dllname), pid); - //TODO? consider serialization - continue; - } - else - { - assert(!isHuman); - nInt = CDynLibHandler::getNewAI(dllname); - } - } - else - { - assert(isHuman); - nInt = std::make_shared(pid); - } - - nInt->dllName = dllname; - nInt->human = isHuman; - nInt->playerID = pid; - - nInt->loadGame(h, version); - if(settings["session"]["onlyai"].Bool() && isHuman) - { - removeGUI(); - nInt.reset(); - dllname = aiNameForPlayer(false); - nInt = CDynLibHandler::getNewAI(dllname); - nInt->dllName = dllname; - nInt->human = false; - nInt->playerID = pid; - installNewPlayerInterface(nInt, pid); - GH.totalRedraw(); - } - else - { - if(playerIDs.count(pid)) - installNewPlayerInterface(nInt, pid); - } - } - if(settings["session"]["spectate"].Bool()) - { - removeGUI(); - auto p = std::make_shared(PlayerColor::SPECTATOR); - installNewPlayerInterface(p, PlayerColor::SPECTATOR, true); - GH.curInt = p.get(); - LOCPLINT->activateForSpectator(); - GH.totalRedraw(); - } - - if(playerIDs.count(PlayerColor::NEUTRAL)) - loadNeutralBattleAI(); - } -} - -void CClient::handlePack( CPack * pack ) -{ - if(pack == nullptr) - { - logNetwork->error("Dropping nullptr CPack! You should check whether client and server ABI matches."); - return; - } - CBaseForCLApply *apply = applier->getApplier(typeList.getTypeID(pack)); //find the applier - if(apply) - { - boost::unique_lock guiLock(*CPlayerInterface::pim); - apply->applyOnClBefore(this, pack); - logNetwork->trace("\tMade first apply on cl"); - gs->apply(pack); - logNetwork->trace("\tApplied on gs"); - apply->applyOnClAfter(this, pack); - logNetwork->trace("\tMade second apply on cl"); - } - else - { - logNetwork->error("Message %s cannot be applied, cannot find applier!", typeList.getTypeInfo(pack)->name()); - } - delete pack; -} - -void CClient::finishCampaign( std::shared_ptr camp ) -{ -} - -void CClient::proposeNextMission(std::shared_ptr camp) -{ - GH.pushInt(new CBonusSelection(camp)); -} - -void CClient::stopConnection() -{ - terminate = true; - - if(serv) - { - boost::unique_lock(*serv->wmx); - if(serv->isHost()) //request closing connection - { - logNetwork->info("Connection has been requested to be closed."); - CloseServer close_server; - sendRequest(&close_server, PlayerColor::NEUTRAL); - logNetwork->info("Sent closing signal to the server"); - } - else - { - LeaveGame leave_Game; - sendRequest(&leave_Game, PlayerColor::NEUTRAL); - logNetwork->info("Sent leaving signal to the server"); - } - } - - { - TLockGuard _(connectionHandlerMutex); - if(connectionHandler)//end connection handler - { - if(connectionHandler->get_id() != boost::this_thread::get_id()) - connectionHandler->join(); - - logNetwork->info("Connection handler thread joined"); - connectionHandler.reset(); - } - } - - - if (serv) //and delete connection - { - serv->close(); - vstd::clear_pointer(serv); - logNetwork->warn("Our socket has been closed."); - } -} - -void CClient::battleStarted(const BattleInfo * info) -{ - for(auto &battleCb : battleCallbacks) - { - if(vstd::contains_if(info->sides, [&](const SideInBattle& side) {return side.color == battleCb.first; }) - || battleCb.first >= PlayerColor::PLAYER_LIMIT) - { - battleCb.second->setBattle(info); - } - } -// for(ui8 side : info->sides) -// if(battleCallbacks.count(side)) -// battleCallbacks[side]->setBattle(info); - - std::shared_ptr att, def; - auto &leftSide = info->sides[0], &rightSide = info->sides[1]; - - - //If quick combat is not, do not prepare interfaces for battleint - if(!settings["adventure"]["quickCombat"].Bool()) - { - if(vstd::contains(playerint, leftSide.color) && playerint[leftSide.color]->human) - att = std::dynamic_pointer_cast( playerint[leftSide.color] ); - - if(vstd::contains(playerint, rightSide.color) && playerint[rightSide.color]->human) - def = std::dynamic_pointer_cast( playerint[rightSide.color] ); - } - - if(!settings["session"]["headless"].Bool()) - { - if(!!att || !!def) - { - boost::unique_lock un(*CPlayerInterface::pim); - auto bi = new CBattleInterface(leftSide.armyObject, rightSide.armyObject, leftSide.hero, rightSide.hero, - Rect((screen->w - 800)/2, - (screen->h - 600)/2, 800, 600), att, def); - - GH.pushInt(bi); - } - else if(settings["session"]["spectate"].Bool() && !settings["session"]["spectate-skip-battle"].Bool()) - { - //TODO: This certainly need improvement - auto spectratorInt = std::dynamic_pointer_cast(playerint[PlayerColor::SPECTATOR]); - spectratorInt->cb->setBattle(info); - boost::unique_lock un(*CPlayerInterface::pim); - auto bi = new CBattleInterface(leftSide.armyObject, rightSide.armyObject, leftSide.hero, rightSide.hero, - Rect((screen->w - 800)/2, - (screen->h - 600)/2, 800, 600), att, def, spectratorInt); - - GH.pushInt(bi); - } - } - - auto callBattleStart = [&](PlayerColor color, ui8 side){ - if(vstd::contains(battleints, color)) - battleints[color]->battleStart(leftSide.armyObject, rightSide.armyObject, info->tile, leftSide.hero, rightSide.hero, side); - }; - - callBattleStart(leftSide.color, 0); - callBattleStart(rightSide.color, 1); - callBattleStart(PlayerColor::UNFLAGGABLE, 1); - if(settings["session"]["spectate"].Bool() && !settings["session"]["spectate-skip-battle"].Bool()) - callBattleStart(PlayerColor::SPECTATOR, 1); - - if(info->tacticDistance && vstd::contains(battleints,info->sides[info->tacticsSide].color)) - { - boost::thread(&CClient::commenceTacticPhaseForInt, this, battleints[info->sides[info->tacticsSide].color]); - } -} - -void CClient::battleFinished() -{ - stopAllBattleActions(); - for(auto & side : gs->curB->sides) - if(battleCallbacks.count(side.color)) - battleCallbacks[side.color]->setBattle(nullptr); - - if(settings["session"]["spectate"].Bool() && !settings["session"]["spectate-skip-battle"].Bool()) - battleCallbacks[PlayerColor::SPECTATOR]->setBattle(nullptr); -} - -void CClient::loadNeutralBattleAI() -{ - installNewBattleInterface(CDynLibHandler::getNewBattleAI(settings["server"]["neutralAI"].String()), PlayerColor::NEUTRAL); -} - -void CClient::commitPackage( CPackForClient *pack ) -{ - CommitPackage cp; - cp.freePack = false; - cp.packToCommit = pack; - sendRequest(&cp, PlayerColor::NEUTRAL); -} - -PlayerColor CClient::getLocalPlayer() const -{ - if(LOCPLINT) - return LOCPLINT->playerID; - return getCurrentPlayer(); -} - -void CClient::commenceTacticPhaseForInt(std::shared_ptr battleInt) -{ - setThreadName("CClient::commenceTacticPhaseForInt"); - try - { - battleInt->yourTacticPhase(gs->curB->tacticDistance); - if(gs && !!gs->curB && gs->curB->tacticDistance) //while awaiting for end of tactics phase, many things can happen (end of battle... or game) - { - MakeAction ma(BattleAction::makeEndOFTacticPhase(gs->curB->playerToSide(battleInt->playerID).get())); - sendRequest(&ma, battleInt->playerID); - } - } - catch(...) - { - handleException(); - } -} - -void CClient::invalidatePaths() -{ - // turn pathfinding info into invalid. It will be regenerated later - boost::unique_lock pathLock(pathInfo->pathMx); - pathInfo->hero = nullptr; -} - -const CPathsInfo * CClient::getPathsInfo(const CGHeroInstance *h) -{ - assert(h); - boost::unique_lock pathLock(pathInfo->pathMx); - if (pathInfo->hero != h) - { - gs->calculatePaths(h, *pathInfo.get()); - } - return pathInfo.get(); -} - -int CClient::sendRequest(const CPack *request, PlayerColor player) -{ - static ui32 requestCounter = 0; - - ui32 requestID = requestCounter++; - logNetwork->trace("Sending a request \"%s\". It'll have an ID=%d.", typeid(*request).name(), requestID); - - waitingRequest.pushBack(requestID); - serv->sendPackToServer(*request, player, requestID); - if(vstd::contains(playerint, player)) - playerint[player]->requestSent(dynamic_cast(request), requestID); - - return requestID; -} - -void CClient::campaignMapFinished( std::shared_ptr camp ) -{ - endGame(false); - - GH.curInt = CGPreGame::create(); - auto & epilogue = camp->camp->scenarios[camp->mapsConquered.back()].epilog; - auto finisher = [=]() - { - if(camp->mapsRemaining.size()) - proposeNextMission(camp); - else - finishCampaign(camp); - }; - if(epilogue.hasPrologEpilog) - { - GH.pushInt(new CPrologEpilogVideo(epilogue, finisher)); - } - else - { - finisher(); - } + return goodAI; } void CClient::installNewPlayerInterface(std::shared_ptr gameInterface, boost::optional color, bool battlecb) @@ -959,30 +417,148 @@ void CClient::installNewBattleInterface(std::shared_ptr ba } } -std::string CClient::aiNameForPlayer(const PlayerSettings &ps, bool battleAI) +void CClient::handlePack(CPack * pack) { - if(ps.name.size()) + CBaseForCLApply * apply = applier->getApplier(typeList.getTypeID(pack)); //find the applier + if(apply) { - const boost::filesystem::path aiPath = VCMIDirs::get().fullLibraryPath("AI", ps.name); - if (boost::filesystem::exists(aiPath)) - return ps.name; + boost::unique_lock guiLock(*CPlayerInterface::pim); + apply->applyOnClBefore(this, pack); + logNetwork->trace("\tMade first apply on cl: %s", typeList.getTypeInfo(pack)->name()); + gs->apply(pack); + logNetwork->trace("\tApplied on gs: %s", typeList.getTypeInfo(pack)->name()); + apply->applyOnClAfter(this, pack); + logNetwork->trace("\tMade second apply on cl: %s", typeList.getTypeInfo(pack)->name()); } - - return aiNameForPlayer(battleAI); + else + { + logNetwork->error("Message %s cannot be applied, cannot find applier!", typeList.getTypeInfo(pack)->name()); + } + delete pack; } -std::string CClient::aiNameForPlayer(bool battleAI) +void CClient::commitPackage(CPackForClient * pack) { - const int sensibleAILimit = settings["session"]["oneGoodAI"].Bool() ? 1 : PlayerColor::PLAYER_LIMIT_I; - std::string goodAI = battleAI ? settings["server"]["neutralAI"].String() : settings["server"]["playerAI"].String(); - std::string badAI = battleAI ? "StupidAI" : "EmptyAI"; + CommitPackage cp; + cp.freePack = false; + cp.packToCommit = pack; + sendRequest(&cp, PlayerColor::NEUTRAL); +} +int CClient::sendRequest(const CPackForServer * request, PlayerColor player) +{ + static ui32 requestCounter = 0; - //TODO what about human players - if(battleints.size() >= sensibleAILimit) - return badAI; + ui32 requestID = requestCounter++; + logNetwork->trace("Sending a request \"%s\". It'll have an ID=%d.", typeid(*request).name(), requestID); - return goodAI; + waitingRequest.pushBack(requestID); + request->requestID = requestID; + request->player = player; + CSH->c->sendPack(request); + if(vstd::contains(playerint, player)) + playerint[player]->requestSent(request, requestID); + + return requestID; +} + +void CClient::battleStarted(const BattleInfo * info) +{ + for(auto & battleCb : battleCallbacks) + { + if(vstd::contains_if(info->sides, [&](const SideInBattle& side) {return side.color == battleCb.first; }) + || battleCb.first >= PlayerColor::PLAYER_LIMIT) + { + battleCb.second->setBattle(info); + } + } +// for(ui8 side : info->sides) +// if(battleCallbacks.count(side)) +// battleCallbacks[side]->setBattle(info); + + std::shared_ptr att, def; + auto & leftSide = info->sides[0], & rightSide = info->sides[1]; + + //If quick combat is not, do not prepare interfaces for battleint + if(!settings["adventure"]["quickCombat"].Bool()) + { + if(vstd::contains(playerint, leftSide.color) && playerint[leftSide.color]->human) + att = std::dynamic_pointer_cast(playerint[leftSide.color]); + + if(vstd::contains(playerint, rightSide.color) && playerint[rightSide.color]->human) + def = std::dynamic_pointer_cast(playerint[rightSide.color]); + } + + if(!settings["session"]["headless"].Bool()) + { + if(!!att || !!def) + { + boost::unique_lock un(*CPlayerInterface::pim); + auto bi = new CBattleInterface(leftSide.armyObject, rightSide.armyObject, leftSide.hero, rightSide.hero, + Rect((screen->w - 800)/2, + (screen->h - 600)/2, 800, 600), att, def); + + GH.pushInt(bi); + } + else if(settings["session"]["spectate"].Bool() && !settings["session"]["spectate-skip-battle"].Bool()) + { + //TODO: This certainly need improvement + auto spectratorInt = std::dynamic_pointer_cast(playerint[PlayerColor::SPECTATOR]); + spectratorInt->cb->setBattle(info); + boost::unique_lock un(*CPlayerInterface::pim); + auto bi = new CBattleInterface(leftSide.armyObject, rightSide.armyObject, leftSide.hero, rightSide.hero, + Rect((screen->w - 800) / 2, + (screen->h - 600) / 2, 800, 600), att, def, spectratorInt); + + GH.pushInt(bi); + } + } + + auto callBattleStart = [&](PlayerColor color, ui8 side) + { + if(vstd::contains(battleints, color)) + battleints[color]->battleStart(leftSide.armyObject, rightSide.armyObject, info->tile, leftSide.hero, rightSide.hero, side); + }; + + callBattleStart(leftSide.color, 0); + callBattleStart(rightSide.color, 1); + callBattleStart(PlayerColor::UNFLAGGABLE, 1); + if(settings["session"]["spectate"].Bool() && !settings["session"]["spectate-skip-battle"].Bool()) + callBattleStart(PlayerColor::SPECTATOR, 1); + + if(info->tacticDistance && vstd::contains(battleints, info->sides[info->tacticsSide].color)) + { + boost::thread(&CClient::commenceTacticPhaseForInt, this, battleints[info->sides[info->tacticsSide].color]); + } +} + +void CClient::commenceTacticPhaseForInt(std::shared_ptr battleInt) +{ + setThreadName("CClient::commenceTacticPhaseForInt"); + try + { + battleInt->yourTacticPhase(gs->curB->tacticDistance); + if(gs && !!gs->curB && gs->curB->tacticDistance) //while awaiting for end of tactics phase, many things can happen (end of battle... or game) + { + MakeAction ma(BattleAction::makeEndOFTacticPhase(gs->curB->playerToSide(battleInt->playerID).get())); + sendRequest(&ma, battleInt->playerID); + } + } + catch(...) + { + handleException(); + } +} + +void CClient::battleFinished() +{ + stopAllBattleActions(); + for(auto & side : gs->curB->sides) + if(battleCallbacks.count(side.color)) + battleCallbacks[side.color]->setBattle(nullptr); + + if(settings["session"]["spectate"].Bool() && !settings["session"]["spectate-skip-battle"].Bool()) + battleCallbacks[PlayerColor::SPECTATOR]->setBattle(nullptr); } void CClient::startPlayerBattleAction(PlayerColor color) @@ -1017,170 +593,53 @@ void CClient::stopAllBattleActions() stopPlayerBattleAction(playerActionThreads.begin()->first); } -void CServerHandler::startServer() +void CClient::waitForMoveAndSend(PlayerColor color) { - if(settings["session"]["donotstartserver"].Bool()) - return; - - th.update(); - -#ifdef VCMI_ANDROID - CAndroidVMHelper envHelper; - envHelper.callStaticVoidMethod(CAndroidVMHelper::NATIVE_METHODS_DEFAULT_CLASS, "startServer", true); -#else - serverThread = new boost::thread(&CServerHandler::callServer, this); //runs server executable; -#endif - if(verbose) - logNetwork->info("Setting up thread calling server: %d ms", th.getDiff()); -} - -void CServerHandler::waitForServer() -{ - if(settings["session"]["donotstartserver"].Bool()) - return; - - if(!serverThread) - startServer(); - - th.update(); - -#ifndef VCMI_ANDROID - if(shared) - shared->sr->waitTillReady(); -#else - logNetwork->info("waiting for server"); - while (!androidTestServerReadyFlag.load()) - { - logNetwork->info("still waiting..."); - boost::this_thread::sleep(boost::posix_time::milliseconds(1000)); - } - logNetwork->info("waiting for server finished..."); - androidTestServerReadyFlag = false; -#endif - if(verbose) - logNetwork->info("Waiting for server: %d ms", th.getDiff()); -} - -CConnection * CServerHandler::connectToServer() -{ - waitForServer(); - - th.update(); //put breakpoint here to attach to server before it does something stupid - -#ifndef VCMI_ANDROID - CConnection *ret = justConnectToServer(settings["server"]["server"].String(), shared ? shared->sr->port : 0); -#else - CConnection *ret = justConnectToServer(settings["server"]["server"].String()); -#endif - - if(verbose) - logNetwork->info("\tConnecting to the server: %d ms", th.getDiff()); - - return ret; -} - -ui16 CServerHandler::getDefaultPort() -{ - if(settings["session"]["serverport"].Integer()) - return settings["session"]["serverport"].Integer(); - else - return settings["server"]["port"].Integer(); -} - -std::string CServerHandler::getDefaultPortStr() -{ - return boost::lexical_cast(getDefaultPort()); -} - -CServerHandler::CServerHandler(bool runServer) -{ - serverThread = nullptr; - shared = nullptr; - verbose = true; - uuid = boost::uuids::to_string(boost::uuids::random_generator()()); - -#ifndef VCMI_ANDROID - if(settings["session"]["donotstartserver"].Bool() || settings["session"]["disable-shm"].Bool()) - return; - - std::string sharedMemoryName = "vcmi_memory"; - if(settings["session"]["enable-shm-uuid"].Bool()) - { - //used or automated testing when multiple clients start simultaneously - sharedMemoryName += "_" + uuid; - } try { - shared = new SharedMemory(sharedMemoryName, true); + setThreadName("CClient::waitForMoveAndSend"); + assert(vstd::contains(battleints, color)); + BattleAction ba = battleints[color]->activeStack(gs->curB->battleGetStackByID(gs->curB->activeStack, false)); + if(ba.actionType != EActionType::CANCEL) + { + logNetwork->trace("Send battle action to server: %s", ba.toString()); + MakeAction temp_action(ba); + sendRequest(&temp_action, color); + } + } + catch(boost::thread_interrupted &) + { + logNetwork->debug("Wait for move thread was interrupted and no action will be send. Was a battle ended by spell?"); } catch(...) { - vstd::clear_pointer(shared); - logNetwork->error("Cannot open interprocess memory. Continue without it..."); + handleException(); } -#endif } -CServerHandler::~CServerHandler() +void CClient::invalidatePaths() { - delete shared; - delete serverThread; //detaches, not kills thread + // turn pathfinding info into invalid. It will be regenerated later + boost::unique_lock pathLock(pathInfo->pathMx); + pathInfo->hero = nullptr; } -void CServerHandler::callServer() +const CPathsInfo * CClient::getPathsInfo(const CGHeroInstance * h) { -#ifndef VCMI_ANDROID - setThreadName("CServerHandler::callServer"); - const std::string logName = (VCMIDirs::get().userCachePath() / "server_log.txt").string(); - std::string comm = VCMIDirs::get().serverPath().string() - + " --port=" + getDefaultPortStr() - + " --run-by-client" - + " --uuid=" + uuid; - if(shared) + assert(h); + boost::unique_lock pathLock(pathInfo->pathMx); + if(pathInfo->hero != h) { - comm += " --enable-shm"; - if(settings["session"]["enable-shm-uuid"].Bool()) - comm += " --enable-shm-uuid"; + gs->calculatePaths(h, *pathInfo.get()); } - comm += " > \"" + logName + '\"'; - - int result = std::system(comm.c_str()); - if (result == 0) - { - logNetwork->info("Server closed correctly"); - serverAlive.setn(false); - } - else - { - logNetwork->error("Error: server failed to close correctly or crashed!"); - logNetwork->error("Check %s for more info", logName); - serverAlive.setn(false); - // TODO: make client return to main menu if server actually crashed during game. -// exit(1);// exit in case of error. Othervice without working server VCMI will hang - } -#endif + return pathInfo.get(); } -CConnection * CServerHandler::justConnectToServer(const std::string &host, const ui16 port) +PlayerColor CClient::getLocalPlayer() const { - CConnection *ret = nullptr; - while(!ret) - { - try - { - logNetwork->info("Establishing connection..."); - ret = new CConnection( host.size() ? host : settings["server"]["server"].String(), - port ? port : getDefaultPort(), - NAME); - ret->connectionID = 1; // TODO: Refactoring for the server so IDs set outside of CConnection - } - catch(...) - { - logNetwork->error("\nCannot establish connection! Retrying within 2 seconds"); - SDL_Delay(2000); - } - } - return ret; + if(LOCPLINT) + return LOCPLINT->playerID; + return getCurrentPlayer(); } #ifdef VCMI_ANDROID diff --git a/client/Client.h b/client/Client.h index 0d7ee709e..cf1b6147a 100644 --- a/client/Client.h +++ b/client/Client.h @@ -9,25 +9,24 @@ */ #pragma once - #include "../lib/IGameCallback.h" #include "../lib/battle/BattleAction.h" #include "../lib/CStopWatch.h" #include "../lib/int3.h" +#include "../lib/CondSh.h" +#include "../lib/CPathfinder.h" struct CPack; +struct CPackForServer; class CCampaignState; class CBattleCallback; class IGameEventsReceiver; class IBattleEventsReceiver; class CBattleGameInterface; -struct StartInfo; class CGameState; class CGameInterface; -class CConnection; class CCallback; -class BattleAction; -struct SharedMemory; +struct BattleAction; class CClient; class CScriptingModule; struct CPathsInfo; @@ -35,31 +34,8 @@ class BinaryDeserializer; class BinarySerializer; namespace boost { class thread; } -/// structure to handle running server and connecting to it -class CServerHandler -{ -private: - void callServer(); //calls server via system(), should be called as thread -public: - CStopWatch th; - boost::thread *serverThread; //thread that called system to run server - SharedMemory * shared; - std::string uuid; - bool verbose; //whether to print log msgs - - //functions setting up local server - void startServer(); //creates a thread with callServer - void waitForServer(); //waits till server is ready - CConnection * connectToServer(); //connects to server - - ////////////////////////////////////////////////////////////////////////// - static CConnection * justConnectToServer(const std::string &host = "", const ui16 port = 0); //connects to given host without taking any other actions (like setting up server) - static ui16 getDefaultPort(); - static std::string getDefaultPortStr(); - - CServerHandler(bool runServer = false); - virtual ~CServerHandler(); -}; +template class CApplier; +class CBaseForCLApply; template class ThreadSafeVector @@ -78,7 +54,7 @@ public: cond.notify_all(); } - void pushBack(const T &item) + void pushBack(const T & item) { TLock lock(mx); items.push_back(item); @@ -97,14 +73,14 @@ public: return TLock(mx); } - void waitWhileContains(const T &item) + void waitWhileContains(const T & item) { auto lock = getLock(); while(vstd::contains(items, item)) cond.wait(lock); } - bool tryRemovingElement(const T&item) //returns false if element was not present + bool tryRemovingElement(const T & item) //returns false if element was not present { auto lock = getLock(); auto itr = vstd::find(items, item); @@ -122,105 +98,104 @@ public: /// Class which handles client - server logic class CClient : public IGameCallback { + std::shared_ptr> applier; std::unique_ptr pathInfo; std::map> playerActionThreads; + void waitForMoveAndSend(PlayerColor color); + public: - std::map > callbacks; //callbacks given to player interfaces - std::map > battleCallbacks; //callbacks given to player interfaces + std::map> callbacks; //callbacks given to player interfaces + std::map> battleCallbacks; //callbacks given to player interfaces std::vector> privilegedGameEventReceivers; //scripting modules, spectator interfaces std::vector> privilegedBattleEventReceivers; //scripting modules, spectator interfaces std::map> playerint; std::map> battleints; - std::map>> additionalPlayerInts; - std::map>> additionalBattleInts; - - bool hotSeat; - CConnection *serv; + std::map>> additionalPlayerInts; + std::map>> additionalBattleInts; boost::optional curbaction; - CScriptingModule *erm; - - static ThreadSafeVector waitingRequest;//FIXME: make this normal field (need to join all threads before client destruction) - - - //void sendRequest(const CPackForServer *request, bool waitForRealization); + CScriptingModule * erm; CClient(); - CClient(CConnection *con, StartInfo *si); - ~CClient(); - void init(); - void newGame(CConnection *con, StartInfo *si); //con - connection to server + void newGame(); + void loadGame(); + void serialize(BinarySerializer & h, const int version); + void serialize(BinaryDeserializer & h, const int version); - void loadNeutralBattleAI(); + void save(const std::string & fname); + void endGame(); + + void initMapHandler(); + void initPlayerInterfaces(); + std::string aiNameForPlayer(const PlayerSettings & ps, bool battleAI); //empty means no AI -> human + std::string aiNameForPlayer(bool battleAI); void installNewPlayerInterface(std::shared_ptr gameInterface, boost::optional color, bool battlecb = false); void installNewBattleInterface(std::shared_ptr battleInterface, boost::optional color, bool needCallback = true); - std::string aiNameForPlayer(const PlayerSettings &ps, bool battleAI); //empty means no AI -> human - std::string aiNameForPlayer(bool battleAI); - void endGame(bool closeConnection = true); - void stopConnection(); - void save(const std::string & fname); - void loadGame(const std::string & fname, const bool server = true, const std::vector& humanplayerindices = std::vector(), const int loadnumplayers = 1, int player_ = -1, const std::string & ipaddr = "", const ui16 port = 0); - void run(); - void campaignMapFinished( std::shared_ptr camp ); - void finishCampaign( std::shared_ptr camp ); - void proposeNextMission(std::shared_ptr camp); + + static ThreadSafeVector waitingRequest; //FIXME: make this normal field (need to join all threads before client destruction) + + void handlePack(CPack * pack); //applies the given pack and deletes it + void commitPackage(CPackForClient * pack) override; + int sendRequest(const CPackForServer * request, PlayerColor player); //returns ID given to that request + + void battleStarted(const BattleInfo * info); + void commenceTacticPhaseForInt(std::shared_ptr battleInt); //will be called as separate thread + void battleFinished(); + void startPlayerBattleAction(PlayerColor color); + void stopPlayerBattleAction(PlayerColor color); + void stopAllBattleActions(); void invalidatePaths(); - const CPathsInfo * getPathsInfo(const CGHeroInstance *h); - - bool terminate; // tell to terminate - std::unique_ptr connectionHandler; //thread running run() method - boost::mutex connectionHandlerMutex; - - ////////////////////////////////////////////////////////////////////////// + const CPathsInfo * getPathsInfo(const CGHeroInstance * h); virtual PlayerColor getLocalPlayer() const override; - ////////////////////////////////////////////////////////////////////////// - //not working yet, will be implement somewhen later with support for local-sim-based gameplay - void changeSpells(const CGHeroInstance * hero, bool give, const std::set &spells) override {}; + friend class CCallback; //handling players actions + friend class CBattleCallback; //handling players actions + + void changeSpells(const CGHeroInstance * hero, bool give, const std::set & spells) override {}; bool removeObject(const CGObjectInstance * obj) override {return false;}; void setBlockVis(ObjectInstanceID objid, bool bv) override {}; void setOwner(const CGObjectInstance * obj, PlayerColor owner) override {}; - void changePrimSkill(const CGHeroInstance * hero, PrimarySkill::PrimarySkill which, si64 val, bool abs=false) override {}; - void changeSecSkill(const CGHeroInstance * hero, SecondarySkill which, int val, bool abs=false) override {}; + void changePrimSkill(const CGHeroInstance * hero, PrimarySkill::PrimarySkill which, si64 val, bool abs = false) override {}; + void changeSecSkill(const CGHeroInstance * hero, SecondarySkill which, int val, bool abs = false) override {}; - void showBlockingDialog(BlockingDialog *iw) override {}; + void showBlockingDialog(BlockingDialog * iw) override {}; void showGarrisonDialog(ObjectInstanceID upobj, ObjectInstanceID hid, bool removableUnits) override {}; - void showTeleportDialog(TeleportDialog *iw) override {}; + void showTeleportDialog(TeleportDialog * iw) override {}; void showThievesGuildWindow(PlayerColor player, ObjectInstanceID requestingObjId) override {}; void giveResource(PlayerColor player, Res::ERes which, int val) override {}; virtual void giveResources(PlayerColor player, TResources resources) override {}; - void giveCreatures(const CArmedInstance * objid, const CGHeroInstance * h, const CCreatureSet &creatures, bool remove) override {}; - void takeCreatures(ObjectInstanceID objid, const std::vector &creatures) override {}; - bool changeStackType(const StackLocation &sl, const CCreature *c) override {return false;}; - bool changeStackCount(const StackLocation &sl, TQuantity count, bool absoluteValue = false) override {return false;}; - bool insertNewStack(const StackLocation &sl, const CCreature *c, TQuantity count) override {return false;}; - bool eraseStack(const StackLocation &sl, bool forceRemoval = false) override{return false;}; - bool swapStacks(const StackLocation &sl1, const StackLocation &sl2) override {return false;} - bool addToSlot(const StackLocation &sl, const CCreature *c, TQuantity count) override {return false;} - void tryJoiningArmy(const CArmedInstance *src, const CArmedInstance *dst, bool removeObjWhenFinished, bool allowMerging) override {} - bool moveStack(const StackLocation &src, const StackLocation &dst, TQuantity count = -1) override {return false;} + void giveCreatures(const CArmedInstance * objid, const CGHeroInstance * h, const CCreatureSet & creatures, bool remove) override {}; + void takeCreatures(ObjectInstanceID objid, const std::vector & creatures) override {}; + bool changeStackType(const StackLocation & sl, const CCreature * c) override {return false;}; + bool changeStackCount(const StackLocation & sl, TQuantity count, bool absoluteValue = false) override {return false;}; + bool insertNewStack(const StackLocation & sl, const CCreature * c, TQuantity count) override {return false;}; + bool eraseStack(const StackLocation & sl, bool forceRemoval = false) override {return false;}; + bool swapStacks(const StackLocation & sl1, const StackLocation & sl2) override {return false;} + bool addToSlot(const StackLocation & sl, const CCreature * c, TQuantity count) override {return false;} + void tryJoiningArmy(const CArmedInstance * src, const CArmedInstance * dst, bool removeObjWhenFinished, bool allowMerging) override {} + bool moveStack(const StackLocation & src, const StackLocation & dst, TQuantity count = -1) override {return false;} - void removeAfterVisit(const CGObjectInstance *object) override {}; + void removeAfterVisit(const CGObjectInstance * object) override {}; - void giveHeroNewArtifact(const CGHeroInstance *h, const CArtifact *artType, ArtifactPosition pos) override {}; - void giveHeroArtifact(const CGHeroInstance *h, const CArtifactInstance *a, ArtifactPosition pos) override {}; - void putArtifact(const ArtifactLocation &al, const CArtifactInstance *a) override {}; - void removeArtifact(const ArtifactLocation &al) override {}; - bool moveArtifact(const ArtifactLocation &al1, const ArtifactLocation &al2) override {return false;}; + void giveHeroNewArtifact(const CGHeroInstance * h, const CArtifact * artType, ArtifactPosition pos) override {}; + void giveHeroArtifact(const CGHeroInstance * h, const CArtifactInstance * a, ArtifactPosition pos) override {}; + void putArtifact(const ArtifactLocation & al, const CArtifactInstance * a) override {}; + void removeArtifact(const ArtifactLocation & al) override {}; + bool moveArtifact(const ArtifactLocation & al1, const ArtifactLocation & al2) override {return false;}; void synchronizeArtifactHandlerLists() override {}; void showCompInfo(ShowInInfobox * comp) override {}; void heroVisitCastle(const CGTownInstance * obj, const CGHeroInstance * hero) override {}; void stopHeroVisitCastle(const CGTownInstance * obj, const CGHeroInstance * hero) override {}; - void startBattlePrimary(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool creatureBank = false, const CGTownInstance *town = nullptr) override {}; //use hero=nullptr for no hero - void startBattleI(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, bool creatureBank = false) override {}; //if any of armies is hero, hero will be used - void startBattleI(const CArmedInstance *army1, const CArmedInstance *army2, bool creatureBank = false) override {}; //if any of armies is hero, hero will be used, visitable tile of second obj is place of battle + void startBattlePrimary(const CArmedInstance * army1, const CArmedInstance * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool creatureBank = false, const CGTownInstance * town = nullptr) override {}; //use hero=nullptr for no hero + void startBattleI(const CArmedInstance * army1, const CArmedInstance * army2, int3 tile, bool creatureBank = false) override {}; //if any of armies is hero, hero will be used + void startBattleI(const CArmedInstance * army1, const CArmedInstance * army2, bool creatureBank = false) override {}; //if any of armies is hero, hero will be used, visitable tile of second obj is place of battle void setAmount(ObjectInstanceID objid, ui32 val) override {}; bool moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, bool transit = false, PlayerColor asker = PlayerColor::NEUTRAL) override {return false;}; void giveHeroBonus(GiveBonus * bonus) override {}; @@ -232,33 +207,5 @@ public: void heroExchange(ObjectInstanceID hero1, ObjectInstanceID hero2) override {}; void changeFogOfWar(int3 center, ui32 radius, PlayerColor player, bool hide) override {} - void changeFogOfWar(std::unordered_set &tiles, PlayerColor player, bool hide) override {} - - ////////////////////////////////////////////////////////////////////////// - friend class CCallback; //handling players actions - friend class CBattleCallback; //handling players actions - - int sendRequest(const CPack *request, PlayerColor player); //returns ID given to that request - - void handlePack( CPack * pack ); //applies the given pack and deletes it - void battleStarted(const BattleInfo * info); - void commenceTacticPhaseForInt(std::shared_ptr battleInt); //will be called as separate thread - - void commitPackage(CPackForClient *pack) override; - - ////////////////////////////////////////////////////////////////////////// - - void serialize(BinarySerializer & h, const int version); - void serialize(BinaryDeserializer & h, const int version); - - void serialize(BinarySerializer & h, const int version, const std::set& playerIDs); - void serialize(BinaryDeserializer & h, const int version, const std::set& playerIDs); - void battleFinished(); - - void startPlayerBattleAction(PlayerColor color); - - void stopPlayerBattleAction(PlayerColor color); - void stopAllBattleActions(); -private: - void waitForMoveAndSend(PlayerColor color); + void changeFogOfWar(std::unordered_set & tiles, PlayerColor player, bool hide) override {} }; diff --git a/client/NetPacksClient.cpp b/client/NetPacksClient.cpp index 7d92cd4f6..b3a8ac3c3 100644 --- a/client/NetPacksClient.cpp +++ b/client/NetPacksClient.cpp @@ -41,6 +41,7 @@ #include "widgets/MiscWidgets.h" #include "widgets/AdventureMapClasses.h" #include "CMT.h" +#include "CServerHandler.h" // TODO: as Tow suggested these template should all be part of CClient // This will require rework spectator interface properly though @@ -357,17 +358,6 @@ void RemoveBonus::applyCl(CClient *cl) } } -void UpdateCampaignState::applyCl(CClient *cl) -{ - cl->stopConnection(); - cl->campaignMapFinished(camp); -} - -void PrepareForAdvancingCampaign::applyCl(CClient *cl) -{ - cl->serv->prepareForSendingHeroes(); -} - void RemoveObject::applyFirstCl(CClient *cl) { const CGObjectInstance *o = cl->getObj(id); @@ -757,7 +747,7 @@ void PackageApplied::applyCl(CClient *cl) { callInterfaceIfPresent(cl, player, &IGameEventsReceiver::requestRealized, this); if(!CClient::waitingRequest.tryRemovingElement(requestID)) - logNetwork->warn("Surprising server message!"); + logNetwork->warn("Surprising server message! PackageApplied for unknown requestID!"); } void SystemMessage::applyCl(CClient *cl) @@ -777,11 +767,13 @@ void PlayerBlocked::applyCl(CClient *cl) void YourTurn::applyCl(CClient *cl) { + logNetwork->debug("Server gives turn to %s", player.getStr()); + callAllInterfaces(cl, &IGameEventsReceiver::playerStartsTurn, player); callOnlyThatInterface(cl, player, &CGameInterface::yourTurn); } -void SaveGame::applyCl(CClient *cl) +void SaveGameClient::applyCl(CClient *cl) { const auto stem = FileInfo::GetPathStem(fname); CResourceHandler::get("local")->createResource(stem.to_string() + ".vcgm1"); @@ -798,7 +790,7 @@ void SaveGame::applyCl(CClient *cl) } } -void PlayerMessage::applyCl(CClient *cl) +void PlayerMessageClient::applyCl(CClient *cl) { logNetwork->debug("Player %s sends a message: %s", player.getStr(), text); diff --git a/client/NetPacksLobbyClient.cpp b/client/NetPacksLobbyClient.cpp new file mode 100644 index 000000000..a2351bb11 --- /dev/null +++ b/client/NetPacksLobbyClient.cpp @@ -0,0 +1,142 @@ +/* + * NetPacksLobbyClient.cpp, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#include "StdInc.h" + +#include "lobby/CSelectionBase.h" +#include "lobby/CLobbyScreen.h" + +#include "lobby/OptionsTab.h" +#include "lobby/RandomMapTab.h" +#include "lobby/SelectionTab.h" +#include "lobby/CBonusSelection.h" + +#include "CServerHandler.h" +#include "CGameInfo.h" +#include "gui/CGuiHandler.h" +#include "widgets/Buttons.h" +#include "widgets/TextControls.h" + +#include "../lib/CConfigHandler.h" +#include "../lib/CGeneralTextHandler.h" +#include "../lib/NetPacksLobby.h" +#include "../lib/serializer/Connection.h" + +bool LobbyClientConnected::applyOnLobbyHandler(CServerHandler * handler) +{ + // Check if it's LobbyClientConnected for our client + if(uuid == handler->c->uuid) + { + handler->c->connectionID = clientId; + if(!settings["session"]["headless"].Bool()) + GH.pushInt(new CLobbyScreen(static_cast(handler->screenType))); + handler->state = EClientState::LOBBY; + return true; + } + return false; +} + +void LobbyClientConnected::applyOnLobbyScreen(CLobbyScreen * lobby, CServerHandler * handler) +{ + if(uuid == handler->c->uuid) + { + } +} + +bool LobbyClientDisconnected::applyOnLobbyHandler(CServerHandler * handler) +{ + if(clientId != c->connectionID) + return false; + + handler->stopServerConnection(); + return true; +} + +void LobbyClientDisconnected::applyOnLobbyScreen(CLobbyScreen * lobby, CServerHandler * handler) +{ + GH.popIntTotally(lobby); +} + +void LobbyChatMessage::applyOnLobbyScreen(CLobbyScreen * lobby, CServerHandler * handler) +{ + if(lobby) + { + lobby->card->chat->addNewMessage(playerName + ": " + message); + lobby->card->setChat(true); + if(lobby->buttonChat) + lobby->buttonChat->addTextOverlay(CGI->generaltexth->allTexts[531], FONT_SMALL); + } +} + +void LobbyGuiAction::applyOnLobbyScreen(CLobbyScreen * lobby, CServerHandler * handler) +{ + if(!handler->isGuest()) + return; + + switch(action) + { + case NO_TAB: + lobby->toggleTab(lobby->curTab); + break; + case OPEN_OPTIONS: + lobby->toggleTab(lobby->tabOpt); + break; + case OPEN_SCENARIO_LIST: + lobby->toggleTab(lobby->tabSel); + break; + case OPEN_RANDOM_MAP_OPTIONS: + lobby->toggleTab(lobby->tabRand); + break; + } +} + +bool LobbyStartGame::applyOnLobbyHandler(CServerHandler * handler) +{ + if(handler->state == EClientState::GAMEPLAY) + { + handler->endGameplay(false); + } + handler->state = EClientState::STARTING; + if(handler->si->mode != StartInfo::LOAD_GAME) + { + handler->si = initializedStartInfo; + } + if(settings["session"]["headless"].Bool()) + handler->startGameplay(); + return true; +} + +void LobbyStartGame::applyOnLobbyScreen(CLobbyScreen * lobby, CServerHandler * handler) +{ + CMM->showLoadingScreen(std::bind(&CServerHandler::startGameplay, handler)); +} + +bool LobbyUpdateState::applyOnLobbyHandler(CServerHandler * handler) +{ + hostChanged = state.hostClientId != handler->hostClientId; + static_cast(*handler) = state; + return true; +} + +void LobbyUpdateState::applyOnLobbyScreen(CLobbyScreen * lobby, CServerHandler * handler) +{ + if(!lobby->bonusSel && handler->si->campState && handler->state == EClientState::LOBBY_CAMPAIGN) + { + lobby->bonusSel = new CBonusSelection(); + GH.pushInt(lobby->bonusSel); + } + + if(lobby->bonusSel) + lobby->bonusSel->updateAfterStateChange(); + else + lobby->updateAfterStateChange(); + + if(hostChanged) + lobby->toggleMode(handler->isHost()); +} diff --git a/client/gui/CGuiHandler.cpp b/client/gui/CGuiHandler.cpp index 7af530ca2..7428a98c5 100644 --- a/client/gui/CGuiHandler.cpp +++ b/client/gui/CGuiHandler.cpp @@ -106,7 +106,7 @@ void CGuiHandler::popInt(IShowActivatable *top) listInt.front()->activate(); totalRedraw(); - pushSDLEvent(SDL_USEREVENT, INTERFACE_CHANGED); + pushSDLEvent(SDL_USEREVENT, EUserEvent::INTERFACE_CHANGED); } void CGuiHandler::popIntTotally(IShowActivatable *top) @@ -132,7 +132,7 @@ void CGuiHandler::pushInt(IShowActivatable *newInt) objsToBlit.push_back(newInt); totalRedraw(); - pushSDLEvent(SDL_USEREVENT, INTERFACE_CHANGED); + pushSDLEvent(SDL_USEREVENT, EUserEvent::INTERFACE_CHANGED); } void CGuiHandler::popInts(int howMany) @@ -155,7 +155,7 @@ void CGuiHandler::popInts(int howMany) } fakeMouseMove(); - pushSDLEvent(SDL_USEREVENT, INTERFACE_CHANGED); + pushSDLEvent(SDL_USEREVENT, EUserEvent::INTERFACE_CHANGED); } IShowActivatable * CGuiHandler::topInt() diff --git a/client/gui/CGuiHandler.h b/client/gui/CGuiHandler.h index 10cc47621..efe5d73c0 100644 --- a/client/gui/CGuiHandler.h +++ b/client/gui/CGuiHandler.h @@ -22,6 +22,20 @@ class IShowable; enum class EIntObjMouseBtnType; template struct CondSh; +// TODO: event handling need refactoring +enum EUserEvent +{ + /*CHANGE_SCREEN_RESOLUTION = 1,*/ + RETURN_TO_MAIN_MENU = 2, + //STOP_CLIENT = 3, + RESTART_GAME = 4, + RETURN_TO_MENU_LOAD, + FULLSCREEN_TOGGLED, + CAMPAIGN_START_SCENARIO, + FORCE_QUIT, //quit client without question + INTERFACE_CHANGED +}; + // A fps manager which holds game updates at a constant rate class CFramerateManager { @@ -119,11 +133,6 @@ public: extern CGuiHandler GH; //global gui handler -template void pushIntT() -{ - GH.pushInt(new T()); -} - struct SObjectConstruction { CIntObject *myObj; @@ -142,5 +151,6 @@ struct SSetCaptureState #define OBJ_CONSTRUCTION SObjectConstruction obj__i(this) #define OBJECT_CONSTRUCTION_CAPTURING(actions) defActions = actions; SSetCaptureState obj__i1(true, actions); SObjectConstruction obj__i(this) #define OBJ_CONSTRUCTION_CAPTURING_ALL defActions = 255; SSetCaptureState obj__i1(true, 255); SObjectConstruction obj__i(this) +#define OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE defActions = 255 - DISPOSE; SSetCaptureState obj__i1(true, 255 - DISPOSE); SObjectConstruction obj__i(this) #define BLOCK_CAPTURING SSetCaptureState obj__i(false, 0) #define BLOCK_CAPTURING_DONT_TOUCH_REC_ACTIONS SSetCaptureState obj__i(false, GH.defActionsDef) diff --git a/client/gui/SDL_Extensions.cpp b/client/gui/SDL_Extensions.cpp index 37b707650..96f81d237 100644 --- a/client/gui/SDL_Extensions.cpp +++ b/client/gui/SDL_Extensions.cpp @@ -20,6 +20,8 @@ const SDL_Color Colors::YELLOW = { 229, 215, 123, 0 }; const SDL_Color Colors::WHITE = { 255, 243, 222, 0 }; const SDL_Color Colors::METALLIC_GOLD = { 173, 142, 66, 0 }; const SDL_Color Colors::GREEN = { 0, 255, 0, 0 }; +const SDL_Color Colors::ORANGE = { 232, 184, 32, 0 }; +const SDL_Color Colors::BRIGHT_YELLOW = { 242, 226, 110, 0 }; const SDL_Color Colors::DEFAULT_KEY_COLOR = {0, 255, 255, 0}; void SDL_UpdateRect(SDL_Surface *surface, int x, int y, int w, int h) diff --git a/client/gui/SDL_Extensions.h b/client/gui/SDL_Extensions.h index 36b0380e5..019f070b5 100644 --- a/client/gui/SDL_Extensions.h +++ b/client/gui/SDL_Extensions.h @@ -89,6 +89,12 @@ public: /** green color used for in-game console */ static const SDL_Color GREEN; + /** the h3 orange color, used for blocked buttons */ + static const SDL_Color ORANGE; + + /** the h3 bright yellow color, used for selection border */ + static const SDL_Color BRIGHT_YELLOW; + /** default key color for all 8 & 24 bit graphics */ static const SDL_Color DEFAULT_KEY_COLOR; }; diff --git a/client/lobby/CBonusSelection.cpp b/client/lobby/CBonusSelection.cpp new file mode 100644 index 000000000..101d22e10 --- /dev/null +++ b/client/lobby/CBonusSelection.cpp @@ -0,0 +1,534 @@ +/* + * CBonusSelection.cpp, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#include "StdInc.h" + +#include "CBonusSelection.h" +#include "CSelectionBase.h" + +#include "../CGameInfo.h" +#include "../CMessage.h" +#include "../CBitmapHandler.h" +#include "../CMusicHandler.h" +#include "../CVideoHandler.h" +#include "../CPlayerInterface.h" +#include "../CServerHandler.h" +#include "../gui/CAnimation.h" +#include "../gui/CGuiHandler.h" +#include "../mainmenu/CMainMenu.h" +#include "../mainmenu/CPrologEpilogVideo.h" +#include "../widgets/CComponent.h" +#include "../widgets/Buttons.h" +#include "../widgets/MiscWidgets.h" +#include "../widgets/ObjectLists.h" +#include "../widgets/TextControls.h" +#include "../windows/GUIClasses.h" +#include "../windows/InfoWindows.h" + +#include "../../lib/filesystem/Filesystem.h" +#include "../../lib/CGeneralTextHandler.h" + +#include "../../lib/CArtHandler.h" +#include "../../lib/CBuildingHandler.h" +#include "../../lib/spells/CSpellHandler.h" + +#include "../../lib/CSkillHandler.h" +#include "../../lib/CTownHandler.h" +#include "../../lib/CHeroHandler.h" +#include "../../lib/CCreatureHandler.h" +#include "../../lib/StartInfo.h" + +#include "../../lib/mapping/CCampaignHandler.h" +#include "../../lib/mapping/CMapService.h" +#include "../../lib/mapping/CMapInfo.h" + +#include "../../lib/mapObjects/CGHeroInstance.h" + +std::shared_ptr CBonusSelection::getCampaign() +{ + return CSH->si->campState; +} + +CBonusSelection::CBonusSelection() + : CWindowObject(BORDERED) +{ + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + static const std::string bgNames[] = + { + "E1_BG.BMP", "G2_BG.BMP", "E2_BG.BMP", "G1_BG.BMP", "G3_BG.BMP", "N1_BG.BMP", + "S1_BG.BMP", "BR_BG.BMP", "IS_BG.BMP", "KR_BG.BMP", "NI_BG.BMP", "TA_BG.BMP", "AR_BG.BMP", "HS_BG.BMP", + "BB_BG.BMP", "NB_BG.BMP", "EL_BG.BMP", "RN_BG.BMP", "UA_BG.BMP", "SP_BG.BMP" + }; + loadPositionsOfGraphics(); + setBackground(bgNames[getCampaign()->camp->header.mapVersion]); + + panelBackground = std::make_shared("CAMPBRF.BMP", 456, 6); + + buttonStart = std::make_shared(Point(475, 536), "CBBEGIB.DEF", CButton::tooltip(), std::bind(&CBonusSelection::startMap, this), SDLK_RETURN); + buttonRestart = std::make_shared(Point(475, 536), "CBRESTB.DEF", CButton::tooltip(), std::bind(&CBonusSelection::restartMap, this), SDLK_RETURN); + buttonBack = std::make_shared(Point(624, 536), "CBCANCB.DEF", CButton::tooltip(), std::bind(&CBonusSelection::goBack, this), SDLK_ESCAPE); + + campaignName = std::make_shared(481, 28, FONT_BIG, EAlignment::TOPLEFT, Colors::YELLOW, CSH->si->getCampaignName()); + + iconsMapSizes = std::make_shared("SCNRMPSZ", 4, 0, 735, 26); + + labelCampaignDescription = std::make_shared(481, 63, FONT_SMALL, EAlignment::TOPLEFT, Colors::YELLOW, CGI->generaltexth->allTexts[38]); + campaignDescription = std::make_shared(getCampaign()->camp->header.description, Rect(480, 86, 286, 117), 1); + + mapName = std::make_shared(481, 219, FONT_BIG, EAlignment::TOPLEFT, Colors::YELLOW, CSH->mi->getName()); + labelMapDescription = std::make_shared(481, 253, FONT_SMALL, EAlignment::TOPLEFT, Colors::YELLOW, CGI->generaltexth->allTexts[496]); + mapDescription = std::make_shared("", Rect(480, 280, 286, 117), 1); + + labelChooseBonus = std::make_shared(511, 432, FONT_SMALL, EAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[71]); + groupBonuses = std::make_shared(std::bind(&IServerAPI::setCampaignBonus, CSH, _1)); + + flagbox = std::make_shared(Rect(486, 407, 335, 23)); + + std::vector difficulty; + boost::split(difficulty, CGI->generaltexth->allTexts[492], boost::is_any_of(" ")); + labelDifficulty = std::make_shared(689, 432, FONT_MEDIUM, EAlignment::TOPLEFT, Colors::WHITE, difficulty.back()); + + for(size_t b = 0; b < difficultyIcons.size(); ++b) + { + difficultyIcons[b] = std::make_shared("GSPBUT" + boost::lexical_cast(b + 3) + ".DEF", 0, 0, 709, 455); + } + + if(getCampaign()->camp->header.difficultyChoosenByPlayer) + { + buttonDifficultyLeft = std::make_shared(Point(694, 508), "SCNRBLF.DEF", CButton::tooltip(), std::bind(&CBonusSelection::decreaseDifficulty, this)); + buttonDifficultyRight = std::make_shared(Point(738, 508), "SCNRBRT.DEF", CButton::tooltip(), std::bind(&CBonusSelection::increaseDifficulty, this)); + } + + for(int g = 0; g < getCampaign()->camp->scenarios.size(); ++g) + { + if(getCampaign()->camp->conquerable(g)) + regions.push_back(std::make_shared(g, true, true, campDescriptions[getCampaign()->camp->header.mapVersion])); + else if(getCampaign()->camp->scenarios[g].conquered) //display as striped + regions.push_back(std::make_shared(g, false, false, campDescriptions[getCampaign()->camp->header.mapVersion])); + } +} + +void CBonusSelection::loadPositionsOfGraphics() +{ + const JsonNode config(ResourceID("config/campaign_regions.json")); + int idx = 0; + + for(const JsonNode & campaign : config["campaign_regions"].Vector()) + { + SCampPositions sc; + + sc.campPrefix = campaign["prefix"].String(); + sc.colorSuffixLength = campaign["color_suffix_length"].Float(); + + for(const JsonNode & desc : campaign["desc"].Vector()) + { + SCampPositions::SRegionDesc rd; + + rd.infix = desc["infix"].String(); + rd.xpos = desc["x"].Float(); + rd.ypos = desc["y"].Float(); + sc.regions.push_back(rd); + } + + campDescriptions.push_back(sc); + + idx++; + } + + assert(idx == CGI->generaltexth->campaignMapNames.size()); +} + +void CBonusSelection::createBonusesIcons() +{ + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + const CCampaignScenario & scenario = getCampaign()->camp->scenarios[CSH->campaignMap]; + const std::vector & bonDescs = scenario.travelOptions.bonusesToChoose; + groupBonuses = std::make_shared(std::bind(&IServerAPI::setCampaignBonus, CSH, _1)); + + static const char * bonusPics[] = + { + "SPELLBON.DEF", // Spell + "TWCRPORT.DEF", // Monster + "", // Building - BO*.BMP + "ARTIFBON.DEF", // Artifact + "SPELLBON.DEF", // Spell scroll + "PSKILBON.DEF", // Primary skill + "SSKILBON.DEF", // Secondary skill + "BORES.DEF", // Resource + "PORTRAITSLARGE", // Hero HPL*.BMP + "PORTRAITSLARGE" + // Player - CREST58.DEF + }; + + for(int i = 0; i < bonDescs.size(); i++) + { + std::string picName = bonusPics[bonDescs[i].type]; + size_t picNumber = bonDescs[i].info2; + + std::string desc; + switch(bonDescs[i].type) + { + case CScenarioTravel::STravelBonus::SPELL: + desc = CGI->generaltexth->allTexts[715]; + boost::algorithm::replace_first(desc, "%s", CGI->spellh->objects[bonDescs[i].info2]->name); + break; + case CScenarioTravel::STravelBonus::MONSTER: + picNumber = bonDescs[i].info2 + 2; + desc = CGI->generaltexth->allTexts[717]; + boost::algorithm::replace_first(desc, "%d", boost::lexical_cast(bonDescs[i].info3)); + boost::algorithm::replace_first(desc, "%s", CGI->creh->creatures[bonDescs[i].info2]->namePl); + break; + case CScenarioTravel::STravelBonus::BUILDING: + { + int faction = -1; + for(auto & elem : CSH->si->playerInfos) + { + if(elem.second.isControlledByHuman()) + { + faction = elem.second.castle; + break; + } + + } + assert(faction != -1); + + BuildingID buildID = CBuildingHandler::campToERMU(bonDescs[i].info1, faction, std::set()); + picName = graphics->ERMUtoPicture[faction][buildID]; + picNumber = -1; + + if(vstd::contains(CGI->townh->factions[faction]->town->buildings, buildID)) + desc = CGI->townh->factions[faction]->town->buildings.find(buildID)->second->Name(); + + break; + } + case CScenarioTravel::STravelBonus::ARTIFACT: + desc = CGI->generaltexth->allTexts[715]; + boost::algorithm::replace_first(desc, "%s", CGI->arth->artifacts[bonDescs[i].info2]->Name()); + break; + case CScenarioTravel::STravelBonus::SPELL_SCROLL: + desc = CGI->generaltexth->allTexts[716]; + boost::algorithm::replace_first(desc, "%s", CGI->spellh->objects[bonDescs[i].info2]->name); + break; + case CScenarioTravel::STravelBonus::PRIMARY_SKILL: + { + int leadingSkill = -1; + std::vector> toPrint; //primary skills to be listed + const ui8 * ptr = reinterpret_cast(&bonDescs[i].info2); + for(int g = 0; g < GameConstants::PRIMARY_SKILLS; ++g) + { + if(leadingSkill == -1 || ptr[g] > ptr[leadingSkill]) + { + leadingSkill = g; + } + if(ptr[g] != 0) + { + toPrint.push_back(std::make_pair(g, ptr[g])); + } + } + picNumber = leadingSkill; + desc = CGI->generaltexth->allTexts[715]; + + std::string substitute; //text to be printed instead of %s + for(int v = 0; v < toPrint.size(); ++v) + { + substitute += boost::lexical_cast(toPrint[v].second); + substitute += " " + CGI->generaltexth->primarySkillNames[toPrint[v].first]; + if(v != toPrint.size() - 1) + { + substitute += ", "; + } + } + + boost::algorithm::replace_first(desc, "%s", substitute); + break; + } + case CScenarioTravel::STravelBonus::SECONDARY_SKILL: + desc = CGI->generaltexth->allTexts[718]; + + boost::algorithm::replace_first(desc, "%s", CGI->generaltexth->levels[bonDescs[i].info3 - 1]); //skill level + boost::algorithm::replace_first(desc, "%s", CGI->skillh->skillName(bonDescs[i].info2)); //skill name + picNumber = bonDescs[i].info2 * 3 + bonDescs[i].info3 - 1; + + break; + case CScenarioTravel::STravelBonus::RESOURCE: + { + int serialResID = 0; + switch(bonDescs[i].info1) + { + case 0: + case 1: + case 2: + case 3: + case 4: + case 5: + case 6: + serialResID = bonDescs[i].info1; + break; + case 0xFD: //wood + ore + serialResID = 7; + break; + case 0xFE: //rare resources + serialResID = 8; + break; + } + picNumber = serialResID; + + desc = CGI->generaltexth->allTexts[717]; + boost::algorithm::replace_first(desc, "%d", boost::lexical_cast(bonDescs[i].info2)); + std::string replacement; + if(serialResID <= 6) + { + replacement = CGI->generaltexth->restypes[serialResID]; + } + else + { + replacement = CGI->generaltexth->allTexts[714 + serialResID]; + } + boost::algorithm::replace_first(desc, "%s", replacement); + break; + } + case CScenarioTravel::STravelBonus::HEROES_FROM_PREVIOUS_SCENARIO: + { + auto superhero = getCampaign()->camp->scenarios[bonDescs[i].info2].strongestHero(PlayerColor(bonDescs[i].info1)); + if(!superhero) + logGlobal->warn("No superhero! How could it be transferred?"); + picNumber = superhero ? superhero->portrait : 0; + desc = CGI->generaltexth->allTexts[719]; + + boost::algorithm::replace_first(desc, "%s", getCampaign()->camp->scenarios[bonDescs[i].info2].scenarioName); + break; + } + + case CScenarioTravel::STravelBonus::HERO: + + desc = CGI->generaltexth->allTexts[718]; + boost::algorithm::replace_first(desc, "%s", CGI->generaltexth->capColors[bonDescs[i].info1]); //hero's color + + if(bonDescs[i].info2 == 0xFFFF) + { + boost::algorithm::replace_first(desc, "%s", CGI->generaltexth->allTexts[101]); //hero's name + picNumber = -1; + picName = "CBONN1A3.BMP"; + } + else + { + boost::algorithm::replace_first(desc, "%s", CGI->heroh->heroes[bonDescs[i].info2]->name); + } + break; + } + + CToggleButton * bonusButton = new CToggleButton(Point(475 + i * 68, 455), "", CButton::tooltip(desc, desc)); + + if(picNumber != -1) + picName += ":" + boost::lexical_cast(picNumber); + + auto anim = std::make_shared(); + anim->setCustom(picName, 0); + bonusButton->setImage(anim); + if(CSH->campaignBonus == i) + bonusButton->setBorderColor(Colors::BRIGHT_YELLOW); + groupBonuses->addToggle(i, bonusButton); + } + + if(vstd::contains(getCampaign()->chosenCampaignBonuses, CSH->campaignMap)) + { + groupBonuses->setSelected(getCampaign()->chosenCampaignBonuses[CSH->campaignMap]); + } +} + +void CBonusSelection::updateAfterStateChange() +{ + if(CSH->state != EClientState::GAMEPLAY) + { + buttonRestart->disable(); + buttonStart->enable(); + if(!getCampaign()->mapsConquered.empty()) + buttonBack->block(true); + else + buttonBack->block(false); + } + else + { + buttonStart->disable(); + buttonRestart->enable(); + buttonBack->block(false); + } + if(CSH->campaignBonus == -1) + { + buttonStart->block(getCampaign()->camp->scenarios[CSH->campaignMap].travelOptions.bonusesToChoose.size()); + } + else if(buttonStart->isBlocked()) + { + buttonStart->block(false); + } + + for(auto region : regions) + region->updateState(); + + if(!CSH->mi) + return; + iconsMapSizes->setFrame(CSH->mi->getMapSizeIconId()); + mapDescription->setText(CSH->mi->getDescription()); + for(size_t i = 0; i < difficultyIcons.size(); i++) + { + if(i == CSH->si->difficulty) + difficultyIcons[i]->enable(); + else + difficultyIcons[i]->disable(); + } + flagbox->recreate(); + createBonusesIcons(); +} + +void CBonusSelection::goBack() +{ + if(CSH->state != EClientState::GAMEPLAY) + { + GH.popInts(2); + } + else + { + GH.popIntTotally(this); + } + // TODO: we can actually only pop bonus selection interface for custom campaigns + // Though this would require clearing CLobbyScreen::bonusSel pointer when poping this interface +/* + else + { + GH.popIntTotally(this); + CSH->state = EClientState::LOBBY; + } +*/ +} + +void CBonusSelection::startMap() +{ + auto showPrologVideo = [=]() + { + auto exitCb = [=]() + { + logGlobal->info("Starting scenario %d", CSH->campaignMap); + CSH->sendStartGame(); + }; + + const CCampaignScenario & scenario = getCampaign()->camp->scenarios[CSH->campaignMap]; + if(scenario.prolog.hasPrologEpilog) + { + GH.pushInt(new CPrologEpilogVideo(scenario.prolog, exitCb)); + } + else + { + exitCb(); + } + }; + + if(LOCPLINT) // we're currently ingame, so ask for starting new map and end game + { + GH.popInt(this); + LOCPLINT->showYesNoDialog(CGI->generaltexth->allTexts[67], [=]() + { + GH.curInt = CMainMenu::create(); + showPrologVideo(); + }, 0); + } + else + { + showPrologVideo(); + } +} + +void CBonusSelection::restartMap() +{ + GH.popInt(this); + LOCPLINT->showYesNoDialog(CGI->generaltexth->allTexts[67], [=]() + { + CSH->startCampaignScenario(); + }, 0); +} + +void CBonusSelection::increaseDifficulty() +{ + CSH->setDifficulty(CSH->si->difficulty + 1); +} + +void CBonusSelection::decreaseDifficulty() +{ + CSH->setDifficulty(CSH->si->difficulty - 1); +} + +CBonusSelection::CRegion::CRegion(int id, bool accessible, bool selectable, const SCampPositions & campDsc) + : CIntObject(LCLICK | RCLICK), idOfMapAndRegion(id), accessible(accessible), selectable(selectable) +{ + OBJ_CONSTRUCTION; + static const std::string colors[2][8] = + { + {"R", "B", "N", "G", "O", "V", "T", "P"}, + {"Re", "Bl", "Br", "Gr", "Or", "Vi", "Te", "Pi"} + }; + + const SCampPositions::SRegionDesc & desc = campDsc.regions[idOfMapAndRegion]; + pos.x += desc.xpos; + pos.y += desc.ypos; + + std::string prefix = campDsc.campPrefix + desc.infix + "_"; + std::string suffix = colors[campDsc.colorSuffixLength - 1][CSH->si->campState->camp->scenarios[idOfMapAndRegion].regionColor]; + graphicsNotSelected = std::make_shared(prefix + "En" + suffix + ".BMP"); + graphicsNotSelected->disable(); + graphicsSelected = std::make_shared(prefix + "Se" + suffix + ".BMP"); + graphicsSelected->disable(); + graphicsStriped = std::make_shared(prefix + "Co" + suffix + ".BMP"); + graphicsStriped->disable(); + pos.w = graphicsNotSelected->bg->w; + pos.h = graphicsNotSelected->bg->h; + +} + +void CBonusSelection::CRegion::updateState() +{ + if(!accessible) + { + graphicsNotSelected->disable(); + graphicsSelected->disable(); + graphicsStriped->enable(); + } + else if(CSH->campaignMap == idOfMapAndRegion) + { + graphicsNotSelected->disable(); + graphicsSelected->enable(); + graphicsStriped->disable(); + } + else + { + graphicsNotSelected->enable(); + graphicsSelected->disable(); + graphicsStriped->disable(); + } +} + +void CBonusSelection::CRegion::clickLeft(tribool down, bool previousState) +{ + //select if selectable & clicked inside our graphic + if(indeterminate(down)) + return; + + if(!down && selectable && !CSDL_Ext::isTransparent(*graphicsNotSelected, GH.current->motion.x - pos.x, GH.current->motion.y - pos.y)) + { + CSH->setCampaignMap(idOfMapAndRegion); + } +} + +void CBonusSelection::CRegion::clickRight(tribool down, bool previousState) +{ + // FIXME: For some reason "down" is only ever contain indeterminate_value + auto text = CSH->si->campState->camp->scenarios[idOfMapAndRegion].regionText; + if(!CSDL_Ext::isTransparent(*graphicsNotSelected, GH.current->motion.x - pos.x, GH.current->motion.y - pos.y) && text.size()) + { + CRClickPopup::createAndPush(text); + } +} diff --git a/client/lobby/CBonusSelection.h b/client/lobby/CBonusSelection.h new file mode 100644 index 000000000..b07512a9e --- /dev/null +++ b/client/lobby/CBonusSelection.h @@ -0,0 +1,93 @@ +/* + * CBonusSelection.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once +#include "../windows/CWindowObject.h" + +class SDL_Surface; +class CCampaignState; +class CButton; +class CTextBox; +class CToggleGroup; +class CAnimImage; +class CLabel; +class CFlagBox; + +/// Campaign screen where you can choose one out of three starting bonuses +class CBonusSelection : public CWindowObject +{ +public: + std::shared_ptr getCampaign(); + CBonusSelection(); + + struct SCampPositions + { + std::string campPrefix; + int colorSuffixLength; + + struct SRegionDesc + { + std::string infix; + int xpos, ypos; + }; + + std::vector regions; + + }; + + class CRegion + : public CIntObject + { + CBonusSelection * owner; + std::shared_ptr graphicsNotSelected; + std::shared_ptr graphicsSelected; + std::shared_ptr graphicsStriped; + int idOfMapAndRegion; + bool accessible; // false if region should be striped + bool selectable; // true if region should be selectable + public: + CRegion(int id, bool accessible, bool selectable, const SCampPositions & campDsc); + void updateState(); + void clickLeft(tribool down, bool previousState) override; + void clickRight(tribool down, bool previousState) override; + }; + + void loadPositionsOfGraphics(); + void createBonusesIcons(); + void updateAfterStateChange(); + + // Event handlers + void goBack(); + void startMap(); + void restartMap(); + void increaseDifficulty(); + void decreaseDifficulty(); + + std::shared_ptr panelBackground; + std::shared_ptr buttonStart; + std::shared_ptr buttonRestart; + std::shared_ptr buttonBack; + std::shared_ptr campaignName; + std::shared_ptr labelCampaignDescription; + std::shared_ptr campaignDescription; + std::shared_ptr mapName; + std::shared_ptr labelMapDescription; + std::shared_ptr mapDescription; + std::vector campDescriptions; + std::vector> regions; + std::shared_ptr flagbox; + + std::shared_ptr labelChooseBonus; + std::shared_ptr groupBonuses; + std::shared_ptr labelDifficulty; + std::array, 5> difficultyIcons; + std::shared_ptr buttonDifficultyLeft; + std::shared_ptr buttonDifficultyRight; + std::shared_ptr iconsMapSizes; +}; diff --git a/client/lobby/CLobbyScreen.cpp b/client/lobby/CLobbyScreen.cpp new file mode 100644 index 000000000..cbd1040cb --- /dev/null +++ b/client/lobby/CLobbyScreen.cpp @@ -0,0 +1,201 @@ +/* + * CLobbyScreen.cpp, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#include "StdInc.h" + +#include "CLobbyScreen.h" +#include "CBonusSelection.h" +#include "SelectionTab.h" +#include "RandomMapTab.h" +#include "OptionsTab.h" +#include "../CServerHandler.h" + +#include "../gui/CGuiHandler.h" +#include "../widgets/Buttons.h" +#include "../windows/InfoWindows.h" + +#include "../../CCallback.h" + +#include "../CGameInfo.h" +#include "../../lib/NetPacksLobby.h" +#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/mapping/CMapInfo.h" +#include "../../lib/mapping/CCampaignHandler.h" +#include "../../lib/rmg/CMapGenOptions.h" + +CLobbyScreen::CLobbyScreen(ESelectionScreen screenType) + : CSelectionBase(screenType), bonusSel(nullptr) +{ + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + tabSel = std::make_shared(screenType); + curTab = tabSel; + + auto initLobby = [&]() + { + tabSel->callOnSelect = std::bind(&IServerAPI::setMapInfo, CSH, _1, nullptr); + + buttonSelect = std::make_shared(Point(411, 80), "GSPBUTT.DEF", CGI->generaltexth->zelp[45], 0, SDLK_s); + buttonSelect->addCallback([&]() + { + toggleTab(tabSel); + CSH->setMapInfo(tabSel->getSelectedMapInfo()); + }); + + buttonOptions = std::make_shared(Point(411, 510), "GSPBUTT.DEF", CGI->generaltexth->zelp[46], std::bind(&CLobbyScreen::toggleTab, this, tabOpt), SDLK_a); + }; + + buttonChat = std::make_shared(Point(619, 83), "GSPBUT2.DEF", CGI->generaltexth->zelp[48], std::bind(&CLobbyScreen::toggleChat, this), SDLK_h); + buttonChat->addTextOverlay(CGI->generaltexth->allTexts[532], FONT_SMALL); + + switch(screenType) + { + case ESelectionScreen::newGame: + { + tabOpt = std::make_shared(); + tabRand = std::make_shared(); + tabRand->mapInfoChanged += std::bind(&IServerAPI::setMapInfo, CSH, _1, _2); + buttonRMG = std::make_shared(Point(411, 105), "GSPBUTT.DEF", CGI->generaltexth->zelp[47], 0, SDLK_r); + buttonRMG->addCallback([&]() + { + toggleTab(tabRand); + tabRand->updateMapInfoByHost(); // TODO: This is only needed to force-update mapInfo in CSH when tab is opened + }); + + card->iconDifficulty->addCallback(std::bind(&IServerAPI::setDifficulty, CSH, _1)); + + buttonStart = std::make_shared(Point(411, 535), "SCNRBEG.DEF", CGI->generaltexth->zelp[103], std::bind(&CLobbyScreen::startScenario, this, false), SDLK_b); + initLobby(); + break; + } + case ESelectionScreen::loadGame: + { + tabOpt = std::make_shared(); + buttonStart = std::make_shared(Point(411, 535), "SCNRLOD.DEF", CGI->generaltexth->zelp[103], std::bind(&CLobbyScreen::startScenario, this, false), SDLK_l); + initLobby(); + break; + } + case ESelectionScreen::campaignList: + tabSel->callOnSelect = std::bind(&IServerAPI::setMapInfo, CSH, _1, nullptr); + buttonStart = std::make_shared(Point(411, 535), "SCNRLOD.DEF", CButton::tooltip(), std::bind(&CLobbyScreen::startCampaign, this), SDLK_b); + break; + } + + buttonStart->assignedKeys.insert(SDLK_RETURN); + + buttonBack = std::make_shared(Point(581, 535), "SCNRBACK.DEF", CGI->generaltexth->zelp[105], [&](){CSH->sendClientDisconnecting(); GH.popIntTotally(this);}, SDLK_ESCAPE); +} + +CLobbyScreen::~CLobbyScreen() +{ + // TODO: For now we always destroy whole lobby when leaving bonus selection screen + if(CSH->state == EClientState::LOBBY_CAMPAIGN) + CSH->sendClientDisconnecting(); +} + +void CLobbyScreen::toggleTab(std::shared_ptr tab) +{ + if(tab == curTab) + CSH->sendGuiAction(LobbyGuiAction::NO_TAB); + else if(tab == tabOpt) + CSH->sendGuiAction(LobbyGuiAction::OPEN_OPTIONS); + else if(tab == tabSel) + CSH->sendGuiAction(LobbyGuiAction::OPEN_SCENARIO_LIST); + else if(tab == tabRand) + CSH->sendGuiAction(LobbyGuiAction::OPEN_RANDOM_MAP_OPTIONS); + CSelectionBase::toggleTab(tab); +} + +void CLobbyScreen::startCampaign() +{ + if(CSH->mi) + { + auto ourCampaign = std::make_shared(CCampaignHandler::getCampaign(CSH->mi->fileURI)); + CSH->setCampaignState(ourCampaign); + } +} + +void CLobbyScreen::startScenario(bool allowOnlyAI) +{ + try + { + CSH->sendStartGame(allowOnlyAI); + buttonStart->block(true); + } + catch(ExceptionMapMissing & e) + { + + } + catch(ExceptionNoHuman & e) + { + // You must position yourself prior to starting the game. + CInfoWindow::showYesNoDialog(std::ref(CGI->generaltexth->allTexts[530]), nullptr, 0, std::bind(&CLobbyScreen::startScenario, this, true), false, PlayerColor(1)); + } + catch(ExceptionNoTemplate & e) + { + GH.pushInt(CInfoWindow::create(CGI->generaltexth->allTexts[751])); + } + catch(...) + { + + } +} + +void CLobbyScreen::toggleMode(bool host) +{ + tabSel->toggleMode(); + buttonStart->block(!host); + if(screenType == ESelectionScreen::campaignList) + return; + + auto buttonColor = host ? Colors::WHITE : Colors::ORANGE; + buttonSelect->addTextOverlay(CGI->generaltexth->allTexts[500], FONT_SMALL, buttonColor); + buttonOptions->addTextOverlay(CGI->generaltexth->allTexts[501], FONT_SMALL, buttonColor); + if(buttonRMG) + { + buttonRMG->addTextOverlay(CGI->generaltexth->allTexts[740], FONT_SMALL, buttonColor); + buttonRMG->block(!host); + } + buttonSelect->block(!host); + buttonOptions->block(!host); + + if(CSH->mi) + tabOpt->recreate(); +} + +void CLobbyScreen::toggleChat() +{ + card->toggleChat(); + if(card->showChat) + buttonChat->addTextOverlay(CGI->generaltexth->allTexts[531], FONT_SMALL); + else + buttonChat->addTextOverlay(CGI->generaltexth->allTexts[532], FONT_SMALL); +} + +void CLobbyScreen::updateAfterStateChange() +{ + if(CSH->mi && tabOpt) + tabOpt->recreate(); + + card->changeSelection(); + if(card->iconDifficulty) + card->iconDifficulty->setSelected(CSH->si->difficulty); + + if(curTab == tabRand && CSH->si->mapGenOptions) + tabRand->setMapGenOptions(CSH->si->mapGenOptions); +} + +const StartInfo * CLobbyScreen::getStartInfo() +{ + return CSH->si.get(); +} + +const CMapInfo * CLobbyScreen::getMapInfo() +{ + return CSH->mi.get(); +} diff --git a/client/lobby/CLobbyScreen.h b/client/lobby/CLobbyScreen.h new file mode 100644 index 000000000..679ed5634 --- /dev/null +++ b/client/lobby/CLobbyScreen.h @@ -0,0 +1,35 @@ +/* + * CLobbyScreen.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +#include "CSelectionBase.h" + +class CBonusSelection; + +class CLobbyScreen : public CSelectionBase +{ +public: + std::shared_ptr buttonChat; + + CLobbyScreen(ESelectionScreen type); + ~CLobbyScreen(); + void toggleTab(std::shared_ptr tab) override; + void startCampaign(); + void startScenario(bool allowOnlyAI = false); + void toggleMode(bool host); + void toggleChat(); + + void updateAfterStateChange(); + + const CMapInfo * getMapInfo() override; + const StartInfo * getStartInfo() override; + + CBonusSelection * bonusSel; +}; diff --git a/client/lobby/CSavingScreen.cpp b/client/lobby/CSavingScreen.cpp new file mode 100644 index 000000000..c6324ff3e --- /dev/null +++ b/client/lobby/CSavingScreen.cpp @@ -0,0 +1,97 @@ +/* + * CSavingScreen.cpp, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#include "StdInc.h" + +#include "CSavingScreen.h" +#include "SelectionTab.h" + +#include "../CGameInfo.h" +#include "../CPlayerInterface.h" +#include "../gui/CGuiHandler.h" +#include "../widgets/Buttons.h" +#include "../widgets/TextControls.h" + +#include "../../CCallback.h" +#include "../../lib/CConfigHandler.h" +#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/StartInfo.h" +#include "../../lib/filesystem/Filesystem.h" +#include "../../lib/mapping/CMapInfo.h" + +CSavingScreen::CSavingScreen() + : CSelectionBase(ESelectionScreen::saveGame) +{ + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + center(pos); + // TODO: we should really use std::shared_ptr for passing StartInfo around. + localSi = new StartInfo(*LOCPLINT->cb->getStartInfo()); + localMi = new CMapInfo(); + localMi->mapHeader = std::unique_ptr(new CMapHeader(*LOCPLINT->cb->getMapHeader())); + + tabSel = std::make_shared(screenType); + curTab = tabSel; + tabSel->toggleMode(); + + tabSel->callOnSelect = std::bind(&CSavingScreen::changeSelection, this, _1); + buttonStart = std::make_shared(Point(411, 535), "SCNRSAV.DEF", CGI->generaltexth->zelp[103], std::bind(&CSavingScreen::saveGame, this), SDLK_s); + buttonStart->assignedKeys.insert(SDLK_RETURN); +} + +CSavingScreen::~CSavingScreen() +{ + vstd::clear_pointer(localMi); +} + +const CMapInfo * CSavingScreen::getMapInfo() +{ + return localMi; +} + +const StartInfo * CSavingScreen::getStartInfo() +{ + return localSi; +} + +void CSavingScreen::changeSelection(std::shared_ptr to) +{ + if(localMi == to.get()) + return; + + localMi = to.get(); + localSi = localMi->scenarioOptionsOfSave; + card->changeSelection(); +} + +void CSavingScreen::saveGame() +{ + if(!(tabSel && tabSel->inputName && tabSel->inputName->text.size())) + return; + + std::string path = "Saves/" + tabSel->inputName->text; + + auto overWrite = [&]() -> void + { + Settings lastSave = settings.write["general"]["lastSave"]; + lastSave->String() = path; + LOCPLINT->cb->save(path); + GH.popIntTotally(this); + }; + + if(CResourceHandler::get("local")->existsResource(ResourceID(path, EResType::CLIENT_SAVEGAME))) + { + std::string hlp = CGI->generaltexth->allTexts[493]; //%s exists. Overwrite? + boost::algorithm::replace_first(hlp, "%s", tabSel->inputName->text); + LOCPLINT->showYesNoDialog(hlp, overWrite, 0, false); + } + else + { + overWrite(); + } +} diff --git a/client/lobby/CSavingScreen.h b/client/lobby/CSavingScreen.h new file mode 100644 index 000000000..db26be082 --- /dev/null +++ b/client/lobby/CSavingScreen.h @@ -0,0 +1,32 @@ +/* + * CSavingScreen.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +#include "CSelectionBase.h" + +class CSelectionBase; +struct StartInfo; +class CMapInfo; + +class CSavingScreen : public CSelectionBase +{ +public: + const StartInfo * localSi; + CMapInfo * localMi; + + CSavingScreen(); + ~CSavingScreen(); + + void changeSelection(std::shared_ptr to); + void saveGame(); + + const CMapInfo * getMapInfo() override; + const StartInfo * getStartInfo() override; +}; diff --git a/client/lobby/CScenarioInfoScreen.cpp b/client/lobby/CScenarioInfoScreen.cpp new file mode 100644 index 000000000..88402ec48 --- /dev/null +++ b/client/lobby/CScenarioInfoScreen.cpp @@ -0,0 +1,59 @@ +/* + * CScenarioInfoScreen.cpp, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#include "StdInc.h" + +#include "CScenarioInfoScreen.h" +#include "OptionsTab.h" + +#include "../CGameInfo.h" +#include "../CPlayerInterface.h" +#include "../gui/CGuiHandler.h" +#include "../widgets/Buttons.h" + +#include "../../CCallback.h" + +#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/StartInfo.h" +#include "../../lib/mapping/CMapInfo.h" + +CScenarioInfoScreen::CScenarioInfoScreen() +{ + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + localSi = new StartInfo(*LOCPLINT->cb->getStartInfo()); + localMi = new CMapInfo(); + localMi->mapHeader = std::unique_ptr(new CMapHeader(*LOCPLINT->cb->getMapHeader())); + + screenType = ESelectionScreen::scenarioInfo; + + card = std::make_shared(); + opt = std::make_shared(); + opt->recActions = UPDATE | SHOWALL; + opt->recreate(); + card->changeSelection(); + + card->iconDifficulty->setSelected(getCurrentDifficulty()); + buttonBack = std::make_shared(Point(584, 535), "SCNRBACK.DEF", CGI->generaltexth->zelp[105], std::bind(&CGuiHandler::popIntTotally, &GH, this), SDLK_ESCAPE); +} + +CScenarioInfoScreen::~CScenarioInfoScreen() +{ + vstd::clear_pointer(localSi); + vstd::clear_pointer(localMi); +} + +const CMapInfo * CScenarioInfoScreen::getMapInfo() +{ + return localMi; +} + +const StartInfo * CScenarioInfoScreen::getStartInfo() +{ + return localSi; +} diff --git a/client/lobby/CScenarioInfoScreen.h b/client/lobby/CScenarioInfoScreen.h new file mode 100644 index 000000000..63892bb77 --- /dev/null +++ b/client/lobby/CScenarioInfoScreen.h @@ -0,0 +1,30 @@ +/* + * CScenarioInfoScreen.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +#include "CSelectionBase.h" + +/// Scenario information screen shown during the game +class CScenarioInfoScreen : public CIntObject, public ISelectionScreenInfo +{ +public: + std::shared_ptr buttonBack; + std::shared_ptr card; + std::shared_ptr opt; + + const StartInfo * localSi; + CMapInfo * localMi; + + CScenarioInfoScreen(); + ~CScenarioInfoScreen(); + + const CMapInfo * getMapInfo() override; + const StartInfo * getStartInfo() override; +}; diff --git a/client/lobby/CSelectionBase.cpp b/client/lobby/CSelectionBase.cpp new file mode 100644 index 000000000..6ea17a392 --- /dev/null +++ b/client/lobby/CSelectionBase.cpp @@ -0,0 +1,407 @@ +/* + * CSelectionBase.cpp, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#include "StdInc.h" + +#include "CSelectionBase.h" +#include "CBonusSelection.h" +#include "CLobbyScreen.h" +#include "OptionsTab.h" +#include "RandomMapTab.h" +#include "SelectionTab.h" + +#include "../../CCallback.h" + +#include "../CGameInfo.h" +#include "../CPlayerInterface.h" +#include "../CMessage.h" +#include "../CBitmapHandler.h" +#include "../CMusicHandler.h" +#include "../CVideoHandler.h" +#include "../CPlayerInterface.h" +#include "../CServerHandler.h" +#include "../gui/CAnimation.h" +#include "../gui/CGuiHandler.h" +#include "../mainmenu/CMainMenu.h" +#include "../widgets/CComponent.h" +#include "../widgets/Buttons.h" +#include "../widgets/MiscWidgets.h" +#include "../widgets/ObjectLists.h" +#include "../widgets/TextControls.h" +#include "../windows/GUIClasses.h" +#include "../windows/InfoWindows.h" + +#include "../../lib/NetPacksLobby.h" +#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/CHeroHandler.h" +#include "../../lib/CThreadHelper.h" +#include "../../lib/filesystem/Filesystem.h" +#include "../../lib/mapping/CMapInfo.h" +#include "../../lib/serializer/Connection.h" + +ISelectionScreenInfo::ISelectionScreenInfo(ESelectionScreen ScreenType) + : screenType(ScreenType) +{ + assert(!SEL); + SEL = this; +} + +ISelectionScreenInfo::~ISelectionScreenInfo() +{ + assert(SEL == this); + SEL = nullptr; +} + +int ISelectionScreenInfo::getCurrentDifficulty() +{ + return getStartInfo()->difficulty; +} + +PlayerInfo ISelectionScreenInfo::getPlayerInfo(int color) +{ + return getMapInfo()->mapHeader->players[color]; +} + +CSelectionBase::CSelectionBase(ESelectionScreen type) + : CWindowObject(BORDERED | SHADOW_DISABLED), ISelectionScreenInfo(type) +{ + CMainMenu::create(); //we depend on its graphics + + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + IShowActivatable::type = BLOCK_ADV_HOTKEYS; + pos.w = 762; + pos.h = 584; + if(screenType == ESelectionScreen::campaignList) + { + setBackground("CamCust.bmp"); + } + else + { + const JsonVector & bgNames = CMainMenuConfig::get().getConfig()["game-select"].Vector(); + setBackground(RandomGeneratorUtil::nextItem(bgNames, CRandomGenerator::getDefault())->String()); + } + pos = background->center(); + card = std::make_shared(); + buttonBack = std::make_shared(Point(581, 535), "SCNRBACK.DEF", CGI->generaltexth->zelp[105], std::bind(&CGuiHandler::popIntTotally, &GH, this), SDLK_ESCAPE); +} + +void CSelectionBase::toggleTab(std::shared_ptr tab) +{ + if(curTab && curTab->active) + { + curTab->deactivate(); + curTab->recActions = 0; + } + + if(curTab != tab) + { + tab->recActions = 255 - DISPOSE; + tab->activate(); + curTab = tab; + } + else + { + curTab.reset(); + } + GH.totalRedraw(); +} + +InfoCard::InfoCard() + : showChat(true) +{ + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + CIntObject::type |= REDRAW_PARENT; + pos.x += 393; + pos.y += 6; + + labelSaveDate = std::make_shared(158, 19, FONT_SMALL, TOPLEFT, Colors::WHITE); + mapName = std::make_shared(26, 39, FONT_BIG, TOPLEFT, Colors::YELLOW); + Rect descriptionRect(26, 149, 320, 115); + mapDescription = std::make_shared("", descriptionRect, 1); + playerListBg = std::make_shared("CHATPLUG.bmp", 16, 276); + chat = std::make_shared(Rect(26, 132, 340, 132)); + + if(SEL->screenType == ESelectionScreen::campaignList) + { + labelCampaignDescription = std::make_shared(26, 132, FONT_SMALL, TOPLEFT, Colors::YELLOW, CGI->generaltexth->allTexts[38]); + } + else + { + background = std::make_shared("GSELPOP1.bmp", 0, 0); + parent->addChild(background.get()); + auto it = vstd::find(parent->children, this); //our position among parent children + parent->children.insert(it, background.get()); //put BG before us + parent->children.pop_back(); + pos.w = background->pos.w; + pos.h = background->pos.h; + iconsMapSizes = std::make_shared("SCNRMPSZ", 4, 0, 318, 22); //let it be custom size (frame 4) by default + + iconDifficulty = std::make_shared(0); + { + static const char * difButns[] = {"GSPBUT3.DEF", "GSPBUT4.DEF", "GSPBUT5.DEF", "GSPBUT6.DEF", "GSPBUT7.DEF"}; + + for(int i = 0; i < 5; i++) + { + auto button = new CToggleButton(Point(110 + i * 32, 450), difButns[i], CGI->generaltexth->zelp[24 + i]); + + iconDifficulty->addToggle(i, button); + if(SEL->screenType != ESelectionScreen::newGame) + button->block(true); + } + } + + flagbox = std::make_shared(Rect(24, 400, 335, 23)); + labelMapDiff = std::make_shared(33, 430, FONT_SMALL, TOPLEFT, Colors::YELLOW, CGI->generaltexth->allTexts[494]); + labelPlayerDifficulty = std::make_shared(133, 430, FONT_SMALL, TOPLEFT, Colors::YELLOW, CGI->generaltexth->allTexts[492] + ":"); + labelRating = std::make_shared(290, 430, FONT_SMALL, TOPLEFT, Colors::YELLOW, CGI->generaltexth->allTexts[218] + ":"); + labelScenarioName = std::make_shared(26, 22, FONT_SMALL, TOPLEFT, Colors::YELLOW, CGI->generaltexth->allTexts[495]); + labelScenarioDescription = std::make_shared(26, 132, FONT_SMALL, TOPLEFT, Colors::YELLOW, CGI->generaltexth->allTexts[496]); + labelVictoryCondition = std::make_shared(26, 283, FONT_SMALL, TOPLEFT, Colors::YELLOW, CGI->generaltexth->allTexts[497]); + labelLossCondition = std::make_shared(26, 339, FONT_SMALL, TOPLEFT, Colors::YELLOW, CGI->generaltexth->allTexts[498]); + iconsVictoryCondition = std::make_shared("SCNRVICT", 0, 0, 24, 302); + iconsLossCondition = std::make_shared("SCNRLOSS", 0, 0, 24, 359); + + labelVictoryConditionText = std::make_shared(60, 307, FONT_SMALL, TOPLEFT, Colors::WHITE); + labelLossConditionText = std::make_shared(60, 366, FONT_SMALL, TOPLEFT, Colors::WHITE); + + labelDifficulty = std::make_shared(62, 472, FONT_SMALL, CENTER, Colors::WHITE); + labelDifficultyPercent = std::make_shared(311, 472, FONT_SMALL, CENTER, Colors::WHITE); + + labelGroupPlayersAssigned = std::make_shared(FONT_SMALL, TOPLEFT, Colors::WHITE); + labelGroupPlayersUnassigned = std::make_shared(FONT_SMALL, TOPLEFT, Colors::WHITE); + } + setChat(false); +} + +void InfoCard::changeSelection() +{ + if(!SEL->getMapInfo()) + return; + + labelSaveDate->setText(SEL->getMapInfo()->date); + mapName->setText(SEL->getMapInfo()->getName()); + mapDescription->setText(SEL->getMapInfo()->getDescription()); + + mapDescription->label->scrollTextTo(0); + if(mapDescription->slider) + mapDescription->slider->moveToMin(); + + if(SEL->screenType == ESelectionScreen::campaignList) + return; + + iconsMapSizes->setFrame(SEL->getMapInfo()->getMapSizeIconId()); + const CMapHeader * header = SEL->getMapInfo()->mapHeader.get(); + iconsVictoryCondition->setFrame(header->victoryIconIndex); + labelVictoryConditionText->setText(header->victoryMessage); + iconsLossCondition->setFrame(header->defeatIconIndex); + labelLossConditionText->setText(header->defeatMessage); + flagbox->recreate(); + labelDifficulty->setText(CGI->generaltexth->arraytxt[142 + SEL->getMapInfo()->mapHeader->difficulty]); + iconDifficulty->setSelected(SEL->getCurrentDifficulty()); + const std::array difficultyPercent = {"80%", "100%", "130%", "160%", "200%"}; + labelDifficultyPercent->setText(difficultyPercent[SEL->getCurrentDifficulty()]); + + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + // FIXME: We recreate them each time because CLabelGroup don't use smart pointers + labelGroupPlayersAssigned = std::make_shared(FONT_SMALL, TOPLEFT, Colors::WHITE); + labelGroupPlayersUnassigned = std::make_shared(FONT_SMALL, TOPLEFT, Colors::WHITE); + if(!showChat) + { + labelGroupPlayersAssigned->disable(); + labelGroupPlayersUnassigned->disable(); + } + for(auto & p : CSH->playerNames) + { + const auto pset = CSH->si->getPlayersSettings(p.first); + int pid = p.first; + if(pset) + { + auto name = boost::str(boost::format("%s (%d-%d %s)") % p.second.name % p.second.connection % pid % pset->color.getStr()); + labelGroupPlayersAssigned->add(24, 285 + labelGroupPlayersAssigned->currentSize()*graphics->fonts[FONT_SMALL]->getLineHeight(), name); + } + else + { + auto name = boost::str(boost::format("%s (%d-%d)") % p.second.name % p.second.connection % pid); + labelGroupPlayersUnassigned->add(193, 285 + labelGroupPlayersUnassigned->currentSize()*graphics->fonts[FONT_SMALL]->getLineHeight(), name); + } + } +} + +void InfoCard::toggleChat() +{ + setChat(!showChat); +} + +void InfoCard::setChat(bool activateChat) +{ + if(showChat == activateChat) + return; + + if(activateChat) + { + if(SEL->screenType == ESelectionScreen::campaignList) + { + labelCampaignDescription->disable(); + } + else + { + labelScenarioDescription->disable(); + labelVictoryCondition->disable(); + labelLossCondition->disable(); + iconsVictoryCondition->disable(); + labelVictoryConditionText->disable(); + iconsLossCondition->disable(); + labelLossConditionText->disable(); + labelGroupPlayersAssigned->enable(); + labelGroupPlayersUnassigned->enable(); + } + mapDescription->disable(); + chat->enable(); + playerListBg->enable(); + } + else + { + mapDescription->enable(); + chat->disable(); + playerListBg->disable(); + + if(SEL->screenType == ESelectionScreen::campaignList) + { + labelCampaignDescription->enable(); + } + else + { + labelScenarioDescription->enable(); + labelVictoryCondition->enable(); + labelLossCondition->enable(); + iconsVictoryCondition->enable(); + iconsLossCondition->enable(); + labelVictoryConditionText->enable(); + labelLossConditionText->enable(); + labelGroupPlayersAssigned->disable(); + labelGroupPlayersUnassigned->disable(); + } + } + + showChat = activateChat; + GH.totalRedraw(); +} + +CChatBox::CChatBox(const Rect & rect) + : CIntObject(KEYBOARD | TEXTINPUT) +{ + OBJ_CONSTRUCTION; + pos += rect; + captureAllKeys = true; + type |= REDRAW_PARENT; + + const int height = graphics->fonts[FONT_SMALL]->getLineHeight(); + inputBox = std::make_shared(Rect(0, rect.h - height, rect.w, height)); + inputBox->removeUsedEvents(KEYBOARD); + chatHistory = std::make_shared("", Rect(0, 0, rect.w, rect.h - height), 1); + + chatHistory->label->color = Colors::GREEN; +} + +void CChatBox::keyPressed(const SDL_KeyboardEvent & key) +{ + if(key.keysym.sym == SDLK_RETURN && key.state == SDL_PRESSED && inputBox->text.size()) + { + CSH->sendMessage(inputBox->text); + inputBox->setText(""); + } + else + inputBox->keyPressed(key); +} + +void CChatBox::addNewMessage(const std::string & text) +{ + CCS->soundh->playSound("CHAT"); + chatHistory->setText(chatHistory->label->text + text + "\n"); + if(chatHistory->slider) + chatHistory->slider->moveToMax(); +} + +CFlagBox::CFlagBox(const Rect & rect) + : CIntObject(RCLICK) +{ + pos += rect; + pos.w = rect.w; + pos.h = rect.h; + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + + labelAllies = std::make_shared(0, 0, FONT_SMALL, EAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[390] + ":"); + labelEnemies = std::make_shared(133, 0, FONT_SMALL, EAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[391] + ":"); + + iconsTeamFlags = std::make_shared("ITGFLAGS.DEF"); + iconsTeamFlags->preload(); +} + +void CFlagBox::recreate() +{ + flagsAllies.clear(); + flagsEnemies.clear(); + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + const int alliesX = 5 + labelAllies->getWidth(); + const int enemiesX = 5 + 133 + labelEnemies->getWidth(); + for(auto i = CSH->si->playerInfos.cbegin(); i != CSH->si->playerInfos.cend(); i++) + { + auto flag = std::make_shared(iconsTeamFlags, i->first.getNum(), 0); + if(i->first == CSH->myFirstColor() || CSH->getPlayerTeamId(i->first) == CSH->getPlayerTeamId(CSH->myFirstColor())) + { + flag->moveTo(Point(pos.x + alliesX + flagsAllies.size()*flag->pos.w, pos.y)); + flagsAllies.push_back(flag); + } + else + { + flag->moveTo(Point(pos.x + enemiesX + flagsEnemies.size()*flag->pos.w, pos.y)); + flagsEnemies.push_back(flag); + } + } +} + +void CFlagBox::clickRight(tribool down, bool previousState) +{ + if(down && SEL->getMapInfo()) + GH.pushInt(new CFlagBoxTooltipBox(iconsTeamFlags)); +} + +CFlagBox::CFlagBoxTooltipBox::CFlagBoxTooltipBox(std::shared_ptr icons) + : CWindowObject(BORDERED | RCLICK_POPUP | SHADOW_DISABLED, "DIBOXBCK") +{ + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + pos.w = 256; + pos.h = 90 + 50 * SEL->getMapInfo()->mapHeader->howManyTeams; + + labelTeamAlignment = std::make_shared(128, 30, FONT_MEDIUM, CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[657]); + labelGroupTeams = std::make_shared(FONT_SMALL, EAlignment::CENTER, Colors::WHITE); + for(int i = 0; i < SEL->getMapInfo()->mapHeader->howManyTeams; i++) + { + std::vector flags; + labelGroupTeams->add(128, 65 + 50 * i, boost::str(boost::format(CGI->generaltexth->allTexts[656]) % (i+1))); + + for(int j = 0; j < PlayerColor::PLAYER_LIMIT_I; j++) + { + if((SEL->getPlayerInfo(j).canHumanPlay || SEL->getPlayerInfo(j).canComputerPlay) + && SEL->getPlayerInfo(j).team == TeamID(i)) + { + flags.push_back(j); + } + } + + int curx = 128 - 9 * flags.size(); + for(auto & flag : flags) + { + iconsFlags.push_back(std::make_shared(icons, flag, 0, curx, 75 + 50 * i)); + curx += 18; + } + } + background->scaleTo(Point(pos.w, pos.h)); + center(); +} diff --git a/client/lobby/CSelectionBase.h b/client/lobby/CSelectionBase.h new file mode 100644 index 000000000..497960b9f --- /dev/null +++ b/client/lobby/CSelectionBase.h @@ -0,0 +1,146 @@ +/* + * CSelectionBase.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +#include "../mainmenu/CMainMenu.h" + +class CButton; +class CTextBox; +class CTextInput; +class CAnimImage; +class CToggleGroup; +class RandomMapTab; +class OptionsTab; +class SelectionTab; +class InfoCard; +class CChatBox; +class CMapInfo; +struct StartInfo; +struct PlayerInfo; +class CLabel; +class CFlagBox; +class CLabelGroup; + +class ISelectionScreenInfo +{ +public: + ESelectionScreen screenType; + + ISelectionScreenInfo(ESelectionScreen ScreenType = ESelectionScreen::unknown); + virtual ~ISelectionScreenInfo(); + virtual const CMapInfo * getMapInfo() = 0; + virtual const StartInfo * getStartInfo() = 0; + + virtual int getCurrentDifficulty(); + virtual PlayerInfo getPlayerInfo(int color); + +}; + +/// The actual map selection screen which consists of the options and selection tab +class CSelectionBase : public CWindowObject, public ISelectionScreenInfo +{ +public: + std::shared_ptr card; + + std::shared_ptr buttonSelect; + std::shared_ptr buttonRMG; + std::shared_ptr buttonOptions; + std::shared_ptr buttonStart; + std::shared_ptr buttonBack; + + std::shared_ptr tabSel; + std::shared_ptr tabOpt; + std::shared_ptr tabRand; + std::shared_ptr curTab; + + CSelectionBase(ESelectionScreen type); + virtual void toggleTab(std::shared_ptr tab); +}; + +class InfoCard : public CIntObject +{ + std::shared_ptr playerListBg; + std::shared_ptr background; + + std::shared_ptr iconsVictoryCondition; + std::shared_ptr iconsLossCondition; + std::shared_ptr iconsMapSizes; + + std::shared_ptr labelSaveDate; + std::shared_ptr labelScenarioName; + std::shared_ptr labelScenarioDescription; + std::shared_ptr labelVictoryCondition; + std::shared_ptr labelLossCondition; + std::shared_ptr labelMapDiff; + std::shared_ptr labelPlayerDifficulty; + std::shared_ptr labelRating; + std::shared_ptr labelCampaignDescription; + + std::shared_ptr mapName; + std::shared_ptr mapDescription; + std::shared_ptr labelDifficulty; + std::shared_ptr labelDifficultyPercent; + std::shared_ptr labelVictoryConditionText; + std::shared_ptr labelLossConditionText; + + std::shared_ptr labelGroupPlayersAssigned; + std::shared_ptr labelGroupPlayersUnassigned; +public: + + bool showChat; + std::shared_ptr chat; + std::shared_ptr flagbox; + + std::shared_ptr iconDifficulty; + + InfoCard(); + void changeSelection(); + void toggleChat(); + void setChat(bool activateChat); +}; + +class CChatBox : public CIntObject +{ +public: + std::shared_ptr chatHistory; + std::shared_ptr inputBox; + + CChatBox(const Rect & rect); + + void keyPressed(const SDL_KeyboardEvent & key) override; + + void addNewMessage(const std::string & text); +}; + +class CFlagBox : public CIntObject +{ + std::shared_ptr iconsTeamFlags; + std::shared_ptr labelAllies; + std::shared_ptr labelEnemies; + std::vector> flagsAllies; + std::vector> flagsEnemies; + +public: + CFlagBox(const Rect & rect); + void recreate(); + void clickRight(tribool down, bool previousState) override; + void showTeamsPopup(); + + class CFlagBoxTooltipBox : public CWindowObject + { + std::shared_ptr labelTeamAlignment; + std::shared_ptr labelGroupTeams; + std::vector> iconsFlags; + public: + CFlagBoxTooltipBox(std::shared_ptr icons); + }; +}; + +extern ISelectionScreenInfo * SEL; diff --git a/client/lobby/OptionsTab.cpp b/client/lobby/OptionsTab.cpp new file mode 100644 index 000000000..7ff0aeefd --- /dev/null +++ b/client/lobby/OptionsTab.cpp @@ -0,0 +1,537 @@ +/* + * OptionsTab.cpp, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#include "StdInc.h" + +#include "CSelectionBase.h" +#include "OptionsTab.h" + +#include "../CBitmapHandler.h" +#include "../CGameInfo.h" +#include "../CServerHandler.h" +#include "../gui/CAnimation.h" +#include "../gui/CGuiHandler.h" +#include "../widgets/CComponent.h" +#include "../widgets/Buttons.h" +#include "../widgets/MiscWidgets.h" +#include "../widgets/ObjectLists.h" +#include "../widgets/TextControls.h" +#include "../windows/GUIClasses.h" +#include "../windows/InfoWindows.h" + +#include "../../lib/NetPacksLobby.h" +#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/CArtHandler.h" +#include "../../lib/CTownHandler.h" +#include "../../lib/CHeroHandler.h" +#include "../../lib/mapping/CMap.h" +#include "../../lib/mapping/CMapInfo.h" + +OptionsTab::OptionsTab() +{ + recActions = 0; + OBJ_CONSTRUCTION; + background = std::make_shared("ADVOPTBK", 0, 6); + pos = background->pos; + labelTitle = std::make_shared(222, 30, FONT_BIG, CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[515]); + labelSubTitle = std::make_shared(Rect(60, 44, 320, graphics->fonts[EFonts::FONT_SMALL]->getLineHeight()*2), EFonts::FONT_SMALL, EAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[516]); + + labelPlayerNameAndHandicap = std::make_shared(Rect(58, 86, 100, graphics->fonts[EFonts::FONT_SMALL]->getLineHeight()*2), EFonts::FONT_SMALL, EAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[517]); + labelStartingTown = std::make_shared(Rect(163, 86, 70, graphics->fonts[EFonts::FONT_SMALL]->getLineHeight()*2), EFonts::FONT_SMALL, EAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[518]); + labelStartingHero = std::make_shared(Rect(239, 86, 70, graphics->fonts[EFonts::FONT_SMALL]->getLineHeight()*2), EFonts::FONT_SMALL, EAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[519]); + labelStartingBonus = std::make_shared(Rect(315, 86, 70, graphics->fonts[EFonts::FONT_SMALL]->getLineHeight()*2), EFonts::FONT_SMALL, EAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[520]); + if(SEL->screenType == ESelectionScreen::newGame || SEL->screenType == ESelectionScreen::loadGame || SEL->screenType == ESelectionScreen::scenarioInfo) + { + sliderTurnDuration = std::make_shared(Point(55, 551), 194, std::bind(&IServerAPI::setTurnLength, CSH, _1), 1, GameConstants::POSSIBLE_TURNTIME.size(), GameConstants::POSSIBLE_TURNTIME.size(), true, CSlider::BLUE); + labelPlayerTurnDuration = std::make_shared(222, 538, FONT_SMALL, EAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[521]); + labelTurnDurationValue = std::make_shared(319, 559, FONT_SMALL, EAlignment::CENTER, Colors::WHITE); + } +} + +void OptionsTab::recreate() +{ + entries.clear(); + + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + for(auto & pInfo : SEL->getStartInfo()->playerInfos) + { + entries.insert(std::make_pair(pInfo.first, std::make_shared(pInfo.second))); + } + + if(sliderTurnDuration) + { + sliderTurnDuration->moveTo(vstd::find_pos(GameConstants::POSSIBLE_TURNTIME, SEL->getStartInfo()->turnTime)); + labelTurnDurationValue->setText(CGI->generaltexth->turnDurations[sliderTurnDuration->getValue()]); + } +} + +size_t OptionsTab::CPlayerSettingsHelper::getImageIndex() +{ + enum EBonusSelection //frames of bonuses file + { + WOOD_ORE = 0, CRYSTAL = 1, GEM = 2, + MERCURY = 3, SULFUR = 5, GOLD = 8, + ARTIFACT = 9, RANDOM = 10, + WOOD = 0, ORE = 0, MITHRIL = 10, // resources unavailable in bonuses file + + TOWN_RANDOM = 38, TOWN_NONE = 39, // Special frames in ITPA + HERO_RANDOM = 163, HERO_NONE = 164 // Special frames in PortraitsSmall + }; + + switch(type) + { + case TOWN: + switch(settings.castle) + { + case PlayerSettings::NONE: + return TOWN_NONE; + case PlayerSettings::RANDOM: + return TOWN_RANDOM; + default: + return CGI->townh->factions[settings.castle]->town->clientInfo.icons[true][false] + 2; + } + + case HERO: + switch(settings.hero) + { + case PlayerSettings::NONE: + return HERO_NONE; + case PlayerSettings::RANDOM: + return HERO_RANDOM; + default: + { + if(settings.heroPortrait >= 0) + return settings.heroPortrait; + return CGI->heroh->heroes[settings.hero]->imageIndex; + } + } + + case BONUS: + { + switch(settings.bonus) + { + case PlayerSettings::RANDOM: + return RANDOM; + case PlayerSettings::ARTIFACT: + return ARTIFACT; + case PlayerSettings::GOLD: + return GOLD; + case PlayerSettings::RESOURCE: + { + switch(CGI->townh->factions[settings.castle]->town->primaryRes) + { + case Res::WOOD_AND_ORE: + return WOOD_ORE; + case Res::WOOD: + return WOOD; + case Res::MERCURY: + return MERCURY; + case Res::ORE: + return ORE; + case Res::SULFUR: + return SULFUR; + case Res::CRYSTAL: + return CRYSTAL; + case Res::GEMS: + return GEM; + case Res::GOLD: + return GOLD; + case Res::MITHRIL: + return MITHRIL; + } + } + } + } + } + return 0; +} + +std::string OptionsTab::CPlayerSettingsHelper::getImageName() +{ + switch(type) + { + case OptionsTab::TOWN: + return "ITPA"; + case OptionsTab::HERO: + return "PortraitsSmall"; + case OptionsTab::BONUS: + return "SCNRSTAR"; + } + return ""; +} + +std::string OptionsTab::CPlayerSettingsHelper::getName() +{ + switch(type) + { + case TOWN: + { + switch(settings.castle) + { + case PlayerSettings::NONE: + return CGI->generaltexth->allTexts[523]; + case PlayerSettings::RANDOM: + return CGI->generaltexth->allTexts[522]; + default: + return CGI->townh->factions[settings.castle]->name; + } + } + case HERO: + { + switch(settings.hero) + { + case PlayerSettings::NONE: + return CGI->generaltexth->allTexts[523]; + case PlayerSettings::RANDOM: + return CGI->generaltexth->allTexts[522]; + default: + { + if(!settings.heroName.empty()) + return settings.heroName; + return CGI->heroh->heroes[settings.hero]->name; + } + } + } + case BONUS: + { + switch(settings.bonus) + { + case PlayerSettings::RANDOM: + return CGI->generaltexth->allTexts[522]; + default: + return CGI->generaltexth->arraytxt[214 + settings.bonus]; + } + } + } + return ""; +} + + +std::string OptionsTab::CPlayerSettingsHelper::getTitle() +{ + switch(type) + { + case OptionsTab::TOWN: + return (settings.castle < 0) ? CGI->generaltexth->allTexts[103] : CGI->generaltexth->allTexts[80]; + case OptionsTab::HERO: + return (settings.hero < 0) ? CGI->generaltexth->allTexts[101] : CGI->generaltexth->allTexts[77]; + case OptionsTab::BONUS: + { + switch(settings.bonus) + { + case PlayerSettings::RANDOM: + return CGI->generaltexth->allTexts[86]; //{Random Bonus} + case PlayerSettings::ARTIFACT: + return CGI->generaltexth->allTexts[83]; //{Artifact Bonus} + case PlayerSettings::GOLD: + return CGI->generaltexth->allTexts[84]; //{Gold Bonus} + case PlayerSettings::RESOURCE: + return CGI->generaltexth->allTexts[85]; //{Resource Bonus} + } + } + } + return ""; +} +std::string OptionsTab::CPlayerSettingsHelper::getSubtitle() +{ + switch(type) + { + case TOWN: + return getName(); + case HERO: + { + if(settings.hero >= 0) + return getName() + " - " + CGI->heroh->heroes[settings.hero]->heroClass->name; + return getName(); + } + + case BONUS: + { + switch(settings.bonus) + { + case PlayerSettings::GOLD: + return CGI->generaltexth->allTexts[87]; //500-1000 + case PlayerSettings::RESOURCE: + { + switch(CGI->townh->factions[settings.castle]->town->primaryRes) + { + case Res::MERCURY: + return CGI->generaltexth->allTexts[694]; + case Res::SULFUR: + return CGI->generaltexth->allTexts[695]; + case Res::CRYSTAL: + return CGI->generaltexth->allTexts[692]; + case Res::GEMS: + return CGI->generaltexth->allTexts[693]; + case Res::WOOD_AND_ORE: + return CGI->generaltexth->allTexts[89]; //At the start of the game, 5-10 wood and 5-10 ore are added to your Kingdom's resource pool + } + } + } + } + } + return ""; +} + +std::string OptionsTab::CPlayerSettingsHelper::getDescription() +{ + switch(type) + { + case TOWN: + return CGI->generaltexth->allTexts[104]; + case HERO: + return CGI->generaltexth->allTexts[102]; + case BONUS: + { + switch(settings.bonus) + { + case PlayerSettings::RANDOM: + return CGI->generaltexth->allTexts[94]; //Gold, wood and ore, or an artifact is randomly chosen as your starting bonus + case PlayerSettings::ARTIFACT: + return CGI->generaltexth->allTexts[90]; //An artifact is randomly chosen and equipped to your starting hero + case PlayerSettings::GOLD: + return CGI->generaltexth->allTexts[92]; //At the start of the game, 500-1000 gold is added to your Kingdom's resource pool + case PlayerSettings::RESOURCE: + { + switch(CGI->townh->factions[settings.castle]->town->primaryRes) + { + case Res::MERCURY: + return CGI->generaltexth->allTexts[690]; + case Res::SULFUR: + return CGI->generaltexth->allTexts[691]; + case Res::CRYSTAL: + return CGI->generaltexth->allTexts[688]; + case Res::GEMS: + return CGI->generaltexth->allTexts[689]; + case Res::WOOD_AND_ORE: + return CGI->generaltexth->allTexts[93]; //At the start of the game, 5-10 wood and 5-10 ore are added to your Kingdom's resource pool + } + } + } + } + } + return ""; +} + +OptionsTab::CPlayerOptionTooltipBox::CPlayerOptionTooltipBox(CPlayerSettingsHelper & helper) + : CWindowObject(BORDERED | RCLICK_POPUP), CPlayerSettingsHelper(helper) +{ + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + + int value = PlayerSettings::NONE; + + switch(CPlayerSettingsHelper::type) + { + break; + case TOWN: + value = settings.castle; + break; + case HERO: + value = settings.hero; + break; + case BONUS: + value = settings.bonus; + } + + if(value == PlayerSettings::RANDOM) + genBonusWindow(); + else if(CPlayerSettingsHelper::type == BONUS) + genBonusWindow(); + else if(CPlayerSettingsHelper::type == HERO) + genHeroWindow(); + else if(CPlayerSettingsHelper::type == TOWN) + genTownWindow(); + + center(); +} + +void OptionsTab::CPlayerOptionTooltipBox::genHeader() +{ + backgroundTexture = std::make_shared("DIBOXBCK", pos); + updateShadow(); + + labelTitle = std::make_shared(pos.w / 2 + 8, 21, FONT_MEDIUM, CENTER, Colors::YELLOW, getTitle()); + labelSubTitle = std::make_shared(pos.w / 2, 88, FONT_SMALL, CENTER, Colors::WHITE, getSubtitle()); + image = std::make_shared(getImageName(), getImageIndex(), 0, pos.w / 2 - 24, 45); +} + +void OptionsTab::CPlayerOptionTooltipBox::genTownWindow() +{ + pos = Rect(0, 0, 228, 290); + genHeader(); + labelAssociatedCreatures = std::make_shared(pos.w / 2 + 8, 122, FONT_MEDIUM, CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[79]); + + std::vector components; + const CTown * town = CGI->townh->factions[settings.castle]->town; + for(auto & elem : town->creatures) + { + if(!elem.empty()) + components.push_back(new CComponent(CComponent::creature, elem.front(), 0, CComponent::tiny)); + } + boxAssociatedCreatures = std::make_shared(components, Rect(10, 140, pos.w - 20, 140)); +} + +void OptionsTab::CPlayerOptionTooltipBox::genHeroWindow() +{ + pos = Rect(0, 0, 292, 226); + genHeader(); + labelHeroSpeciality = std::make_shared(pos.w / 2 + 4, 117, FONT_MEDIUM, CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[78]); + + imageSpeciality = std::make_shared("UN44", CGI->heroh->heroes[settings.hero]->imageIndex, 0, pos.w / 2 - 22, 134); + labelSpecialityName = std::make_shared(pos.w / 2, 188, FONT_SMALL, CENTER, Colors::WHITE, CGI->heroh->heroes[settings.hero]->specName); +} + +void OptionsTab::CPlayerOptionTooltipBox::genBonusWindow() +{ + pos = Rect(0, 0, 228, 162); + genHeader(); + + textBonusDescription = std::make_shared(getDescription(), Rect(10, 100, pos.w - 20, 70), 0, FONT_SMALL, CENTER, Colors::WHITE); +} + +OptionsTab::SelectedBox::SelectedBox(Point position, PlayerSettings & settings, SelType type) + : CIntObject(RCLICK, position), CPlayerSettingsHelper(settings, type) +{ + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + + image = std::make_shared(getImageName(), getImageIndex()); + subtitle = std::make_shared(23, 39, FONT_TINY, CENTER, Colors::WHITE, getName()); + + pos = image->pos; +} + +void OptionsTab::SelectedBox::update() +{ + image->setFrame(getImageIndex()); + subtitle->setText(getName()); +} + +void OptionsTab::SelectedBox::clickRight(tribool down, bool previousState) +{ + if(down) + { + // cases when we do not need to display a message + if(settings.castle == -2 && CPlayerSettingsHelper::type == TOWN) + return; + if(settings.hero == -2 && !SEL->getPlayerInfo(settings.color.getNum()).hasCustomMainHero() && CPlayerSettingsHelper::type == HERO) + return; + + GH.pushInt(new CPlayerOptionTooltipBox(*this)); + } +} + +OptionsTab::PlayerOptionsEntry::PlayerOptionsEntry(const PlayerSettings & S) + : pi(SEL->getPlayerInfo(S.color.getNum())), s(S) +{ + OBJ_CONSTRUCTION; + defActions |= SHARE_POS; + + int serial = 0; + for(int g = 0; g < s.color.getNum(); ++g) + { + auto itred = SEL->getPlayerInfo(g); + if(itred.canComputerPlay || itred.canHumanPlay) + serial++; + } + + pos.x += 54; + pos.y += 122 + serial * 50; + + assert(CSH->mi && CSH->mi->mapHeader); + const PlayerInfo & p = SEL->getPlayerInfo(s.color.getNum()); + assert(p.canComputerPlay || p.canHumanPlay); //someone must be able to control this player + if(p.canHumanPlay && p.canComputerPlay) + whoCanPlay = HUMAN_OR_CPU; + else if(p.canComputerPlay) + whoCanPlay = CPU; + else + whoCanPlay = HUMAN; + + static const char * flags[] = + { + "AOFLGBR.DEF", "AOFLGBB.DEF", "AOFLGBY.DEF", "AOFLGBG.DEF", + "AOFLGBO.DEF", "AOFLGBP.DEF", "AOFLGBT.DEF", "AOFLGBS.DEF" + }; + static const char * bgs[] = + { + "ADOPRPNL.bmp", "ADOPBPNL.bmp", "ADOPYPNL.bmp", "ADOPGPNL.bmp", + "ADOPOPNL.bmp", "ADOPPPNL.bmp", "ADOPTPNL.bmp", "ADOPSPNL.bmp" + }; + + background = std::make_shared(BitmapHandler::loadBitmap(bgs[s.color.getNum()]), 0, 0, true); + labelPlayerName = std::make_shared(55, 10, EFonts::FONT_SMALL, EAlignment::CENTER, Colors::WHITE, s.name); + labelWhoCanPlay = std::make_shared(Rect(6, 23, 45, graphics->fonts[EFonts::FONT_TINY]->getLineHeight()*2), EFonts::FONT_TINY, EAlignment::CENTER, Colors::WHITE, CGI->generaltexth->arraytxt[206 + whoCanPlay]); + + if(SEL->screenType == ESelectionScreen::newGame) + { + buttonTownLeft = std::make_shared(Point(107, 5), "ADOPLFA.DEF", CGI->generaltexth->zelp[132], std::bind(&IServerAPI::setPlayerOption, CSH, LobbyChangePlayerOption::TOWN, -1, s.color)); + buttonTownRight = std::make_shared(Point(168, 5), "ADOPRTA.DEF", CGI->generaltexth->zelp[133], std::bind(&IServerAPI::setPlayerOption, CSH, LobbyChangePlayerOption::TOWN, +1, s.color)); + buttonHeroLeft = std::make_shared(Point(183, 5), "ADOPLFA.DEF", CGI->generaltexth->zelp[148], std::bind(&IServerAPI::setPlayerOption, CSH, LobbyChangePlayerOption::HERO, -1, s.color)); + buttonHeroRight = std::make_shared(Point(244, 5), "ADOPRTA.DEF", CGI->generaltexth->zelp[149], std::bind(&IServerAPI::setPlayerOption, CSH, LobbyChangePlayerOption::HERO, +1, s.color)); + buttonBonusLeft = std::make_shared(Point(259, 5), "ADOPLFA.DEF", CGI->generaltexth->zelp[164], std::bind(&IServerAPI::setPlayerOption, CSH, LobbyChangePlayerOption::BONUS, -1, s.color)); + buttonBonusRight = std::make_shared(Point(320, 5), "ADOPRTA.DEF", CGI->generaltexth->zelp[165], std::bind(&IServerAPI::setPlayerOption, CSH, LobbyChangePlayerOption::BONUS, +1, s.color)); + } + + hideUnavailableButtons(); + + if(SEL->screenType != ESelectionScreen::scenarioInfo && SEL->getPlayerInfo(s.color.getNum()).canHumanPlay) + { + flag = std::make_shared(Point(-43, 2), flags[s.color.getNum()], CGI->generaltexth->zelp[180], std::bind(&IServerAPI::setPlayer, CSH, s.color)); + flag->hoverable = true; + flag->block(CSH->isGuest()); + } + else + flag = nullptr; + + town = std::make_shared(Point(119, 2), s, TOWN); + hero = std::make_shared(Point(195, 2), s, HERO); + bonus = std::make_shared(Point(271, 2), s, BONUS); +} + +void OptionsTab::PlayerOptionsEntry::hideUnavailableButtons() +{ + if(!buttonTownLeft) + return; + + const bool foreignPlayer = CSH->isGuest() && !CSH->isMyColor(s.color); + + if((pi.allowedFactions.size() < 2 && !pi.isFactionRandom) || foreignPlayer) + { + buttonTownLeft->disable(); + buttonTownRight->disable(); + } + else + { + buttonTownLeft->enable(); + buttonTownRight->enable(); + } + + if((pi.defaultHero() != -1 || s.castle < 0) //fixed hero + || foreignPlayer) //or not our player + { + buttonHeroLeft->disable(); + buttonHeroRight->disable(); + } + else + { + buttonHeroLeft->enable(); + buttonHeroRight->enable(); + } + + if(foreignPlayer) + { + buttonBonusLeft->disable(); + buttonBonusRight->disable(); + } + else + { + buttonBonusLeft->enable(); + buttonBonusRight->enable(); + } +} diff --git a/client/lobby/OptionsTab.h b/client/lobby/OptionsTab.h new file mode 100644 index 000000000..8e5db6e90 --- /dev/null +++ b/client/lobby/OptionsTab.h @@ -0,0 +1,127 @@ +/* + * OptionsTab.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +#include "../../lib/StartInfo.h" +#include "../../lib/mapping/CMap.h" + +class CSlider; +class CLabel; +class CMultiLineLabel; +class CFilledTexture; +class CAnimImage; +class CComponentBox; +/// The options tab which is shown at the map selection phase. +class OptionsTab : public CIntObject +{ + std::shared_ptr background; + std::shared_ptr labelTitle; + std::shared_ptr labelSubTitle; + std::shared_ptr labelPlayerNameAndHandicap; + std::shared_ptr labelStartingTown; + std::shared_ptr labelStartingHero; + std::shared_ptr labelStartingBonus; + + std::shared_ptr labelPlayerTurnDuration; + std::shared_ptr labelTurnDurationValue; + +public: + enum SelType + { + TOWN, + HERO, + BONUS + }; + + struct CPlayerSettingsHelper + { + const PlayerSettings & settings; + const SelType type; + + CPlayerSettingsHelper(const PlayerSettings & settings, SelType type) + : settings(settings), type(type) + {} + + /// visible image settings + size_t getImageIndex(); + std::string getImageName(); + + std::string getName(); /// name visible in options dialog + std::string getTitle(); /// title in popup box + std::string getSubtitle(); /// popup box subtitle + std::string getDescription(); /// popup box description, not always present + }; + + class CPlayerOptionTooltipBox : public CWindowObject, public CPlayerSettingsHelper + { + std::shared_ptr backgroundTexture; + std::shared_ptr labelTitle; + std::shared_ptr labelSubTitle; + std::shared_ptr image; + + std::shared_ptr labelAssociatedCreatures; + std::shared_ptr boxAssociatedCreatures; + + std::shared_ptr labelHeroSpeciality; + std::shared_ptr imageSpeciality; + std::shared_ptr labelSpecialityName; + + std::shared_ptr textBonusDescription; + + void genHeader(); + void genTownWindow(); + void genHeroWindow(); + void genBonusWindow(); + + public: + CPlayerOptionTooltipBox(CPlayerSettingsHelper & helper); + }; + + /// Image with current town/hero/bonus + struct SelectedBox : public CIntObject, public CPlayerSettingsHelper + { + std::shared_ptr image; + std::shared_ptr subtitle; + + SelectedBox(Point position, PlayerSettings & settings, SelType type); + void clickRight(tribool down, bool previousState) override; + + void update(); + }; + + struct PlayerOptionsEntry : public CIntObject + { + PlayerInfo pi; + PlayerSettings s; + std::shared_ptr labelPlayerName; + std::shared_ptr labelWhoCanPlay; + std::shared_ptr background; + std::shared_ptr buttonTownLeft; + std::shared_ptr buttonTownRight; + std::shared_ptr buttonHeroLeft; + std::shared_ptr buttonHeroRight; + std::shared_ptr buttonBonusLeft; + std::shared_ptr buttonBonusRight; + std::shared_ptr flag; + std::shared_ptr town; + std::shared_ptr hero; + std::shared_ptr bonus; + enum {HUMAN_OR_CPU, HUMAN, CPU} whoCanPlay; + + PlayerOptionsEntry(const PlayerSettings & S); + void hideUnavailableButtons(); + }; + + std::shared_ptr sliderTurnDuration; + std::map> entries; + + OptionsTab(); + void recreate(); +}; diff --git a/client/lobby/RandomMapTab.cpp b/client/lobby/RandomMapTab.cpp new file mode 100644 index 000000000..8879ddc25 --- /dev/null +++ b/client/lobby/RandomMapTab.cpp @@ -0,0 +1,300 @@ +/* + * RandomMapTab.cpp, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#include "StdInc.h" + +#include "RandomMapTab.h" +#include "CSelectionBase.h" + +#include "../CGameInfo.h" +#include "../CServerHandler.h" +#include "../gui/CAnimation.h" +#include "../gui/CGuiHandler.h" +#include "../widgets/CComponent.h" +#include "../widgets/Buttons.h" +#include "../widgets/MiscWidgets.h" +#include "../widgets/ObjectLists.h" +#include "../widgets/TextControls.h" +#include "../windows/GUIClasses.h" +#include "../windows/InfoWindows.h" + +#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/mapping/CMapInfo.h" +#include "../../lib/rmg/CMapGenOptions.h" + +RandomMapTab::RandomMapTab() +{ + recActions = 0; + mapGenOptions = std::make_shared(); + OBJ_CONSTRUCTION; + background = std::make_shared("RANMAPBK", 0, 6); + + labelHeadlineBig = std::make_shared(222, 36, FONT_BIG, EAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[738]); + labelHeadlineSmall = std::make_shared(222, 56, FONT_SMALL, EAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[739]); + + labelMapSize = std::make_shared(104, 97, FONT_SMALL, EAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[752]); + groupMapSize = std::make_shared(0); + groupMapSize->pos.y += 81; + groupMapSize->pos.x += 158; + const std::vector mapSizeBtns = {"RANSIZS", "RANSIZM", "RANSIZL", "RANSIZX"}; + addButtonsToGroup(groupMapSize.get(), mapSizeBtns, 0, 3, 47, 198); + groupMapSize->setSelected(1); + groupMapSize->addCallback([&](int btnId) + { + auto mapSizeVal = getPossibleMapSizes(); + mapGenOptions->setWidth(mapSizeVal[btnId]); + mapGenOptions->setHeight(mapSizeVal[btnId]); + updateMapInfoByHost(); + }); + + buttonTwoLevels = std::make_shared(Point(346, 81), "RANUNDR", CGI->generaltexth->zelp[202]); + buttonTwoLevels->setSelected(true); + buttonTwoLevels->addCallback([&](bool on) + { + mapGenOptions->setHasTwoLevels(on); + updateMapInfoByHost(); + }); + + labelGroupForOptions = std::make_shared(FONT_SMALL, EAlignment::TOPLEFT, Colors::WHITE); + // Create number defs list + std::vector numberDefs; + for(int i = 0; i <= 8; ++i) + { + numberDefs.push_back("RANNUM" + boost::lexical_cast(i)); + } + + const int NUMBERS_WIDTH = 32; + const int BTNS_GROUP_LEFT_MARGIN = 67; + labelGroupForOptions->add(68, 133, CGI->generaltexth->allTexts[753]); + groupMaxPlayers = std::make_shared(0); + groupMaxPlayers->pos.y += 153; + groupMaxPlayers->pos.x += BTNS_GROUP_LEFT_MARGIN; + addButtonsWithRandToGroup(groupMaxPlayers.get(), numberDefs, 1, 8, NUMBERS_WIDTH, 204, 212); + groupMaxPlayers->addCallback([&](int btnId) + { + mapGenOptions->setPlayerCount(btnId); + deactivateButtonsFrom(groupMaxTeams.get(), btnId); + deactivateButtonsFrom(groupCompOnlyPlayers.get(), btnId); + validatePlayersCnt(btnId); + updateMapInfoByHost(); + }); + + labelGroupForOptions->add(68, 199, CGI->generaltexth->allTexts[754]); + groupMaxTeams = std::make_shared(0); + groupMaxTeams->pos.y += 219; + groupMaxTeams->pos.x += BTNS_GROUP_LEFT_MARGIN; + addButtonsWithRandToGroup(groupMaxTeams.get(), numberDefs, 0, 7, NUMBERS_WIDTH, 214, 222); + groupMaxTeams->addCallback([&](int btnId) + { + mapGenOptions->setTeamCount(btnId); + updateMapInfoByHost(); + }); + + labelGroupForOptions->add(68, 265, CGI->generaltexth->allTexts[755]); + groupCompOnlyPlayers = std::make_shared(0); + groupCompOnlyPlayers->pos.y += 285; + groupCompOnlyPlayers->pos.x += BTNS_GROUP_LEFT_MARGIN; + addButtonsWithRandToGroup(groupCompOnlyPlayers.get(), numberDefs, 0, 7, NUMBERS_WIDTH, 224, 232); + groupCompOnlyPlayers->addCallback([&](int btnId) + { + mapGenOptions->setCompOnlyPlayerCount(btnId); + deactivateButtonsFrom(groupCompOnlyTeams.get(), (btnId == 0 ? 1 : btnId)); + validateCompOnlyPlayersCnt(btnId); + updateMapInfoByHost(); + }); + + labelGroupForOptions->add(68, 331, CGI->generaltexth->allTexts[756]); + groupCompOnlyTeams = std::make_shared(0); + groupCompOnlyTeams->pos.y += 351; + groupCompOnlyTeams->pos.x += BTNS_GROUP_LEFT_MARGIN; + addButtonsWithRandToGroup(groupCompOnlyTeams.get(), numberDefs, 0, 6, NUMBERS_WIDTH, 234, 241); + deactivateButtonsFrom(groupCompOnlyTeams.get(), 1); + groupCompOnlyTeams->addCallback([&](int btnId) + { + mapGenOptions->setCompOnlyTeamCount(btnId); + updateMapInfoByHost(); + }); + + labelGroupForOptions->add(68, 398, CGI->generaltexth->allTexts[757]); + const int WIDE_BTN_WIDTH = 85; + groupWaterContent = std::make_shared(0); + groupWaterContent->pos.y += 419; + groupWaterContent->pos.x += BTNS_GROUP_LEFT_MARGIN; + const std::vector waterContentBtns = {"RANNONE", "RANNORM", "RANISLD"}; + addButtonsWithRandToGroup(groupWaterContent.get(), waterContentBtns, 0, 2, WIDE_BTN_WIDTH, 243, 246); + groupWaterContent->addCallback([&](int btnId) + { + mapGenOptions->setWaterContent(static_cast(btnId)); + updateMapInfoByHost(); + }); + + labelGroupForOptions->add(68, 465, CGI->generaltexth->allTexts[758]); + groupMonsterStrength = std::make_shared(0); + groupMonsterStrength->pos.y += 485; + groupMonsterStrength->pos.x += BTNS_GROUP_LEFT_MARGIN; + const std::vector monsterStrengthBtns = {"RANWEAK", "RANNORM", "RANSTRG"}; + addButtonsWithRandToGroup(groupMonsterStrength.get(), monsterStrengthBtns, 0, 2, WIDE_BTN_WIDTH, 248, 251); + groupMonsterStrength->addCallback([&](int btnId) + { + if(btnId < 0) + mapGenOptions->setMonsterStrength(EMonsterStrength::RANDOM); + else + mapGenOptions->setMonsterStrength(static_cast(btnId + EMonsterStrength::GLOBAL_WEAK)); //value 2 to 4 + updateMapInfoByHost(); + }); + + buttonShowRandomMaps = std::make_shared(Point(54, 535), "RANSHOW", CGI->generaltexth->zelp[252]); + + updateMapInfoByHost(); +} + +void RandomMapTab::updateMapInfoByHost() +{ + if(CSH->isGuest()) + return; + + // Generate header info + mapInfo = std::make_shared(); + mapInfo->isRandomMap = true; + mapInfo->mapHeader = make_unique(); + mapInfo->mapHeader->version = EMapFormat::SOD; + mapInfo->mapHeader->name = CGI->generaltexth->allTexts[740]; + mapInfo->mapHeader->description = CGI->generaltexth->allTexts[741]; + mapInfo->mapHeader->difficulty = 1; // Normal + mapInfo->mapHeader->height = mapGenOptions->getHeight(); + mapInfo->mapHeader->width = mapGenOptions->getWidth(); + mapInfo->mapHeader->twoLevel = mapGenOptions->getHasTwoLevels(); + + // Generate player information + mapInfo->mapHeader->players.clear(); + int playersToGen = PlayerColor::PLAYER_LIMIT_I; + if(mapGenOptions->getPlayerCount() != CMapGenOptions::RANDOM_SIZE) + playersToGen = mapGenOptions->getPlayerCount(); + mapInfo->mapHeader->howManyTeams = playersToGen; + + for(int i = 0; i < playersToGen; ++i) + { + PlayerInfo player; + player.isFactionRandom = true; + player.canComputerPlay = true; + if(mapGenOptions->getCompOnlyPlayerCount() != CMapGenOptions::RANDOM_SIZE && i >= mapGenOptions->getHumanOnlyPlayerCount()) + { + player.canHumanPlay = false; + } + else + { + player.canHumanPlay = true; + } + player.team = TeamID(i); + player.hasMainTown = true; + player.generateHeroAtMainTown = true; + mapInfo->mapHeader->players.push_back(player); + } + + mapInfoChanged(mapInfo, mapGenOptions); +} + +void RandomMapTab::setMapGenOptions(std::shared_ptr opts) +{ + groupMapSize->setSelected(vstd::find_pos(getPossibleMapSizes(), opts->getWidth())); + buttonTwoLevels->setSelected(opts->getHasTwoLevels()); + groupMaxPlayers->setSelected(opts->getPlayerCount()); + groupMaxTeams->setSelected(opts->getTeamCount()); + groupCompOnlyPlayers->setSelected(opts->getCompOnlyPlayerCount()); + groupCompOnlyTeams->setSelected(opts->getCompOnlyTeamCount()); + groupWaterContent->setSelected(opts->getWaterContent()); + groupMonsterStrength->setSelected(opts->getMonsterStrength()); +} + +void RandomMapTab::addButtonsWithRandToGroup(CToggleGroup * group, const std::vector & defs, int nStart, int nEnd, int btnWidth, int helpStartIndex, int helpRandIndex) const +{ + addButtonsToGroup(group, defs, nStart, nEnd, btnWidth, helpStartIndex); + + // Buttons are relative to button group, TODO better solution? + SObjectConstruction obj__i(group); + const std::string RANDOM_DEF = "RANRAND"; + group->addToggle(CMapGenOptions::RANDOM_SIZE, new CToggleButton(Point(256, 0), RANDOM_DEF, CGI->generaltexth->zelp[helpRandIndex])); + group->setSelected(CMapGenOptions::RANDOM_SIZE); +} + +void RandomMapTab::addButtonsToGroup(CToggleGroup * group, const std::vector & defs, int nStart, int nEnd, int btnWidth, int helpStartIndex) const +{ + // Buttons are relative to button group, TODO better solution? + SObjectConstruction obj__i(group); + int cnt = nEnd - nStart + 1; + for(int i = 0; i < cnt; ++i) + { + auto button = new CToggleButton(Point(i * btnWidth, 0), defs[i + nStart], CGI->generaltexth->zelp[helpStartIndex + i]); + // For blocked state we should use pressed image actually + button->setImageOrder(0, 1, 1, 3); + + group->addToggle(i + nStart, button); + } +} + +void RandomMapTab::deactivateButtonsFrom(CToggleGroup * group, int startId) +{ + logGlobal->debug("Blocking buttons from %d", startId); + for(auto toggle : group->buttons) + { + if(auto button = dynamic_cast(toggle.second)) + { + if(startId == CMapGenOptions::RANDOM_SIZE || toggle.first < startId) + { + button->block(false); + } + else + { + button->block(true); + } + } + } +} + +void RandomMapTab::validatePlayersCnt(int playersCnt) +{ + if(playersCnt == CMapGenOptions::RANDOM_SIZE) + { + return; + } + + if(mapGenOptions->getTeamCount() >= playersCnt) + { + mapGenOptions->setTeamCount(playersCnt - 1); + groupMaxTeams->setSelected(mapGenOptions->getTeamCount()); + } + if(mapGenOptions->getCompOnlyPlayerCount() >= playersCnt) + { + mapGenOptions->setCompOnlyPlayerCount(playersCnt - 1); + groupCompOnlyPlayers->setSelected(mapGenOptions->getCompOnlyPlayerCount()); + } + + validateCompOnlyPlayersCnt(mapGenOptions->getCompOnlyPlayerCount()); +} + +void RandomMapTab::validateCompOnlyPlayersCnt(int compOnlyPlayersCnt) +{ + if(compOnlyPlayersCnt == CMapGenOptions::RANDOM_SIZE) + { + return; + } + + if(mapGenOptions->getCompOnlyTeamCount() >= compOnlyPlayersCnt) + { + int compOnlyTeamCount = compOnlyPlayersCnt == 0 ? 0 : compOnlyPlayersCnt - 1; + mapGenOptions->setCompOnlyTeamCount(compOnlyTeamCount); + updateMapInfoByHost(); + groupCompOnlyTeams->setSelected(compOnlyTeamCount); + } +} + +std::vector RandomMapTab::getPossibleMapSizes() +{ + return {CMapHeader::MAP_SIZE_SMALL, CMapHeader::MAP_SIZE_MIDDLE, CMapHeader::MAP_SIZE_LARGE, CMapHeader::MAP_SIZE_XLARGE}; +} diff --git a/client/lobby/RandomMapTab.h b/client/lobby/RandomMapTab.h new file mode 100644 index 000000000..5297f472a --- /dev/null +++ b/client/lobby/RandomMapTab.h @@ -0,0 +1,58 @@ +/* + * RandomMapTab.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +#include "CSelectionBase.h" + +#include "../../lib/FunctionList.h" + +class CMapGenOptions; +class CToggleButton; +class CLabel; +class CLabelGroup; + +class RandomMapTab : public CIntObject +{ +public: + RandomMapTab(); + + void updateMapInfoByHost(); + void setMapGenOptions(std::shared_ptr opts); + + CFunctionList, std::shared_ptr)> mapInfoChanged; + +private: + void addButtonsWithRandToGroup(CToggleGroup * group, const std::vector & defs, int startIndex, int endIndex, int btnWidth, int helpStartIndex, int helpRandIndex) const; + void addButtonsToGroup(CToggleGroup * group, const std::vector & defs, int startIndex, int endIndex, int btnWidth, int helpStartIndex) const; + void deactivateButtonsFrom(CToggleGroup * group, int startId); + void validatePlayersCnt(int playersCnt); + void validateCompOnlyPlayersCnt(int compOnlyPlayersCnt); + std::vector getPossibleMapSizes(); + + + std::shared_ptr background; + std::shared_ptr labelHeadlineBig; + std::shared_ptr labelHeadlineSmall; + + std::shared_ptr labelMapSize; + std::shared_ptr groupMapSize; + std::shared_ptr buttonTwoLevels; + + std::shared_ptr labelGroupForOptions; + std::shared_ptr groupMaxPlayers; + std::shared_ptr groupMaxTeams; + std::shared_ptr groupCompOnlyPlayers; + std::shared_ptr groupCompOnlyTeams; + std::shared_ptr groupWaterContent; + std::shared_ptr groupMonsterStrength; + std::shared_ptr buttonShowRandomMaps; + std::shared_ptr mapGenOptions; + std::shared_ptr mapInfo; +}; diff --git a/client/lobby/SelectionTab.cpp b/client/lobby/SelectionTab.cpp new file mode 100644 index 000000000..6d85bcc2d --- /dev/null +++ b/client/lobby/SelectionTab.cpp @@ -0,0 +1,664 @@ +/* + * SelectionTab.cpp, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#include "StdInc.h" + +#include "SelectionTab.h" +#include "CSelectionBase.h" +#include "CLobbyScreen.h" + +#include "../CGameInfo.h" +#include "../CMessage.h" +#include "../CBitmapHandler.h" +#include "../CPlayerInterface.h" +#include "../CServerHandler.h" +#include "../gui/CAnimation.h" +#include "../gui/CGuiHandler.h" +#include "../widgets/CComponent.h" +#include "../widgets/Buttons.h" +#include "../widgets/MiscWidgets.h" +#include "../widgets/ObjectLists.h" +#include "../widgets/TextControls.h" +#include "../windows/GUIClasses.h" +#include "../windows/InfoWindows.h" + +#include "../../CCallback.h" + +#include "../../lib/NetPacksLobby.h" +#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/CModHandler.h" +#include "../../lib/filesystem/Filesystem.h" +#include "../../lib/mapping/CMapInfo.h" +#include "../../lib/serializer/Connection.h" + + +bool mapSorter::operator()(const std::shared_ptr aaa, const std::shared_ptr bbb) +{ + auto a = aaa->mapHeader.get(); + auto b = bbb->mapHeader.get(); + if(a && b) //if we are sorting scenarios + { + switch(sortBy) + { + case _format: //by map format (RoE, WoG, etc) + return (a->version < b->version); + break; + case _loscon: //by loss conditions + return (a->defeatMessage < b->defeatMessage); + break; + case _playerAm: //by player amount + int playerAmntB, humenPlayersB, playerAmntA, humenPlayersA; + playerAmntB = humenPlayersB = playerAmntA = humenPlayersA = 0; + for(int i = 0; i < 8; i++) + { + if(a->players[i].canHumanPlay) + { + playerAmntA++; + humenPlayersA++; + } + else if(a->players[i].canComputerPlay) + { + playerAmntA++; + } + if(b->players[i].canHumanPlay) + { + playerAmntB++; + humenPlayersB++; + } + else if(b->players[i].canComputerPlay) + { + playerAmntB++; + } + } + if(playerAmntB != playerAmntA) + return (playerAmntA < playerAmntB); + else + return (humenPlayersA < humenPlayersB); + break; + case _size: //by size of map + return (a->width < b->width); + break; + case _viccon: //by victory conditions + return (a->victoryMessage < b->victoryMessage); + break; + case _name: //by name + return boost::ilexicographical_compare(a->name, b->name); + case _fileName: //by filename + return boost::ilexicographical_compare(aaa->fileURI, bbb->fileURI); + default: + return boost::ilexicographical_compare(a->name, b->name); + } + } + else //if we are sorting campaigns + { + switch(sortBy) + { + case _numOfMaps: //by number of maps in campaign + return CGI->generaltexth->campaignRegionNames[aaa->campaignHeader->mapVersion].size() < + CGI->generaltexth->campaignRegionNames[bbb->campaignHeader->mapVersion].size(); + break; + case _name: //by name + return boost::ilexicographical_compare(aaa->campaignHeader->name, bbb->campaignHeader->name); + default: + return boost::ilexicographical_compare(aaa->campaignHeader->name, bbb->campaignHeader->name); + } + } +} + +SelectionTab::SelectionTab(ESelectionScreen Type) + : CIntObject(LCLICK | WHEEL | KEYBOARD | DOUBLECLICK), callOnSelect(nullptr), tabType(Type), selectionPos(0), sortModeAscending(true) +{ + OBJ_CONSTRUCTION; + if(tabType != ESelectionScreen::campaignList) + { + sortingBy = _format; + background = std::make_shared("SCSELBCK.bmp", 0, 6); + pos = background->pos; + inputName = std::make_shared(Rect(32, 539, 350, 20), Point(-32, -25), "GSSTRIP.bmp", 0); + inputName->filters += CTextInput::filenameFilter; + labelMapSizes = std::make_shared(87, 62, FONT_SMALL, EAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[510]); + + int sizes[] = {36, 72, 108, 144, 0}; + const char * filterIconNmes[] = {"SCSMBUT.DEF", "SCMDBUT.DEF", "SCLGBUT.DEF", "SCXLBUT.DEF", "SCALBUT.DEF"}; + for(int i = 0; i < 5; i++) + buttonsSortBy.push_back(std::make_shared(Point(158 + 47 * i, 46), filterIconNmes[i], CGI->generaltexth->zelp[54 + i], std::bind(&SelectionTab::filter, this, sizes[i], true))); + + int xpos[] = {23, 55, 88, 121, 306, 339}; + const char * sortIconNames[] = {"SCBUTT1.DEF", "SCBUTT2.DEF", "SCBUTCP.DEF", "SCBUTT3.DEF", "SCBUTT4.DEF", "SCBUTT5.DEF"}; + for(int i = 0; i < 6; i++) + { + ESortBy criteria = (ESortBy)i; + if(criteria == _name) + criteria = generalSortingBy; + + buttonsSortBy.push_back(std::make_shared(Point(xpos[i], 86), sortIconNames[i], CGI->generaltexth->zelp[107 + i], std::bind(&SelectionTab::sortBy, this, criteria))); + } + } + + int positionsToShow = 18; + std::string tabTitle; + switch(tabType) + { + case ESelectionScreen::newGame: + generalSortingBy = ESortBy::_name; + tabTitle = CGI->generaltexth->arraytxt[229]; + break; + case ESelectionScreen::loadGame: + generalSortingBy = ESortBy::_fileName; + tabTitle = CGI->generaltexth->arraytxt[230]; + break; + case ESelectionScreen::saveGame: + positionsToShow = 16; + generalSortingBy = ESortBy::_fileName; + tabTitle = CGI->generaltexth->arraytxt[231]; + break; + case ESelectionScreen::campaignList: + generalSortingBy = ESortBy::_name; + tabTitle = CGI->generaltexth->allTexts[726]; + type |= REDRAW_PARENT; // we use parent background so we need to make sure it's will be redrawn too + pos.w = parent->pos.w; + pos.h = parent->pos.h; + pos.x += 3; + pos.y += 6; + + buttonsSortBy.push_back(std::make_shared(Point(23, 86), "CamCusM.DEF", CButton::tooltip(), std::bind(&SelectionTab::sortBy, this, _numOfMaps))); + buttonsSortBy.push_back(std::make_shared(Point(55, 86), "CamCusL.DEF", CButton::tooltip(), std::bind(&SelectionTab::sortBy, this, _name))); + break; + default: + assert(0); + break; + } + + iconsMapFormats = std::make_shared("SCSELC.DEF"); + iconsMapFormats->preload(); + iconsVictoryCondition = std::make_shared("SCNRVICT.DEF"); + iconsVictoryCondition->preload(); + iconsLossCondition = std::make_shared("SCNRLOSS.DEF"); + iconsLossCondition->preload(); + for(int i = 0; i < positionsToShow; i++) + listItems.push_back(std::make_shared(Point(30, 129 + i * 25), iconsMapFormats, iconsVictoryCondition, iconsLossCondition)); + + labelTabTitle = std::make_shared(205, 28, FONT_MEDIUM, EAlignment::CENTER, Colors::YELLOW, tabTitle); + slider = std::make_shared(Point(372, 86), tabType != ESelectionScreen::saveGame ? 480 : 430, std::bind(&SelectionTab::sliderMove, this, _1), positionsToShow, curItems.size(), 0, false, CSlider::BLUE); + filter(0); +} + +void SelectionTab::toggleMode() +{ + if(CSH->isGuest()) + { + allItems.clear(); + curItems.clear(); + if(slider) + slider->block(true); + } + else + { + switch(tabType) + { + case ESelectionScreen::newGame: + inputName->disable(); + parseMaps(getFiles("Maps/", EResType::MAP)); + break; + + case ESelectionScreen::loadGame: + inputName->disable(); + parseSaves(getFiles("Saves/", EResType::CLIENT_SAVEGAME)); + break; + + case ESelectionScreen::saveGame: + parseSaves(getFiles("Saves/", EResType::CLIENT_SAVEGAME)); + inputName->enable(); + restoreLastSelection(); + break; + + case ESelectionScreen::campaignList: + parseCampaigns(getFiles("Maps/", EResType::CAMPAIGN)); + break; + + default: + assert(0); + break; + } + if(slider) + { + slider->block(false); + filter(0); + } + + if(CSH->campaignStateToSend) + { + CSH->setCampaignState(CSH->campaignStateToSend); + CSH->campaignStateToSend.reset(); + } + else + { + restoreLastSelection(); + } + } + slider->setAmount(curItems.size()); + updateListItems(); + redraw(); +} + +void SelectionTab::clickLeft(tribool down, bool previousState) +{ + if(down) + { + int line = getLine(); + if(line != -1) + select(line); + } +} +void SelectionTab::keyPressed(const SDL_KeyboardEvent & key) +{ + if(key.state != SDL_PRESSED) + return; + + int moveBy = 0; + switch(key.keysym.sym) + { + case SDLK_UP: + moveBy = -1; + break; + case SDLK_DOWN: + moveBy = +1; + break; + case SDLK_PAGEUP: + moveBy = -listItems.size() + 1; + break; + case SDLK_PAGEDOWN: + moveBy = +listItems.size() - 1; + break; + case SDLK_HOME: + select(-slider->getValue()); + return; + case SDLK_END: + select(curItems.size() - slider->getValue()); + return; + default: + return; + } + select(selectionPos - slider->getValue() + moveBy); +} + +void SelectionTab::onDoubleClick() +{ + if(getLine() != -1) //double clicked scenarios list + { + (static_cast(parent))->buttonStart->clickLeft(false, true); + } +} + +// A new size filter (Small, Medium, ...) has been selected. Populate +// selMaps with the relevant data. +void SelectionTab::filter(int size, bool selectFirst) +{ + curItems.clear(); + + if(tabType == ESelectionScreen::campaignList) + { + for(auto elem : allItems) + curItems.push_back(elem); + } + else + { + for(auto elem : allItems) + { + if(elem->mapHeader && elem->mapHeader->version && (!size || elem->mapHeader->width == size)) + curItems.push_back(elem); + } + } + + if(curItems.size()) + { + slider->block(false); + slider->setAmount(curItems.size()); + sort(); + if(selectFirst) + { + slider->moveTo(0); + callOnSelect(curItems[0]); + selectAbs(0); + } + } + else + { + slider->block(true); + if(callOnSelect) + callOnSelect(nullptr); + } +} + +void SelectionTab::sortBy(int criteria) +{ + if(criteria == sortingBy) + { + sortModeAscending = !sortModeAscending; + } + else + { + sortingBy = (ESortBy)criteria; + sortModeAscending = true; + } + sort(); + + selectAbs(0); +} + +void SelectionTab::sort() +{ + if(sortingBy != generalSortingBy) + std::stable_sort(curItems.begin(), curItems.end(), mapSorter(generalSortingBy)); + std::stable_sort(curItems.begin(), curItems.end(), mapSorter(sortingBy)); + + if(!sortModeAscending) + std::reverse(curItems.begin(), curItems.end()); + + updateListItems(); + redraw(); +} + +void SelectionTab::select(int position) +{ + if(!curItems.size()) + return; + + // New selection. py is the index in curItems. + int py = position + slider->getValue(); + vstd::amax(py, 0); + vstd::amin(py, curItems.size() - 1); + + selectionPos = py; + + if(position < 0) + slider->moveBy(position); + else if(position >= listItems.size()) + slider->moveBy(position - listItems.size() + 1); + + rememberCurrentSelection(); + + if(inputName && inputName->active) + { + auto filename = *CResourceHandler::get("local")->getResourceName(ResourceID(curItems[py]->fileURI, EResType::CLIENT_SAVEGAME)); + inputName->setText(filename.stem().string()); + } + updateListItems(); + if(callOnSelect) + callOnSelect(curItems[py]); +} + +void SelectionTab::selectAbs(int position) +{ + select(position - slider->getValue()); +} + +void SelectionTab::sliderMove(int slidPos) +{ + if(!slider) + return; // ignore spurious call when slider is being created + updateListItems(); + redraw(); +} + +void SelectionTab::updateListItems() +{ + // elemIdx is the index of the maps or saved game to display on line 0 + // slider->capacity contains the number of available screen lines + // slider->positionsAmnt is the number of elements after filtering + int elemIdx = slider->getValue(); + for(auto item : listItems) + { + if(elemIdx < curItems.size()) + { + item->updateItem(curItems[elemIdx], elemIdx == selectionPos); + elemIdx++; + } + else + { + item->updateItem(); + } + } +} + +int SelectionTab::getLine() +{ + int line = -1; + Point clickPos(GH.current->button.x, GH.current->button.y); + clickPos = clickPos - pos.topLeft(); + + // Ignore clicks on save name area + int maxPosY; + if(tabType == ESelectionScreen::saveGame) + maxPosY = 516; + else + maxPosY = 564; + + if(clickPos.y > 115 && clickPos.y < maxPosY && clickPos.x > 22 && clickPos.x < 371) + { + line = (clickPos.y - 115) / 25; //which line + } + + return line; +} + +void SelectionTab::selectFileName(std::string fname) +{ + boost::to_upper(fname); + for(int i = curItems.size() - 1; i >= 0; i--) + { + if(curItems[i]->fileURI == fname) + { + slider->moveTo(i); + selectAbs(i); + return; + } + } + + selectAbs(0); +} + +std::shared_ptr SelectionTab::getSelectedMapInfo() const +{ + return curItems.empty() ? nullptr : curItems[selectionPos]; +} + +void SelectionTab::rememberCurrentSelection() +{ + // TODO: this can be more elegant + if(tabType == ESelectionScreen::newGame) + { + Settings lastMap = settings.write["general"]["lastMap"]; + lastMap->String() = getSelectedMapInfo()->fileURI; + } + else if(tabType == ESelectionScreen::loadGame) + { + Settings lastSave = settings.write["general"]["lastSave"]; + lastSave->String() = getSelectedMapInfo()->fileURI; + } + else if(tabType == ESelectionScreen::campaignList) + { + Settings lastCampaign = settings.write["general"]["lastCampaign"]; + lastCampaign->String() = getSelectedMapInfo()->fileURI; + } +} + +void SelectionTab::restoreLastSelection() +{ + switch(tabType) + { + case ESelectionScreen::newGame: + selectFileName(settings["general"]["lastMap"].String()); + break; + case ESelectionScreen::campaignList: + selectFileName(settings["general"]["lastCampaign"].String()); + break; + case ESelectionScreen::loadGame: + case ESelectionScreen::saveGame: + selectFileName(settings["general"]["lastSave"].String()); + } +} + +void SelectionTab::parseMaps(const std::unordered_set & files) +{ + logGlobal->debug("Parsing %d maps", files.size()); + allItems.clear(); + for(auto & file : files) + { + try + { + auto mapInfo = std::make_shared(); + mapInfo->mapInit(file.getName()); + + // ignore unsupported map versions (e.g. WoG maps without WoG) + // but accept VCMI maps + if((mapInfo->mapHeader->version >= EMapFormat::VCMI) || (mapInfo->mapHeader->version <= CGI->modh->settings.data["textData"]["mapVersion"].Float())) + allItems.push_back(mapInfo); + } + catch(std::exception & e) + { + logGlobal->error("Map %s is invalid. Message: %s", file.getName(), e.what()); + } + } +} + +void SelectionTab::parseSaves(const std::unordered_set & files) +{ + for(auto & file : files) + { + try + { + auto mapInfo = std::make_shared(); + mapInfo->saveInit(file); + + // Filter out other game modes + bool isCampaign = mapInfo->scenarioOptionsOfSave->mode == StartInfo::CAMPAIGN; + bool isMultiplayer = mapInfo->amountOfHumanPlayersInSave > 1; + switch(CSH->getLoadMode()) + { + case ELoadMode::SINGLE: + if(isMultiplayer || isCampaign) + mapInfo->mapHeader.reset(); + break; + case ELoadMode::CAMPAIGN: + if(!isCampaign) + mapInfo->mapHeader.reset(); + break; + default: + if(!isMultiplayer) + mapInfo->mapHeader.reset(); + break; + } + + allItems.push_back(mapInfo); + } + catch(const std::exception & e) + { + logGlobal->error("Error: Failed to process %s: %s", file.getName(), e.what()); + } + } +} + +void SelectionTab::parseCampaigns(const std::unordered_set & files) +{ + allItems.reserve(files.size()); + for(auto & file : files) + { + auto info = std::make_shared(); + //allItems[i].date = std::asctime(std::localtime(&files[i].date)); + info->fileURI = file.getName(); + info->campaignInit(); + allItems.push_back(info); + } +} + +std::unordered_set SelectionTab::getFiles(std::string dirURI, int resType) +{ + boost::to_upper(dirURI); + CResourceHandler::get()->updateFilteredFiles([&](const std::string & mount) + { + return boost::algorithm::starts_with(mount, dirURI); + }); + + std::unordered_set ret = CResourceHandler::get()->getFilteredFiles([&](const ResourceID & ident) + { + return ident.getType() == resType && boost::algorithm::starts_with(ident.getName(), dirURI); + }); + + return ret; +} + +SelectionTab::ListItem::ListItem(Point position, std::shared_ptr iconsFormats, std::shared_ptr iconsVictory, std::shared_ptr iconsLoss) + : CIntObject(LCLICK, position) +{ + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + labelName = std::make_shared(184, 0, FONT_SMALL, EAlignment::CENTER, Colors::WHITE); + labelName->setAutoRedraw(false); + labelAmountOfPlayers = std::make_shared(8, 0, FONT_SMALL, EAlignment::CENTER, Colors::WHITE); + labelAmountOfPlayers->setAutoRedraw(false); + labelNumberOfCampaignMaps = std::make_shared(8, 0, FONT_SMALL, EAlignment::CENTER, Colors::WHITE); + labelNumberOfCampaignMaps->setAutoRedraw(false); + labelMapSizeLetter = std::make_shared(41, 0, FONT_SMALL, EAlignment::CENTER, Colors::WHITE); + labelMapSizeLetter->setAutoRedraw(false); + // FIXME: This -12 should not be needed, but for some reason CAnimImage displaced otherwise + iconFormat = std::make_shared(iconsFormats, 0, 0, 59, -12); + iconVictoryCondition = std::make_shared(iconsVictory, 0, 0, 277, -12); + iconLossCondition = std::make_shared(iconsLoss, 0, 0, 310, -12); +} + +void SelectionTab::ListItem::updateItem(std::shared_ptr info, bool selected) +{ + if(!info) + { + labelAmountOfPlayers->disable(); + labelMapSizeLetter->disable(); + iconFormat->disable(); + iconVictoryCondition->disable(); + iconLossCondition->disable(); + labelNumberOfCampaignMaps->disable(); + labelName->disable(); + return; + } + + auto color = selected ? Colors::YELLOW : Colors::WHITE; + if(info->campaignHeader) + { + labelAmountOfPlayers->disable(); + labelMapSizeLetter->disable(); + iconFormat->disable(); + iconVictoryCondition->disable(); + iconLossCondition->disable(); + labelNumberOfCampaignMaps->enable(); + std::ostringstream ostr(std::ostringstream::out); + ostr << CGI->generaltexth->campaignRegionNames[info->campaignHeader->mapVersion].size(); + labelNumberOfCampaignMaps->setText(ostr.str()); + labelNumberOfCampaignMaps->setColor(color); + } + else + { + labelNumberOfCampaignMaps->disable(); + std::ostringstream ostr(std::ostringstream::out); + ostr << info->amountOfPlayersOnMap << "/" << info->amountOfHumanControllablePlayers; + labelAmountOfPlayers->enable(); + labelAmountOfPlayers->setText(ostr.str()); + labelAmountOfPlayers->setColor(color); + labelMapSizeLetter->enable(); + labelMapSizeLetter->setText(info->getMapSizeName()); + labelMapSizeLetter->setColor(color); + iconFormat->enable(); + iconFormat->setFrame(info->getMapSizeFormatIconId().first, info->getMapSizeFormatIconId().second); + iconVictoryCondition->enable(); + iconVictoryCondition->setFrame(info->mapHeader->victoryIconIndex, 0); + iconLossCondition->enable(); + iconLossCondition->setFrame(info->mapHeader->defeatIconIndex, 0); + } + labelName->enable(); + labelName->setText(info->getNameForList()); + labelName->setColor(color); +} diff --git a/client/lobby/SelectionTab.h b/client/lobby/SelectionTab.h new file mode 100644 index 000000000..a6a974796 --- /dev/null +++ b/client/lobby/SelectionTab.h @@ -0,0 +1,98 @@ +/* + * SelectionTab.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +#include "CSelectionBase.h" + +class CSlider; +class CLabel; + +enum ESortBy +{ + _playerAm, _size, _format, _name, _viccon, _loscon, _numOfMaps, _fileName +}; //_numOfMaps is for campaigns + +/// Class which handles map sorting by different criteria +class mapSorter +{ +public: + ESortBy sortBy; + bool operator()(const std::shared_ptr aaa, const std::shared_ptr bbb); + mapSorter(ESortBy es) : sortBy(es){}; +}; + +class SelectionTab : public CIntObject +{ + struct ListItem : public CIntObject + { + std::shared_ptr labelAmountOfPlayers; + std::shared_ptr labelNumberOfCampaignMaps; + std::shared_ptr labelMapSizeLetter; + std::shared_ptr iconFormat; + std::shared_ptr iconVictoryCondition; + std::shared_ptr iconLossCondition; + std::shared_ptr labelName; + + ListItem(Point position, std::shared_ptr iconsFormats, std::shared_ptr iconsVictory, std::shared_ptr iconsLoss); + void updateItem(std::shared_ptr info = {}, bool selected = false); + }; + std::vector> listItems; + + std::shared_ptr iconsMapFormats; + // FIXME: CSelectionBase use them too! + std::shared_ptr iconsVictoryCondition; + std::shared_ptr iconsLossCondition; + +public: + std::vector> allItems; + std::vector> curItems; + size_t selectionPos; + std::function)> callOnSelect; + + ESortBy sortingBy; + ESortBy generalSortingBy; + bool sortModeAscending; + + std::shared_ptr inputName; + + SelectionTab(ESelectionScreen Type); + void toggleMode(); + + void clickLeft(tribool down, bool previousState) override; + void keyPressed(const SDL_KeyboardEvent & key) override; + void onDoubleClick() override; + + void filter(int size, bool selectFirst = false); //0 - all + void sortBy(int criteria); + void sort(); + void select(int position); //position: <0 - positions> position on the screen + void selectAbs(int position); //position: absolute position in curItems vector + void sliderMove(int slidPos); + void updateListItems(); + int getLine(); + void selectFileName(std::string fname); + std::shared_ptr getSelectedMapInfo() const; + void rememberCurrentSelection(); + void restoreLastSelection(); + +private: + + std::shared_ptr background; + std::shared_ptr slider; + std::vector> buttonsSortBy; + std::shared_ptr labelTabTitle; + std::shared_ptr labelMapSizes; + ESelectionScreen tabType; + + void parseMaps(const std::unordered_set & files); + void parseSaves(const std::unordered_set & files); + void parseCampaigns(const std::unordered_set & files); + std::unordered_set getFiles(std::string dirURI, int resType); +}; diff --git a/client/mainmenu/CCampaignScreen.cpp b/client/mainmenu/CCampaignScreen.cpp new file mode 100644 index 000000000..d51a6b715 --- /dev/null +++ b/client/mainmenu/CCampaignScreen.cpp @@ -0,0 +1,156 @@ +/* + * CCampaignScreen.cpp, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ + +#include "StdInc.h" +#include "../mainmenu/CMainMenu.h" +#include "CCampaignScreen.h" + +#include "../CGameInfo.h" +#include "../CMessage.h" +#include "../CBitmapHandler.h" +#include "../CMusicHandler.h" +#include "../CVideoHandler.h" +#include "../CPlayerInterface.h" +#include "../CServerHandler.h" +#include "../gui/CAnimation.h" +#include "../gui/CGuiHandler.h" +#include "../widgets/CComponent.h" +#include "../widgets/Buttons.h" +#include "../widgets/MiscWidgets.h" +#include "../widgets/ObjectLists.h" +#include "../widgets/TextControls.h" +#include "../windows/GUIClasses.h" +#include "../windows/InfoWindows.h" +#include "../windows/CWindowObject.h" + +#include "../../lib/filesystem/Filesystem.h" +#include "../../lib/CGeneralTextHandler.h" + +#include "../../lib/CArtHandler.h" +#include "../../lib/CBuildingHandler.h" +#include "../../lib/spells/CSpellHandler.h" + +#include "../../lib/CSkillHandler.h" +#include "../../lib/CTownHandler.h" +#include "../../lib/CHeroHandler.h" +#include "../../lib/CCreatureHandler.h" + +#include "../../lib/mapping/CCampaignHandler.h" +#include "../../lib/mapping/CMapService.h" + +#include "../../lib/mapObjects/CGHeroInstance.h" + +CCampaignScreen::CCampaignScreen(const JsonNode & config) + : CWindowObject(BORDERED) +{ + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + + for(const JsonNode & node : config["images"].Vector()) + images.push_back(CMainMenu::createPicture(node)); + + if(!images.empty()) + { + images[0]->center(); // move background to center + moveTo(images[0]->pos.topLeft()); // move everything else to center + images[0]->moveTo(pos.topLeft()); // restore moved twice background + pos = images[0]->pos; // fix height\width of this window + } + + if(!config["exitbutton"].isNull()) + { + buttonBack = createExitButton(config["exitbutton"]); + buttonBack->hoverable = true; + } + + for(const JsonNode & node : config["items"].Vector()) + campButtons.push_back(std::make_shared(node)); +} + +std::shared_ptr CCampaignScreen::createExitButton(const JsonNode & button) +{ + std::pair help; + if(!button["help"].isNull() && button["help"].Float() > 0) + help = CGI->generaltexth->zelp[button["help"].Float()]; + + std::function close = std::bind(&CGuiHandler::popIntTotally, &GH, this); + return std::make_shared(Point(button["x"].Float(), button["y"].Float()), button["name"].String(), help, close, button["hotkey"].Float()); +} + +CCampaignScreen::CCampaignButton::CCampaignButton(const JsonNode & config) +{ + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + + pos.x += config["x"].Float(); + pos.y += config["y"].Float(); + pos.w = 200; + pos.h = 116; + + campFile = config["file"].String(); + video = config["video"].String(); + + status = config["open"].Bool() ? CCampaignScreen::ENABLED : CCampaignScreen::DISABLED; + + CCampaignHeader header = CCampaignHandler::getHeader(campFile); + hoverText = header.name; + + if(status != CCampaignScreen::DISABLED) + { + addUsedEvents(LCLICK | HOVER); + graphicsImage = std::make_shared(config["image"].String()); + + hoverLabel = std::make_shared(pos.w / 2, pos.h + 20, FONT_MEDIUM, CENTER, Colors::YELLOW, ""); + parent->addChild(hoverLabel.get()); + } + + if(status == CCampaignScreen::COMPLETED) + graphicsCompleted = std::make_shared("CAMPCHK"); +} + +void CCampaignScreen::CCampaignButton::show(SDL_Surface * to) +{ + if(status == CCampaignScreen::DISABLED) + return; + + CIntObject::show(to); + + // Play the campaign button video when the mouse cursor is placed over the button + if(hovered) + { + if(CCS->videoh->fname != video) + CCS->videoh->open(video); + + CCS->videoh->update(pos.x, pos.y, to, true, false); // plays sequentially frame by frame, starts at the beginning when the video is over + } + else if(CCS->videoh->fname == video) // When you got out of the bounds of the button then close the video + { + CCS->videoh->close(); + redraw(); + } +} + +void CCampaignScreen::CCampaignButton::clickLeft(tribool down, bool previousState) +{ + if(down) + { + CCS->videoh->close(); + CMainMenu::openCampaignLobby(campFile); + } +} + +void CCampaignScreen::CCampaignButton::hover(bool on) +{ + if(hoverLabel) + { + if(on) + hoverLabel->setText(hoverText); // Shows the name of the campaign when you get into the bounds of the button + else + hoverLabel->setText(" "); + } +} diff --git a/client/mainmenu/CCampaignScreen.h b/client/mainmenu/CCampaignScreen.h new file mode 100644 index 000000000..159d26f52 --- /dev/null +++ b/client/mainmenu/CCampaignScreen.h @@ -0,0 +1,55 @@ +/* + * CCampaignScreen.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +class CLabel; +class CPicture; +class CButton; +class SDL_Surface; +class JsonNode; + +class CCampaignScreen : public CWindowObject +{ +public: + enum CampaignStatus {DEFAULT = 0, ENABLED, DISABLED, COMPLETED}; // the status of the campaign + +private: + /// A button which plays a video when you move the mouse cursor over it + class CCampaignButton : public CIntObject + { + private: + std::shared_ptr hoverLabel; + std::shared_ptr graphicsImage; + std::shared_ptr graphicsCompleted; + CampaignStatus status; + + std::string campFile; // the filename/resourcename of the campaign + std::string video; // the resource name of the video + std::string hoverText; + + void clickLeft(tribool down, bool previousState) override; + void hover(bool on) override; + + public: + CCampaignButton(const JsonNode & config); + void show(SDL_Surface * to) override; + }; + + std::vector> campButtons; + std::vector> images; + std::shared_ptr buttonBack; + + std::shared_ptr createExitButton(const JsonNode & button); + +public: + enum CampaignSet {ROE, AB, SOD, WOG}; + + CCampaignScreen(const JsonNode & config); +}; diff --git a/client/mainmenu/CMainMenu.cpp b/client/mainmenu/CMainMenu.cpp new file mode 100644 index 000000000..53ef11bc5 --- /dev/null +++ b/client/mainmenu/CMainMenu.cpp @@ -0,0 +1,566 @@ +/* + * CMainMenu.cpp, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#include "StdInc.h" +#include "CMainMenu.h" + +#include "CCampaignScreen.h" +#include "CreditsScreen.h" + +#include "../lobby/CBonusSelection.h" +#include "../lobby/CSelectionBase.h" +#include "../lobby/CLobbyScreen.h" + +#include "../../lib/filesystem/Filesystem.h" +#include "../../lib/filesystem/CCompressedStream.h" + +#include "../gui/SDL_Extensions.h" +#include "../gui/CCursorHandler.h" + +#include "../CGameInfo.h" +#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/JsonNode.h" +#include "../CMusicHandler.h" +#include "../CVideoHandler.h" +#include "../Graphics.h" +#include "../../lib/serializer/Connection.h" +#include "../../lib/serializer/CTypeList.h" +#include "../../lib/VCMIDirs.h" +#include "../../lib/mapping/CMap.h" +#include "../windows/GUIClasses.h" +#include "../CPlayerInterface.h" +#include "../../CCallback.h" +#include "../CMessage.h" +#include "../CBitmapHandler.h" +#include "../Client.h" +#include "../gui/CGuiHandler.h" +#include "../gui/CAnimation.h" +#include "../widgets/CComponent.h" +#include "../widgets/Buttons.h" +#include "../widgets/MiscWidgets.h" +#include "../widgets/ObjectLists.h" +#include "../widgets/TextControls.h" +#include "../windows/InfoWindows.h" +#include "../CServerHandler.h" +#include "../../lib/CStopWatch.h" +#include "../../lib/NetPacksLobby.h" +#include "../../lib/CThreadHelper.h" +#include "../../lib/CConfigHandler.h" +#include "../../lib/GameConstants.h" +#include "../../lib/CRandomGenerator.h" +#include "../../lib/CondSh.h" +#include "../../lib/mapping/CCampaignHandler.h" + + +namespace fs = boost::filesystem; + +CMainMenu * CMM = nullptr; +ISelectionScreenInfo * SEL; + +static void do_quit() +{ + SDL_Event event; + event.quit.type = SDL_QUIT; + SDL_PushEvent(&event); +} + +CMenuScreen::CMenuScreen(const JsonNode & configNode) + : CWindowObject(BORDERED), config(configNode) +{ + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + + background = std::make_shared(config["background"].String()); + if(config["scalable"].Bool()) + { + if(background->bg->format->palette) + background->convertToScreenBPP(); + background->scaleTo(Point(screen->w, screen->h)); + } + + pos = background->center(); + + for(const JsonNode & node : config["items"].Vector()) + menuNameToEntry.push_back(node["name"].String()); + + for(const JsonNode & node : config["images"].Vector()) + images.push_back(CMainMenu::createPicture(node)); + + //Hardcoded entry + menuNameToEntry.push_back("credits"); + + tabs = std::make_shared(std::bind(&CMenuScreen::createTab, this, _1), CTabbedInt::DestroyFunc()); + tabs->type |= REDRAW_PARENT; +} + +CIntObject * CMenuScreen::createTab(size_t index) +{ + if(config["items"].Vector().size() == index) + return new CreditsScreen(); + + return new CMenuEntry(this, config["items"].Vector()[index]); +} + +void CMenuScreen::show(SDL_Surface * to) +{ + if(!config["video"].isNull()) + CCS->videoh->update(config["video"]["x"].Float() + pos.x, config["video"]["y"].Float() + pos.y, to, true, false); + CIntObject::show(to); +} + +void CMenuScreen::activate() +{ + CCS->musich->playMusic("Music/MainMenu", true); + if(!config["video"].isNull()) + CCS->videoh->open(config["video"]["name"].String()); + CIntObject::activate(); +} + +void CMenuScreen::deactivate() +{ + if(!config["video"].isNull()) + CCS->videoh->close(); + + CIntObject::deactivate(); +} + +void CMenuScreen::switchToTab(size_t index) +{ + tabs->setActive(index); +} + +//funciton for std::string -> std::function conversion for main menu +static std::function genCommand(CMenuScreen * menu, std::vector menuType, const std::string & string) +{ + static const std::vector commandType = {"to", "campaigns", "start", "load", "exit", "highscores"}; + + static const std::vector gameType = {"single", "multi", "campaign", "tutorial"}; + + std::list commands; + boost::split(commands, string, boost::is_any_of("\t ")); + + if(!commands.empty()) + { + size_t index = std::find(commandType.begin(), commandType.end(), commands.front()) - commandType.begin(); + commands.pop_front(); + if(index > 3 || !commands.empty()) + { + switch(index) + { + case 0: //to - switch to another tab, if such tab exists + { + size_t index2 = std::find(menuType.begin(), menuType.end(), commands.front()) - menuType.begin(); + if(index2 != menuType.size()) + return std::bind(&CMenuScreen::switchToTab, menu, index2); + break; + } + case 1: //open campaign selection window + { + return std::bind(&CMainMenu::openCampaignScreen, CMM, commands.front()); + break; + } + case 2: //start + { + switch(std::find(gameType.begin(), gameType.end(), commands.front()) - gameType.begin()) + { + case 0: + return std::bind(CMainMenu::openLobby, ESelectionScreen::newGame, true, nullptr, ELoadMode::NONE); + case 1: + return []() { GH.pushInt(new CMultiMode(ESelectionScreen::newGame)); }; + case 2: + return std::bind(CMainMenu::openLobby, ESelectionScreen::campaignList, true, nullptr, ELoadMode::NONE); + case 3: + return std::bind(CInfoWindow::showInfoDialog, "Sorry, tutorial is not implemented yet\n", (const std::vector *)nullptr, false, PlayerColor(1)); + } + break; + } + case 3: //load + { + switch(std::find(gameType.begin(), gameType.end(), commands.front()) - gameType.begin()) + { + case 0: + return std::bind(CMainMenu::openLobby, ESelectionScreen::loadGame, true, nullptr, ELoadMode::SINGLE); + case 1: + return []() { GH.pushInt(new CMultiMode(ESelectionScreen::loadGame)); }; + case 2: + return std::bind(CMainMenu::openLobby, ESelectionScreen::loadGame, true, nullptr, ELoadMode::CAMPAIGN); + case 3: + return std::bind(CInfoWindow::showInfoDialog, "Sorry, tutorial is not implemented yet\n", (const std::vector *)nullptr, false, PlayerColor(1)); + } + } + break; + case 4: //exit + { + return std::bind(CInfoWindow::showYesNoDialog, std::ref(CGI->generaltexth->allTexts[69]), (const std::vector *)nullptr, do_quit, 0, false, PlayerColor(1)); + } + break; + case 5: //highscores + { + return std::bind(CInfoWindow::showInfoDialog, "Sorry, high scores menu is not implemented yet\n", (const std::vector *)nullptr, false, PlayerColor(1)); + } + } + } + } + logGlobal->error("Failed to parse command: %s", string); + return std::function(); +} + +std::shared_ptr CMenuEntry::createButton(CMenuScreen * parent, const JsonNode & button) +{ + std::function command = genCommand(parent, parent->menuNameToEntry, button["command"].String()); + + std::pair help; + if(!button["help"].isNull() && button["help"].Float() > 0) + help = CGI->generaltexth->zelp[button["help"].Float()]; + + int posx = button["x"].Float(); + if(posx < 0) + posx = pos.w + posx; + + int posy = button["y"].Float(); + if(posy < 0) + posy = pos.h + posy; + + return std::make_shared(Point(posx, posy), button["name"].String(), help, command, button["hotkey"].Float()); +} + +CMenuEntry::CMenuEntry(CMenuScreen * parent, const JsonNode & config) +{ + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + type |= REDRAW_PARENT; + pos = parent->pos; + + for(const JsonNode & node : config["images"].Vector()) + images.push_back(CMainMenu::createPicture(node)); + + for(const JsonNode & node : config["buttons"].Vector()) + { + buttons.push_back(createButton(parent, node)); + buttons.back()->hoverable = true; + buttons.back()->type |= REDRAW_PARENT; + } +} + +CMainMenuConfig::CMainMenuConfig() + : campaignSets(JsonNode(ResourceID("config/campaignSets.json"))), config(JsonNode(ResourceID("config/mainmenu.json"))) +{ + +} + +CMainMenuConfig & CMainMenuConfig::get() +{ + static CMainMenuConfig config; + return config; +} + +const JsonNode & CMainMenuConfig::getConfig() const +{ + return config; +} + +const JsonNode & CMainMenuConfig::getCampaigns() const +{ + return campaignSets; +} + +CMainMenu::CMainMenu() +{ + pos.w = screen->w; + pos.h = screen->h; + + GH.defActionsDef = 63; + CMM = this; + menu = new CMenuScreen(CMainMenuConfig::get().getConfig()["window"]); + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + backgroundAroundMenu = std::make_shared("DIBOXBCK", pos); +} + +CMainMenu::~CMainMenu() +{ + boost::unique_lock lock(*CPlayerInterface::pim); + if(CMM == this) + CMM = nullptr; + + if(GH.curInt == this) + GH.curInt = nullptr; +} + +void CMainMenu::update() +{ + if(CMM != this) //don't update if you are not a main interface + return; + + if(GH.listInt.empty()) + { + GH.pushInt(this); + GH.pushInt(menu); + menu->switchToTab(0); + } + + // Handles mouse and key input + GH.updateTime(); + GH.handleEvents(); + + // check for null othervice crash on finishing a campaign + // /FIXME: find out why GH.listInt is empty to begin with + if(GH.topInt() != nullptr) + GH.topInt()->show(screen); +} + +void CMainMenu::openLobby(ESelectionScreen screenType, bool host, const std::vector * names, ELoadMode loadMode) +{ + CSH->resetStateForLobby(screenType == ESelectionScreen::newGame ? StartInfo::NEW_GAME : StartInfo::LOAD_GAME, names); + CSH->screenType = screenType; + CSH->loadMode = loadMode; + + GH.pushInt(new CSimpleJoinScreen(host)); +} + +void CMainMenu::openCampaignLobby(const std::string & campaignFileName) +{ + auto ourCampaign = std::make_shared(CCampaignHandler::getCampaign(campaignFileName)); + openCampaignLobby(ourCampaign); +} + +void CMainMenu::openCampaignLobby(std::shared_ptr campaign) +{ + CSH->resetStateForLobby(StartInfo::CAMPAIGN); + CSH->screenType = ESelectionScreen::campaignList; + CSH->campaignStateToSend = campaign; + GH.pushInt(new CSimpleJoinScreen()); +} + +void CMainMenu::openCampaignScreen(std::string name) +{ + if(vstd::contains(CMainMenuConfig::get().getCampaigns().Struct(), name)) + { + GH.pushInt(new CCampaignScreen(CMainMenuConfig::get().getCampaigns()[name])); + return; + } + logGlobal->error("Unknown campaign set: %s", name); +} + +CMainMenu * CMainMenu::create() +{ + if(!CMM) + CMM = new CMainMenu(); + + GH.terminate_cond->set(false); + return CMM; +} + +void CMainMenu::removeFromGui() +{ + //remove everything but main menu and background + GH.popInts(GH.listInt.size() - 2); + GH.popInt(GH.topInt()); //remove main menu + GH.popInt(GH.topInt()); //remove background +} + +void CMainMenu::showLoadingScreen(std::function loader) +{ + if(GH.listInt.size() && GH.listInt.front() == CMM) + CMM->removeFromGui(); + GH.pushInt(new CLoadingScreen(loader)); +} + +std::shared_ptr CMainMenu::createPicture(const JsonNode & config) +{ + return std::make_shared(config["name"].String(), config["x"].Float(), config["y"].Float()); +} + +CMultiMode::CMultiMode(ESelectionScreen ScreenType) + : screenType(ScreenType) +{ + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + background = std::make_shared("MUPOPUP.bmp"); + background->convertToScreenBPP(); //so we could draw without problems + blitAt(CPicture("MUMAP.bmp"), 16, 77, *background); + pos = background->center(); //center, window has size of bg graphic + + statusBar = std::make_shared(new CPicture(Rect(7, 465, 440, 18), 0)); //226, 472 + playerName = std::make_shared(Rect(19, 436, 334, 16), *background); + playerName->setText(settings["general"]["playerName"].String()); + playerName->cb += std::bind(&CMultiMode::onNameChange, this, _1); + + buttonHotseat = std::make_shared(Point(373, 78), "MUBHOT.DEF", CGI->generaltexth->zelp[266], std::bind(&CMultiMode::hostTCP, this)); + buttonHost = std::make_shared(Point(373, 78 + 57 * 1), "MUBHOST.DEF", CButton::tooltip("Host TCP/IP game", ""), std::bind(&CMultiMode::hostTCP, this)); + buttonJoin = std::make_shared(Point(373, 78 + 57 * 2), "MUBJOIN.DEF", CButton::tooltip("Join TCP/IP game", ""), std::bind(&CMultiMode::joinTCP, this)); + buttonCancel = std::make_shared(Point(373, 424), "MUBCANC.DEF", CGI->generaltexth->zelp[288], [&]() { GH.popIntTotally(this);}, SDLK_ESCAPE); +} + +void CMultiMode::hostTCP() +{ + GH.popIntTotally(this); + GH.pushInt(new CMultiPlayers(settings["general"]["playerName"].String(), screenType, true, ELoadMode::MULTI)); +} + +void CMultiMode::joinTCP() +{ + GH.popIntTotally(this); + GH.pushInt(new CMultiPlayers(settings["general"]["playerName"].String(), screenType, false, ELoadMode::MULTI)); +} + +void CMultiMode::onNameChange(std::string newText) +{ + Settings name = settings.write["general"]["playerName"]; + name->String() = newText; +} + +CMultiPlayers::CMultiPlayers(const std::string & firstPlayer, ESelectionScreen ScreenType, bool Host, ELoadMode LoadMode) + : loadMode(LoadMode), screenType(ScreenType), host(Host) +{ + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + background = std::make_shared("MUHOTSEA.bmp"); + pos = background->center(); //center, window has size of bg graphic + + std::string text = CGI->generaltexth->allTexts[446]; + boost::replace_all(text, "\t", "\n"); + textTitle = std::make_shared(text, Rect(25, 20, 315, 50), 0, FONT_BIG, CENTER, Colors::WHITE); //HOTSEAT Please enter names + + for(int i = 0; i < inputNames.size(); i++) + { + inputNames[i] = std::make_shared(Rect(60, 85 + i * 30, 280, 16), *background); + inputNames[i]->cb += std::bind(&CMultiPlayers::onChange, this, _1); + } + + buttonOk = std::make_shared(Point(95, 338), "MUBCHCK.DEF", CGI->generaltexth->zelp[560], std::bind(&CMultiPlayers::enterSelectionScreen, this), SDLK_RETURN); + buttonCancel = std::make_shared(Point(205, 338), "MUBCANC.DEF", CGI->generaltexth->zelp[561], std::bind(&CGuiHandler::popIntTotally, std::ref(GH), this), SDLK_ESCAPE); + statusBar = std::make_shared(new CPicture(Rect(7, 381, 348, 18), 0)); //226, 472 + + inputNames[0]->setText(firstPlayer, true); + inputNames[0]->giveFocus(); +} + +void CMultiPlayers::onChange(std::string newText) +{ + size_t namesCount = 0; + + for(auto & elem : inputNames) + if(!elem->text.empty()) + namesCount++; +} + +void CMultiPlayers::enterSelectionScreen() +{ + std::vector names; + for(auto name : inputNames) + { + if(name->text.length()) + names.push_back(name->text); + } + + Settings name = settings.write["general"]["playerName"]; + name->String() = names[0]; + + CMainMenu::openLobby(screenType, host, &names, loadMode); +} + +CSimpleJoinScreen::CSimpleJoinScreen(bool host) +{ + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + background = std::make_shared("MUDIALOG.bmp"); // address background + pos = background->center(); //center, window has size of bg graphic (x,y = 396,278 w=232 h=212) + + textTitle = std::make_shared("", Rect(20, 20, 205, 50), 0, FONT_BIG, CENTER, Colors::WHITE); + inputAddress = std::make_shared(Rect(25, 68, 175, 16), *background.get()); + inputPort = std::make_shared(Rect(25, 115, 175, 16), *background.get()); + if(host && !settings["session"]["donotstartserver"].Bool()) + { + textTitle->setText("Connecting..."); + boost::thread(&CSimpleJoinScreen::connectThread, this, "", 0); + } + else + { + textTitle->setText("Enter address:"); + inputAddress->cb += std::bind(&CSimpleJoinScreen::onChange, this, _1); + inputPort->cb += std::bind(&CSimpleJoinScreen::onChange, this, _1); + inputPort->filters += std::bind(&CTextInput::numberFilter, _1, _2, 0, 65535); + buttonOk = std::make_shared(Point(26, 142), "MUBCHCK.DEF", CGI->generaltexth->zelp[560], std::bind(&CSimpleJoinScreen::connectToServer, this), SDLK_RETURN); + + inputAddress->giveFocus(); + } + inputAddress->setText(settings["server"]["server"].String(), true); + inputPort->setText(CServerHandler::getDefaultPortStr(), true); + + buttonCancel = std::make_shared(Point(142, 142), "MUBCANC.DEF", CGI->generaltexth->zelp[561], std::bind(&CSimpleJoinScreen::leaveScreen, this), SDLK_ESCAPE); + statusBar = std::make_shared(new CPicture(Rect(7, 186, 218, 18), 0)); +} + +void CSimpleJoinScreen::connectToServer() +{ + textTitle->setText("Connecting..."); + buttonOk->block(true); + + boost::thread(&CSimpleJoinScreen::connectThread, this, inputAddress->text, boost::lexical_cast(inputPort->text)); +} + +void CSimpleJoinScreen::leaveScreen() +{ + if(CSH->state == EClientState::CONNECTING) + { + textTitle->setText("Closing..."); + CSH->state = EClientState::CONNECTION_CANCELLED; + } + else if(GH.listInt.size() && GH.listInt.front() == this) + { + GH.popIntTotally(this); + } +} + +void CSimpleJoinScreen::onChange(const std::string & newText) +{ + buttonOk->block(inputAddress->text.empty() || inputPort->text.empty()); +} + +void CSimpleJoinScreen::connectThread(const std::string addr, const ui16 port) +{ + setThreadName("CSimpleJoinScreen::connectThread"); + if(!addr.length()) + CSH->startLocalServerAndConnect(); + else + CSH->justConnectToServer(addr, port); + + if(GH.listInt.size() && GH.listInt.front() == this) + { + GH.popIntTotally(this); + } +} + +CLoadingScreen::CLoadingScreen(std::function loader) + : CWindowObject(BORDERED, getBackground()), loadingThread(loader) +{ + CCS->musich->stopMusic(5000); +} + +CLoadingScreen::~CLoadingScreen() +{ + loadingThread.join(); +} + +void CLoadingScreen::showAll(SDL_Surface * to) +{ + Rect rect(0, 0, to->w, to->h); + SDL_FillRect(to, &rect, 0); + + CWindowObject::showAll(to); +} + +std::string CLoadingScreen::getBackground() +{ + const auto & conf = CMainMenuConfig::get().getConfig()["loading"].Vector(); + + if(conf.empty()) + { + return "loadbar"; + } + else + { + return RandomGeneratorUtil::nextItem(conf, CRandomGenerator::getDefault())->String(); + } +} diff --git a/client/mainmenu/CMainMenu.h b/client/mainmenu/CMainMenu.h new file mode 100644 index 000000000..22e75e4ff --- /dev/null +++ b/client/mainmenu/CMainMenu.h @@ -0,0 +1,183 @@ +/* + * CMainMenu.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +#include "../windows/CWindowObject.h" +#include "../../lib/JsonNode.h" + +class CCampaignState; +class CTextInput; +class CGStatusBar; +class CTextBox; +class CTabbedInt; +class CAnimation; +class CButton; +class CFilledTexture; + + +// TODO: Find new location for these enums +enum ESelectionScreen : ui8 { + unknown = 0, newGame, loadGame, saveGame, scenarioInfo, campaignList +}; + +enum ELoadMode : ui8 +{ + NONE = 0, SINGLE, MULTI, CAMPAIGN +}; + +/// The main menu screens listed in the EState enum +class CMenuScreen : public CWindowObject +{ + const JsonNode & config; + + std::shared_ptr tabs; + + std::shared_ptr background; + std::vector> images; + + CIntObject * createTab(size_t index); + +public: + std::vector menuNameToEntry; + + CMenuScreen(const JsonNode & configNode); + + void show(SDL_Surface * to) override; + void activate() override; + void deactivate() override; + + void switchToTab(size_t index); +}; + +class CMenuEntry : public CIntObject +{ + std::vector> images; + std::vector> buttons; + + std::shared_ptr createButton(CMenuScreen * parent, const JsonNode & button); + +public: + CMenuEntry(CMenuScreen * parent, const JsonNode & config); +}; + +/// Multiplayer mode +class CMultiMode : public CIntObject +{ +public: + ESelectionScreen screenType; + std::shared_ptr background; + std::shared_ptr playerName; + std::shared_ptr buttonHotseat; + std::shared_ptr buttonHost; + std::shared_ptr buttonJoin; + std::shared_ptr buttonCancel; + std::shared_ptr statusBar; + + CMultiMode(ESelectionScreen ScreenType); + void hostTCP(); + void joinTCP(); + + void onNameChange(std::string newText); +}; + +/// Hot seat player window +class CMultiPlayers : public CIntObject +{ + bool host; + ELoadMode loadMode; + ESelectionScreen screenType; + std::shared_ptr background; + std::shared_ptr textTitle; + std::array, 8> inputNames; + std::shared_ptr buttonOk; + std::shared_ptr buttonCancel; + std::shared_ptr statusBar; + + void onChange(std::string newText); + void enterSelectionScreen(); + +public: + CMultiPlayers(const std::string & firstPlayer, ESelectionScreen ScreenType, bool Host, ELoadMode LoadMode); +}; + +/// Manages the configuration of pregame GUI elements like campaign screen, main menu, loading screen,... +class CMainMenuConfig +{ +public: + static CMainMenuConfig & get(); + const JsonNode & getConfig() const; + const JsonNode & getCampaigns() const; + +private: + CMainMenuConfig(); + + const JsonNode campaignSets; + const JsonNode config; +}; + +/// Handles background screen, loads graphics for victory/loss condition and random town or hero selection +class CMainMenu : public CIntObject, public IUpdateable +{ + std::shared_ptr backgroundAroundMenu; + + CMainMenu(); //Use CMainMenu::create + +public: + CMenuScreen * menu; + + ~CMainMenu(); + void update() override; + static void openLobby(ESelectionScreen screenType, bool host, const std::vector * names, ELoadMode loadMode); + static void openCampaignLobby(const std::string & campaignFileName); + static void openCampaignLobby(std::shared_ptr campaign); + void openCampaignScreen(std::string name); + + static CMainMenu * create(); + void removeFromGui(); + static void showLoadingScreen(std::function loader); + + static std::shared_ptr createPicture(const JsonNode & config); + +}; + +/// Simple window to enter the server's address. +class CSimpleJoinScreen : public CIntObject +{ + std::shared_ptr background; + std::shared_ptr textTitle; + std::shared_ptr buttonOk; + std::shared_ptr buttonCancel; + std::shared_ptr statusBar; + std::shared_ptr inputAddress; + std::shared_ptr inputPort; + + void connectToServer(); + void leaveScreen(); + void onChange(const std::string & newText); + void connectThread(const std::string addr = "", const ui16 inputPort = 0); + +public: + CSimpleJoinScreen(bool host = true); +}; + +class CLoadingScreen : public CWindowObject +{ + boost::thread loadingThread; + + std::string getBackground(); + +public: + CLoadingScreen(std::function loader); + ~CLoadingScreen(); + + void showAll(SDL_Surface * to) override; +}; + +extern CMainMenu * CMM; diff --git a/client/mainmenu/CPrologEpilogVideo.cpp b/client/mainmenu/CPrologEpilogVideo.cpp new file mode 100644 index 000000000..ce7e825bb --- /dev/null +++ b/client/mainmenu/CPrologEpilogVideo.cpp @@ -0,0 +1,64 @@ +/* + * CPrologEpilogVideo.cpp, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ + +#include "StdInc.h" + +#include "CPrologEpilogVideo.h" +#include "../CGameInfo.h" +#include "../CMusicHandler.h" +#include "../CVideoHandler.h" +#include "../gui/CGuiHandler.h" +#include "../widgets/TextControls.h" + +#include "../../lib/mapping/CCampaignHandler.h" + + +CPrologEpilogVideo::CPrologEpilogVideo(CCampaignScenario::SScenarioPrologEpilog _spe, std::function callback) + : CWindowObject(BORDERED), spe(_spe), positionCounter(0), voiceSoundHandle(-1), exitCb(callback) +{ + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + addUsedEvents(LCLICK); + pos = center(Rect(0, 0, 800, 600)); + updateShadow(); + + CCS->videoh->open(CCampaignHandler::prologVideoName(spe.prologVideo)); + CCS->musich->playMusic("Music/" + CCampaignHandler::prologMusicName(spe.prologMusic), true); + // MPTODO: Custom campaign crashing on this? +// voiceSoundHandle = CCS->soundh->playSound(CCampaignHandler::prologVoiceName(spe.prologVideo)); + + text = std::make_shared(Rect(100, 500, 600, 100), EFonts::FONT_BIG, CENTER, Colors::METALLIC_GOLD, spe.prologText); + text->scrollTextTo(-100); +} + +void CPrologEpilogVideo::show(SDL_Surface * to) +{ + CSDL_Ext::fillRectBlack(to, &pos); + //BUG: some videos are 800x600 in size while some are 800x400 + //VCMI should center them in the middle of the screen. Possible but needs modification + //of video player API which I'd like to avoid until we'll get rid of Windows-specific player + CCS->videoh->update(pos.x, pos.y, to, true, false); + + //move text every 5 calls/frames; seems to be good enough + ++positionCounter; + if(positionCounter % 5 == 0) + text->scrollTextBy(1); + else + text->showAll(to); // blit text over video, if needed + + if(text->textSize.y + 100 < positionCounter / 5) + clickLeft(false, false); +} + +void CPrologEpilogVideo::clickLeft(tribool down, bool previousState) +{ + GH.popInt(this); + CCS->soundh->stopSound(voiceSoundHandle); + exitCb(); +} diff --git a/client/mainmenu/CPrologEpilogVideo.h b/client/mainmenu/CPrologEpilogVideo.h new file mode 100644 index 000000000..1f6417d0d --- /dev/null +++ b/client/mainmenu/CPrologEpilogVideo.h @@ -0,0 +1,31 @@ +/* + * CPrologEpilogVideo.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once +#include "../windows/CWindowObject.h" +#include "../../lib/mapping/CCampaignHandler.h" + +class CMultiLineLabel; +class SDL_Surface; + +class CPrologEpilogVideo : public CWindowObject +{ + CCampaignScenario::SScenarioPrologEpilog spe; + int positionCounter; + int voiceSoundHandle; + std::function exitCb; + + std::shared_ptr text; + +public: + CPrologEpilogVideo(CCampaignScenario::SScenarioPrologEpilog _spe, std::function callback); + + void clickLeft(tribool down, bool previousState) override; + void show(SDL_Surface * to) override; +}; diff --git a/client/mainmenu/CreditsScreen.cpp b/client/mainmenu/CreditsScreen.cpp new file mode 100644 index 000000000..9999c2ecb --- /dev/null +++ b/client/mainmenu/CreditsScreen.cpp @@ -0,0 +1,59 @@ +/* + * CreditsScreen.cpp, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ + +#include "StdInc.h" + +#include "CreditsScreen.h" +#include "../mainmenu/CMainMenu.h" +#include "../gui/CGuiHandler.h" +#include "../widgets/TextControls.h" +#include "../widgets/ObjectLists.h" + +#include "../../lib/filesystem/Filesystem.h" + +CreditsScreen::CreditsScreen() + : positionCounter(0) +{ + addUsedEvents(LCLICK | RCLICK); + type |= REDRAW_PARENT; + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + pos.w = CMM->menu->pos.w; + pos.h = CMM->menu->pos.h; + auto textFile = CResourceHandler::get()->load(ResourceID("DATA/CREDITS.TXT"))->readAll(); + std::string text((char *)textFile.first.get(), textFile.second); + size_t firstQuote = text.find('\"') + 1; + text = text.substr(firstQuote, text.find('\"', firstQuote) - firstQuote); + credits = std::make_shared(Rect(pos.w - 350, 0, 350, 600), FONT_CREDITS, CENTER, Colors::WHITE, text); + credits->scrollTextTo(-600); // move all text below the screen +} + +void CreditsScreen::show(SDL_Surface * to) +{ + CIntObject::show(to); + positionCounter++; + if(positionCounter % 2 == 0) + credits->scrollTextBy(1); + + //end of credits, close this screen + if(credits->textSize.y + 600 < positionCounter / 2) + clickRight(false, false); +} + +void CreditsScreen::clickLeft(tribool down, bool previousState) +{ + clickRight(down, previousState); +} + +void CreditsScreen::clickRight(tribool down, bool previousState) +{ + CTabbedInt * menu = dynamic_cast(parent); + assert(menu); + menu->setActive(0); +} diff --git a/client/mainmenu/CreditsScreen.h b/client/mainmenu/CreditsScreen.h new file mode 100644 index 000000000..654944d12 --- /dev/null +++ b/client/mainmenu/CreditsScreen.h @@ -0,0 +1,27 @@ +/* + * CreditsScreen.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +#include "../windows/CWindowObject.h" + +class CMultiLineLabel; +class SDL_Surface; + +class CreditsScreen : public CIntObject +{ + int positionCounter; + std::shared_ptr credits; + +public: + CreditsScreen(); + void show(SDL_Surface * to) override; + void clickLeft(tribool down, bool previousState) override; + void clickRight(tribool down, bool previousState) override; +}; diff --git a/client/widgets/AdventureMapClasses.cpp b/client/widgets/AdventureMapClasses.cpp index 8ef9f1cd8..cc35567cc 100644 --- a/client/widgets/AdventureMapClasses.cpp +++ b/client/widgets/AdventureMapClasses.cpp @@ -18,7 +18,7 @@ #include "../CGameInfo.h" #include "../CMusicHandler.h" #include "../CPlayerInterface.h" -#include "../CPreGame.h" +#include "../mainmenu/CMainMenu.h" #include "../Graphics.h" #include "../CMessage.h" diff --git a/client/windows/CAdvmapInterface.cpp b/client/windows/CAdvmapInterface.cpp index 610413330..913d049af 100644 --- a/client/windows/CAdvmapInterface.cpp +++ b/client/windows/CAdvmapInterface.cpp @@ -22,7 +22,10 @@ #include "../CMessage.h" #include "../CMusicHandler.h" #include "../CPlayerInterface.h" -#include "../CPreGame.h" +#include "../mainmenu/CMainMenu.h" +#include "../lobby/CBonusSelection.h" +#include "../lobby/CSavingScreen.h" +#include "../lobby/CScenarioInfoScreen.h" #include "../Graphics.h" #include "../mapHandler.h" @@ -47,6 +50,7 @@ #include "../../lib/mapping/CMap.h" #include "../../lib/UnlockGuard.h" #include "../../lib/VCMI_Lib.h" +#include "../../lib/StartInfo.h" #ifdef _MSC_VER #pragma warning (disable : 4355) @@ -935,7 +939,8 @@ void CAdvMapInt::activate() } minimap.activate(); terrain.activate(); - LOCPLINT->cingconsole->activate(); + if(LOCPLINT) + LOCPLINT->cingconsole->activate(); GH.fakeMouseMove(); //to restore the cursor } @@ -1215,7 +1220,7 @@ void CAdvMapInt::keyPressed(const SDL_KeyboardEvent & key) return; case SDLK_s: if(isActive() && key.type == SDL_KEYUP) - GH.pushInt(new CSavingScreen(CPlayerInterface::howManyPeople > 1)); + GH.pushInt(new CSavingScreen()); return; case SDLK_d: { @@ -1235,7 +1240,7 @@ void CAdvMapInt::keyPressed(const SDL_KeyboardEvent & key) if(isActive() && LOCPLINT->ctrlPressed()) { LOCPLINT->showYesNoDialog("Are you sure you want to restart game?", - [](){ LOCPLINT->sendCustomEvent(RESTART_GAME); }, + [](){ LOCPLINT->sendCustomEvent(EUserEvent::RESTART_GAME); }, [](){}, true); } return; @@ -1947,14 +1952,13 @@ CAdventureOptions::CAdventureOptions(): void CAdventureOptions::showScenarioInfo() { - auto campState = LOCPLINT->cb->getStartInfo()->campState; - if(campState) + if(LOCPLINT->cb->getStartInfo()->campState) { - GH.pushInt(new CBonusSelection(campState)); + GH.pushInt(new CBonusSelection()); } else { - GH.pushInt(new CScenarioInfo(LOCPLINT->cb->getMapHeader(), LOCPLINT->cb->getStartInfo())); + GH.pushInt(new CScenarioInfoScreen()); } } diff --git a/client/windows/GUIClasses.cpp b/client/windows/GUIClasses.cpp index b0af3ad91..579d4c4d7 100644 --- a/client/windows/GUIClasses.cpp +++ b/client/windows/GUIClasses.cpp @@ -21,10 +21,10 @@ #include "../CMessage.h" #include "../CMusicHandler.h" #include "../CPlayerInterface.h" -#include "../CPreGame.h" #include "../CVideoHandler.h" #include "../Graphics.h" #include "../mapHandler.h" +#include "../CServerHandler.h" #include "../battle/CBattleInterfaceClasses.h" #include "../battle/CBattleInterface.h" @@ -38,6 +38,8 @@ #include "../widgets/MiscWidgets.h" #include "../windows/InfoWindows.h" +#include "../lobby/CSavingScreen.h" + #include "../../CCallback.h" #include "../lib/mapObjects/CGHeroInstance.h" @@ -487,6 +489,13 @@ CSystemOptionsWindow::CSystemOptionsWindow(): restart = new CButton (Point(246, 357), "SORSTRT", CGI->generaltexth->zelp[323], [&](){ brestartf(); }, SDLK_r); restart->setImageOrder(1, 0, 2, 3); + if(CSH->isGuest()) + { + load->block(true); + save->block(true); + restart->block(true); + } + mainMenu = new CButton (Point(357, 357), "SOMAIN.DEF", CGI->generaltexth->zelp[320], [&](){ bmainmenuf(); }, SDLK_m); mainMenu->setImageOrder(1, 0, 2, 3); @@ -593,7 +602,7 @@ void CSystemOptionsWindow::setGameRes(int index) void CSystemOptionsWindow::bquitf() { - LOCPLINT->showYesNoDialog(CGI->generaltexth->allTexts[578], [this](){ closeAndPushEvent(SDL_USEREVENT, FORCE_QUIT); }, 0); + LOCPLINT->showYesNoDialog(CGI->generaltexth->allTexts[578], [this](){ closeAndPushEvent(SDL_USEREVENT, EUserEvent::FORCE_QUIT); }, 0); } void CSystemOptionsWindow::breturnf() @@ -603,7 +612,7 @@ void CSystemOptionsWindow::breturnf() void CSystemOptionsWindow::bmainmenuf() { - LOCPLINT->showYesNoDialog(CGI->generaltexth->allTexts[578], [this](){ closeAndPushEvent(SDL_USEREVENT, RETURN_TO_MAIN_MENU); }, 0); + LOCPLINT->showYesNoDialog(CGI->generaltexth->allTexts[578], [this](){ closeAndPushEvent(SDL_USEREVENT, EUserEvent::RETURN_TO_MAIN_MENU); }, 0); } void CSystemOptionsWindow::bloadf() @@ -615,12 +624,12 @@ void CSystemOptionsWindow::bloadf() void CSystemOptionsWindow::bsavef() { GH.popIntTotally(this); - GH.pushInt(new CSavingScreen(CPlayerInterface::howManyPeople > 1)); + GH.pushInt(new CSavingScreen()); } void CSystemOptionsWindow::brestartf() { - LOCPLINT->showYesNoDialog(CGI->generaltexth->allTexts[67], [this](){ closeAndPushEvent(SDL_USEREVENT, RESTART_GAME); }, 0); + LOCPLINT->showYesNoDialog(CGI->generaltexth->allTexts[67], [this](){ closeAndPushEvent(SDL_USEREVENT, EUserEvent::RESTART_GAME); }, 0); } void CSystemOptionsWindow::closeAndPushEvent(int eventType, int code) diff --git a/lib/CGameState.cpp b/lib/CGameState.cpp index 4977137ec..114045ffb 100644 --- a/lib/CGameState.cpp +++ b/lib/CGameState.cpp @@ -64,8 +64,6 @@ public: } }; -static CApplier *applierGs = nullptr; - void MetaString::getLocalString(const std::pair &txt, std::string &dst) const { int type = txt.first, ser = txt.second; @@ -673,9 +671,9 @@ int CGameState::getDate(Date::EDateType mode) const CGameState::CGameState() { gs = this; - applierGs = new CApplier(); - registerTypesClientPacks1(*applierGs); - registerTypesClientPacks2(*applierGs); + applier = std::make_shared>(); + registerTypesClientPacks1(*applier); + registerTypesClientPacks2(*applier); //objCaller = new CObjectCallersHandler(); globalEffects.setDescription("Global effects"); globalEffects.setNodeType(CBonusSystemNode::GLOBAL_EFFECTS); @@ -686,10 +684,6 @@ CGameState::~CGameState() { map.dellNull(); curB.dellNull(); - //delete scenarioOps; //TODO: fix for loading ind delete - //delete initialOpts; - delete applierGs; - //delete objCaller; for(auto ptr : hpool.heroesPool) // clean hero pool ptr.second.dellNull(); @@ -709,7 +703,7 @@ void CGameState::init(const IMapService * mapService, StartInfo * si, bool allow initNewGame(mapService, allowSavingRandomMap); break; case StartInfo::CAMPAIGN: - initCampaign(mapService); + initCampaign(); break; default: logGlobal->error("Wrong mode: %d", static_cast(scenarioOps->mode)); @@ -765,6 +759,13 @@ void CGameState::init(const IMapService * mapService, StartInfo * si, bool allow } } +void CGameState::updateOnLoad(StartInfo * si) +{ + scenarioOps->playerInfos = si->playerInfos; + for(auto & i : si->playerInfos) + gs->players[i.first].human = i.second.isControlledByHuman(); +} + void CGameState::initNewGame(const IMapService * mapService, bool allowSavingRandomMap) { if(scenarioOps->createRandomMap()) @@ -815,7 +816,7 @@ void CGameState::initNewGame(const IMapService * mapService, bool allowSavingRan playerSettings.compOnly = !playerInfo.canHumanPlay; playerSettings.team = playerInfo.team; playerSettings.castle = playerInfo.defaultCastle(); - if(playerSettings.playerID == PlayerSettings::PLAYER_AI && playerSettings.name.empty()) + if(playerSettings.isControlledByAI() && playerSettings.name.empty()) { playerSettings.name = VLC->generaltexth->allTexts[468]; } @@ -837,19 +838,10 @@ void CGameState::initNewGame(const IMapService * mapService, bool allowSavingRan } } -void CGameState::initCampaign(const IMapService * mapService) +void CGameState::initCampaign() { logGlobal->info("Open campaign map file: %d", scenarioOps->campState->currentMap.get()); - auto campaign = scenarioOps->campState; - assert(vstd::contains(campaign->camp->mapPieces, *scenarioOps->campState->currentMap)); - - std::string scenarioName = scenarioOps->mapname.substr(0, scenarioOps->mapname.find('.')); - boost::to_lower(scenarioName); - scenarioName += ':' + boost::lexical_cast(*campaign->currentMap); - - std::string & mapContent = campaign->camp->mapPieces[*campaign->currentMap]; - auto buffer = reinterpret_cast(mapContent.data()); - map = mapService->loadMap(buffer, mapContent.size(), scenarioName).release(); + map = scenarioOps->campState->getMap(); } void CGameState::checkMapChecksum() @@ -962,13 +954,11 @@ void CGameState::initPlayerStates() for(auto & elem : scenarioOps->playerInfos) { PlayerState & p = players[elem.first]; - //std::pair ins(elem.first,PlayerState()); p.color=elem.first; - p.human = elem.second.playerID; + p.human = elem.second.isControlledByHuman(); p.team = map->players[elem.first.getNum()].team; teams[p.team].id = p.team;//init team teams[p.team].players.insert(elem.first);//add player to team - //players.insert(ins); } } @@ -1091,13 +1081,26 @@ CGameState::CrossoverHeroesList CGameState::getCrossoverHeroesFromPreviousScenar auto bonus = campaignState->getBonusForCurrentMap(); if (bonus && bonus->type == CScenarioTravel::STravelBonus::HEROES_FROM_PREVIOUS_SCENARIO) { - crossoverHeroes.heroesFromAnyPreviousScenarios = crossoverHeroes.heroesFromPreviousScenario = campaignState->camp->scenarios[bonus->info2].crossoverHeroes; + std::vector heroes; + for(auto & node : campaignState->camp->scenarios[bonus->info2].crossoverHeroes) + { + auto h = CCampaignState::crossoverDeserialize(node); + heroes.push_back(h); + } + crossoverHeroes.heroesFromAnyPreviousScenarios = crossoverHeroes.heroesFromPreviousScenario = heroes; } else { if(!campaignState->mapsConquered.empty()) { - crossoverHeroes.heroesFromPreviousScenario = campaignState->camp->scenarios[campaignState->mapsConquered.back()].crossoverHeroes; + std::vector heroes; + for(auto & node : campaignState->camp->scenarios[campaignState->mapsConquered.back()].crossoverHeroes) + { + auto h = CCampaignState::crossoverDeserialize(node); + heroes.push_back(h); + } + crossoverHeroes.heroesFromAnyPreviousScenarios = crossoverHeroes.heroesFromPreviousScenario = heroes; + crossoverHeroes.heroesFromPreviousScenario = heroes; for(auto mapNr : campaignState->mapsConquered) { @@ -1108,15 +1111,22 @@ CGameState::CrossoverHeroesList CGameState::getCrossoverHeroesFromPreviousScenar // remove heroes which didn't reached the end of the scenario, but were available at the start for(auto hero : lostCrossoverHeroes) { - vstd::erase_if(crossoverHeroes.heroesFromAnyPreviousScenarios, - CGObjectInstanceBySubIdFinder(hero)); +// auto hero = CCampaignState::crossoverDeserialize(node); + vstd::erase_if(crossoverHeroes.heroesFromAnyPreviousScenarios, [hero](CGHeroInstance * h) + { + return hero->subID == h->subID; + }); } // now add heroes which completed the scenario - for(auto hero : scenario.crossoverHeroes) + for(auto node : scenario.crossoverHeroes) { + auto hero = CCampaignState::crossoverDeserialize(node); // add new heroes and replace old heroes with newer ones - auto it = range::find_if(crossoverHeroes.heroesFromAnyPreviousScenarios, CGObjectInstanceBySubIdFinder(hero)); + auto it = range::find_if(crossoverHeroes.heroesFromAnyPreviousScenarios, [hero](CGHeroInstance * h) + { + return hero->subID == h->subID; + }); if (it != crossoverHeroes.heroesFromAnyPreviousScenarios.end()) { // replace old hero with newer one @@ -1306,7 +1316,7 @@ void CGameState::initStartingResources() for(auto it = scenarioOps->playerInfos.cbegin(); it != scenarioOps->playerInfos.cend(); ++it) { - if(it->second.playerID != PlayerSettings::PLAYER_AI) + if(it->second.isControlledByHuman()) ret.push_back(&it->second); } @@ -1955,7 +1965,7 @@ PlayerRelations::PlayerRelations CGameState::getPlayerRelations( PlayerColor col void CGameState::apply(CPack *pack) { ui16 typ = typeList.getTypeID(pack); - applierGs->getApplier(typ)->applyOnGS(this,pack); + applier->getApplier(typ)->applyOnGS(this, pack); } void CGameState::calculatePaths(const CGHeroInstance *hero, CPathsInfo &out) @@ -2806,7 +2816,7 @@ void CGameState::replaceHeroesPlaceholders(const std::vectorobjects[heroToPlace->id.getNum()] = heroToPlace; map->addBlockVisTiles(heroToPlace); - scenarioOps->campState->getCurrentScenario().placedCrossoverHeroes.push_back(heroToPlace); + scenarioOps->campState->getCurrentScenario().placedCrossoverHeroes.push_back(CCampaignState::crossoverSerialize(heroToPlace)); } } diff --git a/lib/CGameState.h b/lib/CGameState.h index 359dff9cc..ecb4848f2 100644 --- a/lib/CGameState.h +++ b/lib/CGameState.h @@ -60,6 +60,9 @@ namespace boost class shared_mutex; } +template class CApplier; +class CBaseForGSApply; + struct DLL_LINKAGE SThievesGuildInfo { std::vector playerColors; //colors of players that are in-game @@ -153,6 +156,7 @@ public: virtual ~CGameState(); void init(const IMapService * mapService, StartInfo * si, bool allowSavingRandomMap = false); + void updateOnLoad(StartInfo * si); ConstTransitivePtr scenarioOps, initialOpts; //second one is a copy of settings received from pregame (not randomized) PlayerColor currentPlayer; //ID of player currently having turn @@ -246,7 +250,7 @@ private: // ----- initialization ----- void initNewGame(const IMapService * mapService, bool allowSavingRandomMap); - void initCampaign(const IMapService * mapService); + void initCampaign(); void checkMapChecksum(); void initGrailPosition(); void initRandomFactionsForPlayers(); @@ -291,6 +295,7 @@ private: int pickNextHeroType(PlayerColor owner); // picks next free hero type of the H3 hero init sequence -> chosen starting hero, then unused hero type randomly // ---- data ----- + std::shared_ptr> applier; CRandomGenerator rand; friend class CCallback; diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index c982cd72c..ec9cfc669 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -74,7 +74,7 @@ set(lib_SRCS registerTypes/TypesMapObjects1.cpp registerTypes/TypesMapObjects2.cpp registerTypes/TypesMapObjects3.cpp - registerTypes/TypesPregamePacks.cpp + registerTypes/TypesLobbyPacks.cpp registerTypes/TypesServerPacks.cpp rmg/CMapGenerator.cpp @@ -152,6 +152,7 @@ set(lib_SRCS JsonNode.cpp LogicalExpression.cpp NetPacksLib.cpp + StartInfo.cpp ResourceSet.cpp VCMIDirs.cpp VCMI_Lib.cpp @@ -336,6 +337,7 @@ set(lib_HEADERS LogicalExpression.h NetPacksBase.h NetPacks.h + NetPacksLobby.h ResourceSet.h ScopeGuard.h StartInfo.h diff --git a/lib/GameConstants.h b/lib/GameConstants.h index bf63ed05d..52bf15b50 100644 --- a/lib/GameConstants.h +++ b/lib/GameConstants.h @@ -51,6 +51,8 @@ namespace GameConstants const ui32 BASE_MOVEMENT_COST = 100; //default cost for non-diagonal movement const int HERO_PORTRAIT_SHIFT = 30;// 2 special frames + some extra portraits + + const std::array POSSIBLE_TURNTIME = {1, 2, 4, 6, 8, 10, 15, 20, 25, 30, 0}; } class CArtifact; diff --git a/lib/IGameCallback.cpp b/lib/IGameCallback.cpp index 16fde4418..11e688b88 100644 --- a/lib/IGameCallback.cpp +++ b/lib/IGameCallback.cpp @@ -28,6 +28,9 @@ #include "CGameState.h" #include "mapping/CMap.h" #include "CPlayerState.h" +#include "CSkillHandler.h" + +#include "serializer/Connection.h" void CPrivilegedInfoCallback::getFreeTiles(std::vector & tiles) const { diff --git a/lib/NetPacks.h b/lib/NetPacks.h index 013c17f4b..20defab38 100644 --- a/lib/NetPacks.h +++ b/lib/NetPacks.h @@ -22,16 +22,15 @@ #include "spells/ViewSpellInt.h" -class CCampaignState; +class CClient; +class CGameState; +class CGameHandler; class CArtifact; -class CSelectionScreen; class CGObjectInstance; class CArtifactInstance; struct StackLocation; struct ArtSlotInfo; struct QuestInfo; -class CMapInfo; -struct StartInfo; class IBattleState; struct Query : public CPackForClient @@ -438,18 +437,6 @@ struct RemoveBonus : public CPackForClient } }; -struct UpdateCampaignState : public CPackForClient -{ - UpdateCampaignState(){} - std::shared_ptr camp; - void applyCl(CClient *cl); - - template void serialize(Handler &h, const int version) - { - h & camp; - } -}; - struct SetCommanderProperty : public CPackForClient { enum ECommanderProperty {ALIVE, BONUS, SECONDARY_SKILL, EXPERIENCE, SPECIAL_SKILL}; @@ -495,16 +482,6 @@ struct AddQuest : public CPackForClient } }; -struct PrepareForAdvancingCampaign : public CPackForClient -{ - PrepareForAdvancingCampaign(){} - - void applyCl(CClient *cl); - template void serialize(Handler &h, const int version) - { - } -}; - struct UpdateArtHandlerLists : public CPackForClient { UpdateArtHandlerLists(){} @@ -1889,30 +1866,18 @@ struct CommitPackage : public CPackForServer template void serialize(Handler &h, const int version) { + h & static_cast(*this); h & packToCommit; } }; - -struct CloseServer : public CPackForServer -{ - bool applyGh(CGameHandler *gh); - template void serialize(Handler &h, const int version) - {} -}; - -struct LeaveGame : public CPackForServer -{ - bool applyGh(CGameHandler *gh); - template void serialize(Handler &h, const int version) - {} -}; - struct EndTurn : public CPackForServer { bool applyGh(CGameHandler *gh); template void serialize(Handler &h, const int version) - {} + { + h & static_cast(*this); + } }; struct DismissHero : public CPackForServer @@ -1924,6 +1889,7 @@ struct DismissHero : public CPackForServer bool applyGh(CGameHandler *gh); template void serialize(Handler &h, const int version) { + h & static_cast(*this); h & hid; } }; @@ -1939,6 +1905,7 @@ struct MoveHero : public CPackForServer bool applyGh(CGameHandler *gh); template void serialize(Handler &h, const int version) { + h & static_cast(*this); h & dest; h & hid; h & transit; @@ -1956,6 +1923,7 @@ struct CastleTeleportHero : public CPackForServer bool applyGh(CGameHandler *gh); template void serialize(Handler &h, const int version) { + h & static_cast(*this); h & dest; h & hid; } @@ -1974,6 +1942,7 @@ struct ArrangeStacks : public CPackForServer bool applyGh(CGameHandler *gh); template void serialize(Handler &h, const int version) { + h & static_cast(*this); h & what; h & p1; h & p2; @@ -1993,6 +1962,7 @@ struct DisbandCreature : public CPackForServer bool applyGh(CGameHandler *gh); template void serialize(Handler &h, const int version) { + h & static_cast(*this); h & pos; h & id; } @@ -2008,6 +1978,7 @@ struct BuildStructure : public CPackForServer bool applyGh(CGameHandler *gh); template void serialize(Handler &h, const int version) { + h & static_cast(*this); h & tid; h & bid; } @@ -2033,6 +2004,7 @@ struct RecruitCreatures : public CPackForServer bool applyGh(CGameHandler *gh); template void serialize(Handler &h, const int version) { + h & static_cast(*this); h & tid; h & dst; h & crid; @@ -2052,6 +2024,7 @@ struct UpgradeCreature : public CPackForServer bool applyGh(CGameHandler *gh); template void serialize(Handler &h, const int version) { + h & static_cast(*this); h & pos; h & id; h & cid; @@ -2067,6 +2040,7 @@ struct GarrisonHeroSwap : public CPackForServer bool applyGh(CGameHandler *gh); template void serialize(Handler &h, const int version) { + h & static_cast(*this); h & tid; } }; @@ -2079,6 +2053,7 @@ struct ExchangeArtifacts : public CPackForServer bool applyGh(CGameHandler *gh); template void serialize(Handler &h, const int version) { + h & static_cast(*this); h & src; h & dst; } @@ -2097,6 +2072,7 @@ struct AssembleArtifacts : public CPackForServer bool applyGh(CGameHandler *gh); template void serialize(Handler &h, const int version) { + h & static_cast(*this); h & heroID; h & artifactSlot; h & assemble; @@ -2114,6 +2090,7 @@ struct BuyArtifact : public CPackForServer bool applyGh(CGameHandler *gh); template void serialize(Handler &h, const int version) { + h & static_cast(*this); h & hid; h & aid; } @@ -2135,6 +2112,7 @@ struct TradeOnMarketplace : public CPackForServer bool applyGh(CGameHandler *gh); template void serialize(Handler &h, const int version) { + h & static_cast(*this); h & marketId; h & heroId; h & mode; @@ -2154,6 +2132,7 @@ struct SetFormation : public CPackForServer bool applyGh(CGameHandler *gh); template void serialize(Handler &h, const int version) { + h & static_cast(*this); h & hid; h & formation; } @@ -2170,6 +2149,7 @@ struct HireHero : public CPackForServer bool applyGh(CGameHandler *gh); template void serialize(Handler &h, const int version) { + h & static_cast(*this); h & hid; h & tid; h & player; @@ -2184,6 +2164,7 @@ struct BuildBoat : public CPackForServer bool applyGh(CGameHandler *gh); template void serialize(Handler &h, const int version) { + h & static_cast(*this); h & objid; } @@ -2200,6 +2181,7 @@ struct QueryReply : public CPackForServer bool applyGh(CGameHandler *gh); template void serialize(Handler &h, const int version) { + h & static_cast(*this); h & qid; h & player; h & reply; @@ -2215,6 +2197,7 @@ struct MakeAction : public CPackForServer bool applyGh(CGameHandler *gh); template void serialize(Handler &h, const int version) { + h & static_cast(*this); h & ba; } }; @@ -2228,6 +2211,7 @@ struct MakeCustomAction : public CPackForServer bool applyGh(CGameHandler *gh); template void serialize(Handler &h, const int version) { + h & static_cast(*this); h & ba; } }; @@ -2240,6 +2224,7 @@ struct DigWithHero : public CPackForServer bool applyGh(CGameHandler *gh); template void serialize(Handler &h, const int version) { + h & static_cast(*this); h & id; } }; @@ -2254,6 +2239,7 @@ struct CastAdvSpell : public CPackForServer bool applyGh(CGameHandler *gh); template void serialize(Handler &h, const int version) { + h & static_cast(*this); h & hid; h & sid; h & pos; @@ -2262,43 +2248,73 @@ struct CastAdvSpell : public CPackForServer /***********************************************************************************************************/ -struct SaveGame : public CPackForClient, public CPackForServer +struct SaveGame : public CPackForServer { SaveGame(){}; SaveGame(const std::string &Fname) :fname(Fname){}; std::string fname; - void applyCl(CClient *cl); void applyGs(CGameState *gs){}; bool applyGh(CGameHandler *gh); template void serialize(Handler &h, const int version) + { + h & static_cast(*this); + h & fname; + } +}; + +// TODO: Eventually we should re-merge both SaveGame and PlayerMessage +struct SaveGameClient : public CPackForClient +{ + SaveGameClient(){}; + SaveGameClient(const std::string &Fname) :fname(Fname){}; + std::string fname; + + void applyCl(CClient *cl); + template void serialize(Handler &h, const int version) { h & fname; } }; -struct PlayerMessage : public CPackForClient, public CPackForServer +struct PlayerMessage : public CPackForServer { PlayerMessage(){}; - PlayerMessage(PlayerColor Player, const std::string &Text, ObjectInstanceID obj) - :player(Player),text(Text), currObj(obj) + PlayerMessage(const std::string &Text, ObjectInstanceID obj) + : text(Text), currObj(obj) {}; - void applyCl(CClient *cl); void applyGs(CGameState *gs){}; bool applyGh(CGameHandler *gh); - PlayerColor player; std::string text; ObjectInstanceID currObj; // optional parameter that specifies current object. For cheats :) template void serialize(Handler &h, const int version) { + h & static_cast(*this); h & text; - h & player; h & currObj; } }; +struct PlayerMessageClient : public CPackForClient +{ + PlayerMessageClient(){}; + PlayerMessageClient(PlayerColor Player, const std::string &Text) + : player(Player), text(Text) + {} + void applyCl(CClient *cl); + + PlayerColor player; + std::string text; + + template void serialize(Handler &h, const int version) + { + h & player; + h & text; + } +}; + struct CenterView : public CPackForClient { CenterView():focusTime(0){}; @@ -2315,159 +2331,3 @@ struct CenterView : public CPackForClient h & focusTime; } }; - -/***********************************************************************************************************/ - -struct CPackForSelectionScreen : public CPack -{ - void apply(CSelectionScreen *selScreen) {} // implemented in CPreGame.cpp -}; - -class CPregamePackToPropagate : public CPackForSelectionScreen -{}; - -class CPregamePackToHost : public CPackForSelectionScreen -{}; - -struct ChatMessage : public CPregamePackToPropagate -{ - std::string playerName, message; - - void apply(CSelectionScreen *selScreen); - template void serialize(Handler &h, const int version) - { - h & playerName; - h & message; - } -}; - -struct QuitMenuWithoutStarting : public CPregamePackToPropagate -{ - void apply(CSelectionScreen *selScreen); //that functions are implemented in CPreGame.cpp - - template void serialize(Handler &h, const int version) - {} -}; - -struct PlayerJoined : public CPregamePackToHost -{ - std::string playerName; - ui8 connectionID; - - void apply(CSelectionScreen *selScreen); //that functions are implemented in CPreGame.cpp - - template void serialize(Handler &h, const int version) - { - h & playerName; - h & connectionID; - } -}; - -struct ELF_VISIBILITY SelectMap : public CPregamePackToPropagate -{ - const CMapInfo *mapInfo; - bool free;//local flag, do not serialize - - DLL_LINKAGE SelectMap(const CMapInfo &src); - DLL_LINKAGE SelectMap(); - DLL_LINKAGE ~SelectMap(); - - void apply(CSelectionScreen *selScreen); //that functions are implemented in CPreGame.cpp - - template void serialize(Handler &h, const int version) - { - h & mapInfo; - } - -}; - -struct ELF_VISIBILITY UpdateStartOptions : public CPregamePackToPropagate -{ - StartInfo *options; - bool free;//local flag, do not serialize - - void apply(CSelectionScreen *selScreen); //that functions are implemented in CPreGame.cpp - - DLL_LINKAGE UpdateStartOptions(StartInfo &src); - DLL_LINKAGE UpdateStartOptions(); - DLL_LINKAGE ~UpdateStartOptions(); - - template void serialize(Handler &h, const int version) - { - h & options; - } - -}; - -struct PregameGuiAction : public CPregamePackToPropagate -{ - enum {NO_TAB, OPEN_OPTIONS, OPEN_SCENARIO_LIST, OPEN_RANDOM_MAP_OPTIONS} - action; - - void apply(CSelectionScreen *selScreen); //that functions are implemented in CPreGame.cpp - - template void serialize(Handler &h, const int version) - { - h & action; - } -}; - -struct RequestOptionsChange : public CPregamePackToHost -{ - enum EWhat {TOWN, HERO, BONUS}; - ui8 what; - si8 direction; //-1 or +1 - ui8 playerID; - - RequestOptionsChange(ui8 What, si8 Dir, ui8 Player) - :what(What), direction(Dir), playerID(Player) - {} - RequestOptionsChange() - :what(0), direction(0), playerID(0) - {} - - void apply(CSelectionScreen *selScreen); //that functions are implemented in CPreGame.cpp - - template void serialize(Handler &h, const int version) - { - h & what; - h & direction; - h & playerID; - } -}; - -struct PlayerLeft : public CPregamePackToPropagate -{ - ui8 playerID; - - void apply(CSelectionScreen *selScreen); //that functions are implemented in CPreGame.cpp - - template void serialize(Handler &h, const int version) - { - h & playerID; - } -}; - -struct PlayersNames : public CPregamePackToPropagate -{ -public: - std::map playerNames; - - void apply(CSelectionScreen *selScreen); //that functions are implemented in CPreGame.cpp - - template void serialize(Handler &h, const int version) - { - h & playerNames; - } -}; - -struct StartWithCurrentSettings : public CPregamePackToPropagate -{ -public: - void apply(CSelectionScreen *selScreen); //that functions are implemented in CPreGame.cpp - - template void serialize(Handler &h, const int version) - { - //h & playerNames; - } -}; diff --git a/lib/NetPacksBase.h b/lib/NetPacksBase.h index 7d3e3a431..4cb2b908f 100644 --- a/lib/NetPacksBase.h +++ b/lib/NetPacksBase.h @@ -20,7 +20,6 @@ class CArmedInstance; class CArtifactSet; class CBonusSystemNode; struct ArtSlotInfo; -class BattleInfo; #include "ConstTransitivePtr.h" #include "GameConstants.h" @@ -28,7 +27,9 @@ class BattleInfo; struct DLL_LINKAGE CPack { - CPack() {}; + std::shared_ptr c; // Pointer to connection that pack received from + + CPack() : c(nullptr) {}; virtual ~CPack() {}; template void serialize(Handler &h, const int version) @@ -54,12 +55,11 @@ struct CPackForClient : public CPack struct CPackForServer : public CPack { - PlayerColor player; - CConnection *c; + mutable PlayerColor player; + mutable si32 requestID; CGameState* GS(CGameHandler *gh); CPackForServer(): - player(PlayerColor::NEUTRAL), - c(nullptr) + player(PlayerColor::NEUTRAL) { } @@ -69,6 +69,12 @@ struct CPackForServer : public CPack return false; } + template void serialize(Handler &h, const int version) + { + h & player; + h & requestID; + } + protected: void throwNotAllowedAction(); void throwOnWrongOwner(CGameHandler * gh, ObjectInstanceID id); diff --git a/lib/NetPacksLib.cpp b/lib/NetPacksLib.cpp index ca7a28b61..ec708facb 100644 --- a/lib/NetPacksLib.cpp +++ b/lib/NetPacksLib.cpp @@ -56,40 +56,6 @@ DLL_LINKAGE void SetSecSkill::applyGs(CGameState *gs) hero->setSecSkillLevel(which, val, abs); } -DLL_LINKAGE SelectMap::SelectMap(const CMapInfo &src) -{ - mapInfo = &src; - free = false; -} -DLL_LINKAGE SelectMap::SelectMap() -{ - mapInfo = nullptr; - free = true; -} - -DLL_LINKAGE SelectMap::~SelectMap() -{ - if(free) - delete mapInfo; -} - -DLL_LINKAGE UpdateStartOptions::UpdateStartOptions(StartInfo &src) -{ - options = &src; - free = false; -} -DLL_LINKAGE UpdateStartOptions::UpdateStartOptions() -{ - options = nullptr; - free = true; -} - -DLL_LINKAGE UpdateStartOptions::~UpdateStartOptions() -{ - if(free) - delete options; -} - DLL_LINKAGE void SetCommanderProperty::applyGs(CGameState *gs) { CCommanderInstance * commander = gs->getHero(heroid)->commander; @@ -346,8 +312,43 @@ DLL_LINKAGE void ChangeObjectVisitors::applyGs(CGameState *gs) DLL_LINKAGE void PlayerEndsGame::applyGs(CGameState *gs) { PlayerState *p = gs->getPlayer(player); - if(victoryLossCheckResult.victory()) p->status = EPlayerStatus::WINNER; - else p->status = EPlayerStatus::LOSER; + if(victoryLossCheckResult.victory()) + { + p->status = EPlayerStatus::WINNER; + + // TODO: Campaign-specific code might as well go somewhere else + if(p->human && gs->scenarioOps->campState) + { + std::vector crossoverHeroes; + for (CGHeroInstance * hero : gs->map->heroesOnMap) + { + if (hero->tempOwner == player) + { + // keep all heroes from the winning player + crossoverHeroes.push_back(hero); + } + else if (vstd::contains(gs->scenarioOps->campState->getCurrentScenario().keepHeroes, HeroTypeID(hero->subID))) + { + // keep hero whether lost or won (like Xeron in AB campaign) + crossoverHeroes.push_back(hero); + } + } + // keep lost heroes which are in heroes pool + for (auto & heroPair : gs->hpool.heroesPool) + { + if (vstd::contains(gs->scenarioOps->campState->getCurrentScenario().keepHeroes, HeroTypeID(heroPair.first))) + { + crossoverHeroes.push_back(heroPair.second.get()); + } + } + + gs->scenarioOps->campState->setCurrentMapAsConquered(crossoverHeroes); + } + } + else + { + p->status = EPlayerStatus::LOSER; + } } DLL_LINKAGE void RemoveBonus::applyGs(CGameState *gs) diff --git a/lib/NetPacksLobby.h b/lib/NetPacksLobby.h new file mode 100644 index 000000000..f635d3fa0 --- /dev/null +++ b/lib/NetPacksLobby.h @@ -0,0 +1,311 @@ +/* + * NetPacksLobby.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +#include "NetPacksBase.h" + +#include "StartInfo.h" + +class CCampaignState; +class CLobbyScreen; +class CServerHandler; +class CMapInfo; +struct StartInfo; +class CMapGenOptions; +struct ClientPlayer; +class CVCMIServer; + +struct CPackForLobby : public CPack +{ + bool checkClientPermissions(CVCMIServer * srv) const + { + return false; + } + + bool applyOnServer(CVCMIServer * srv) + { + return true; + } + + void applyOnServerAfterAnnounce(CVCMIServer * srv) {} + + bool applyOnLobbyHandler(CServerHandler * handler) + { + return true; + } + + void applyOnLobbyScreen(CLobbyScreen * lobby, CServerHandler * handler) {} +}; + +struct CLobbyPackToPropagate : public CPackForLobby +{ + +}; + +struct CLobbyPackToServer : public CPackForLobby +{ + bool checkClientPermissions(CVCMIServer * srv) const; + void applyOnServerAfterAnnounce(CVCMIServer * srv); +}; + +struct LobbyClientConnected : public CLobbyPackToPropagate +{ + // Set by client before sending pack to server + std::string uuid; + std::vector names; + StartInfo::EMode mode; + // Changed by server before announcing pack + int clientId; + int hostClientId; + + LobbyClientConnected() + : mode(StartInfo::INVALID), clientId(-1), hostClientId(-1) + {} + + bool checkClientPermissions(CVCMIServer * srv) const; + bool applyOnLobbyHandler(CServerHandler * handler); + void applyOnLobbyScreen(CLobbyScreen * lobby, CServerHandler * handler); + bool applyOnServer(CVCMIServer * srv); + void applyOnServerAfterAnnounce(CVCMIServer * srv); + + template void serialize(Handler & h, const int version) + { + h & uuid; + h & names; + h & mode; + + h & clientId; + h & hostClientId; + } +}; + +struct LobbyClientDisconnected : public CLobbyPackToPropagate +{ + int clientId; + bool shutdownServer; + + LobbyClientDisconnected() : shutdownServer(false) {} + bool checkClientPermissions(CVCMIServer * srv) const; + bool applyOnServer(CVCMIServer * srv); + void applyOnServerAfterAnnounce(CVCMIServer * srv); + bool applyOnLobbyHandler(CServerHandler * handler); + void applyOnLobbyScreen(CLobbyScreen * lobby, CServerHandler * handler); + + template void serialize(Handler &h, const int version) + { + h & clientId; + h & shutdownServer; + } +}; + +struct LobbyChatMessage : public CLobbyPackToPropagate +{ + std::string playerName, message; + + bool checkClientPermissions(CVCMIServer * srv) const; + void applyOnLobbyScreen(CLobbyScreen * lobby, CServerHandler * handler); + + template void serialize(Handler &h, const int version) + { + h & playerName; + h & message; + } +}; + +struct LobbyGuiAction : public CLobbyPackToPropagate +{ + enum EAction : ui8 { + NONE, NO_TAB, OPEN_OPTIONS, OPEN_SCENARIO_LIST, OPEN_RANDOM_MAP_OPTIONS + } action; + + LobbyGuiAction() : action(NONE) {} + bool checkClientPermissions(CVCMIServer * srv) const; + void applyOnLobbyScreen(CLobbyScreen * lobby, CServerHandler * handler); + + template void serialize(Handler &h, const int version) + { + h & action; + } +}; + +struct LobbyStartGame : public CLobbyPackToPropagate +{ + // Set by server + std::shared_ptr initializedStartInfo; + + LobbyStartGame() : initializedStartInfo(nullptr) {} + bool checkClientPermissions(CVCMIServer * srv) const; + bool applyOnServer(CVCMIServer * srv); + void applyOnServerAfterAnnounce(CVCMIServer * srv); + bool applyOnLobbyHandler(CServerHandler * handler); + void applyOnLobbyScreen(CLobbyScreen * lobby, CServerHandler * handler); + + template void serialize(Handler &h, const int version) + { + h & initializedStartInfo; + } +}; + +struct LobbyChangeHost : public CLobbyPackToPropagate +{ + int newHostConnectionId; + + LobbyChangeHost() : newHostConnectionId(-1) {} + bool checkClientPermissions(CVCMIServer * srv) const; + bool applyOnServer(CVCMIServer * srv); + bool applyOnServerAfterAnnounce(CVCMIServer * srv); + + template void serialize(Handler & h, const int version) + { + h & newHostConnectionId; + } +}; + +struct LobbyUpdateState : public CLobbyPackToPropagate +{ + LobbyState state; + bool hostChanged; // Used on client-side only + + LobbyUpdateState() : hostChanged(false) {} + bool applyOnLobbyHandler(CServerHandler * handler); + void applyOnLobbyScreen(CLobbyScreen * lobby, CServerHandler * handler); + + template void serialize(Handler &h, const int version) + { + h & state; + } +}; + +struct LobbySetMap : public CLobbyPackToServer +{ + std::shared_ptr mapInfo; + std::shared_ptr mapGenOpts; + + LobbySetMap() : mapInfo(nullptr), mapGenOpts(nullptr) {} + bool applyOnServer(CVCMIServer * srv); + + template void serialize(Handler &h, const int version) + { + h & mapInfo; + h & mapGenOpts; + } +}; + +struct LobbySetCampaign : public CLobbyPackToServer +{ + std::shared_ptr ourCampaign; + + LobbySetCampaign() {} + bool applyOnServer(CVCMIServer * srv); + + template void serialize(Handler &h, const int version) + { + h & ourCampaign; + } +}; + +struct LobbySetCampaignMap : public CLobbyPackToServer +{ + int mapId; + + LobbySetCampaignMap() : mapId(-1) {} + bool applyOnServer(CVCMIServer * srv); + + template void serialize(Handler &h, const int version) + { + h & mapId; + } +}; + +struct LobbySetCampaignBonus : public CLobbyPackToServer +{ + int bonusId; + + LobbySetCampaignBonus() : bonusId(-1) {} + bool applyOnServer(CVCMIServer * srv); + + template void serialize(Handler &h, const int version) + { + h & bonusId; + } +}; + +struct LobbyChangePlayerOption : public CLobbyPackToServer +{ + enum EWhat : ui8 {UNKNOWN, TOWN, HERO, BONUS}; + ui8 what; + si8 direction; //-1 or +1 + PlayerColor color; + + LobbyChangePlayerOption() : what(UNKNOWN), direction(0), color(PlayerColor::CANNOT_DETERMINE) {} + bool checkClientPermissions(CVCMIServer * srv) const; + bool applyOnServer(CVCMIServer * srv); + + template void serialize(Handler &h, const int version) + { + h & what; + h & direction; + h & color; + } +}; + +struct LobbySetPlayer : public CLobbyPackToServer +{ + PlayerColor clickedColor; + + LobbySetPlayer() : clickedColor(PlayerColor::CANNOT_DETERMINE){} + bool applyOnServer(CVCMIServer * srv); + + template void serialize(Handler &h, const int version) + { + h & clickedColor; + } +}; + +struct LobbySetTurnTime : public CLobbyPackToServer +{ + ui8 turnTime; + + LobbySetTurnTime() : turnTime(0) {} + bool applyOnServer(CVCMIServer * srv); + + template void serialize(Handler &h, const int version) + { + h & turnTime; + } +}; + +struct LobbySetDifficulty : public CLobbyPackToServer +{ + ui8 difficulty; + + LobbySetDifficulty() : difficulty(0) {} + bool applyOnServer(CVCMIServer * srv); + + template void serialize(Handler &h, const int version) + { + h & difficulty; + } +}; + +struct LobbyForceSetPlayer : public CLobbyPackToServer +{ + ui8 targetConnectedPlayer; + PlayerColor targetPlayerColor; + + LobbyForceSetPlayer() : targetConnectedPlayer(-1), targetPlayerColor(PlayerColor::CANNOT_DETERMINE) {} + bool applyOnServer(CVCMIServer * srv); + + template void serialize(Handler & h, const int version) + { + h & targetConnectedPlayer; + h & targetPlayerColor; + } +}; diff --git a/lib/StartInfo.cpp b/lib/StartInfo.cpp new file mode 100644 index 000000000..0b92a5e56 --- /dev/null +++ b/lib/StartInfo.cpp @@ -0,0 +1,195 @@ +/* + * StartInfo.cpp, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#include "StdInc.h" +#include "StartInfo.h" + +#include "CGeneralTextHandler.h" +#include "rmg/CMapGenOptions.h" +#include "mapping/CMapInfo.h" + +PlayerSettings::PlayerSettings() + : bonus(RANDOM), castle(NONE), hero(RANDOM), heroPortrait(RANDOM), color(0), handicap(NO_HANDICAP), team(0), compOnly(false) +{ + +} + +bool PlayerSettings::isControlledByAI() const +{ + return !connectedPlayerIDs.size(); +} + +bool PlayerSettings::isControlledByHuman() const +{ + return connectedPlayerIDs.size(); +} + +PlayerSettings & StartInfo::getIthPlayersSettings(PlayerColor no) +{ + if(playerInfos.find(no) != playerInfos.end()) + return playerInfos[no]; + logGlobal->error("Cannot find info about player %s. Throwing...", no.getStr()); + throw std::runtime_error("Cannot find info about player"); +} +const PlayerSettings & StartInfo::getIthPlayersSettings(PlayerColor no) const +{ + return const_cast(*this).getIthPlayersSettings(no); +} + +PlayerSettings * StartInfo::getPlayersSettings(const ui8 connectedPlayerId) +{ + for(auto & elem : playerInfos) + { + if(vstd::contains(elem.second.connectedPlayerIDs, connectedPlayerId)) + return &elem.second; + } + + return nullptr; +} + +std::string StartInfo::getCampaignName() const +{ + if(campState->camp->header.name.length()) + return campState->camp->header.name; + else + return VLC->generaltexth->allTexts[508]; +} + +void LobbyInfo::verifyStateBeforeStart(bool ignoreNoHuman) const +{ + if(!mi) + throw ExceptionMapMissing(); + + //there must be at least one human player before game can be started + std::map::const_iterator i; + for(i = si->playerInfos.cbegin(); i != si->playerInfos.cend(); i++) + if(i->second.isControlledByHuman()) + break; + + if(i == si->playerInfos.cend() && !ignoreNoHuman) + throw ExceptionNoHuman(); + + if(si->mapGenOptions && si->mode == StartInfo::NEW_GAME) + { + if(!si->mapGenOptions->checkOptions()) + throw ExceptionNoTemplate(); + } +} + +bool LobbyInfo::isClientHost(int clientId) const +{ + return clientId == hostClientId; +} + +std::set LobbyInfo::getAllClientPlayers(int clientId) +{ + std::set players; + for(auto & elem : si->playerInfos) + { + if(isClientHost(clientId) && elem.second.isControlledByAI()) + players.insert(elem.first); + + for(ui8 id : elem.second.connectedPlayerIDs) + { + if(vstd::contains(getConnectedPlayerIdsForClient(clientId), id)) + players.insert(elem.first); + } + } + if(isClientHost(clientId)) + players.insert(PlayerColor::NEUTRAL); + + return players; +} + +std::vector LobbyInfo::getConnectedPlayerIdsForClient(int clientId) const +{ + std::vector ids; + + for(auto & pair : playerNames) + { + if(pair.second.connection == clientId) + { + for(auto & elem : si->playerInfos) + { + if(vstd::contains(elem.second.connectedPlayerIDs, pair.first)) + ids.push_back(pair.first); + } + } + } + return ids; +} + +std::set LobbyInfo::clientHumanColors(int clientId) +{ + std::set players; + for(auto & elem : si->playerInfos) + { + for(ui8 id : elem.second.connectedPlayerIDs) + { + if(vstd::contains(getConnectedPlayerIdsForClient(clientId), id)) + { + players.insert(elem.first); + } + } + } + + return players; +} + + +PlayerColor LobbyInfo::clientFirstColor(int clientId) const +{ + for(auto & pair : si->playerInfos) + { + if(isClientColor(clientId, pair.first)) + return pair.first; + } + + return PlayerColor::CANNOT_DETERMINE; +} + +bool LobbyInfo::isClientColor(int clientId, PlayerColor color) const +{ + if(si->playerInfos.find(color) != si->playerInfos.end()) + { + for(ui8 id : si->playerInfos.find(color)->second.connectedPlayerIDs) + { + if(playerNames.find(id) != playerNames.end()) + { + if(playerNames.find(id)->second.connection == clientId) + return true; + } + } + } + return false; +} + +ui8 LobbyInfo::clientFirstId(int clientId) const +{ + for(auto & pair : playerNames) + { + if(pair.second.connection == clientId) + return pair.first; + } + + return 0; +} + +PlayerInfo & LobbyInfo::getPlayerInfo(int color) +{ + return mi->mapHeader->players[color]; +} + +TeamID LobbyInfo::getPlayerTeamId(PlayerColor color) +{ + if(color < PlayerColor::PLAYER_LIMIT) + return getPlayerInfo(color.getNum()).team; + else + return TeamID::NO_TEAM; +} diff --git a/lib/StartInfo.h b/lib/StartInfo.h index b8645af9e..5fbdced25 100644 --- a/lib/StartInfo.h +++ b/lib/StartInfo.h @@ -13,9 +13,13 @@ class CMapGenOptions; class CCampaignState; +class CMapInfo; +struct PlayerInfo; +class PlayerColor; +class SharedMemory; /// Struct which describes the name, the color, the starting bonus of a player -struct PlayerSettings +struct DLL_LINKAGE PlayerSettings { enum { PLAYER_AI = 0 }; // for use in playerID @@ -39,7 +43,7 @@ struct PlayerSettings TeamID team; std::string name; - ui8 playerID; //0 - AI, non-0 serves as player id + std::set connectedPlayerIDs; //Empty - AI, or connectrd player ids bool compOnly; //true if this player is a computer only player; required for RMG template void serialize(Handler &h, const int version) @@ -52,20 +56,30 @@ struct PlayerSettings h & color; h & handicap; h & name; - h & playerID; + if(version < 787) + { + ui8 oldConnectedId = 0; + h & oldConnectedId; + if(oldConnectedId) + { + connectedPlayerIDs.insert(oldConnectedId); + } + } + else + { + h & connectedPlayerIDs; + } h & team; h & compOnly; } - PlayerSettings() : bonus(RANDOM), castle(NONE), hero(RANDOM), heroPortrait(RANDOM), - color(0), handicap(NO_HANDICAP), team(0), playerID(PLAYER_AI), compOnly(false) - { - - } + PlayerSettings(); + bool isControlledByAI() const; + bool isControlledByHuman() const; }; /// Struct which describes the difficulty, the turn time,.. of a heroes match. -struct StartInfo +struct DLL_LINKAGE StartInfo { enum EMode {NEW_GAME, LOAD_GAME, CAMPAIGN, INVALID = 255}; @@ -85,26 +99,12 @@ struct StartInfo std::shared_ptr campState; - PlayerSettings & getIthPlayersSettings(PlayerColor no) - { - if(playerInfos.find(no) != playerInfos.end()) - return playerInfos[no]; - logGlobal->error("Cannot find info about player %s. Throwing...", no.getStr()); - throw std::runtime_error("Cannot find info about player"); - } - const PlayerSettings & getIthPlayersSettings(PlayerColor no) const - { - return const_cast(*this).getIthPlayersSettings(no); - } + PlayerSettings & getIthPlayersSettings(PlayerColor no); + const PlayerSettings & getIthPlayersSettings(PlayerColor no) const; + PlayerSettings * getPlayersSettings(const ui8 connectedPlayerId); - PlayerSettings *getPlayersSettings(const ui8 nameID) - { - for(auto it=playerInfos.begin(); it != playerInfos.end(); ++it) - if(it->second.playerID == nameID) - return &it->second; - - return nullptr; - } + // TODO: Must be client-side + std::string getCampaignName() const; template void serialize(Handler &h, const int version) @@ -127,3 +127,66 @@ struct StartInfo } }; + +struct ClientPlayer +{ + int connection; + std::string name; + + template void serialize(Handler &h, const int version) + { + h & connection; + h & name; + } +}; + +struct LobbyState +{ + std::shared_ptr si; + std::shared_ptr mi; + std::map playerNames; // id of player <-> player name; 0 is reserved as ID of AI "players" + int hostClientId; + // TODO: Campaign-only and we don't really need either of them. + // Before start both go into CCampaignState that is part of StartInfo + int campaignMap; + int campaignBonus; + + LobbyState() : si(new StartInfo()), hostClientId(-1), campaignMap(-1), campaignBonus(-1) {} + + template void serialize(Handler &h, const int version) + { + h & si; + h & mi; + h & playerNames; + h & hostClientId; + h & campaignMap; + h & campaignBonus; + } +}; + +struct DLL_LINKAGE LobbyInfo : public LobbyState +{ + boost::mutex stateMutex; + std::string uuid; + std::shared_ptr shm; + + LobbyInfo() {} + + void verifyStateBeforeStart(bool ignoreNoHuman = false) const; + + bool isClientHost(int clientId) const; + std::set getAllClientPlayers(int clientId); + std::vector getConnectedPlayerIdsForClient(int clientId) const; + + // Helpers for lobby state access + std::set clientHumanColors(int clientId); + PlayerColor clientFirstColor(int clientId) const; + bool isClientColor(int clientId, PlayerColor color) const; + ui8 clientFirstId(int clientId) const; // Used by chat only! + PlayerInfo & getPlayerInfo(int color); + TeamID getPlayerTeamId(PlayerColor color); +}; + +class ExceptionMapMissing : public std::exception {}; +class ExceptionNoHuman : public std::exception {}; +class ExceptionNoTemplate : public std::exception {}; diff --git a/lib/mapObjects/CGHeroInstance.h b/lib/mapObjects/CGHeroInstance.h index 3185e36de..5c91f2329 100644 --- a/lib/mapObjects/CGHeroInstance.h +++ b/lib/mapObjects/CGHeroInstance.h @@ -39,6 +39,8 @@ public: class DLL_LINKAGE CGHeroInstance : public CArmedInstance, public IBoatGenerator, public CArtifactSet, public spells::Caster { + // We serialize heroes into JSON for crossover + friend class CCampaignState; public: ////////////////////////////////////////////////////////////////////////// diff --git a/lib/mapObjects/CGPandoraBox.cpp b/lib/mapObjects/CGPandoraBox.cpp index d6c432432..4e978375d 100644 --- a/lib/mapObjects/CGPandoraBox.cpp +++ b/lib/mapObjects/CGPandoraBox.cpp @@ -468,7 +468,7 @@ void CGEvent::onHeroVisit( const CGHeroInstance * h ) const { if(!(availableFor & (1 << h->tempOwner.getNum()))) return; - if(cb->getPlayerSettings(h->tempOwner)->playerID) + if(cb->getPlayerSettings(h->tempOwner)->isControlledByHuman()) { if(humanActivate) activated(h); diff --git a/lib/mapObjects/CGTownInstance.cpp b/lib/mapObjects/CGTownInstance.cpp index 7aee5494a..c970aac73 100644 --- a/lib/mapObjects/CGTownInstance.cpp +++ b/lib/mapObjects/CGTownInstance.cpp @@ -1350,6 +1350,12 @@ void CGTownInstance::afterAddToMap(CMap * map) map->towns.push_back(this); } +void CGTownInstance::reset() +{ + CGTownInstance::merchantArtifacts.clear(); + CGTownInstance::universitySkills.clear(); +} + void CGTownInstance::serializeJsonOptions(JsonSerializeFormat & handler) { CGObjectInstance::serializeJsonOwner(handler); diff --git a/lib/mapObjects/CGTownInstance.h b/lib/mapObjects/CGTownInstance.h index a7fa51dd9..203d510aa 100644 --- a/lib/mapObjects/CGTownInstance.h +++ b/lib/mapObjects/CGTownInstance.h @@ -291,6 +291,7 @@ public: std::string getObjectName() const override; void afterAddToMap(CMap * map) override; + static void reset(); protected: void setPropertyDer(ui8 what, ui32 val) override; void serializeJsonOptions(JsonSerializeFormat & handler) override; diff --git a/lib/mapping/CCampaignHandler.cpp b/lib/mapping/CCampaignHandler.cpp index a88a00ab5..af41c1951 100644 --- a/lib/mapping/CCampaignHandler.cpp +++ b/lib/mapping/CCampaignHandler.cpp @@ -23,8 +23,12 @@ #include "../CHeroHandler.h" #include "CMapService.h" #include "CMap.h" +#include "CMapInfo.h" -namespace fs = boost::filesystem; +// For hero crossover +#include "serializer/CSerializer.h" +#include "serializer/JsonDeserializer.h" +#include "serializer/JsonSerializer.h" CCampaignHeader::CCampaignHeader() : version(0), mapVersion(0), difficultyChoosenByPlayer(0), music(0), filename(), loadFromLod(0) @@ -373,25 +377,42 @@ bool CCampaignScenario::isNotVoid() const return mapName.size() > 0; } -const CGHeroInstance * CCampaignScenario::strongestHero( PlayerColor owner ) const +const CGHeroInstance * CCampaignScenario::strongestHero(PlayerColor owner) { - using boost::adaptors::filtered; - std::function isOwned = [=](const CGHeroInstance *h){ return h->tempOwner == owner; }; - auto ownedHeroes = crossoverHeroes | filtered(isOwned); + std::function isOwned = [owner](JsonNode & node) + { + auto h = CCampaignState::crossoverDeserialize(node); + bool result = h->tempOwner == owner; + vstd::clear_pointer(h); + return result; + }; + auto ownedHeroes = crossoverHeroes | boost::adaptors::filtered(isOwned); - auto i = vstd::maxElementByFun(ownedHeroes, - [](const CGHeroInstance * h) {return h->getHeroStrength();}); - return i == ownedHeroes.end() ? nullptr : *i; + auto i = vstd::maxElementByFun(ownedHeroes, [](JsonNode & node) + { + auto h = CCampaignState::crossoverDeserialize(node); + double result = h->getHeroStrength(); + vstd::clear_pointer(h); + return result; + }); + return i == ownedHeroes.end() ? nullptr : CCampaignState::crossoverDeserialize(*i); } -std::vector CCampaignScenario::getLostCrossoverHeroes() const +std::vector CCampaignScenario::getLostCrossoverHeroes() { std::vector lostCrossoverHeroes; if(conquered) { - for(auto hero : placedCrossoverHeroes) + for(auto node2 : placedCrossoverHeroes) { - auto it = range::find_if(crossoverHeroes, CGObjectInstanceBySubIdFinder(hero)); + auto hero = CCampaignState::crossoverDeserialize(node2); + auto it = range::find_if(crossoverHeroes, [hero](JsonNode node) + { + auto h = CCampaignState::crossoverDeserialize(node); + bool result = hero->subID == h->subID; + vstd::clear_pointer(h); + return result; + }); if(it == crossoverHeroes.end()) { lostCrossoverHeroes.push_back(hero); @@ -401,9 +422,25 @@ std::vector CCampaignScenario::getLostCrossoverHeroes() const return lostCrossoverHeroes; } -void CCampaignState::setCurrentMapAsConquered( const std::vector & heroes ) +std::vector CCampaignScenario::update787(std::vector & heroes) { - camp->scenarios[*currentMap].crossoverHeroes = heroes; + static_assert(MINIMAL_SERIALIZATION_VERSION < 787, "No longer needed CCampaignScenario::update787"); + std::vector heroesNew; + for(auto hero : heroes) + { + heroesNew.push_back(CCampaignState::crossoverSerialize(hero)); + } + return heroesNew; +} + +void CCampaignState::setCurrentMapAsConquered(const std::vector & heroes) +{ + camp->scenarios[*currentMap].crossoverHeroes.clear(); + for(CGHeroInstance * hero : heroes) + { + camp->scenarios[*currentMap].crossoverHeroes.push_back(crossoverSerialize(hero)); + } + mapsConquered.push_back(*currentMap); mapsRemaining -= *currentMap; camp->scenarios[*currentMap].conquered = true; @@ -449,6 +486,63 @@ CCampaignState::CCampaignState( std::unique_ptr _camp ) : camp(std::m } } +CMap * CCampaignState::getMap(int scenarioId) const +{ + // FIXME: there is certainly better way to handle maps inside campaigns + if(scenarioId == -1) + scenarioId = currentMap.get(); + std::string scenarioName = camp->header.filename.substr(0, camp->header.filename.find('.')); + boost::to_lower(scenarioName); + scenarioName += ':' + boost::lexical_cast(scenarioId); + std::string & mapContent = camp->mapPieces.find(scenarioId)->second; + auto buffer = reinterpret_cast(mapContent.data()); + CMapService mapService; + return mapService.loadMap(buffer, mapContent.size(), scenarioName).release(); +} + +std::unique_ptr CCampaignState::getHeader(int scenarioId) const +{ + if(scenarioId == -1) + scenarioId = currentMap.get(); + + std::string scenarioName = camp->header.filename.substr(0, camp->header.filename.find('.')); + boost::to_lower(scenarioName); + scenarioName += ':' + boost::lexical_cast(scenarioId); + std::string & mapContent = camp->mapPieces.find(scenarioId)->second; + auto buffer = reinterpret_cast(mapContent.data()); + CMapService mapService; + return mapService.loadMapHeader(buffer, mapContent.size(), scenarioName); +} + +std::shared_ptr CCampaignState::getMapInfo(int scenarioId) const +{ + if(scenarioId == -1) + scenarioId = currentMap.get(); + + auto mapInfo = std::make_shared(); + mapInfo->fileURI = camp->header.filename; + mapInfo->mapHeader = getHeader(scenarioId); + mapInfo->countPlayers(); + return mapInfo; +} + +JsonNode CCampaignState::crossoverSerialize(CGHeroInstance * hero) +{ + JsonNode node; + JsonSerializer handler(nullptr, node); + hero->serializeJsonOptions(handler); + return node; +} + +CGHeroInstance * CCampaignState::crossoverDeserialize(JsonNode & node) +{ + JsonDeserializer handler(nullptr, node); + auto hero = new CGHeroInstance(); + hero->ID = Obj::HERO; + hero->serializeJsonOptions(handler); + return hero; +} + std::string CCampaignHandler::prologVideoName(ui8 index) { JsonNode config(ResourceID(std::string("CONFIG/campaignMedia"), EResType::TEXT)); diff --git a/lib/mapping/CCampaignHandler.h b/lib/mapping/CCampaignHandler.h index c1d209660..d977881d9 100644 --- a/lib/mapping/CCampaignHandler.h +++ b/lib/mapping/CCampaignHandler.h @@ -14,6 +14,10 @@ struct StartInfo; class CGHeroInstance; class CBinaryReader; +class CMap; +class CMapHeader; +class CMapInfo; +class JsonNode; namespace CampaignVersion { @@ -134,13 +138,15 @@ public: CScenarioTravel travelOptions; std::vector keepHeroes; // contains list of heroes which should be kept for next scenario (doesn't matter if they lost) - std::vector crossoverHeroes; // contains all heroes with the same state when the campaign scenario was finished - std::vector placedCrossoverHeroes; // contains all placed crossover heroes defined by hero placeholders when the scenario was started + std::vector crossoverHeroes; // contains all heroes with the same state when the campaign scenario was finished + std::vector placedCrossoverHeroes; // contains all placed crossover heroes defined by hero placeholders when the scenario was started - const CGHeroInstance * strongestHero(PlayerColor owner) const; void loadPreconditionRegions(ui32 regions); bool isNotVoid() const; - std::vector getLostCrossoverHeroes() const; /// returns a list of crossover heroes which started the scenario, but didn't complete it + // FIXME: due to usage of JsonNode I can't make these methods const + const CGHeroInstance * strongestHero(PlayerColor owner); + std::vector getLostCrossoverHeroes(); /// returns a list of crossover heroes which started the scenario, but didn't complete it + std::vector update787(std::vector & heroes); CCampaignScenario(); @@ -157,8 +163,19 @@ public: h & prolog; h & epilog; h & travelOptions; - h & crossoverHeroes; - h & placedCrossoverHeroes; + if(formatVersion < 787) + { + std::vector crossoverHeroesOld, placedCrossoverHeroesOld; + h & crossoverHeroesOld; + h & placedCrossoverHeroesOld; + crossoverHeroes = update787(crossoverHeroesOld); + placedCrossoverHeroes = update787(placedCrossoverHeroesOld); + } + else + { + h & crossoverHeroes; + h & placedCrossoverHeroes; + } h & keepHeroes; } }; @@ -192,13 +209,19 @@ public: std::map chosenCampaignBonuses; - //void initNewCampaign(const StartInfo &si); void setCurrentMapAsConquered(const std::vector & heroes); boost::optional getBonusForCurrentMap() const; const CCampaignScenario & getCurrentScenario() const; CCampaignScenario & getCurrentScenario(); ui8 currentBonusID() const; + CMap * getMap(int scenarioId = -1) const; + std::unique_ptr getHeader(int scenarioId = -1) const; + std::shared_ptr getMapInfo(int scenarioId = -1) const; + + static JsonNode crossoverSerialize(CGHeroInstance * hero); + static CGHeroInstance * crossoverDeserialize(JsonNode & node); + CCampaignState(); CCampaignState(std::unique_ptr _camp); ~CCampaignState(){}; diff --git a/lib/mapping/CMap.cpp b/lib/mapping/CMap.cpp index b3e19466b..9e52d460a 100644 --- a/lib/mapping/CMap.cpp +++ b/lib/mapping/CMap.cpp @@ -184,11 +184,6 @@ bool TerrainTile::isWater() const return terType == ETerrainType::WATER; } -const int CMapHeader::MAP_SIZE_SMALL = 36; -const int CMapHeader::MAP_SIZE_MIDDLE = 72; -const int CMapHeader::MAP_SIZE_LARGE = 108; -const int CMapHeader::MAP_SIZE_XLARGE = 144; - void CMapHeader::setupEvents() { EventCondition victoryCondition(EventCondition::STANDARD_WIN); @@ -269,6 +264,8 @@ CMap::~CMap() for(auto quest : quests) quest.dellNull(); + + resetStaticData(); } void CMap::removeBlockVisTiles(CGObjectInstance * obj, bool total) @@ -647,3 +644,11 @@ CMapEditManager * CMap::getEditManager() if(!editManager) editManager = make_unique(this); return editManager.get(); } + +void CMap::resetStaticData() +{ + CGKeys::reset(); + CGMagi::reset(); + CGObelisk::reset(); + CGTownInstance::reset(); +} diff --git a/lib/mapping/CMap.h b/lib/mapping/CMap.h index e36636b95..7ff0fc5de 100644 --- a/lib/mapping/CMap.h +++ b/lib/mapping/CMap.h @@ -288,10 +288,11 @@ class DLL_LINKAGE CMapHeader { void setupEvents(); public: - static const int MAP_SIZE_SMALL; - static const int MAP_SIZE_MIDDLE; - static const int MAP_SIZE_LARGE; - static const int MAP_SIZE_XLARGE; + + static const int MAP_SIZE_SMALL = 36; + static const int MAP_SIZE_MIDDLE = 72; + static const int MAP_SIZE_LARGE = 108; + static const int MAP_SIZE_XLARGE = 144; CMapHeader(); virtual ~CMapHeader(); @@ -382,6 +383,8 @@ public: /// Sets the victory/loss condition objectives ?? void checkForObjectives(); + void resetStaticData(); + ui32 checksum; std::vector rumors; std::vector disposedHeroes; diff --git a/lib/mapping/CMapInfo.cpp b/lib/mapping/CMapInfo.cpp index 7a7d7d686..25b6a78b8 100644 --- a/lib/mapping/CMapInfo.cpp +++ b/lib/mapping/CMapInfo.cpp @@ -15,54 +15,23 @@ #include "../GameConstants.h" #include "CMapService.h" -void CMapInfo::countPlayers() -{ - actualHumanPlayers = playerAmnt = humanPlayers = 0; - for(int i=0; iplayers[i].canHumanPlay) - { - playerAmnt++; - humanPlayers++; - } - else if(mapHeader->players[i].canComputerPlay) - { - playerAmnt++; - } - } +#include "../filesystem/Filesystem.h" +#include "../serializer/CMemorySerializer.h" +#include "../CGeneralTextHandler.h" +#include "../rmg/CMapGenOptions.h" +#include "../CCreatureHandler.h" +#include "../CHeroHandler.h" +#include "../CModHandler.h" - if(scenarioOpts) - for (auto i = scenarioOpts->playerInfos.cbegin(); i != scenarioOpts->playerInfos.cend(); i++) - if(i->second.playerID != PlayerSettings::PLAYER_AI) - actualHumanPlayers++; -} - -CMapInfo::CMapInfo() : scenarioOpts(nullptr), playerAmnt(0), humanPlayers(0), - actualHumanPlayers(0), isRandomMap(false) +CMapInfo::CMapInfo() + : scenarioOptionsOfSave(nullptr), amountOfPlayersOnMap(0), amountOfHumanControllablePlayers(0), amountOfHumanPlayersInSave(0), isRandomMap(false) { } -#define STEAL(x) x = std::move(tmp.x) - -CMapInfo::CMapInfo(CMapInfo && tmp): - scenarioOpts(nullptr), playerAmnt(0), humanPlayers(0), - actualHumanPlayers(0), isRandomMap(false) -{ - std::swap(scenarioOpts, tmp.scenarioOpts); - STEAL(mapHeader); - STEAL(campaignHeader); - STEAL(fileURI); - STEAL(date); - STEAL(playerAmnt); - STEAL(humanPlayers); - STEAL(actualHumanPlayers); - STEAL(isRandomMap); -} - CMapInfo::~CMapInfo() { - vstd::clear_pointer(scenarioOpts); + vstd::clear_pointer(scenarioOptionsOfSave); } void CMapInfo::mapInit(const std::string & fname) @@ -73,23 +42,143 @@ void CMapInfo::mapInit(const std::string & fname) countPlayers(); } +void CMapInfo::saveInit(ResourceID file) +{ + CLoadFile lf(*CResourceHandler::get()->getResourceName(file), MINIMAL_SERIALIZATION_VERSION); + lf.checkMagicBytes(SAVEGAME_MAGIC); + + mapHeader = make_unique(); + lf >> *(mapHeader.get()) >> scenarioOptionsOfSave; + fileURI = file.getName(); + countPlayers(); + std::time_t time = boost::filesystem::last_write_time(*CResourceHandler::get()->getResourceName(file)); + date = std::asctime(std::localtime(&time)); + // We absolutely not need this data for lobby and server will read it from save + // FIXME: actually we don't want them in CMapHeader! + mapHeader->triggeredEvents.clear(); +} + void CMapInfo::campaignInit() { campaignHeader = std::unique_ptr(new CCampaignHeader(CCampaignHandler::getHeader(fileURI))); } -CMapInfo & CMapInfo::operator=(CMapInfo &&tmp) +void CMapInfo::countPlayers() { - STEAL(mapHeader); - STEAL(campaignHeader); - STEAL(scenarioOpts); - STEAL(fileURI); - STEAL(date); - STEAL(playerAmnt); - STEAL(humanPlayers); - STEAL(actualHumanPlayers); - STEAL(isRandomMap); - return *this; + for(int i=0; iplayers[i].canHumanPlay) + { + amountOfPlayersOnMap++; + amountOfHumanControllablePlayers++; + } + else if(mapHeader->players[i].canComputerPlay) + { + amountOfPlayersOnMap++; + } + } + + if(scenarioOptionsOfSave) + for (auto i = scenarioOptionsOfSave->playerInfos.cbegin(); i != scenarioOptionsOfSave->playerInfos.cend(); i++) + if(i->second.isControlledByHuman()) + amountOfHumanPlayersInSave++; } -#undef STEAL +std::string CMapInfo::getName() const +{ + if(campaignHeader && campaignHeader->name.length()) + return campaignHeader->name; + else if(mapHeader && mapHeader->name.length()) + return mapHeader->name; + else + return VLC->generaltexth->allTexts[508]; +} + +std::string CMapInfo::getNameForList() const +{ + if(scenarioOptionsOfSave) + { + // TODO: this could be handled differently + std::vector path; + boost::split(path, fileURI, boost::is_any_of("\\/")); + return path[path.size()-1]; + } + else + { + return getName(); + } +} + +std::string CMapInfo::getDescription() const +{ + if(campaignHeader) + return campaignHeader->description; + else + return mapHeader->description; +} + +int CMapInfo::getMapSizeIconId() const +{ + if(!mapHeader) + return 4; + + switch(mapHeader->width) + { + case CMapHeader::MAP_SIZE_SMALL: + return 0; + case CMapHeader::MAP_SIZE_MIDDLE: + return 1; + case CMapHeader::MAP_SIZE_LARGE: + return 2; + case CMapHeader::MAP_SIZE_XLARGE: + return 3; + default: + return 4; + } +} + +std::pair CMapInfo::getMapSizeFormatIconId() const +{ + int frame = -1, group = 0; + switch(mapHeader->version) + { + case EMapFormat::ROE: + frame = 0; + break; + case EMapFormat::AB: + frame = 1; + break; + case EMapFormat::SOD: + frame = 2; + break; + case EMapFormat::WOG: + frame = 3; + break; + case EMapFormat::VCMI: + frame = 0; + group = 1; + break; + default: + // Unknown version. Be safe and ignore that map + //logGlobal->warn("Warning: %s has wrong version!", currentItem->fileURI); + break; + } + return std::make_pair(frame, group); +} + +std::string CMapInfo::getMapSizeName() const +{ + switch(mapHeader->width) + { + case CMapHeader::MAP_SIZE_SMALL: + return "S"; + case CMapHeader::MAP_SIZE_MIDDLE: + return "M"; + case CMapHeader::MAP_SIZE_LARGE: + return "L"; + case CMapHeader::MAP_SIZE_XLARGE: + return "XL"; + default: + return "C"; + } +} diff --git a/lib/mapping/CMapInfo.h b/lib/mapping/CMapInfo.h index 70645fdcf..c04058bd9 100644 --- a/lib/mapping/CMapInfo.h +++ b/lib/mapping/CMapInfo.h @@ -31,34 +31,41 @@ class DLL_LINKAGE CMapInfo public: std::unique_ptr mapHeader; //may be nullptr if campaign std::unique_ptr campaignHeader; //may be nullptr if scenario - StartInfo * scenarioOpts; //options with which scenario has been started (used only with saved games) + StartInfo * scenarioOptionsOfSave; // Options with which scenario has been started (used only with saved games) std::string fileURI; std::string date; - int playerAmnt; //players in map - int humanPlayers; //players ALLOWED to be controlled by human - int actualHumanPlayers; // >1 if multiplayer game - bool isRandomMap; // true if the map will be created randomly, false if not + int amountOfPlayersOnMap; + int amountOfHumanControllablePlayers; + int amountOfHumanPlayersInSave; + bool isRandomMap; CMapInfo(); - CMapInfo(CMapInfo && tmp); virtual ~CMapInfo(); CMapInfo &operator=(CMapInfo &&other); void mapInit(const std::string & fname); + void saveInit(ResourceID file); void campaignInit(); void countPlayers(); + // TODO: Those must be on client-side + std::string getName() const; + std::string getNameForList() const; + std::string getDescription() const; + int getMapSizeIconId() const; + std::pair getMapSizeFormatIconId() const; + std::string getMapSizeName() const; template void serialize(Handler &h, const int Version) { h & mapHeader; h & campaignHeader; - h & scenarioOpts; + h & scenarioOptionsOfSave; h & fileURI; h & date; - h & playerAmnt; - h & humanPlayers; - h & actualHumanPlayers; + h & amountOfPlayersOnMap; + h & amountOfHumanControllablePlayers; + h & amountOfHumanPlayersInSave; h & isRandomMap; } }; diff --git a/lib/registerTypes/RegisterTypes.cpp b/lib/registerTypes/RegisterTypes.cpp index 2f6f832bd..a3db27100 100644 --- a/lib/registerTypes/RegisterTypes.cpp +++ b/lib/registerTypes/RegisterTypes.cpp @@ -44,7 +44,7 @@ DEFINE_EXTERNAL_METHOD(registerTypesMapObjects2) DEFINE_EXTERNAL_METHOD(registerTypesClientPacks1) DEFINE_EXTERNAL_METHOD(registerTypesClientPacks2) DEFINE_EXTERNAL_METHOD(registerTypesServerPacks) -DEFINE_EXTERNAL_METHOD(registerTypesPregamePacks) +DEFINE_EXTERNAL_METHOD(registerTypesLobbyPacks) template void registerTypes(BinaryDeserializer & s); template void registerTypes(BinarySerializer & s); diff --git a/lib/registerTypes/RegisterTypes.h b/lib/registerTypes/RegisterTypes.h index 150b247ab..274d0c0e3 100644 --- a/lib/registerTypes/RegisterTypes.h +++ b/lib/registerTypes/RegisterTypes.h @@ -10,6 +10,7 @@ #pragma once #include "../NetPacks.h" +#include "../NetPacksLobby.h" #include "../VCMI_Lib.h" #include "../CArtHandler.h" #include "../CPlayerState.h" @@ -234,8 +235,6 @@ void registerTypesClientPacks1(Serializer &s) s.template registerType(); s.template registerType(); s.template registerType(); - s.template registerType(); - s.template registerType(); s.template registerType(); s.template registerType(); s.template registerType(); @@ -312,16 +311,14 @@ void registerTypesClientPacks2(Serializer &s) s.template registerType(); s.template registerType(); - s.template registerType(); - s.template registerType(); + s.template registerType(); + s.template registerType(); } template void registerTypesServerPacks(Serializer &s) { s.template registerType(); - s.template registerType(); - s.template registerType(); s.template registerType(); s.template registerType(); s.template registerType(); @@ -351,23 +348,34 @@ void registerTypesServerPacks(Serializer &s) } template -void registerTypesPregamePacks(Serializer &s) +void registerTypesLobbyPacks(Serializer &s) { - s.template registerType(); - s.template registerType(); - s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); + // Any client can sent + s.template registerType(); + s.template registerType(); + s.template registerType(); + // Only host client send + s.template registerType(); + s.template registerType(); + s.template registerType(); + // Only server send + s.template registerType(); - s.template registerType(); - s.template registerType(); + // For client with permissions + s.template registerType(); + // Only for host client + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); } template @@ -379,7 +387,7 @@ void registerTypes(Serializer &s) registerTypesClientPacks1(s); registerTypesClientPacks2(s); registerTypesServerPacks(s); - registerTypesPregamePacks(s); + registerTypesLobbyPacks(s); } #ifndef INSTANTIATE_REGISTER_TYPES_HERE diff --git a/lib/registerTypes/TypesPregamePacks.cpp b/lib/registerTypes/TypesLobbyPacks.cpp similarity index 73% rename from lib/registerTypes/TypesPregamePacks.cpp rename to lib/registerTypes/TypesLobbyPacks.cpp index 3b90407a3..e7360a6e3 100644 --- a/lib/registerTypes/TypesPregamePacks.cpp +++ b/lib/registerTypes/TypesLobbyPacks.cpp @@ -1,37 +1,37 @@ -/* - * TypesPregamePacks.cpp, part of VCMI engine - * - * Authors: listed in file AUTHORS in main folder - * - * License: GNU General Public License v2.0 or later - * Full text of license available in license.txt file, in main folder - * - */ -#include "StdInc.h" -#include "RegisterTypes.h" - -#include "../mapping/CMapInfo.h" -#include "../StartInfo.h" -#include "../CGameState.h" -#include "../mapping/CMap.h" -#include "../CModHandler.h" -#include "../mapObjects/CObjectHandler.h" -#include "../CCreatureHandler.h" -#include "../VCMI_Lib.h" -#include "../CArtHandler.h" -#include "../CHeroHandler.h" -#include "../spells/CSpellHandler.h" -#include "../CTownHandler.h" -#include "../mapping/CCampaignHandler.h" -#include "../NetPacks.h" -#include "../mapObjects/CObjectClassesHandler.h" -#include "../rmg/CMapGenOptions.h" - -#include "../serializer/BinaryDeserializer.h" -#include "../serializer/BinarySerializer.h" -#include "../serializer/CTypeList.h" - -template void registerTypesPregamePacks(BinaryDeserializer & s); -template void registerTypesPregamePacks(BinarySerializer & s); -template void registerTypesPregamePacks(CTypeList & s); - +/* + * TypesLobbyPacks.cpp, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#include "StdInc.h" +#include "RegisterTypes.h" + +#include "../mapping/CMapInfo.h" +#include "../StartInfo.h" +#include "../CGameState.h" +#include "../mapping/CMap.h" +#include "../CModHandler.h" +#include "../mapObjects/CObjectHandler.h" +#include "../CCreatureHandler.h" +#include "../VCMI_Lib.h" +#include "../CArtHandler.h" +#include "../CHeroHandler.h" +#include "../spells/CSpellHandler.h" +#include "../CTownHandler.h" +#include "../mapping/CCampaignHandler.h" +#include "../NetPacks.h" +#include "../mapObjects/CObjectClassesHandler.h" +#include "../rmg/CMapGenOptions.h" + +#include "../serializer/BinaryDeserializer.h" +#include "../serializer/BinarySerializer.h" +#include "../serializer/CTypeList.h" + +template void registerTypesLobbyPacks(BinaryDeserializer & s); +template void registerTypesLobbyPacks(BinarySerializer & s); +template void registerTypesLobbyPacks(CTypeList & s); + diff --git a/lib/rmg/CMapGenOptions.cpp b/lib/rmg/CMapGenOptions.cpp index 78bca4a0a..321ae123d 100644 --- a/lib/rmg/CMapGenOptions.cpp +++ b/lib/rmg/CMapGenOptions.cpp @@ -17,7 +17,8 @@ #include "../VCMI_Lib.h" #include "../CTownHandler.h" -CMapGenOptions::CMapGenOptions() : width(CMapHeader::MAP_SIZE_MIDDLE), height(CMapHeader::MAP_SIZE_MIDDLE), hasTwoLevels(false), +CMapGenOptions::CMapGenOptions() + : width(CMapHeader::MAP_SIZE_MIDDLE), height(CMapHeader::MAP_SIZE_MIDDLE), hasTwoLevels(true), playerCount(RANDOM_SIZE), teamCount(RANDOM_SIZE), compOnlyPlayerCount(RANDOM_SIZE), compOnlyTeamCount(RANDOM_SIZE), humanPlayersCount(0), waterContent(EWaterContent::RANDOM), monsterStrength(EMonsterStrength::RANDOM), mapTemplate(nullptr) { diff --git a/lib/serializer/CSerializer.h b/lib/serializer/CSerializer.h index d6ff9e31e..1e16806c6 100644 --- a/lib/serializer/CSerializer.h +++ b/lib/serializer/CSerializer.h @@ -12,7 +12,7 @@ #include "../ConstTransitivePtr.h" #include "../GameConstants.h" -const ui32 SERIALIZATION_VERSION = 786; +const ui32 SERIALIZATION_VERSION = 787; const ui32 MINIMAL_SERIALIZATION_VERSION = 753; const std::string SAVEGAME_MAGIC = "VCMISVG"; diff --git a/lib/serializer/Connection.cpp b/lib/serializer/Connection.cpp index ae1810e8b..9d444e7c2 100644 --- a/lib/serializer/Connection.cpp +++ b/lib/serializer/Connection.cpp @@ -35,8 +35,9 @@ using namespace boost::asio::ip; void CConnection::init() { - boost::asio::ip::tcp::no_delay option(true); - socket->set_option(option); + socket->set_option(boost::asio::ip::tcp::no_delay(true)); + socket->set_option(boost::asio::socket_base::send_buffer_size(4194304)); + socket->set_option(boost::asio::socket_base::receive_buffer_size(4194304)); enableSmartPointerSerialization(); disableStackSendingByID(); @@ -50,25 +51,21 @@ void CConnection::init() connected = true; std::string pom; //we got connection - oser & std::string("Aiya!\n") & name & myEndianess; //identify ourselves - iser & pom & pom & contactEndianess; - logNetwork->info("Established connection with %s", pom); - wmx = new boost::mutex(); - rmx = new boost::mutex(); + oser & std::string("Aiya!\n") & name & uuid & myEndianess; //identify ourselves + iser & pom & pom & contactUuid & contactEndianess; + logNetwork->info("Established connection with %s. UUID: %s", pom, contactUuid); + mutexRead = std::make_shared(); + mutexWrite = std::make_shared(); - handler = nullptr; - receivedStop = sendStop = false; - static int cid = 1; - connectionID = cid++; iser.fileVersion = SERIALIZATION_VERSION; } -CConnection::CConnection(std::string host, ui16 port, std::string Name) -:iser(this), oser(this), io_service(new asio::io_service), name(Name) +CConnection::CConnection(std::string host, ui16 port, std::string Name, std::string UUID) + : iser(this), oser(this), io_service(std::make_shared()), connectionID(0), name(Name), uuid(UUID) { int i; boost::system::error_code error = asio::error::host_not_found; - socket = new tcp::socket(*io_service); + socket = std::make_shared(*io_service); tcp::resolver resolver(*io_service); tcp::resolver::iterator end, pom, endpoint_iterator = resolver.resolve(tcp::resolver::query(host, std::to_string(port)),error); if(error) @@ -114,25 +111,23 @@ connerror1: logNetwork->error(error.message()); else logNetwork->error("No error info. "); - delete io_service; - //delete socket; throw std::runtime_error("Can't establish connection :("); } -CConnection::CConnection(TSocket * Socket, std::string Name ) - :iser(this), oser(this), socket(Socket),io_service(&Socket->get_io_service()), name(Name)//, send(this), rec(this) +CConnection::CConnection(std::shared_ptr Socket, std::string Name, std::string UUID) + : iser(this), oser(this), socket(Socket), io_service(&Socket->get_io_service()), connectionID(0), name(Name), uuid(UUID) { init(); } -CConnection::CConnection(TAcceptor * acceptor, boost::asio::io_service *Io_service, std::string Name) -: iser(this), oser(this), name(Name)//, send(this), rec(this) +CConnection::CConnection(std::shared_ptr acceptor, std::shared_ptr Io_service, std::string Name, std::string UUID) + : iser(this), oser(this), connectionID(0), name(Name), uuid(UUID) { boost::system::error_code error = asio::error::host_not_found; - socket = new tcp::socket(*io_service); + socket = std::make_shared(*io_service); acceptor->accept(*socket,error); if (error) { logNetwork->error("Error on accepting: %s", error.message()); - delete socket; + socket.reset(); throw std::runtime_error("Can't establish connection :("); } init(); @@ -171,12 +166,7 @@ CConnection::~CConnection() if(handler) handler->join(); - delete handler; - close(); - delete io_service; - delete wmx; - delete rmx; } template @@ -193,7 +183,7 @@ void CConnection::close() if(socket) { socket->close(); - vstd::clear_pointer(socket); + socket.reset(); } } @@ -202,11 +192,6 @@ bool CConnection::isOpen() const return socket && connected; } -bool CConnection::isHost() const -{ - return connectionID == 1; -} - void CConnection::reportState(vstd::CLoggerBase * out) { out->debug("CConnection"); @@ -219,19 +204,26 @@ void CConnection::reportState(vstd::CLoggerBase * out) CPack * CConnection::retrievePack() { - CPack *ret = nullptr; - boost::unique_lock lock(*rmx); - logNetwork->trace("Listening... "); - iser & ret; - logNetwork->trace("\treceived server message of type %s", (ret? typeid(*ret).name() : "nullptr")); - return ret; + CPack * pack = nullptr; + boost::unique_lock lock(*mutexRead); + iser & pack; + logNetwork->trace("\treceived CPack of type %s", (pack ? typeid(*pack).name() : "nullptr")); + if(pack == nullptr) + { + logNetwork->error("Received a nullptr CPack! You should check whether client and server ABI matches."); + } + else + { + pack->c = this->shared_from_this(); + } + return pack; } -void CConnection::sendPackToServer(const CPack &pack, PlayerColor player, ui32 requestID) +void CConnection::sendPack(const CPack * pack) { - boost::unique_lock lock(*wmx); - logNetwork->trace("Sending to server a pack of type %s", typeid(pack).name()); - oser & player & requestID & &pack; //packs has to be sent as polymorphic pointers! + boost::unique_lock lock(*mutexWrite); + logNetwork->trace("Sending a pack of type %s", typeid(*pack).name()); + oser & pack; } void CConnection::disableStackSendingByID() @@ -254,16 +246,7 @@ void CConnection::enableSmartPointerSerialization() iser.smartPointerSerialization = oser.smartPointerSerialization = true; } -void CConnection::prepareForSendingHeroes() -{ - iser.loadedPointers.clear(); - oser.savedPointers.clear(); - disableSmartVectorMemberSerialization(); - enableSmartPointerSerialization(); - disableStackSendingByID(); -} - -void CConnection::enterPregameConnectionMode() +void CConnection::enterLobbyConnectionMode() { iser.loadedPointers.clear(); oser.savedPointers.clear(); @@ -271,6 +254,13 @@ void CConnection::enterPregameConnectionMode() disableSmartPointerSerialization(); } +void CConnection::enterGameplayConnectionMode(CGameState * gs) +{ + enableStackSendingByID(); + disableSmartPointerSerialization(); + addStdVecItems(gs); +} + void CConnection::disableSmartVectorMemberSerialization() { CSerializer::smartVectorMembersSerialization = false; @@ -283,7 +273,7 @@ void CConnection::enableSmartVectorMemberSerializatoin() std::string CConnection::toString() const { - boost::format fmt("Connection with %s (ID: %d)"); - fmt % name % connectionID; - return fmt.str(); + boost::format fmt("Connection with %s (ID: %d UUID: %s)"); + fmt % name % connectionID % uuid; + return fmt.str(); } diff --git a/lib/serializer/Connection.h b/lib/serializer/Connection.h index cb134e2b6..8c4bcc756 100644 --- a/lib/serializer/Connection.h +++ b/lib/serializer/Connection.h @@ -47,7 +47,7 @@ typedef boost::asio::basic_socket_acceptor { CConnection(); @@ -60,31 +60,31 @@ public: BinaryDeserializer iser; BinarySerializer oser; - boost::mutex *rmx, *wmx; // read/write mutexes - TSocket * socket; + std::shared_ptr mutexRead; + std::shared_ptr mutexWrite; + std::shared_ptr socket; bool connected; bool myEndianess, contactEndianess; //true if little endian, if endianness is different we'll have to revert received multi-byte vars - boost::asio::io_service *io_service; + std::string contactUuid; + std::shared_ptr io_service; std::string name; //who uses this connection + std::string uuid; int connectionID; - boost::thread *handler; + std::shared_ptr handler; - bool receivedStop, sendStop; - - CConnection(std::string host, ui16 port, std::string Name); - CConnection(TAcceptor * acceptor, boost::asio::io_service *Io_service, std::string Name); - CConnection(TSocket * Socket, std::string Name); //use immediately after accepting connection into socket + CConnection(std::string host, ui16 port, std::string Name, std::string UUID); + CConnection(std::shared_ptr acceptor, std::shared_ptr Io_service, std::string Name, std::string UUID); + CConnection(std::shared_ptr Socket, std::string Name, std::string UUID); //use immediately after accepting connection into socket void close(); bool isOpen() const; - bool isHost() const; template CConnection &operator&(const T&); virtual ~CConnection(); - CPack * retrievePack(); //gets from server next pack (allocates it with new) - void sendPackToServer(const CPack &pack, PlayerColor player, ui32 requestID); + CPack * retrievePack(); + void sendPack(const CPack * pack); void disableStackSendingByID(); void enableStackSendingByID(); @@ -93,8 +93,8 @@ public: void disableSmartVectorMemberSerialization(); void enableSmartVectorMemberSerializatoin(); - void prepareForSendingHeroes(); //disables sending vectorized, enables smart pointer serialization, clears saved/loaded ptr cache - void enterPregameConnectionMode(); + void enterLobbyConnectionMode(); + void enterGameplayConnectionMode(CGameState * gs); std::string toString() const; diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 7e6f2ee82..85553f1c4 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -50,7 +50,6 @@ #ifndef _MSC_VER #include #endif -extern std::atomic serverShuttingDown; #define COMPLAIN_RET_IF(cond, txt) do {if (cond){complain(txt); return;}} while(0) #define COMPLAIN_RET_FALSE_IF(cond, txt) do {if (cond){complain(txt); return false;}} while(0) @@ -177,7 +176,7 @@ template class CApplyOnGH; class CBaseForGHApply { public: - virtual bool applyOnGH(CGameHandler *gh, CConnection *c, void *pack, PlayerColor player) const =0; + virtual bool applyOnGH(CGameHandler * gh, void * pack) const =0; virtual ~CBaseForGHApply(){} template static CBaseForGHApply *getApplier(const U * t=nullptr) { @@ -188,11 +187,9 @@ public: template class CApplyOnGH : public CBaseForGHApply { public: - bool applyOnGH(CGameHandler *gh, CConnection *c, void *pack, PlayerColor player) const override + bool applyOnGH(CGameHandler * gh, void * pack) const override { T *ptr = static_cast(pack); - ptr->c = c; - ptr->player = player; try { return ptr->applyGh(gh); @@ -212,7 +209,7 @@ template <> class CApplyOnGH : public CBaseForGHApply { public: - bool applyOnGH(CGameHandler *gh, CConnection *c, void *pack, PlayerColor player) const override + bool applyOnGH(CGameHandler * gh, void * pack) const override { logGlobal->error("Cannot apply on GH plain CPack!"); assert(0); @@ -220,8 +217,6 @@ public: } }; -static CApplier *applier = nullptr; - static inline double distance(int3 a, int3 b) { return std::sqrt((double)(a.x-b.x)*(a.x-b.x) + (a.y-b.y)*(a.y-b.y)); @@ -1194,114 +1189,64 @@ void CGameHandler::applyBattleEffects(BattleAttack & bat, std::shared_ptr players, CConnection &c) -{ - setThreadName("CGameHandler::handleConnection"); - auto handleDisconnection = [&](const std::exception & e) +void CGameHandler::handleClientDisconnection(std::shared_ptr c) +{ + for(auto playerConns : connections) { - boost::unique_lock lock(*c.wmx); - assert(!c.connected); //make sure that connection has been marked as broken - logGlobal->error(e.what()); - conns -= &c; - for(auto playerConn : connections) + for(auto conn : playerConns.second) { - if(!serverShuttingDown && playerConn.second == &c) + if(lobby->state != EServerState::SHUTDOWN && conn == c) { + vstd::erase_if_present(playerConns.second, conn); + if(playerConns.second.size()) + continue; PlayerCheated pc; - pc.player = playerConn.first; + pc.player = playerConns.first; pc.losingCheatCode = true; sendAndApply(&pc); - checkVictoryLossConditionsForPlayer(playerConn.first); + checkVictoryLossConditionsForPlayer(playerConns.first); } } + } +} + +void CGameHandler::handleReceivedPack(CPackForServer * pack) +{ + //prepare struct informing that action was applied + auto sendPackageResponse = [&](bool succesfullyApplied) + { + PackageApplied applied; + applied.player = pack->player; + applied.result = succesfullyApplied; + applied.packType = typeList.getTypeID(pack); + applied.requestID = pack->requestID; + pack->c->sendPack(&applied); }; - try + CBaseForGHApply * apply = applier->getApplier(typeList.getTypeID(pack)); //and appropriate applier object + if(isBlockedByQueries(pack, pack->player)) { - while(1)//server should never shut connection first //was: while(!end2) - { - CPack *pack = nullptr; - PlayerColor player = PlayerColor::NEUTRAL; - si32 requestID = -999; - int packType = 0; - - { - boost::unique_lock lock(*c.rmx); - if(!c.connected) - throw clientDisconnectedException(); - c >> player >> requestID >> pack; //get the package - - if (!pack) - { - logGlobal->error("Received a null package marked as request %d from player %d", requestID, player); - } - else - { - packType = typeList.getTypeID(pack); //get the id of type - - logGlobal->trace("Received client message (request %d by player %d (%s)) of type with ID=%d (%s).\n", - requestID, player, player.getStr(), packType, typeid(*pack).name()); - } - } - - //prepare struct informing that action was applied - auto sendPackageResponse = [&](bool succesfullyApplied) - { - //dont reply to disconnected client - //TODO: this must be implemented as option of CPackForServer - if(dynamic_cast(pack) || dynamic_cast(pack)) - return; - - PackageApplied applied; - applied.player = player; - applied.result = succesfullyApplied; - applied.packType = packType; - applied.requestID = requestID; - boost::unique_lock lock(*c.wmx); - c << &applied; - }; - CBaseForGHApply *apply = applier->getApplier(packType); //and appropriate applier object - if(isBlockedByQueries(pack, player)) - { - sendPackageResponse(false); - } - else if (apply) - { - const bool result = apply->applyOnGH(this, &c, pack, player); - if (result) - logGlobal->trace("Message %s successfully applied!", typeid(*pack).name()); - else - complain((boost::format("Got false in applying %s... that request must have been fishy!") - % typeid(*pack).name()).str()); - - sendPackageResponse(true); - } - else - { - logGlobal->error("Message cannot be applied, cannot find applier (unregistered type)!"); - sendPackageResponse(false); - } - - vstd::clear_pointer(pack); - } + sendPackageResponse(false); } - catch(boost::system::system_error &e) //for boost errors just log, not crash - probably client shut down connection + else if(apply) { - handleDisconnection(e); + const bool result = apply->applyOnGH(this, pack); + if(result) + logGlobal->trace("Message %s successfully applied!", typeid(*pack).name()); + else + complain((boost::format("Got false in applying %s... that request must have been fishy!") + % typeid(*pack).name()).str()); + + sendPackageResponse(true); } - catch(clientDisconnectedException & e) + else { - handleDisconnection(e); - } - catch(...) - { - serverShuttingDown = true; - handleException(); - throw; + logGlobal->error("Message cannot be applied, cannot find applier (unregistered type)!"); + sendPackageResponse(false); } - logGlobal->error("Ended handling connection"); + vstd::clear_pointer(pack); } int CGameHandler::moveStack(int stack, BattleHex dest) @@ -1569,12 +1514,12 @@ int CGameHandler::moveStack(int stack, BattleHex dest) return ret; } -CGameHandler::CGameHandler() +CGameHandler::CGameHandler(CVCMIServer * lobby) + : lobby(lobby) { QID = 1; - //gs = nullptr; IObjectInterface::cb = this; - applier = new CApplier(); + applier = std::make_shared>(); registerTypesServerPacks(*applier); visitObjectAfterVictory = false; @@ -1584,8 +1529,6 @@ CGameHandler::CGameHandler() CGameHandler::~CGameHandler() { delete spellEnv; - delete applier; - applier = nullptr; delete gs; } @@ -1988,16 +1931,9 @@ void CGameHandler::run(bool resume) LOG_TRACE_PARAMS(logGlobal, "resume=%d", resume); using namespace boost::posix_time; - for (CConnection *cc : conns) + for (auto cc : lobby->connections) { - if (!resume) - { - (*cc) << gs->initialOpts; // gs->scenarioOps - } - - std::set players; - (*cc) >> players; //how many players will be handled at that client - + auto players = lobby->getAllClientPlayers(cc->connectionID); std::stringstream sbuffer; sbuffer << "Connection " << cc->connectionID << " will handle " << players.size() << " player: "; for (PlayerColor color : players) @@ -2005,30 +1941,15 @@ void CGameHandler::run(bool resume) sbuffer << color << " "; { boost::unique_lock lock(gsm); - if(!color.isSpectator()) // there can be more than one spectator - connections[color] = cc; + connections[color].insert(cc); } } logGlobal->info(sbuffer.str()); - - cc->addStdVecItems(gs); - cc->enableStackSendingByID(); - cc->disableSmartPointerSerialization(); - } - - for (auto & elem : conns) - { - std::set pom; - for (auto j = connections.cbegin(); j!=connections.cend();j++) - if (j->second == elem) - pom.insert(j->first); - - boost::thread(std::bind(&CGameHandler::handleConnection,this,pom,std::ref(*elem))); } auto playerTurnOrder = generatePlayerTurnOrder(); - while(!serverShuttingDown) + while(lobby->state == EServerState::GAMEPLAY) { if (!resume) newTurn(); @@ -2069,12 +1990,14 @@ void CGameHandler::run(bool resume) //wait till turn is done boost::unique_lock lock(states.mx); - while(states.players.at(playerColor).makingTurn && !serverShuttingDown) + while(states.players.at(playerColor).makingTurn && lobby->state == EServerState::GAMEPLAY) { static time_duration p = milliseconds(100); states.cv.timed_wait(lock, p); } } + if(lobby->state != EServerState::GAMEPLAY) + break; } } //additional check that game is not finished @@ -2084,11 +2007,9 @@ void CGameHandler::run(bool resume) if (gs->players[player].status == EPlayerStatus::INGAME) activePlayer = true; } - if (!activePlayer) - serverShuttingDown = true; + if(!activePlayer) + lobby->state = EServerState::GAMEPLAY_ENDED; } - while(conns.size() && (*conns.begin())->isOpen()) - boost::this_thread::sleep(boost::posix_time::milliseconds(5)); //give time client to close socket } std::list CGameHandler::generatePlayerTurnOrder() const @@ -2619,12 +2540,12 @@ void CGameHandler::changeSpells(const CGHeroInstance * hero, bool give, const st sendAndApply(&cs); } -void CGameHandler::sendMessageTo(CConnection &c, const std::string &message) +void CGameHandler::sendMessageTo(std::shared_ptr c, const std::string &message) { SystemMessage sm; sm.text = message; - boost::unique_lock lock(*c.wmx); - c << &sm; + boost::unique_lock lock(*c->mutexWrite); + *(c.get()) << &sm; } void CGameHandler::giveHeroBonus(GiveBonus * bonus) @@ -2776,13 +2697,12 @@ void CGameHandler::heroExchange(ObjectInstanceID hero1, ObjectInstanceID hero2) void CGameHandler::sendToAllClients(CPackForClient * info) { logNetwork->trace("Sending to all clients a package of type %s", typeid(*info).name()); - for (auto & elem : conns) + for (auto c : lobby->connections) { - if(!elem->isOpen()) + if(!c->isOpen()) continue; - boost::unique_lock lock(*(elem)->wmx); - *elem << info; + c->sendPack(info); } } @@ -2818,14 +2738,14 @@ void CGameHandler::sendAndApply(NewStructures * info) void CGameHandler::save(const std::string & filename) { - logGlobal->info("Saving to %s", filename); + logGlobal->info("Loading from %s", filename); const auto stem = FileInfo::GetPathStem(filename); const auto savefname = stem.to_string() + ".vsgm1"; CResourceHandler::get("local")->createResource(savefname); { logGlobal->info("Ordering clients to serialize..."); - SaveGame sg(savefname); + SaveGameClient sg(savefname); sendToAllClients(&sg); } @@ -2845,34 +2765,26 @@ void CGameHandler::save(const std::string & filename) } } -void CGameHandler::close() +void CGameHandler::load(const std::string & filename) { - logGlobal->info("We have been requested to close."); - serverShuttingDown = true; + logGlobal->info("Loading from %s", filename); + const auto stem = FileInfo::GetPathStem(filename); - for (auto & elem : conns) + try { - if(!elem->isOpen()) - continue; - - boost::unique_lock lock(*(elem)->wmx); - elem->close(); - elem->connected = false; - } -} - -void CGameHandler::playerLeftGame(int cid) -{ - for (auto & elem : conns) - { - if(elem->isOpen() && elem->connectionID == cid) { - boost::unique_lock lock(*(elem)->wmx); - elem->close(); - elem->connected = false; - break; + CLoadFile lf(*CResourceHandler::get("local")->getResourceName(ResourceID(stem.to_string(), EResType::SERVER_SAVEGAME)), MINIMAL_SERIALIZATION_VERSION); + loadCommonState(lf); + logGlobal->info("Loading server state"); + lf >> *this; } + logGlobal->info("Game has been successfully loaded!"); } + catch(std::exception &e) + { + logGlobal->error("Failed to load game: %s", e.what()); + } + gs->updateOnLoad(lobby->si.get()); } bool CGameHandler::arrangeStacks(ObjectInstanceID id1, ObjectInstanceID id2, ui8 what, SlotID p1, SlotID p2, si32 val, PlayerColor player) @@ -3014,11 +2926,11 @@ bool CGameHandler::arrangeStacks(ObjectInstanceID id1, ObjectInstanceID id2, ui8 return true; } -PlayerColor CGameHandler::getPlayerAt(CConnection *c) const +PlayerColor CGameHandler::getPlayerAt(std::shared_ptr c) const { std::set all; for (auto i=connections.cbegin(); i!=connections.cend(); i++) - if (i->second == c) + if(vstd::contains(i->second, c)) all.insert(i->first); switch(all.size()) @@ -4567,7 +4479,7 @@ bool CGameHandler::makeBattleAction(BattleAction &ba) void CGameHandler::playerMessage(PlayerColor player, const std::string &message, ObjectInstanceID currObj) { bool cheated = true; - PlayerMessage temp_message(player, message, ObjectInstanceID(-1)); // don't inform other client on selected object + PlayerMessageClient temp_message(player, message); sendAndApply(&temp_message); std::vector cheat; @@ -5304,51 +5216,9 @@ void CGameHandler::checkVictoryLossConditionsForPlayer(PlayerColor player) } } - if (p->human) + if(p->human) { - serverShuttingDown = true; - - if (gs->scenarioOps->campState) - { - std::vector crossoverHeroes; - for (CGHeroInstance * hero : gs->map->heroesOnMap) - { - if (hero->tempOwner == player) - { - // keep all heroes from the winning player - crossoverHeroes.push_back(hero); - } - else if (vstd::contains(gs->scenarioOps->campState->getCurrentScenario().keepHeroes, HeroTypeID(hero->subID))) - { - // keep hero whether lost or won (like Xeron in AB campaign) - crossoverHeroes.push_back(hero); - } - } - // keep lost heroes which are in heroes pool - for (auto & heroPair : gs->hpool.heroesPool) - { - if (vstd::contains(gs->scenarioOps->campState->getCurrentScenario().keepHeroes, HeroTypeID(heroPair.first))) - { - crossoverHeroes.push_back(heroPair.second.get()); - } - } - - gs->scenarioOps->campState->setCurrentMapAsConquered(crossoverHeroes); - - //Request clients to change connection mode - PrepareForAdvancingCampaign pfac; - sendAndApply(&pfac); - //Change connection mode - if (getPlayer(player)->human && getStartInfo()->campState) - { - for (auto connection : conns) - connection->prepareForSendingHeroes(); - } - - UpdateCampaignState ucs; - ucs.camp = gs->scenarioOps->campState; - sendAndApply(&ucs); - } + lobby->state = EServerState::GAMEPLAY_ENDED; } } else diff --git a/server/CGameHandler.h b/server/CGameHandler.h index 085d7d4d3..8b8df069a 100644 --- a/server/CGameHandler.h +++ b/server/CGameHandler.h @@ -30,6 +30,9 @@ class IMarket; class SpellCastEnvironment; +template class CApplier; +class CBaseForGHApply; + struct PlayerStatus { bool makingTurn; @@ -74,6 +77,8 @@ struct CasualtiesAfterBattle class CGameHandler : public IGameCallback, CBattleInfoCallback { + CVCMIServer * lobby; + std::shared_ptr> applier; public: using FireShieldInfo = std::vector>; //use enums as parameters, because doMove(sth, true, false, true) is not readable @@ -81,9 +86,8 @@ public: enum EVisitDest {VISIT_DEST, DONT_VISIT_DEST}; enum ELEaveTile {LEAVING_TILE, REMAINING_ON_TILE}; - std::map connections; //player color -> connection to client with interface of that player + std::map>> connections; //player color -> connection to client with interface of that player PlayerStatuses states; //player color -> player state - std::set conns; //queries stuff boost::recursive_mutex gsm; @@ -111,7 +115,7 @@ public: void setupBattle(int3 tile, const CArmedInstance *armies[2], const CGHeroInstance *heroes[2], bool creatureBank, const CGTownInstance *town); void setBattleResult(BattleResult::EResult resultType, int victoriusSide); - CGameHandler(); + CGameHandler(CVCMIServer * lobby); ~CGameHandler(); ////////////////////////////////////////////////////////////////////////// @@ -189,8 +193,9 @@ public: void commitPackage(CPackForClient *pack) override; void init(StartInfo *si); - void handleConnection(std::set players, CConnection &c); - PlayerColor getPlayerAt(CConnection *c) const; + void handleClientDisconnection(std::shared_ptr c); + void handleReceivedPack(CPackForServer * pack); + PlayerColor getPlayerAt(std::shared_ptr c) const; void playerMessage(PlayerColor player, const std::string &message, ObjectInstanceID currObj); void updateGateState(); @@ -225,8 +230,8 @@ public: bool disbandCreature( ObjectInstanceID id, SlotID pos ); bool arrangeStacks( ObjectInstanceID id1, ObjectInstanceID id2, ui8 what, SlotID p1, SlotID p2, si32 val, PlayerColor player); void save(const std::string &fname); - void close(); - void playerLeftGame(int cid); + void load(const std::string &fname); + void handleTimeEvents(); void handleTownEvents(CGTownInstance *town, NewTurn &n); bool complain(const std::string &problem); //sends message to all clients, prints on the logs and return true @@ -248,7 +253,7 @@ public: } void sendMessageToAll(const std::string &message); - void sendMessageTo(CConnection &c, const std::string &message); + void sendMessageTo(std::shared_ptr c, const std::string &message); void sendToAllClients(CPackForClient * info); void sendAndApply(CPackForClient * info) override; void applyAndSend(CPackForClient * info); @@ -308,10 +313,6 @@ private: void checkVictoryLossConditionsForAll(); }; -class clientDisconnectedException : public std::exception -{ - -}; class ExceptionNotAllowedAction : public std::exception { diff --git a/server/CMakeLists.txt b/server/CMakeLists.txt index e2ace12ad..1fc8eb4f0 100644 --- a/server/CMakeLists.txt +++ b/server/CMakeLists.txt @@ -8,6 +8,7 @@ set(server_SRCS CQuery.cpp CVCMIServer.cpp NetPacksServer.cpp + NetPacksLobbyServer.cpp ) set(server_HEADERS diff --git a/server/CVCMIServer.cpp b/server/CVCMIServer.cpp index d459a8e95..b0788f228 100644 --- a/server/CVCMIServer.cpp +++ b/server/CVCMIServer.cpp @@ -1,682 +1,963 @@ -/* - * CVCMIServer.cpp, part of VCMI engine - * - * Authors: listed in file AUTHORS in main folder - * - * License: GNU General Public License v2.0 or later - * Full text of license available in license.txt file, in main folder - * - */ -#include "StdInc.h" - +/* + * CVCMIServer.cpp, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#include "StdInc.h" + #if BOOST_VERSION >= 106600 #define BOOST_ASIO_ENABLE_OLD_SERVICES -#endif -#include - -#include "../lib/filesystem/Filesystem.h" -#include "../lib/mapping/CCampaignHandler.h" -#include "../lib/CThreadHelper.h" -#include "../lib/serializer/Connection.h" -#include "../lib/CModHandler.h" -#include "../lib/CArtHandler.h" -#include "../lib/CGeneralTextHandler.h" -#include "../lib/CHeroHandler.h" -#include "../lib/CTownHandler.h" -#include "../lib/CBuildingHandler.h" -#include "../lib/spells/CSpellHandler.h" -#include "../lib/CCreatureHandler.h" -#include "zlib.h" -#include "CVCMIServer.h" -#include "../lib/StartInfo.h" -#include "../lib/mapping/CMap.h" -#include "../lib/rmg/CMapGenOptions.h" -#ifdef VCMI_ANDROID -#include "lib/CAndroidVMHelper.h" -#else -#include "../lib/Interprocess.h" -#endif -#include "../lib/VCMI_Lib.h" -#include "../lib/VCMIDirs.h" -#include "CGameHandler.h" -#include "../lib/mapping/CMapInfo.h" -#include "../lib/GameConstants.h" -#include "../lib/logging/CBasicLogConfigurator.h" -#include "../lib/CConfigHandler.h" -#include "../lib/ScopeGuard.h" - -#include "../lib/UnlockGuard.h" - -#if defined(__GNUC__) && !defined (__MINGW32__) && !defined(VCMI_ANDROID) -#include -#endif - -std::string NAME_AFFIX = "server"; -std::string NAME = GameConstants::VCMI_VERSION + std::string(" (") + NAME_AFFIX + ')'; //application name -std::atomic serverShuttingDown(false); - -boost::program_options::variables_map cmdLineOptions; - -static void vaccept(boost::asio::ip::tcp::acceptor *ac, boost::asio::ip::tcp::socket *s, boost::system::error_code *error) -{ - ac->accept(*s,*error); -} - - - -CPregameServer::CPregameServer(CConnection * Host, TAcceptor * Acceptor) - : host(Host), listeningThreads(0), acceptor(Acceptor), upcomingConnection(nullptr), - curmap(nullptr), curStartInfo(nullptr), state(RUNNING) -{ - initConnection(host); -} - -void CPregameServer::handleConnection(CConnection *cpc) -{ - setThreadName("CPregameServer::handleConnection"); - try - { - while(!cpc->receivedStop) - { - CPackForSelectionScreen *cpfs = nullptr; - *cpc >> cpfs; - - logNetwork->info("Got package to announce %s from %s", typeid(*cpfs).name(), cpc->toString()); - - boost::unique_lock queueLock(mx); - bool quitting = dynamic_ptr_cast(cpfs), - startingGame = dynamic_ptr_cast(cpfs); - if(quitting || startingGame) //host leaves main menu or wants to start game -> we end - { - cpc->receivedStop = true; - if(!cpc->sendStop) - sendPack(cpc, *cpfs); - - if(cpc == host) - toAnnounce.push_back(cpfs); - } - else - toAnnounce.push_back(cpfs); - - if(startingGame) - { - //wait for sending thread to announce start - auto unlock = vstd::makeUnlockGuard(mx); - while(state == RUNNING) boost::this_thread::sleep(boost::posix_time::milliseconds(50)); - } - else if(quitting) // Server must be stopped if host is leaving from lobby to avoid crash - { - serverShuttingDown = true; - } - } - } - catch (const std::exception& e) - { - boost::unique_lock queueLock(mx); - logNetwork->error("%s dies... \nWhat happened: %s", cpc->toString(), e.what()); - } - - boost::unique_lock queueLock(mx); - if(state != ENDING_AND_STARTING_GAME) - { - connections -= cpc; - - //notify other players about leaving - auto pl = new PlayerLeft(); - pl->playerID = cpc->connectionID; - announceTxt(cpc->name + " left the game"); - toAnnounce.push_back(pl); - - if(connections.empty()) - { - logNetwork->error("Last connection lost, server will close itself..."); - boost::this_thread::sleep(boost::posix_time::seconds(2)); //we should never be hasty when networking - state = ENDING_WITHOUT_START; - } - } - - logNetwork->info("Thread listening for %s ended", cpc->toString()); - listeningThreads--; - vstd::clear_pointer(cpc->handler); -} - -void CPregameServer::run() -{ - startListeningThread(host); - start_async_accept(); - - while(state == RUNNING) - { - { - boost::unique_lock myLock(mx); - while(!toAnnounce.empty()) - { - processPack(toAnnounce.front()); - toAnnounce.pop_front(); - } - -// //we end sending thread if we ordered all our connections to stop -// ending = true; -// for(CPregameConnection *pc : connections) -// if(!pc->sendStop) -// ending = false; - - if(state != RUNNING) - { - logNetwork->info("Stopping listening for connections..."); - if(acceptor) - acceptor->close(); - } - - if(acceptor) - { - acceptor->get_io_service().reset(); - acceptor->get_io_service().poll(); - } - } //frees lock - - boost::this_thread::sleep(boost::posix_time::milliseconds(50)); - } - - logNetwork->info("Thread handling connections ended"); - - if(state == ENDING_AND_STARTING_GAME) - { - logNetwork->info("Waiting for listening thread to finish..."); - while(listeningThreads) boost::this_thread::sleep(boost::posix_time::milliseconds(50)); - logNetwork->info("Preparing new game"); - } -} - -CPregameServer::~CPregameServer() -{ - delete acceptor; - delete upcomingConnection; - - for(CPackForSelectionScreen *pack : toAnnounce) - delete pack; - - toAnnounce.clear(); - - //TODO pregameconnections -} - -void CPregameServer::connectionAccepted(const boost::system::error_code& ec) -{ - if(ec) - { - logNetwork->info("Something wrong during accepting: %s", ec.message()); - return; - } - - try - { - logNetwork->info("We got a new connection! :)"); - std::string name = NAME; - CConnection *pc = new CConnection(upcomingConnection, name.append(" STATE_PREGAME")); - initConnection(pc); - upcomingConnection = nullptr; - - startListeningThread(pc); - - *pc << (ui8)pc->connectionID << curmap; - - announceTxt(pc->name + " joins the game"); - auto pj = new PlayerJoined(); - pj->playerName = pc->name; - pj->connectionID = pc->connectionID; - toAnnounce.push_back(pj); - } - catch(std::exception& e) - { - upcomingConnection = nullptr; - logNetwork->info("I guess it was just my imagination!"); - } - - start_async_accept(); -} - -void CPregameServer::start_async_accept() -{ - assert(!upcomingConnection); - assert(acceptor); - - upcomingConnection = new TSocket(acceptor->get_io_service()); - acceptor->async_accept(*upcomingConnection, std::bind(&CPregameServer::connectionAccepted, this, _1)); -} - -void CPregameServer::announceTxt(const std::string &txt, const std::string &playerName) -{ - logNetwork->info("%s says: %s", playerName, txt); - ChatMessage cm; - cm.playerName = playerName; - cm.message = txt; - - boost::unique_lock queueLock(mx); - toAnnounce.push_front(new ChatMessage(cm)); -} - -void CPregameServer::announcePack(const CPackForSelectionScreen &pack) -{ - for(CConnection *pc : connections) - sendPack(pc, pack); -} - -void CPregameServer::sendPack(CConnection * pc, const CPackForSelectionScreen & pack) -{ - if(!pc->sendStop) - { - logNetwork->info("\tSending pack of type %s to %s", typeid(pack).name(), pc->toString()); - *pc << &pack; - } - - if(dynamic_ptr_cast(&pack)) - { - pc->sendStop = true; - } - else if(dynamic_ptr_cast(&pack)) - { - pc->sendStop = true; - } -} - -void CPregameServer::processPack(CPackForSelectionScreen * pack) -{ - if(dynamic_ptr_cast(pack)) - { - sendPack(host, *pack); - } - else if(SelectMap *sm = dynamic_ptr_cast(pack)) - { - vstd::clear_pointer(curmap); - curmap = sm->mapInfo; - sm->free = false; - announcePack(*pack); - } - else if(UpdateStartOptions *uso = dynamic_ptr_cast(pack)) - { - vstd::clear_pointer(curStartInfo); - curStartInfo = uso->options; - uso->free = false; - announcePack(*pack); - } - else if(dynamic_ptr_cast(pack)) - { - state = ENDING_AND_STARTING_GAME; - announcePack(*pack); - } - else - announcePack(*pack); - - delete pack; -} - -void CPregameServer::initConnection(CConnection *c) -{ - *c >> c->name; - connections.insert(c); - logNetwork->info("Pregame connection with player %s established!", c->name); -} - -void CPregameServer::startListeningThread(CConnection * pc) -{ - listeningThreads++; - pc->enterPregameConnectionMode(); - pc->handler = new boost::thread(&CPregameServer::handleConnection, this, pc); -} - -CVCMIServer::CVCMIServer() - : port(3030), io(new boost::asio::io_service()), firstConnection(nullptr), shared(nullptr) -{ - logNetwork->trace("CVCMIServer created!"); - if(cmdLineOptions.count("port")) - port = cmdLineOptions["port"].as(); - logNetwork->info("Port %d will be used", port); - try - { - acceptor = new TAcceptor(*io, boost::asio::ip::tcp::endpoint(boost::asio::ip::tcp::v4(), port)); - } - catch(...) - { - logNetwork->info("Port %d is busy, trying to use random port instead", port); - if(cmdLineOptions.count("run-by-client") && !cmdLineOptions.count("enable-shm")) - { - logNetwork->error("Cant pass port number to client without shared memory!", port); - exit(0); - } - acceptor = new TAcceptor(*io, boost::asio::ip::tcp::endpoint(boost::asio::ip::tcp::v4(), 0)); - port = acceptor->local_endpoint().port(); - } - logNetwork->info("Listening for connections at port %d", port); -} -CVCMIServer::~CVCMIServer() -{ - //delete io; - //delete acceptor; - //delete firstConnection; -} - -std::shared_ptr CVCMIServer::initGhFromHostingConnection(CConnection &c) -{ - auto gh = std::make_shared(); - StartInfo si; - c >> si; //get start options - - if(!si.createRandomMap()) - { - bool mapFound = CResourceHandler::get()->existsResource(ResourceID(si.mapname, EResType::MAP)); - - //TODO some checking for campaigns - if(!mapFound && si.mode == StartInfo::NEW_GAME) - { - c << ui8(1); //WRONG! - gh.reset(); - return gh; - } - } - - c << ui8(0); //OK! - - gh->init(&si); - gh->conns.insert(&c); - - return gh; -} - -void CVCMIServer::newGame() -{ - CConnection &c = *firstConnection; - ui8 clients; - c >> clients; //how many clients should be connected - assert(clients == 1); //multi goes now by newPregame, TODO: custom lobbies - - auto gh = initGhFromHostingConnection(c); - - if(gh) - gh->run(false); -} - -void CVCMIServer::newPregame() -{ - auto cps = new CPregameServer(firstConnection, acceptor); - cps->run(); - if(cps->state == CPregameServer::ENDING_WITHOUT_START) - { - delete cps; - return; - } - - if(cps->state == CPregameServer::ENDING_AND_STARTING_GAME) - { - CGameHandler gh; - gh.conns = cps->connections; - gh.init(cps->curStartInfo); - - for(CConnection *c : gh.conns) - c->addStdVecItems(gh.gs); - - gh.run(false); - } -} - -void CVCMIServer::start() -{ -#ifndef VCMI_ANDROID - if(cmdLineOptions.count("enable-shm")) - { - std::string sharedMemoryName = "vcmi_memory"; - if(cmdLineOptions.count("enable-shm-uuid") && cmdLineOptions.count("uuid")) - { - sharedMemoryName += "_" + cmdLineOptions["uuid"].as(); - } - shared = new SharedMemory(sharedMemoryName); - } -#endif - - boost::system::error_code error; - for (;;) - { - try - { - auto s = new boost::asio::ip::tcp::socket(acceptor->get_io_service()); - boost::thread acc(std::bind(vaccept,acceptor,s,&error)); -#ifdef VCMI_ANDROID - { // in block to clean-up vm helper after use, because we don't need to keep this thread attached to vm - CAndroidVMHelper envHelper; - envHelper.callStaticVoidMethod(CAndroidVMHelper::NATIVE_METHODS_DEFAULT_CLASS, "onServerReady"); - logNetwork->info("Sending server ready message to client"); - } -#else - if(shared) - { - shared->sr->setToReadyAndNotify(port); - } -#endif - - acc.join(); - if (error) - { - logNetwork->warn("Got connection but there is an error %s", error.message()); - return; - } - logNetwork->info("We've accepted someone... "); - std::string name = NAME; - firstConnection = new CConnection(s, name.append(" STATE_WAITING")); - logNetwork->info("Got connection!"); - while(!serverShuttingDown) - { - ui8 mode; - *firstConnection >> mode; - switch (mode) - { - case 0: - firstConnection->close(); - exit(0); - case 1: - firstConnection->close(); - return; - case 2: - newGame(); - break; - case 3: - loadGame(); - break; - case 4: - newPregame(); - break; - } - } - break; - } - catch(std::exception& e) - { - vstd::clear_pointer(firstConnection); - logNetwork->info("I guess it was just my imagination!"); - } - } -} - -void CVCMIServer::loadGame() -{ - CConnection &c = *firstConnection; - std::string fname; - CGameHandler gh; - boost::system::error_code error; - ui8 clients; - - c >> clients >> fname; //how many clients should be connected - - { - CLoadFile lf(*CResourceHandler::get("local")->getResourceName(ResourceID(fname, EResType::SERVER_SAVEGAME)), MINIMAL_SERIALIZATION_VERSION); - gh.loadCommonState(lf); - lf >> gh; - } - - c << ui8(0); - - gh.conns.insert(firstConnection); - - for(int i=1; i(acceptor->get_io_service()); - acceptor->accept(*s,error); - if(error) //retry - { - logNetwork->warn("Cannot establish connection - retrying..."); - i--; - continue; - } - - gh.conns.insert(new CConnection(s.release(),NAME)); - } - - gh.run(true); -} - - - -static void handleCommandOptions(int argc, char *argv[]) -{ - namespace po = boost::program_options; - po::options_description opts("Allowed options"); - opts.add_options() - ("help,h", "display help and exit") - ("version,v", "display version information and exit") - ("run-by-client", "indicate that server launched by client on same machine") - ("uuid", po::value(), "") - ("enable-shm-uuid", "use UUID for shared memory identifier") - ("enable-shm", "enable usage of shared memory") - ("port", po::value(), "port at which server will listen to connections from client"); - - if(argc > 1) - { - try - { - po::store(po::parse_command_line(argc, argv, opts), cmdLineOptions); - } - catch(std::exception &e) - { - std::cerr << "Failure during parsing command-line options:\n" << e.what() << std::endl; - } - } - - po::notify(cmdLineOptions); - if (cmdLineOptions.count("help")) - { - auto time = std::time(0); - printf("%s - A Heroes of Might and Magic 3 clone\n", GameConstants::VCMI_VERSION.c_str()); - printf("Copyright (C) 2007-%d VCMI dev team - see AUTHORS file\n", std::localtime(&time)->tm_year + 1900); - printf("This is free software; see the source for copying conditions. There is NO\n"); - printf("warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n"); - printf("\n"); - std::cout << opts; - exit(0); - } - - if (cmdLineOptions.count("version")) - { - printf("%s\n", GameConstants::VCMI_VERSION.c_str()); - std::cout << VCMIDirs::get().genHelpString(); - exit(0); - } -} - -#if defined(__GNUC__) && !defined (__MINGW32__) && !defined(VCMI_ANDROID) -void handleLinuxSignal(int sig) -{ - const int STACKTRACE_SIZE = 100; - void * buffer[STACKTRACE_SIZE]; - int ptrCount = backtrace(buffer, STACKTRACE_SIZE); - char ** strings; - - logGlobal->error("Error: signal %d :", sig); - strings = backtrace_symbols(buffer, ptrCount); - if(strings == nullptr) - { - logGlobal->error("There are no symbols."); - } - else - { - for(int i = 0; i < ptrCount; ++i) - { - logGlobal->error(strings[i]); - } - free(strings); - } - - _exit(EXIT_FAILURE); -} -#endif - -int main(int argc, char * argv[]) -{ -#ifndef VCMI_ANDROID - // Correct working dir executable folder (not bundle folder) so we can use executable relative paths - boost::filesystem::current_path(boost::filesystem::system_complete(argv[0]).parent_path()); -#endif - // Installs a sig sev segmentation violation handler - // to log stacktrace - #if defined(__GNUC__) && !defined (__MINGW32__) && !defined(VCMI_ANDROID) - signal(SIGSEGV, handleLinuxSignal); - #endif - - console = new CConsoleHandler(); - CBasicLogConfigurator logConfig(VCMIDirs::get().userCachePath() / "VCMI_Server_log.txt", console); - logConfig.configureDefault(); - logGlobal->info(NAME); - - handleCommandOptions(argc, argv); - preinitDLL(console); - settings.init(); - logConfig.configure(); - - loadDLLClasses(); - srand ( (ui32)time(nullptr) ); - try - { - boost::asio::io_service io_service; - CVCMIServer server; - - try - { - while(!serverShuttingDown) - { - server.start(); - } - io_service.run(); - } - catch (boost::system::system_error &e) //for boost errors just log, not crash - probably client shut down connection - { - logNetwork->error(e.what()); - serverShuttingDown = true; - } - catch (...) - { - handleException(); - } - } - catch(boost::system::system_error &e) - { - logNetwork->error(e.what()); - //catch any startup errors (e.g. can't access port) errors - //and return non-zero status so client can detect error - throw; - } -#ifdef VCMI_ANDROID - CAndroidVMHelper envHelper; - envHelper.callStaticVoidMethod(CAndroidVMHelper::NATIVE_METHODS_DEFAULT_CLASS, "killServer"); -#endif - vstd::clear_pointer(VLC); - CResourceHandler::clear(); - return 0; -} - -#ifdef VCMI_ANDROID - -void CVCMIServer::create() -{ - const char * foo[1] = {"android-server"}; - main(1, const_cast(foo)); -} - -#endif +#endif +#include + +#include "../lib/filesystem/Filesystem.h" +#include "../lib/mapping/CCampaignHandler.h" +#include "../lib/CThreadHelper.h" +#include "../lib/serializer/Connection.h" +#include "../lib/CModHandler.h" +#include "../lib/CArtHandler.h" +#include "../lib/CGeneralTextHandler.h" +#include "../lib/CHeroHandler.h" +#include "../lib/CTownHandler.h" +#include "../lib/CBuildingHandler.h" +#include "../lib/spells/CSpellHandler.h" +#include "../lib/CCreatureHandler.h" +#include "zlib.h" +#include "CVCMIServer.h" +#include "../lib/StartInfo.h" +#include "../lib/mapping/CMap.h" +#include "../lib/rmg/CMapGenOptions.h" +#ifdef VCMI_ANDROID +#include "lib/CAndroidVMHelper.h" +#else +#include "../lib/Interprocess.h" +#endif +#include "../lib/VCMI_Lib.h" +#include "../lib/VCMIDirs.h" +#include "CGameHandler.h" +#include "../lib/mapping/CMapInfo.h" +#include "../lib/GameConstants.h" +#include "../lib/logging/CBasicLogConfigurator.h" +#include "../lib/CConfigHandler.h" +#include "../lib/ScopeGuard.h" + +#include "../lib/UnlockGuard.h" + +// for applier +#include "../lib/registerTypes/RegisterTypes.h" + +// UUID generation +#include +#include +#include + +#include "../lib/CGameState.h" + +#if defined(__GNUC__) && !defined(__MINGW32__) && !defined(VCMI_ANDROID) +#include +#endif + +template class CApplyOnServer; + +class CBaseForServerApply +{ +public: + virtual bool applyOnServerBefore(CVCMIServer * srv, void * pack) const =0; + virtual void applyOnServerAfter(CVCMIServer * srv, void * pack) const =0; + virtual ~CBaseForServerApply() {} + template static CBaseForServerApply * getApplier(const U * t = nullptr) + { + return new CApplyOnServer(); + } +}; + +template class CApplyOnServer : public CBaseForServerApply +{ +public: + bool applyOnServerBefore(CVCMIServer * srv, void * pack) const override + { + T * ptr = static_cast(pack); + if(ptr->checkClientPermissions(srv)) + { + boost::unique_lock stateLock(srv->stateMutex); + return ptr->applyOnServer(srv); + } + else + return false; + } + + void applyOnServerAfter(CVCMIServer * srv, void * pack) const override + { + T * ptr = static_cast(pack); + ptr->applyOnServerAfterAnnounce(srv); + } +}; + +template <> +class CApplyOnServer : public CBaseForServerApply +{ +public: + bool applyOnServerBefore(CVCMIServer * srv, void * pack) const override + { + logGlobal->error("Cannot apply plain CPack!"); + assert(0); + return false; + } + void applyOnServerAfter(CVCMIServer * srv, void * pack) const override + { + logGlobal->error("Cannot apply plain CPack!"); + assert(0); + } +}; + +std::string NAME_AFFIX = "server"; +std::string NAME = GameConstants::VCMI_VERSION + std::string(" (") + NAME_AFFIX + ')'; + +CVCMIServer::CVCMIServer(boost::program_options::variables_map & opts) + : port(3030), io(std::make_shared()), state(EServerState::LOBBY), cmdLineOptions(opts), currentClientId(1), currentPlayerId(1), restartGameplay(false) +{ + uuid = boost::uuids::to_string(boost::uuids::random_generator()()); + logNetwork->trace("CVCMIServer created! UUID: %s", uuid); + applier = std::make_shared>(); + registerTypesLobbyPacks(*applier); + + if(cmdLineOptions.count("port")) + port = cmdLineOptions["port"].as(); + logNetwork->info("Port %d will be used", port); + try + { + acceptor = std::make_shared(*io, boost::asio::ip::tcp::endpoint(boost::asio::ip::tcp::v4(), port)); + } + catch(...) + { + logNetwork->info("Port %d is busy, trying to use random port instead", port); + if(cmdLineOptions.count("run-by-client") && !cmdLineOptions.count("enable-shm")) + { + logNetwork->error("Cant pass port number to client without shared memory!", port); + exit(0); + } + acceptor = std::make_shared(*io, boost::asio::ip::tcp::endpoint(boost::asio::ip::tcp::v4(), 0)); + port = acceptor->local_endpoint().port(); + } + logNetwork->info("Listening for connections at port %d", port); +} + +CVCMIServer::~CVCMIServer() +{ + + for(CPackForLobby * pack : announceQueue) + delete pack; + + announceQueue.clear(); +} + +void CVCMIServer::run() +{ + if(!restartGameplay) + { + boost::thread(&CVCMIServer::threadAnnounceLobby, this); +#ifndef VCMI_ANDROID + if(cmdLineOptions.count("enable-shm")) + { + std::string sharedMemoryName = "vcmi_memory"; + if(cmdLineOptions.count("enable-shm-uuid") && cmdLineOptions.count("uuid")) + { + sharedMemoryName += "_" + cmdLineOptions["uuid"].as(); + } + shm = std::make_shared(sharedMemoryName); + } +#endif + + startAsyncAccept(); + if(shm) + { + shm->sr->setToReadyAndNotify(port); + } + } + + while(state == EServerState::LOBBY) + boost::this_thread::sleep(boost::posix_time::milliseconds(50)); + + logNetwork->info("Thread handling connections ended"); + + if(state == EServerState::GAMEPLAY) + { + gh->run(si->mode == StartInfo::LOAD_GAME); + } +} + +void CVCMIServer::threadAnnounceLobby() +{ + while(state != EServerState::SHUTDOWN) + { + { + boost::unique_lock myLock(mx); + while(!announceQueue.empty()) + { + announcePack(announceQueue.front()); + announceQueue.pop_front(); + } + if(state != EServerState::LOBBY) + { + if(acceptor) + acceptor->close(); + } + + if(acceptor) + { + acceptor->get_io_service().reset(); + acceptor->get_io_service().poll(); + } + } + + boost::this_thread::sleep(boost::posix_time::milliseconds(50)); + } +} + +void CVCMIServer::prepareToStartGame() +{ + if(state == EServerState::GAMEPLAY) + { + restartGameplay = true; + state = EServerState::LOBBY; + // FIXME: dirry hack to make sure old CGameHandler::run is finished + boost::this_thread::sleep(boost::posix_time::milliseconds(1000)); + } + + gh = std::make_shared(this); + switch(si->mode) + { + case StartInfo::CAMPAIGN: + logNetwork->info("Preparing to start new campaign"); + si->campState->currentMap = boost::make_optional(campaignMap); + si->campState->chosenCampaignBonuses[campaignMap] = campaignBonus; + gh->init(si.get()); + break; + + case StartInfo::NEW_GAME: + logNetwork->info("Preparing to start new game"); + gh->init(si.get()); + break; + + case StartInfo::LOAD_GAME: + logNetwork->info("Preparing to start loaded game"); + gh->load(si->mapname); + break; + default: + logNetwork->error("Wrong mode in StartInfo!"); + assert(0); + break; + } +} + +void CVCMIServer::startGameImmidiately() +{ + for(auto c : connections) + c->enterGameplayConnectionMode(gh->gs); + + state = EServerState::GAMEPLAY; +} + +void CVCMIServer::startAsyncAccept() +{ + assert(!upcomingConnection); + assert(acceptor); + + upcomingConnection = std::make_shared(acceptor->get_io_service()); + acceptor->async_accept(*upcomingConnection, std::bind(&CVCMIServer::connectionAccepted, this, _1)); +} + +void CVCMIServer::connectionAccepted(const boost::system::error_code & ec) +{ + if(ec) + { + if(state != EServerState::SHUTDOWN) + logNetwork->info("Something wrong during accepting: %s", ec.message()); + return; + } + + try + { + logNetwork->info("We got a new connection! :)"); + auto c = std::make_shared(upcomingConnection, NAME, uuid); + upcomingConnection.reset(); + connections.insert(c); + c->handler = std::make_shared(&CVCMIServer::threadHandleClient, this, c); + } + catch(std::exception & e) + { + upcomingConnection.reset(); + logNetwork->info("I guess it was just my imagination!"); + } + + startAsyncAccept(); +} + +void CVCMIServer::threadHandleClient(std::shared_ptr c) +{ + setThreadName("CVCMIServer::handleConnection"); + c->enterLobbyConnectionMode(); + + try + { + while(c->connected) + { + CPack * pack = c->retrievePack(); + if(auto lobbyPack = dynamic_ptr_cast(pack)) + { + handleReceivedPack(lobbyPack); + } + else if(auto serverPack = dynamic_ptr_cast(pack)) + { + gh->handleReceivedPack(serverPack); + } + } + } + catch(boost::system::system_error & e) + { + if(state != EServerState::LOBBY) + gh->handleClientDisconnection(c); + } + catch(const std::exception & e) + { + boost::unique_lock queueLock(mx); + logNetwork->error("%s dies... \nWhat happened: %s", c->toString(), e.what()); + } + catch(...) + { + state = EServerState::SHUTDOWN; + handleException(); + throw; + } + + boost::unique_lock queueLock(mx); +// if(state != ENDING_AND_STARTING_GAME) + { + auto lcd = new LobbyClientDisconnected(); + lcd->c = c; + lcd->clientId = c->connectionID; + handleReceivedPack(lcd); + } + + logNetwork->info("Thread listening for %s ended", c->toString()); + c->handler.reset(); +} + +void CVCMIServer::handleReceivedPack(CPackForLobby * pack) +{ + CBaseForServerApply * apply = applier->getApplier(typeList.getTypeID(pack)); + if(apply->applyOnServerBefore(this, pack)) + addToAnnounceQueue(pack); + else + delete pack; +} + +void CVCMIServer::announcePack(CPackForLobby * pack) +{ + for(auto c : connections) + { + // FIXME: we need to avoid senting something to client that not yet get answer for LobbyClientConnected + // Until UUID set we only pass LobbyClientConnected to this client + if(c->uuid == uuid && !dynamic_cast(pack)) + continue; + + c->sendPack(pack); + } + + applier->getApplier(typeList.getTypeID(pack))->applyOnServerAfter(this, pack); + + delete pack; +} + +void CVCMIServer::announceTxt(const std::string & txt, const std::string & playerName) +{ + logNetwork->info("%s says: %s", playerName, txt); + auto cm = new LobbyChatMessage(); + cm->playerName = playerName; + cm->message = txt; + addToAnnounceQueue(cm); +} + +void CVCMIServer::addToAnnounceQueue(CPackForLobby * pack) +{ + boost::unique_lock queueLock(mx); + announceQueue.push_back(pack); +} + +bool CVCMIServer::passHost(int toConnectionId) +{ + for(auto c : connections) + { + if(isClientHost(c->connectionID)) + continue; + if(c->connectionID != toConnectionId) + continue; + + hostClient = c; + hostClientId = c->connectionID; + announceTxt(boost::str(boost::format("Pass host to connection %d") % toConnectionId)); + return true; + } + return false; +} + +void CVCMIServer::clientConnected(std::shared_ptr c, std::vector & names, std::string uuid, StartInfo::EMode mode) +{ + c->connectionID = currentClientId++; + + if(!hostClient) + { + hostClient = c; + hostClientId = c->connectionID; + si->mode = mode; + } + + logNetwork->info("Connection with client %d established. UUID: %s", c->connectionID, c->uuid); + for(auto & name : names) + { + logNetwork->info("Client %d player: %s", c->connectionID, name); + ui8 id = currentPlayerId++; + + ClientPlayer cp; + cp.connection = c->connectionID; + cp.name = name; + playerNames.insert(std::make_pair(id, cp)); + announceTxt(boost::str(boost::format("%s (pid %d cid %d) joins the game") % name % id % c->connectionID)); + + //put new player in first slot with AI + for(auto & elem : si->playerInfos) + { + if(elem.second.isControlledByAI() && !elem.second.compOnly) + { + setPlayerConnectedId(elem.second, id); + break; + } + } + } +} + +void CVCMIServer::clientDisconnected(std::shared_ptr c) +{ + connections -= c; + for(auto & pair : playerNames) + { + if(pair.second.connection != c->connectionID) + continue; + + int id = pair.first; + announceTxt(boost::str(boost::format("%s (pid %d cid %d) left the game") % id % playerNames[id].name % c->connectionID)); + playerNames.erase(id); + + // Reset in-game players client used back to AI + if(PlayerSettings * s = si->getPlayersSettings(id)) + { + setPlayerConnectedId(*s, PlayerSettings::PLAYER_AI); + } + } +} + +void CVCMIServer::setPlayerConnectedId(PlayerSettings & pset, ui8 player) const +{ + if(vstd::contains(playerNames, player)) + pset.name = playerNames.find(player)->second.name; + else + pset.name = VLC->generaltexth->allTexts[468]; //Computer + + pset.connectedPlayerIDs.clear(); + if(player != PlayerSettings::PLAYER_AI) + pset.connectedPlayerIDs.insert(player); +} + +void CVCMIServer::updateStartInfoOnMapChange(std::shared_ptr mapInfo, std::shared_ptr mapGenOpts) +{ + mi = mapInfo; + if(!mi) + return; + + auto namesIt = playerNames.cbegin(); + si->playerInfos.clear(); + if(mi->scenarioOptionsOfSave) + { + si = std::shared_ptr(mi->scenarioOptionsOfSave); + si->mode = StartInfo::LOAD_GAME; + if(si->campState) + campaignMap = si->campState->currentMap.get(); + + for(auto & ps : si->playerInfos) + { + if(!ps.second.compOnly && ps.second.connectedPlayerIDs.size() && namesIt != playerNames.cend()) + { + setPlayerConnectedId(ps.second, namesIt++->first); + } + else + { + setPlayerConnectedId(ps.second, PlayerSettings::PLAYER_AI); + } + } + } + else if(si->mode == StartInfo::NEW_GAME || si->mode == StartInfo::CAMPAIGN) + { + if(mi->campaignHeader) + return; + + for(int i = 0; i < mi->mapHeader->players.size(); i++) + { + const PlayerInfo & pinfo = mi->mapHeader->players[i]; + + //neither computer nor human can play - no player + if(!(pinfo.canHumanPlay || pinfo.canComputerPlay)) + continue; + + PlayerSettings & pset = si->playerInfos[PlayerColor(i)]; + pset.color = PlayerColor(i); + if(pinfo.canHumanPlay && namesIt != playerNames.cend()) + { + setPlayerConnectedId(pset, namesIt++->first); + } + else + { + setPlayerConnectedId(pset, PlayerSettings::PLAYER_AI); + if(!pinfo.canHumanPlay) + { + pset.compOnly = true; + } + } + + pset.castle = pinfo.defaultCastle(); + pset.hero = pinfo.defaultHero(); + + if(pset.hero != PlayerSettings::RANDOM && pinfo.hasCustomMainHero()) + { + pset.hero = pinfo.mainCustomHeroId; + pset.heroName = pinfo.mainCustomHeroName; + pset.heroPortrait = pinfo.mainCustomHeroPortrait; + } + + pset.handicap = PlayerSettings::NO_HANDICAP; + } + + if(mi->isRandomMap && mapGenOpts) + si->mapGenOptions = std::shared_ptr(mapGenOpts); + else + si->mapGenOptions.reset(); + } + si->mapname = mi->fileURI; +} + +void CVCMIServer::updateAndPropagateLobbyState() +{ + boost::unique_lock stateLock(stateMutex); + // Update player settings for RMG + // TODO: find appropriate location for this code + if(si->mapGenOptions && si->mode == StartInfo::NEW_GAME) + { + for(const auto & psetPair : si->playerInfos) + { + const auto & pset = psetPair.second; + si->mapGenOptions->setStartingTownForPlayer(pset.color, pset.castle); + if(pset.isControlledByHuman()) + { + si->mapGenOptions->setPlayerTypeForStandardPlayer(pset.color, EPlayerType::HUMAN); + } + } + } + + auto lus = new LobbyUpdateState(); + lus->state = *this; + addToAnnounceQueue(lus); +} + +void CVCMIServer::setPlayer(PlayerColor clickedColor) +{ + struct PlayerToRestore + { + PlayerColor color; + int id; + void reset() { id = -1; color = PlayerColor::CANNOT_DETERMINE; } + PlayerToRestore(){ reset(); } + } playerToRestore; + + + PlayerSettings & clicked = si->playerInfos[clickedColor]; + PlayerSettings * old = nullptr; + + //identify clicked player + int clickedNameID = 0; //number of player - zero means AI, assume it initially + if(clicked.isControlledByHuman()) + clickedNameID = *(clicked.connectedPlayerIDs.begin()); //if not AI - set appropiate ID + + if(clickedNameID > 0 && playerToRestore.id == clickedNameID) //player to restore is about to being replaced -> put him back to the old place + { + PlayerSettings & restPos = si->playerInfos[playerToRestore.color]; + setPlayerConnectedId(restPos, playerToRestore.id); + playerToRestore.reset(); + } + + int newPlayer; //which player will take clicked position + + //who will be put here? + if(!clickedNameID) //AI player clicked -> if possible replace computer with unallocated player + { + newPlayer = getIdOfFirstUnallocatedPlayer(); + if(!newPlayer) //no "free" player -> get just first one + newPlayer = playerNames.begin()->first; + } + else //human clicked -> take next + { + auto i = playerNames.find(clickedNameID); //clicked one + i++; //player AFTER clicked one + + if(i != playerNames.end()) + newPlayer = i->first; + else + newPlayer = 0; //AI if we scrolled through all players + } + + setPlayerConnectedId(clicked, newPlayer); //put player + + //if that player was somewhere else, we need to replace him with computer + if(newPlayer) //not AI + { + for(auto i = si->playerInfos.begin(); i != si->playerInfos.end(); i++) + { + int curNameID = *(i->second.connectedPlayerIDs.begin()); + if(i->first != clickedColor && curNameID == newPlayer) + { + assert(i->second.connectedPlayerIDs.size()); + playerToRestore.color = i->first; + playerToRestore.id = newPlayer; + setPlayerConnectedId(i->second, PlayerSettings::PLAYER_AI); //set computer + old = &i->second; + break; + } + } + } +} + +void CVCMIServer::optionNextCastle(PlayerColor player, int dir) +{ + PlayerSettings & s = si->playerInfos[player]; + si16 & cur = s.castle; + auto & allowed = getPlayerInfo(player.getNum()).allowedFactions; + const bool allowRandomTown = getPlayerInfo(player.getNum()).isFactionRandom; + + if(cur == PlayerSettings::NONE) //no change + return; + + if(cur == PlayerSettings::RANDOM) //first/last available + { + if(dir > 0) + cur = *allowed.begin(); //id of first town + else + cur = *allowed.rbegin(); //id of last town + + } + else // next/previous available + { + if((cur == *allowed.begin() && dir < 0) || (cur == *allowed.rbegin() && dir > 0)) + { + if(allowRandomTown) + { + cur = PlayerSettings::RANDOM; + } + else + { + if(dir > 0) + cur = *allowed.begin(); + else + cur = *allowed.rbegin(); + } + } + else + { + assert(dir >= -1 && dir <= 1); //othervice std::advance may go out of range + auto iter = allowed.find(cur); + std::advance(iter, dir); + cur = *iter; + } + } + + if(s.hero >= 0 && !getPlayerInfo(player.getNum()).hasCustomMainHero()) // remove hero unless it set to fixed one in map editor + { + s.hero = PlayerSettings::RANDOM; + } + if(cur < 0 && s.bonus == PlayerSettings::RESOURCE) + s.bonus = PlayerSettings::RANDOM; +} + +void CVCMIServer::setCampaignMap(int mapId) +{ + campaignMap = mapId; + si->difficulty = si->campState->camp->scenarios[mapId].difficulty; + campaignBonus = -1; + updateStartInfoOnMapChange(si->campState->getMapInfo(mapId)); +} + +void CVCMIServer::setCampaignBonus(int bonusId) +{ + campaignBonus = bonusId; + + const CCampaignScenario & scenario = si->campState->camp->scenarios[campaignMap]; + const std::vector & bonDescs = scenario.travelOptions.bonusesToChoose; + if(bonDescs[bonusId].type == CScenarioTravel::STravelBonus::HERO) + { + for(auto & elem : si->playerInfos) + { + if(elem.first == PlayerColor(bonDescs[bonusId].info1)) + setPlayerConnectedId(elem.second, 1); + else + setPlayerConnectedId(elem.second, PlayerSettings::PLAYER_AI); + } + } +} + +void CVCMIServer::optionNextHero(PlayerColor player, int dir) +{ + PlayerSettings & s = si->playerInfos[player]; + if(s.castle < 0 || s.hero == PlayerSettings::NONE) + return; + + if(s.hero == PlayerSettings::RANDOM) // first/last available + { + int max = VLC->heroh->heroes.size(), + min = 0; + s.hero = nextAllowedHero(player, min, max, 0, dir); + } + else + { + if(dir > 0) + s.hero = nextAllowedHero(player, s.hero, VLC->heroh->heroes.size(), 1, dir); + else + s.hero = nextAllowedHero(player, -1, s.hero, 1, dir); // min needs to be -1 -- hero at index 0 would be skipped otherwise + } +} + +int CVCMIServer::nextAllowedHero(PlayerColor player, int min, int max, int incl, int dir) +{ + if(dir > 0) + { + for(int i = min + incl; i <= max - incl; i++) + if(canUseThisHero(player, i)) + return i; + } + else + { + for(int i = max - incl; i >= min + incl; i--) + if(canUseThisHero(player, i)) + return i; + } + return -1; +} + +void CVCMIServer::optionNextBonus(PlayerColor player, int dir) +{ + PlayerSettings & s = si->playerInfos[player]; + PlayerSettings::Ebonus & ret = s.bonus = static_cast(static_cast(s.bonus) + dir); + + if(s.hero == PlayerSettings::NONE && + !getPlayerInfo(player.getNum()).heroesNames.size() && + ret == PlayerSettings::ARTIFACT) //no hero - can't be artifact + { + if(dir < 0) + ret = PlayerSettings::RANDOM; + else + ret = PlayerSettings::GOLD; + } + + if(ret > PlayerSettings::RESOURCE) + ret = PlayerSettings::RANDOM; + if(ret < PlayerSettings::RANDOM) + ret = PlayerSettings::RESOURCE; + + if(s.castle == PlayerSettings::RANDOM && ret == PlayerSettings::RESOURCE) //random castle - can't be resource + { + if(dir < 0) + ret = PlayerSettings::GOLD; + else + ret = PlayerSettings::RANDOM; + } +} + +bool CVCMIServer::canUseThisHero(PlayerColor player, int ID) +{ + return VLC->heroh->heroes.size() > ID + && si->playerInfos[player].castle == VLC->heroh->heroes[ID]->heroClass->faction + && !vstd::contains(getUsedHeroes(), ID) + && mi->mapHeader->allowedHeroes[ID]; +} + +std::vector CVCMIServer::getUsedHeroes() +{ + std::vector heroIds; + for(auto & p : si->playerInfos) + { + const auto & heroes = getPlayerInfo(p.first.getNum()).heroesNames; + for(auto & hero : heroes) + if(hero.heroId >= 0) //in VCMI map format heroId = -1 means random hero + heroIds.push_back(hero.heroId); + + if(p.second.hero != PlayerSettings::RANDOM) + heroIds.push_back(p.second.hero); + } + return heroIds; +} + +ui8 CVCMIServer::getIdOfFirstUnallocatedPlayer() const +{ + for(auto i = playerNames.cbegin(); i != playerNames.cend(); i++) + { + if(!si->getPlayersSettings(i->first)) + return i->first; + } + + return 0; +} + +#if defined(__GNUC__) && !defined(__MINGW32__) && !defined(VCMI_ANDROID) +void handleLinuxSignal(int sig) +{ + const int STACKTRACE_SIZE = 100; + void * buffer[STACKTRACE_SIZE]; + int ptrCount = backtrace(buffer, STACKTRACE_SIZE); + char * * strings; + + logGlobal->error("Error: signal %d :", sig); + strings = backtrace_symbols(buffer, ptrCount); + if(strings == nullptr) + { + logGlobal->error("There are no symbols."); + } + else + { + for(int i = 0; i < ptrCount; ++i) + { + logGlobal->error(strings[i]); + } + free(strings); + } + + _exit(EXIT_FAILURE); +} +#endif + +static void handleCommandOptions(int argc, char * argv[], boost::program_options::variables_map & options) +{ + namespace po = boost::program_options; + po::options_description opts("Allowed options"); + opts.add_options() + ("help,h", "display help and exit") + ("version,v", "display version information and exit") + ("run-by-client", "indicate that server launched by client on same machine") + ("uuid", po::value(), "") + ("enable-shm-uuid", "use UUID for shared memory identifier") + ("enable-shm", "enable usage of shared memory") + ("port", po::value(), "port at which server will listen to connections from client"); + + if(argc > 1) + { + try + { + po::store(po::parse_command_line(argc, argv, opts), options); + } + catch(std::exception & e) + { + std::cerr << "Failure during parsing command-line options:\n" << e.what() << std::endl; + } + } + + po::notify(options); + if(options.count("help")) + { + auto time = std::time(0); + printf("%s - A Heroes of Might and Magic 3 clone\n", GameConstants::VCMI_VERSION.c_str()); + printf("Copyright (C) 2007-%d VCMI dev team - see AUTHORS file\n", std::localtime(&time)->tm_year + 1900); + printf("This is free software; see the source for copying conditions. There is NO\n"); + printf("warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n"); + printf("\n"); + std::cout << opts; + exit(0); + } + + if(options.count("version")) + { + printf("%s\n", GameConstants::VCMI_VERSION.c_str()); + std::cout << VCMIDirs::get().genHelpString(); + exit(0); + } +} + +int main(int argc, char * argv[]) +{ +#ifndef VCMI_ANDROID + // Correct working dir executable folder (not bundle folder) so we can use executable relative paths + boost::filesystem::current_path(boost::filesystem::system_complete(argv[0]).parent_path()); +#endif + // Installs a sig sev segmentation violation handler + // to log stacktrace +#if defined(__GNUC__) && !defined(__MINGW32__) && !defined(VCMI_ANDROID) + signal(SIGSEGV, handleLinuxSignal); +#endif + + console = new CConsoleHandler(); + CBasicLogConfigurator logConfig(VCMIDirs::get().userCachePath() / "VCMI_Server_log.txt", console); + logConfig.configureDefault(); + logGlobal->info(NAME); + + boost::program_options::variables_map opts; + handleCommandOptions(argc, argv, opts); + preinitDLL(console); + settings.init(); + logConfig.configure(); + + loadDLLClasses(); + srand((ui32)time(nullptr)); + try + { + boost::asio::io_service io_service; + CVCMIServer server(opts); + + try + { + while(server.state != EServerState::SHUTDOWN) + { + server.run(); + } + io_service.run(); + } + catch(boost::system::system_error & e) //for boost errors just log, not crash - probably client shut down connection + { + logNetwork->error(e.what()); + server.state = EServerState::SHUTDOWN; + } + catch(...) + { + handleException(); + } + } + catch(boost::system::system_error & e) + { + logNetwork->error(e.what()); + //catch any startup errors (e.g. can't access port) errors + //and return non-zero status so client can detect error + throw; + } +#ifdef VCMI_ANDROID + CAndroidVMHelper envHelper; + envHelper.callStaticVoidMethod(CAndroidVMHelper::NATIVE_METHODS_DEFAULT_CLASS, "killServer"); +#endif + vstd::clear_pointer(VLC); + CResourceHandler::clear(); + return 0; +} + +#ifdef VCMI_ANDROID +void CVCMIServer::create() +{ + const char * foo[1] = {"android-server"}; + main(1, const_cast(foo)); +} +#endif diff --git a/server/CVCMIServer.h b/server/CVCMIServer.h index ad9951f64..0dd83da55 100644 --- a/server/CVCMIServer.h +++ b/server/CVCMIServer.h @@ -1,112 +1,105 @@ -/* - * CVCMIServer.h, part of VCMI engine - * - * Authors: listed in file AUTHORS in main folder - * - * License: GNU General Public License v2.0 or later - * Full text of license available in license.txt file, in main folder - * - */ -#pragma once - -#include - -class CMapInfo; - -class CConnection; -struct CPackForSelectionScreen; -class CGameHandler; -struct SharedMemory; - -namespace boost -{ - namespace asio - { - namespace ip - { - class tcp; - } - -#if BOOST_VERSION >= 106600 // Boost version >= 1.66 - class io_context; - typedef io_context io_service; -#else - class io_service; -#endif - - template class stream_socket_service; - template - class basic_stream_socket; - - template class socket_acceptor_service; - template - class basic_socket_acceptor; - } -}; - -typedef boost::asio::basic_socket_acceptor > TAcceptor; -typedef boost::asio::basic_stream_socket < boost::asio::ip::tcp , boost::asio::stream_socket_service > TSocket; - -class CVCMIServer -{ - ui16 port; - boost::asio::io_service *io; - TAcceptor * acceptor; - SharedMemory * shared; - - CConnection *firstConnection; -public: - CVCMIServer(); - ~CVCMIServer(); - - void start(); - std::shared_ptr initGhFromHostingConnection(CConnection &c); - - void newGame(); - void loadGame(); - void newPregame(); - -#ifdef VCMI_ANDROID - static void create(); -#endif -}; - -struct StartInfo; -class CPregameServer -{ -public: - CConnection *host; - int listeningThreads; - std::set connections; - std::list toAnnounce; - boost::recursive_mutex mx; - - TAcceptor *acceptor; - TSocket *upcomingConnection; - - const CMapInfo *curmap; - StartInfo *curStartInfo; - - CPregameServer(CConnection *Host, TAcceptor *Acceptor = nullptr); - ~CPregameServer(); - - void run(); - - void processPack(CPackForSelectionScreen * pack); - void handleConnection(CConnection *cpc); - void connectionAccepted(const boost::system::error_code& ec); - void initConnection(CConnection *c); - - void start_async_accept(); - - enum { INVALID, RUNNING, ENDING_WITHOUT_START, ENDING_AND_STARTING_GAME - } state; - - void announceTxt(const std::string &txt, const std::string &playerName = "system"); - void announcePack(const CPackForSelectionScreen &pack); - - void sendPack(CConnection * pc, const CPackForSelectionScreen & pack); - void startListeningThread(CConnection * pc); -}; - -extern boost::program_options::variables_map cmdLineOptions; +/* + * CVCMIServer.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +#include "../lib/serializer/Connection.h" +#include "../lib/StartInfo.h" + +#include + +class CMapInfo; + +struct CPackForLobby; +class CGameHandler; +struct SharedMemory; + +struct StartInfo; +struct LobbyInfo; +class PlayerSettings; +class PlayerColor; + +template class CApplier; +class CBaseForServerApply; +class CBaseForGHApply; + +enum class EServerState : ui8 +{ + LOBBY, + GAMEPLAY, + GAMEPLAY_ENDED, + SHUTDOWN +}; + +class CVCMIServer : public LobbyInfo +{ + std::atomic restartGameplay; // FIXME: this is just a hack + std::shared_ptr io; + std::shared_ptr acceptor; + std::shared_ptr upcomingConnection; + std::list announceQueue; + boost::recursive_mutex mx; + std::shared_ptr> applier; + +public: + std::shared_ptr gh; + std::atomic state; + ui16 port; + + boost::program_options::variables_map cmdLineOptions; + std::set> connections; + std::atomic currentClientId; + std::atomic currentPlayerId; + std::shared_ptr hostClient; + + CVCMIServer(boost::program_options::variables_map & opts); + ~CVCMIServer(); + void run(); + void prepareToStartGame(); + void startGameImmidiately(); + + void startAsyncAccept(); + void connectionAccepted(const boost::system::error_code & ec); + void threadHandleClient(std::shared_ptr c); + void threadAnnounceLobby(); + void handleReceivedPack(CPackForLobby * pack); + + void announcePack(CPackForLobby * pack); + bool passHost(int toConnectionId); + + void announceTxt(const std::string & txt, const std::string & playerName = "system"); + void addToAnnounceQueue(CPackForLobby * pack); + + void setPlayerConnectedId(PlayerSettings & pset, ui8 player) const; + void updateStartInfoOnMapChange(std::shared_ptr mapInfo, std::shared_ptr mapGenOpt = {}); + + void clientConnected(std::shared_ptr c, std::vector & names, std::string uuid, StartInfo::EMode mode); + void clientDisconnected(std::shared_ptr c); + + void updateAndPropagateLobbyState(); + + // Work with LobbyInfo + void setPlayer(PlayerColor clickedColor); + void optionNextHero(PlayerColor player, int dir); //dir == -1 or +1 + int nextAllowedHero(PlayerColor player, int min, int max, int incl, int dir); + bool canUseThisHero(PlayerColor player, int ID); + std::vector getUsedHeroes(); + void optionNextBonus(PlayerColor player, int dir); //dir == -1 or +1 + void optionNextCastle(PlayerColor player, int dir); //dir == -1 or + + + // Campaigns + void setCampaignMap(int mapId); + void setCampaignBonus(int bonusId); + + ui8 getIdOfFirstUnallocatedPlayer() const; + +#ifdef VCMI_ANDROID + static void create(); +#endif +}; diff --git a/server/NetPacksLobbyServer.cpp b/server/NetPacksLobbyServer.cpp new file mode 100644 index 000000000..728fe213a --- /dev/null +++ b/server/NetPacksLobbyServer.cpp @@ -0,0 +1,250 @@ +/* + * NetPacksLobbyServer.cpp, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#include "StdInc.h" + +#include "CVCMIServer.h" +#include "CGameHandler.h" + +#include "../lib/NetPacksLobby.h" +#include "../lib/serializer/Connection.h" +#include "../lib/StartInfo.h" + +// Campaigns +#include "../lib/mapping/CCampaignHandler.h" +#include "../lib/mapping/CMapService.h" +#include "../lib/mapping/CMapInfo.h" + +bool CLobbyPackToServer::checkClientPermissions(CVCMIServer * srv) const +{ + return srv->isClientHost(c->connectionID); +} + +void CLobbyPackToServer::applyOnServerAfterAnnounce(CVCMIServer * srv) +{ + // Propogate options after every CLobbyPackToServer + srv->updateAndPropagateLobbyState(); +} + +bool LobbyClientConnected::checkClientPermissions(CVCMIServer * srv) const +{ + return true; +} + +bool LobbyClientConnected::applyOnServer(CVCMIServer * srv) +{ + srv->clientConnected(c, names, uuid, mode); + // Server need to pass some data to newly connected client + clientId = c->connectionID; + mode = srv->si->mode; + hostClientId = srv->hostClientId; + return true; +} + +void LobbyClientConnected::applyOnServerAfterAnnounce(CVCMIServer * srv) +{ + // FIXME: we need to avoid senting something to client that not yet get answer for LobbyClientConnected + // Until UUID set we only pass LobbyClientConnected to this client + c->uuid = uuid; + srv->updateAndPropagateLobbyState(); +} + +bool LobbyClientDisconnected::checkClientPermissions(CVCMIServer * srv) const +{ + if(clientId != c->connectionID) + return false; + + if(shutdownServer) + { + if(!srv->cmdLineOptions.count("run-by-client")) + return false; + + if(c->uuid != srv->cmdLineOptions["uuid"].as()) + return false; + } + + return true; +} + +bool LobbyClientDisconnected::applyOnServer(CVCMIServer * srv) +{ + srv->clientDisconnected(c); + return true; +} + +void LobbyClientDisconnected::applyOnServerAfterAnnounce(CVCMIServer * srv) +{ + if(c->isOpen()) + { + boost::unique_lock lock(*c->mutexWrite); + c->close(); + c->connected = false; + } + + if(shutdownServer) + { + logNetwork->info("Client requested shutdown, server will close itself..."); + srv->state = EServerState::SHUTDOWN; + return; + } + else if(srv->connections.empty()) + { + logNetwork->error("Last connection lost, server will close itself..."); + srv->state = EServerState::SHUTDOWN; + } + else if(c == srv->hostClient) + { + auto ph = new LobbyChangeHost(); + auto newHost = *RandomGeneratorUtil::nextItem(srv->connections, CRandomGenerator::getDefault()); + ph->newHostConnectionId = newHost->connectionID; + srv->addToAnnounceQueue(ph); + } + srv->updateAndPropagateLobbyState(); +} + +bool LobbyChatMessage::checkClientPermissions(CVCMIServer * srv) const +{ + return true; +} + +bool LobbySetMap::applyOnServer(CVCMIServer * srv) +{ + srv->updateStartInfoOnMapChange(mapInfo, mapGenOpts); + return true; +} + +bool LobbySetCampaign::applyOnServer(CVCMIServer * srv) +{ + srv->si->mapname = ourCampaign->camp->header.filename; + srv->si->mode = StartInfo::CAMPAIGN; + srv->si->campState = ourCampaign; + srv->si->turnTime = 0; + bool isCurrentMapConquerable = ourCampaign->currentMap && ourCampaign->camp->conquerable(*ourCampaign->currentMap); + for(int i = 0; i < ourCampaign->camp->scenarios.size(); i++) + { + if(ourCampaign->camp->conquerable(i)) + { + if(!isCurrentMapConquerable || (isCurrentMapConquerable && i == *ourCampaign->currentMap)) + { + srv->setCampaignMap(i); + } + } + } + return true; +} + +bool LobbySetCampaignMap::applyOnServer(CVCMIServer * srv) +{ + srv->setCampaignMap(mapId); + return true; +} + +bool LobbySetCampaignBonus::applyOnServer(CVCMIServer * srv) +{ + srv->setCampaignBonus(bonusId); + return true; +} + +bool LobbyGuiAction::checkClientPermissions(CVCMIServer * srv) const +{ + return srv->isClientHost(c->connectionID); +} + +bool LobbyStartGame::checkClientPermissions(CVCMIServer * srv) const +{ + return srv->isClientHost(c->connectionID); +} + +bool LobbyStartGame::applyOnServer(CVCMIServer * srv) +{ + try + { + srv->verifyStateBeforeStart(true); + } + catch(...) + { + return false; + } + // Server will prepare gamestate and we announce StartInfo to clients + srv->prepareToStartGame(); + initializedStartInfo = std::make_shared(*srv->gh->getStartInfo(true)); + return true; +} + +void LobbyStartGame::applyOnServerAfterAnnounce(CVCMIServer * srv) +{ + srv->startGameImmidiately(); +} + +bool LobbyChangeHost::checkClientPermissions(CVCMIServer * srv) const +{ + return srv->isClientHost(c->connectionID); +} + +bool LobbyChangeHost::applyOnServer(CVCMIServer * srv) +{ + return true; +} + +bool LobbyChangeHost::applyOnServerAfterAnnounce(CVCMIServer * srv) +{ + return srv->passHost(newHostConnectionId); +} + +bool LobbyChangePlayerOption::checkClientPermissions(CVCMIServer * srv) const +{ + if(srv->isClientHost(c->connectionID)) + return true; + + if(vstd::contains(srv->getAllClientPlayers(c->connectionID), color)) + return true; + + return false; +} + +bool LobbyChangePlayerOption::applyOnServer(CVCMIServer * srv) +{ + switch(what) + { + case TOWN: + srv->optionNextCastle(color, direction); + break; + case HERO: + srv->optionNextHero(color, direction); + break; + case BONUS: + srv->optionNextBonus(color, direction); + break; + } + return true; +} + +bool LobbySetPlayer::applyOnServer(CVCMIServer * srv) +{ + srv->setPlayer(clickedColor); + return true; +} + +bool LobbySetTurnTime::applyOnServer(CVCMIServer * srv) +{ + srv->si->turnTime = turnTime; + return true; +} + +bool LobbySetDifficulty::applyOnServer(CVCMIServer * srv) +{ + srv->si->difficulty = vstd::abetween(difficulty, 0, 4); + return true; +} + +bool LobbyForceSetPlayer::applyOnServer(CVCMIServer * srv) +{ + srv->si->playerInfos[targetPlayerColor].connectedPlayerIDs.insert(targetConnectedPlayer); + return true; +} diff --git a/server/NetPacksServer.cpp b/server/NetPacksServer.cpp index 9051796be..34cc67c90 100644 --- a/server/NetPacksServer.cpp +++ b/server/NetPacksServer.cpp @@ -31,7 +31,7 @@ void CPackForServer::throwNotAllowedAction() if(c) { SystemMessage temp_message("You are not allowed to perform this action!"); - *c << &temp_message; + c->sendPack(&temp_message); } logNetwork->error("Player is not allowed to perform this action!"); throw ExceptionNotAllowedAction(); @@ -45,7 +45,7 @@ void CPackForServer::wrongPlayerMessage(CGameHandler * gh, PlayerColor expectedp if(c) { SystemMessage temp_message(oss.str()); - *c << &temp_message; + c->sendPack(&temp_message); } } @@ -92,18 +92,6 @@ bool CommitPackage::applyGh(CGameHandler * gh) return true; } -bool CloseServer::applyGh(CGameHandler * gh) -{ - gh->close(); - return true; -} - -bool LeaveGame::applyGh(CGameHandler * gh) -{ - gh->playerLeftGame(c->connectionID); - return true; -} - bool EndTurn::applyGh(CGameHandler * gh) { PlayerColor player = GS(gh)->currentPlayer; @@ -292,7 +280,7 @@ bool QueryReply::applyGh(CGameHandler * gh) auto playerToConnection = gh->connections.find(player); if(playerToConnection == gh->connections.end()) throwAndCompain(gh, "No such player!"); - if(playerToConnection->second != c) + if(!vstd::contains(playerToConnection->second, c)) throwAndCompain(gh, "Message came from wrong connection!"); if(qid == QueryID(-1)) throwAndCompain(gh, "Cannot answer the query with id -1!"); @@ -312,17 +300,18 @@ bool MakeAction::applyGh(CGameHandler * gh) if(ba.actionType != EActionType::WALK && ba.actionType != EActionType::END_TACTIC_PHASE && ba.actionType != EActionType::RETREAT && ba.actionType != EActionType::SURRENDER) throwNotAllowedAction(); - if(gh->connections[b->sides[b->tacticsSide].color] != c) + if(!vstd::contains(gh->connections[b->sides[b->tacticsSide].color], c)) throwNotAllowedAction(); } else { auto active = b->battleActiveUnit(); - if(!active) throwNotAllowedAction(); + if(!active) + throwNotAllowedAction(); auto unitOwner = b->battleGetOwner(active); - if(gh->connections[unitOwner] != c) throwNotAllowedAction(); + if(!vstd::contains(gh->connections[unitOwner], c)) + throwNotAllowedAction(); } - return gh->makeBattleAction(ba); } @@ -337,7 +326,7 @@ bool MakeCustomAction::applyGh(CGameHandler * gh) if(!active) throwNotAllowedAction(); auto unitOwner = b->battleGetOwner(active); - if(gh->connections[unitOwner] != c) + if(!vstd::contains(gh->connections[unitOwner], c)) throwNotAllowedAction(); if(ba.actionType != EActionType::HERO_SPELL) throwNotAllowedAction(); @@ -373,7 +362,7 @@ bool PlayerMessage::applyGh(CGameHandler * gh) if(!player.isSpectator()) // TODO: clearly not a great way to verify permissions { throwOnWrongPlayer(gh, player); - if(gh->getPlayerAt(c) != player) + if(gh->getPlayerAt(this->c) != player) throwNotAllowedAction(); } gh->playerMessage(player, text, currObj); diff --git a/test/game/CGameStateTest.cpp b/test/game/CGameStateTest.cpp index b9a0a0e7b..0cc18eabc 100644 --- a/test/game/CGameStateTest.cpp +++ b/test/game/CGameStateTest.cpp @@ -108,7 +108,7 @@ public: PlayerSettings & pset = si.playerInfos[PlayerColor(i)]; pset.color = PlayerColor(i); - pset.playerID = i; + pset.connectedPlayerIDs.insert(i); pset.name = "Player"; pset.castle = pinfo.defaultCastle(); From 674242da54b769d0c16946c8ff1cad7a8d0e33c9 Mon Sep 17 00:00:00 2001 From: Arseniy Shestakov Date: Mon, 5 Feb 2018 15:20:56 +0300 Subject: [PATCH 5/7] CCallback: remove unregisterAllInterfaces Not needed since CPlayerInterface no longer handle interface removal. It's far better when CClient::endGame doing it since they owned by CClient. --- CCallback.cpp | 8 -------- CCallback.h | 2 -- 2 files changed, 10 deletions(-) diff --git a/CCallback.cpp b/CCallback.cpp index 167ffa94f..8dadf5252 100644 --- a/CCallback.cpp +++ b/CCallback.cpp @@ -327,14 +327,6 @@ void CCallback::castSpell(const CGHeroInstance *hero, SpellID spellID, const int sendRequest(&cas); } -void CCallback::unregisterAllInterfaces() -{ - for (auto& pi : cl->playerint) - pi.second->finish(); - cl->playerint.clear(); - cl->battleints.clear(); -} - int CCallback::mergeOrSwapStacks(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2) { if(s1->getCreature(p1) == s2->getCreature(p2)) diff --git a/CCallback.h b/CCallback.h index 85faa1477..96c7339ea 100644 --- a/CCallback.h +++ b/CCallback.h @@ -115,8 +115,6 @@ public: void unregisterGameInterface(std::shared_ptr gameEvents); void unregisterBattleInterface(std::shared_ptr battleEvents); - void unregisterAllInterfaces(); //stops delivering information about game events to player interfaces -> can be called ONLY after victory/loss - //commands bool moveHero(const CGHeroInstance *h, int3 dst, bool transit = false) override; //dst must be free, neighbouring tile (this function can move hero only by one tile) bool teleportHero(const CGHeroInstance *who, const CGTownInstance *where); From 74e5c5bf0559321e71640ab66baaf8295ee129b0 Mon Sep 17 00:00:00 2001 From: Arseniy Shestakov Date: Mon, 26 Feb 2018 00:16:15 +0800 Subject: [PATCH 6/7] CGameHandler: rename CPackForClient argument and add network logging --- client/Client.h | 2 +- lib/IGameCallback.h | 2 +- lib/serializer/Connection.cpp | 2 +- lib/spells/Magic.h | 2 +- server/CGameHandler.cpp | 41 ++++++++++++++++---------------- server/CGameHandler.h | 12 +++++----- test/game/CGameStateTest.cpp | 4 ++-- test/mock/mock_IGameCallback.cpp | 4 ++-- test/mock/mock_IGameCallback.h | 2 +- 9 files changed, 36 insertions(+), 35 deletions(-) diff --git a/client/Client.h b/client/Client.h index cf1b6147a..eeff0d36b 100644 --- a/client/Client.h +++ b/client/Client.h @@ -203,7 +203,7 @@ public: void setManaPoints(ObjectInstanceID hid, int val) override {}; void giveHero(ObjectInstanceID id, PlayerColor player) override {}; void changeObjPos(ObjectInstanceID objid, int3 newPos, ui8 flags) override {}; - void sendAndApply(CPackForClient * info) override {}; + void sendAndApply(CPackForClient * pack) override {}; void heroExchange(ObjectInstanceID hero1, ObjectInstanceID hero2) override {}; void changeFogOfWar(int3 center, ui32 radius, PlayerColor player, bool hide) override {} diff --git a/lib/IGameCallback.h b/lib/IGameCallback.h index 93b10250a..dda1da39b 100644 --- a/lib/IGameCallback.h +++ b/lib/IGameCallback.h @@ -91,7 +91,7 @@ public: virtual void setManaPoints(ObjectInstanceID hid, int val)=0; virtual void giveHero(ObjectInstanceID id, PlayerColor player)=0; virtual void changeObjPos(ObjectInstanceID objid, int3 newPos, ui8 flags)=0; - virtual void sendAndApply(CPackForClient * info)=0; + virtual void sendAndApply(CPackForClient * pack) = 0; virtual void heroExchange(ObjectInstanceID hero1, ObjectInstanceID hero2)=0; //when two heroes meet on adventure map virtual void changeFogOfWar(int3 center, ui32 radius, PlayerColor player, bool hide) = 0; virtual void changeFogOfWar(std::unordered_set &tiles, PlayerColor player, bool hide) = 0; diff --git a/lib/serializer/Connection.cpp b/lib/serializer/Connection.cpp index 9d444e7c2..608660e08 100644 --- a/lib/serializer/Connection.cpp +++ b/lib/serializer/Connection.cpp @@ -207,7 +207,7 @@ CPack * CConnection::retrievePack() CPack * pack = nullptr; boost::unique_lock lock(*mutexRead); iser & pack; - logNetwork->trace("\treceived CPack of type %s", (pack ? typeid(*pack).name() : "nullptr")); + logNetwork->trace("Received CPack of type %s", (pack ? typeid(*pack).name() : "nullptr")); if(pack == nullptr) { logNetwork->error("Received a nullptr CPack! You should check whether client and server ABI matches."); diff --git a/lib/spells/Magic.h b/lib/spells/Magic.h index dd0be8a74..485a656a7 100644 --- a/lib/spells/Magic.h +++ b/lib/spells/Magic.h @@ -60,7 +60,7 @@ class DLL_LINKAGE PacketSender { public: virtual ~PacketSender(){}; - virtual void sendAndApply(CPackForClient * info) const = 0; + virtual void sendAndApply(CPackForClient * pack) const = 0; virtual void complain(const std::string & problem) const = 0; }; diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 85553f1c4..c136e45c5 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -61,7 +61,7 @@ class ServerSpellCastEnvironment : public SpellCastEnvironment public: ServerSpellCastEnvironment(CGameHandler * gh); ~ServerSpellCastEnvironment() = default; - void sendAndApply(CPackForClient * info) const override; + void sendAndApply(CPackForClient * pack) const override; CRandomGenerator & getRandomGenerator() const override; void complain(const std::string & problem) const override; const CMap * getMap() const override; @@ -2694,46 +2694,47 @@ void CGameHandler::heroExchange(ObjectInstanceID hero1, ObjectInstanceID hero2) } } -void CGameHandler::sendToAllClients(CPackForClient * info) +void CGameHandler::sendToAllClients(CPackForClient * pack) { - logNetwork->trace("Sending to all clients a package of type %s", typeid(*info).name()); + logNetwork->trace("\tSending to all clients: %s", typeid(*pack).name()); for (auto c : lobby->connections) { if(!c->isOpen()) continue; - c->sendPack(info); + c->sendPack(pack); } } -void CGameHandler::sendAndApply(CPackForClient * info) +void CGameHandler::sendAndApply(CPackForClient * pack) { - sendToAllClients(info); - gs->apply(info); + sendToAllClients(pack); + gs->apply(pack); + logNetwork->trace("\tApplied on gs: %s", typeid(*pack).name()); } -void CGameHandler::applyAndSend(CPackForClient * info) +void CGameHandler::applyAndSend(CPackForClient * pack) { - gs->apply(info); - sendToAllClients(info); + gs->apply(pack); + sendToAllClients(pack); } -void CGameHandler::sendAndApply(CGarrisonOperationPack * info) +void CGameHandler::sendAndApply(CGarrisonOperationPack * pack) { - sendAndApply(static_cast(info)); + sendAndApply(static_cast(pack)); checkVictoryLossConditionsForAll(); } -void CGameHandler::sendAndApply(SetResources * info) +void CGameHandler::sendAndApply(SetResources * pack) { - sendAndApply(static_cast(info)); - checkVictoryLossConditionsForPlayer(info->player); + sendAndApply(static_cast(pack)); + checkVictoryLossConditionsForPlayer(pack->player); } -void CGameHandler::sendAndApply(NewStructures * info) +void CGameHandler::sendAndApply(NewStructures * pack) { - sendAndApply(static_cast(info)); - checkVictoryLossConditionsForPlayer(getTown(info->tid)->tempOwner); + sendAndApply(static_cast(pack)); + checkVictoryLossConditionsForPlayer(getTown(pack->tid)->tempOwner); } void CGameHandler::save(const std::string & filename) @@ -6726,9 +6727,9 @@ ServerSpellCastEnvironment::ServerSpellCastEnvironment(CGameHandler * gh): gh(gh } -void ServerSpellCastEnvironment::sendAndApply(CPackForClient * info) const +void ServerSpellCastEnvironment::sendAndApply(CPackForClient * pack) const { - gh->sendAndApply(info); + gh->sendAndApply(pack); } CRandomGenerator & ServerSpellCastEnvironment::getRandomGenerator() const diff --git a/server/CGameHandler.h b/server/CGameHandler.h index 8b8df069a..d8ebc10ef 100644 --- a/server/CGameHandler.h +++ b/server/CGameHandler.h @@ -254,12 +254,12 @@ public: void sendMessageToAll(const std::string &message); void sendMessageTo(std::shared_ptr c, const std::string &message); - void sendToAllClients(CPackForClient * info); - void sendAndApply(CPackForClient * info) override; - void applyAndSend(CPackForClient * info); - void sendAndApply(CGarrisonOperationPack * info); - void sendAndApply(SetResources * info); - void sendAndApply(NewStructures * info); + void sendToAllClients(CPackForClient * pack); + void sendAndApply(CPackForClient * pack) override; + void applyAndSend(CPackForClient * pack); + void sendAndApply(CGarrisonOperationPack * pack); + void sendAndApply(SetResources * pack); + void sendAndApply(NewStructures * pack); struct FinishingBattleHelper { diff --git a/test/game/CGameStateTest.cpp b/test/game/CGameStateTest.cpp index 0cc18eabc..319aa7b35 100644 --- a/test/game/CGameStateTest.cpp +++ b/test/game/CGameStateTest.cpp @@ -44,9 +44,9 @@ public: IObjectInterface::cb = nullptr; } - void sendAndApply(CPackForClient * info) const override + void sendAndApply(CPackForClient * pack) const override { - gameState->apply(info); + gameState->apply(pack); } void complain(const std::string & problem) const diff --git a/test/mock/mock_IGameCallback.cpp b/test/mock/mock_IGameCallback.cpp index 7930c875b..0ddd163e4 100644 --- a/test/mock/mock_IGameCallback.cpp +++ b/test/mock/mock_IGameCallback.cpp @@ -32,7 +32,7 @@ void GameCallbackMock::commitPackage(CPackForClient * pack) sendAndApply(pack); } -void GameCallbackMock::sendAndApply(CPackForClient * info) +void GameCallbackMock::sendAndApply(CPackForClient * pack) { - upperCallback->sendAndApply(info); + upperCallback->sendAndApply(pack); } diff --git a/test/mock/mock_IGameCallback.h b/test/mock/mock_IGameCallback.h index e37667952..29d343409 100644 --- a/test/mock/mock_IGameCallback.h +++ b/test/mock/mock_IGameCallback.h @@ -86,7 +86,7 @@ public: ///useful callback methods void commitPackage(CPackForClient * pack) override; - void sendAndApply(CPackForClient * info) override; + void sendAndApply(CPackForClient * pack) override; private: const UpperCallback * upperCallback; }; From f5d8e5b0cc709adf9d464d8a50580d769932efc6 Mon Sep 17 00:00:00 2001 From: Arseniy Shestakov Date: Thu, 5 Apr 2018 19:35:08 +0700 Subject: [PATCH 7/7] Move Boost Asio compatibility code to Global.h. Fix by @dydzio06143 --- Global.h | 3 +++ lib/serializer/Connection.cpp | 3 --- server/CVCMIServer.cpp | 3 --- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/Global.h b/Global.h index 5d79c834e..6005dd82d 100644 --- a/Global.h +++ b/Global.h @@ -172,6 +172,9 @@ static_assert(sizeof(bool) == 1, "Bool needs to be 1 byte in size."); #if defined(_MSC_VER) && (_MSC_VER == 1900 || _MSC_VER == 1910 || _MSC_VER == 1911) #define BOOST_NO_CXX11_VARIADIC_TEMPLATES //Variadic templates are buggy in VS2015 and VS2017, so turn this off to avoid compile errors #endif +#if BOOST_VERSION >= 106600 +#define BOOST_ASIO_ENABLE_OLD_SERVICES +#endif #include #include diff --git a/lib/serializer/Connection.cpp b/lib/serializer/Connection.cpp index 608660e08..9a79b70c5 100644 --- a/lib/serializer/Connection.cpp +++ b/lib/serializer/Connection.cpp @@ -14,9 +14,6 @@ #include "../mapping/CMap.h" #include "../CGameState.h" -#if BOOST_VERSION >= 106600 -#define BOOST_ASIO_ENABLE_OLD_SERVICES -#endif #include using namespace boost; diff --git a/server/CVCMIServer.cpp b/server/CVCMIServer.cpp index b0788f228..bd019c839 100644 --- a/server/CVCMIServer.cpp +++ b/server/CVCMIServer.cpp @@ -9,9 +9,6 @@ */ #include "StdInc.h" -#if BOOST_VERSION >= 106600 -#define BOOST_ASIO_ENABLE_OLD_SERVICES -#endif #include #include "../lib/filesystem/Filesystem.h"