diff --git a/Mods/vcmi/config/vcmi/czech.json b/Mods/vcmi/config/vcmi/czech.json index 04aa13f30..1e9f8b2e8 100644 --- a/Mods/vcmi/config/vcmi/czech.json +++ b/Mods/vcmi/config/vcmi/czech.json @@ -37,7 +37,6 @@ "vcmi.radialWheel.moveUnit" : "Přesunout jednotky do jiného oddílu", "vcmi.radialWheel.splitUnit" : "Rozdělit jednotku do jiné pozice", - "vcmi.mainMenu.highscoresNotImplemented" : "Omlouvám se, menu nejvyšší skóre ještě není implementováno\n", "vcmi.mainMenu.serverConnecting" : "Připojování...", "vcmi.mainMenu.serverAddressEnter" : "Zadejte adresu:", "vcmi.mainMenu.serverClosing" : "Zavírání...", diff --git a/Mods/vcmi/config/vcmi/english.json b/Mods/vcmi/config/vcmi/english.json index a4925d63b..a14081798 100644 --- a/Mods/vcmi/config/vcmi/english.json +++ b/Mods/vcmi/config/vcmi/english.json @@ -42,7 +42,6 @@ "vcmi.radialWheel.moveUnit" : "Move creatures to another army", "vcmi.radialWheel.splitUnit" : "Split creature to another slot", - "vcmi.mainMenu.highscoresNotImplemented" : "Sorry, high scores menu is not implemented yet\n", "vcmi.mainMenu.serverConnecting" : "Connecting...", "vcmi.mainMenu.serverAddressEnter" : "Enter address:", "vcmi.mainMenu.serverClosing" : "Closing...", diff --git a/Mods/vcmi/config/vcmi/french.json b/Mods/vcmi/config/vcmi/french.json index 69112035d..377f3da83 100644 --- a/Mods/vcmi/config/vcmi/french.json +++ b/Mods/vcmi/config/vcmi/french.json @@ -30,7 +30,6 @@ "vcmi.capitalColors.6" : "Turquoise", "vcmi.capitalColors.7" : "Rose", - "vcmi.mainMenu.highscoresNotImplemented" : "Désolé, le menu des meilleurs scores n'est pas encore implémenté\n", "vcmi.mainMenu.serverConnecting" : "Connexion...", "vcmi.mainMenu.serverAddressEnter" : "Entrez l'adresse :", "vcmi.mainMenu.serverClosing" : "Fermeture...", diff --git a/Mods/vcmi/config/vcmi/german.json b/Mods/vcmi/config/vcmi/german.json index e6808f378..abe112ebd 100644 --- a/Mods/vcmi/config/vcmi/german.json +++ b/Mods/vcmi/config/vcmi/german.json @@ -41,7 +41,6 @@ "vcmi.radialWheel.moveUnit" : "Verschieben der Kreatur in andere Armee", "vcmi.radialWheel.splitUnit" : "Aufsplitten der Kreatur in anderen Slot", - "vcmi.mainMenu.highscoresNotImplemented" : "Die Highscores sind aktuell noch nicht implementiert\n", "vcmi.mainMenu.serverConnecting" : "Verbinde...", "vcmi.mainMenu.serverAddressEnter" : "Addresse eingeben:", "vcmi.mainMenu.serverClosing" : "Trenne...", diff --git a/Mods/vcmi/config/vcmi/polish.json b/Mods/vcmi/config/vcmi/polish.json index af4eac0c6..51c96dac4 100644 --- a/Mods/vcmi/config/vcmi/polish.json +++ b/Mods/vcmi/config/vcmi/polish.json @@ -36,7 +36,6 @@ "vcmi.radialWheel.moveUnit" : "Przenieś stworzenia do innej armii", "vcmi.radialWheel.splitUnit" : "Podziel jednostkę do wybranego miejsca", - "vcmi.mainMenu.highscoresNotImplemented" : "Przepraszamy, najlepsze wyniki nie zostały jeszcze zaimplementowane\n", "vcmi.mainMenu.serverConnecting" : "Łączenie...", "vcmi.mainMenu.serverAddressEnter" : "Wprowadź adres:", "vcmi.mainMenu.serverClosing" : "Zamykanie...", diff --git a/Mods/vcmi/config/vcmi/ukrainian.json b/Mods/vcmi/config/vcmi/ukrainian.json index 315a837ad..4f5228821 100644 --- a/Mods/vcmi/config/vcmi/ukrainian.json +++ b/Mods/vcmi/config/vcmi/ukrainian.json @@ -37,7 +37,6 @@ "vcmi.radialWheel.moveUnit" : "Перемістити істоту до іншої армії", "vcmi.radialWheel.splitUnit" : "Розділити істоту в інший слот", - "vcmi.mainMenu.highscoresNotImplemented" : "Вибачте, таблицю рекордів ще не реалізовано\n", "vcmi.mainMenu.serverConnecting" : "Підключення...", "vcmi.mainMenu.serverAddressEnter" : "Вкажіть адресу:", "vcmi.mainMenu.serverClosing" : "Завершення...", diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 7e0731cb2..9abc1e961 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -59,6 +59,7 @@ set(client_SRCS mainmenu/CMainMenu.cpp mainmenu/CPrologEpilogVideo.cpp mainmenu/CreditsScreen.cpp + mainmenu/CHighScoreScreen.cpp mapView/MapRenderer.cpp mapView/MapRendererContext.cpp @@ -213,6 +214,7 @@ set(client_HEADERS mainmenu/CMainMenu.h mainmenu/CPrologEpilogVideo.h mainmenu/CreditsScreen.h + mainmenu/CHighScoreScreen.h mapView/IMapRendererContext.h mapView/IMapRendererObserver.h diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index 3be85d404..09bdc174f 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -37,6 +37,7 @@ #include "gui/WindowHandler.h" #include "mainmenu/CMainMenu.h" +#include "mainmenu/CHighScoreScreen.h" #include "mapView/mapHandler.h" @@ -1697,16 +1698,43 @@ void CPlayerInterface::showShipyardDialogOrProblemPopup(const IShipyard *obj) void CPlayerInterface::requestReturningToMainMenu(bool won) { + HighScoreParameter param; + param.difficulty = cb->getStartInfo()->difficulty; + param.day = cb->getDate(); + param.townAmount = cb->howManyTowns(); + param.usedCheat = cb->getPlayerState(*cb->getPlayerID())->cheated; + param.hasGrail = false; + for(const CGHeroInstance * h : cb->getHeroesInfo()) + if(h->hasArt(ArtifactID::GRAIL)) + param.hasGrail = true; + for(const CGTownInstance * t : cb->getTownsInfo()) + if(t->builtBuildings.count(BuildingID::GRAIL)) + param.hasGrail = true; + param.allDefeated = true; + for (PlayerColor player(0); player < PlayerColor::PLAYER_LIMIT; ++player) + { + auto ps = cb->getPlayerState(player, false); + if(ps && player != *cb->getPlayerID()) + if(!ps->checkVanquished()) + param.allDefeated = false; + } + param.scenarioName = cb->getMapHeader()->name; + param.playerName = cb->getStartInfo()->playerInfos.find(*cb->getPlayerID())->second.name; + HighScoreCalculation highScoreCalc; + highScoreCalc.parameters.push_back(param); + highScoreCalc.isCampaign = false; + if(won && cb->getStartInfo()->campState) - CSH->startCampaignScenario(cb->getStartInfo()->campState); + CSH->startCampaignScenario(param, cb->getStartInfo()->campState); else { GH.dispatchMainThread( - []() + [won, highScoreCalc]() { CSH->endGameplay(); GH.defActionsDef = 63; CMM->menu->switchToTab("main"); + GH.windows().createAndPushWindow(won, highScoreCalc); } ); } diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp index 55b495731..952b6feb1 100644 --- a/client/CServerHandler.cpp +++ b/client/CServerHandler.cpp @@ -22,6 +22,7 @@ #include "mainmenu/CMainMenu.h" #include "mainmenu/CPrologEpilogVideo.h" +#include "mainmenu/CHighScoreScreen.h" #ifdef VCMI_ANDROID #include "../lib/CAndroidVMHelper.h" @@ -616,6 +617,8 @@ void CServerHandler::startGameplay(VCMI_LIB_WRAP_NAMESPACE(CGameState) * gameSta CMM->disable(); client = new CClient(); + highScoreCalc = nullptr; + switch(si->mode) { case StartInfo::NEW_GAME: @@ -685,14 +688,23 @@ void CServerHandler::endGameplay(bool closeConnection, bool restart) saveSession->Bool() = false; } -void CServerHandler::startCampaignScenario(std::shared_ptr cs) +void CServerHandler::startCampaignScenario(HighScoreParameter param, std::shared_ptr cs) { std::shared_ptr ourCampaign = cs; if (!cs) ourCampaign = si->campState; - GH.dispatchMainThread([ourCampaign]() + if(highScoreCalc == nullptr) + { + highScoreCalc = std::make_shared(); + highScoreCalc->isCampaign = true; + highScoreCalc->parameters.clear(); + } + param.campaignName = cs->getName(); + highScoreCalc->parameters.push_back(param); + + GH.dispatchMainThread([ourCampaign, this]() { CSH->campaignServerRestartLock.set(true); CSH->endGameplay(); @@ -712,7 +724,10 @@ void CServerHandler::startCampaignScenario(std::shared_ptr cs) if(!ourCampaign->isCampaignFinished()) CMM->openCampaignLobby(ourCampaign); else + { CMM->openCampaignScreen(ourCampaign->campaignSet); + GH.windows().createAndPushWindow(true, *highScoreCalc); + } }; if(epilogue.hasPrologEpilog) { @@ -960,7 +975,7 @@ void CServerHandler::threadRunServer() } comm += " > \"" + logName + '\"'; - logGlobal->info("Server command line: %s", comm); + logGlobal->info("Server command line: %s", comm); #ifdef VCMI_WINDOWS int result = -1; diff --git a/client/CServerHandler.h b/client/CServerHandler.h index 5e520d9ed..4572d5fb8 100644 --- a/client/CServerHandler.h +++ b/client/CServerHandler.h @@ -35,6 +35,9 @@ VCMI_LIB_NAMESPACE_END class CClient; class CBaseForLobbyApply; +class HighScoreCalculation; +class HighScoreParameter; + // TODO: Add mutex so we can't set CONNECTION_CANCELLED if client already connected, but thread not setup yet enum class EClientState : ui8 { @@ -87,6 +90,8 @@ class CServerHandler : public IServerAPI, public LobbyInfo std::vector myNames; + std::shared_ptr highScoreCalc; + void threadHandleConnection(); void threadRunServer(); void onServerFinished(); @@ -161,7 +166,7 @@ public: void startGameplay(VCMI_LIB_WRAP_NAMESPACE(CGameState) * gameState = nullptr); void endGameplay(bool closeConnection = true, bool restart = false); - void startCampaignScenario(std::shared_ptr cs = {}); + void startCampaignScenario(HighScoreParameter param, std::shared_ptr cs = {}); void showServerError(const std::string & txt) const; // TODO: LobbyState must be updated within game so we should always know how many player interfaces our client handle diff --git a/client/CVideoHandler.cpp b/client/CVideoHandler.cpp index ea1d905bc..10bd54bf0 100644 --- a/client/CVideoHandler.cpp +++ b/client/CVideoHandler.cpp @@ -349,7 +349,7 @@ void CVideoPlayer::redraw( int x, int y, SDL_Surface *dst, bool update ) show(x, y, dst, update); } -void CVideoPlayer::update( int x, int y, SDL_Surface *dst, bool forceRedraw, bool update ) +void CVideoPlayer::update( int x, int y, SDL_Surface *dst, bool forceRedraw, bool update, std::function onVideoRestart) { if (sws == nullptr) return; @@ -368,6 +368,8 @@ void CVideoPlayer::update( int x, int y, SDL_Surface *dst, bool forceRedraw, boo show(x,y,dst,update); else { + if(onVideoRestart) + onVideoRestart(); VideoPath filenameToReopen = fname; // create copy to backup this->fname open(filenameToReopen); nextFrame(); diff --git a/client/CVideoHandler.h b/client/CVideoHandler.h index ec9f60278..d447d64e6 100644 --- a/client/CVideoHandler.h +++ b/client/CVideoHandler.h @@ -31,7 +31,7 @@ public: class IMainVideoPlayer : public IVideoPlayer { public: - virtual void update(int x, int y, SDL_Surface *dst, bool forceRedraw, bool update = true){} + virtual void update(int x, int y, SDL_Surface *dst, bool forceRedraw, bool update = true, std::function restart = 0){} virtual bool openAndPlayVideo(const VideoPath & name, int x, int y, bool stopOnKey = false, bool scale = false) { return false; @@ -101,7 +101,7 @@ public: void show(int x, int y, SDL_Surface *dst, bool update = true) override; //blit current frame void redraw(int x, int y, SDL_Surface *dst, bool update = true) override; //reblits buffer - void update(int x, int y, SDL_Surface *dst, bool forceRedraw, bool update = true) override; //moves to next frame if appropriate, and blits it or blits only if redraw parameter is set true + void update(int x, int y, SDL_Surface *dst, bool forceRedraw, bool update = true, std::function onVideoRestart = nullptr) override; //moves to next frame if appropriate, and blits it or blits only if redraw parameter is set true // Opens video, calls playVideo, closes video; returns playVideo result (if whole video has been played) bool openAndPlayVideo(const VideoPath & name, int x, int y, bool stopOnKey = false, bool scale = false) override; diff --git a/client/VCMI_client.cbp b/client/VCMI_client.cbp index 5abaf79fd..57fc618b5 100644 --- a/client/VCMI_client.cbp +++ b/client/VCMI_client.cbp @@ -198,6 +198,8 @@ + + diff --git a/client/VCMI_client.vcxproj b/client/VCMI_client.vcxproj index fd89da223..3fe7331f9 100644 --- a/client/VCMI_client.vcxproj +++ b/client/VCMI_client.vcxproj @@ -217,6 +217,7 @@ + @@ -286,6 +287,7 @@ + diff --git a/client/VCMI_client.vcxproj.filters b/client/VCMI_client.vcxproj.filters index d5f5af0fa..8831d0480 100644 --- a/client/VCMI_client.vcxproj.filters +++ b/client/VCMI_client.vcxproj.filters @@ -132,6 +132,7 @@ + @@ -290,5 +291,6 @@ + \ No newline at end of file diff --git a/client/mainmenu/CHighScoreScreen.cpp b/client/mainmenu/CHighScoreScreen.cpp new file mode 100644 index 000000000..9d39343a5 --- /dev/null +++ b/client/mainmenu/CHighScoreScreen.cpp @@ -0,0 +1,383 @@ +/* + * CHighScoreScreen.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 "CHighScoreScreen.h" +#include "../gui/CGuiHandler.h" +#include "../gui/WindowHandler.h" +#include "../gui/Shortcut.h" +#include "../widgets/TextControls.h" +#include "../widgets/Buttons.h" +#include "../widgets/Images.h" +#include "../widgets/MiscWidgets.h" +#include "../windows/InfoWindows.h" +#include "../render/Canvas.h" + +#include "../CGameInfo.h" +#include "../CVideoHandler.h" +#include "../CMusicHandler.h" +#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/CConfigHandler.h" +#include "../../lib/CCreatureHandler.h" +#include "../../lib/constants/EntityIdentifiers.h" +#include "../../lib/TextOperations.h" + +#include "vstd/DateUtils.h" + +auto HighScoreCalculation::calculate() +{ + struct Result + { + int basic = 0; + int total = 0; + int sumDays = 0; + bool cheater = false; + }; + + Result firstResult, summary; + const std::array difficultyMultipliers{0.8, 1.0, 1.3, 1.6, 2.0}; + for(auto & param : parameters) + { + double tmp = 200 - (param.day + 10) / (param.townAmount + 5) + (param.allDefeated ? 25 : 0) + (param.hasGrail ? 25 : 0); + firstResult = Result{static_cast(tmp), static_cast(tmp * difficultyMultipliers.at(param.difficulty)), param.day, param.usedCheat}; + summary.basic += firstResult.basic * 5.0 / parameters.size(); + summary.total += firstResult.total * 5.0 / parameters.size(); + summary.sumDays += firstResult.sumDays; + summary.cheater |= firstResult.cheater; + } + + if(parameters.size() == 1) + return firstResult; + + return summary; +} + +CreatureID HighScoreCalculation::getCreatureForPoints(int points, bool campaign) +{ + static const JsonNode configCreatures(JsonPath::builtin("CONFIG/highscoreCreatures.json")); + auto creatures = configCreatures["creatures"].Vector(); + int divide = campaign ? 5 : 1; + + for(auto & creature : creatures) + if(points / divide <= creature["max"].Integer() && points / divide >= creature["min"].Integer()) + return CreatureID::decode(creature["creature"].String()); + + return -1; +} + +CHighScoreScreen::CHighScoreScreen(HighScorePage highscorepage, int highlighted) + : CWindowObject(BORDERED), highscorepage(highscorepage), highlighted(highlighted) +{ + addUsedEvents(SHOW_POPUP); + + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + pos = center(Rect(0, 0, 800, 600)); + updateShadow(); + + addHighScores(); + addButtons(); +} + +void CHighScoreScreen::showPopupWindow(const Point & cursorPosition) +{ + for (int i = 0; i < screenRows; i++) + { + bool currentGameNotInListEntry = i == (screenRows - 1) && highlighted > (screenRows - 1); + + Rect r = Rect(80, 40 + i * 50, 635, 50); + if(r.isInside(cursorPosition - pos)) + { + std::string tmp = persistentStorage["highscore"][highscorepage == HighScorePage::SCENARIO ? "scenario" : "campaign"][currentGameNotInListEntry ? highlighted : i]["datetime"].String(); + if(!tmp.empty()) + CRClickPopup::createAndPush(tmp); + } + } +} + +void CHighScoreScreen::addButtons() +{ + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + + buttons.clear(); + + buttons.push_back(std::make_shared(Point(31, 113), AnimationPath::builtin("HISCCAM.DEF"), CButton::tooltip(), [&](){ buttonCampaignClick(); })); + buttons.push_back(std::make_shared(Point(31, 345), AnimationPath::builtin("HISCSTA.DEF"), CButton::tooltip(), [&](){ buttonScenarioClick(); })); + buttons.push_back(std::make_shared(Point(726, 113), AnimationPath::builtin("HISCRES.DEF"), CButton::tooltip(), [&](){ buttonResetClick(); })); + buttons.push_back(std::make_shared(Point(726, 345), AnimationPath::builtin("HISCEXT.DEF"), CButton::tooltip(), [&](){ buttonExitClick(); })); +} + +void CHighScoreScreen::addHighScores() +{ + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + + background = std::make_shared(ImagePath::builtin(highscorepage == HighScorePage::SCENARIO ? "HISCORE" : "HISCORE2")); + + texts.clear(); + images.clear(); + + // Header + texts.push_back(std::make_shared(115, 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.433"))); // rank + texts.push_back(std::make_shared(225, 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.434"))); // player + + if(highscorepage == HighScorePage::SCENARIO) + { + texts.push_back(std::make_shared(405, 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.435"))); // land + texts.push_back(std::make_shared(557, 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.436"))); // days + texts.push_back(std::make_shared(627, 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.75"))); // score + } + else + { + texts.push_back(std::make_shared(405, 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.672"))); // campaign + texts.push_back(std::make_shared(592, 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.75"))); // score + } + + // Content + int y = 66; + auto & data = persistentStorage["highscore"][highscorepage == HighScorePage::SCENARIO ? "scenario" : "campaign"]; + for (int i = 0; i < screenRows; i++) + { + bool currentGameNotInListEntry = (i == (screenRows - 1) && highlighted > (screenRows - 1)); + auto & curData = data[currentGameNotInListEntry ? highlighted : i]; + + ColorRGBA color = (i == highlighted || currentGameNotInListEntry) ? Colors::YELLOW : Colors::WHITE; + + texts.push_back(std::make_shared(115, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, color, std::to_string((currentGameNotInListEntry ? highlighted : i) + 1))); + std::string tmp = curData["player"].String(); + TextOperations::trimRightUnicode(tmp, std::max(0, (int)TextOperations::getUnicodeCharactersCount(tmp) - 13)); + texts.push_back(std::make_shared(225, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, color, tmp)); + + if(highscorepage == HighScorePage::SCENARIO) + { + std::string tmp = curData["scenarioName"].String(); + TextOperations::trimRightUnicode(tmp, std::max(0, (int)TextOperations::getUnicodeCharactersCount(tmp) - 25)); + texts.push_back(std::make_shared(405, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, color, tmp)); + texts.push_back(std::make_shared(557, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, color, std::to_string(curData["days"].Integer()))); + texts.push_back(std::make_shared(627, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, color, std::to_string(curData["points"].Integer()))); + } + else + { + std::string tmp = curData["campaignName"].String(); + TextOperations::trimRightUnicode(tmp, std::max(0, (int)TextOperations::getUnicodeCharactersCount(tmp) - 25)); + texts.push_back(std::make_shared(405, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, color, tmp)); + texts.push_back(std::make_shared(592, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, color, std::to_string(curData["points"].Integer()))); + } + + if(curData["points"].Integer() > 0 && curData["points"].Integer() <= ((highscorepage == HighScorePage::CAMPAIGN) ? 2500 : 500)) + images.push_back(std::make_shared(AnimationPath::builtin("CPRSMALL"), (*CGI->creh)[HighScoreCalculation::getCreatureForPoints(curData["points"].Integer(), highscorepage == HighScorePage::CAMPAIGN)]->getIconIndex(), 0, 670, y - 15 + i * 50)); + } +} + +void CHighScoreScreen::buttonCampaignClick() +{ + highscorepage = HighScorePage::CAMPAIGN; + addHighScores(); + addButtons(); + redraw(); +} + +void CHighScoreScreen::buttonScenarioClick() +{ + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + highscorepage = HighScorePage::SCENARIO; + addHighScores(); + addButtons(); + redraw(); +} + +void CHighScoreScreen::buttonResetClick() +{ + CInfoWindow::showYesNoDialog( + CGI->generaltexth->allTexts[666], + {}, + [this]() + { + Settings entry = persistentStorage.write["highscore"]; + entry->clear(); + addHighScores(); + addButtons(); + redraw(); + }, + 0 + ); +} + +void CHighScoreScreen::buttonExitClick() +{ + close(); +} + +CHighScoreInputScreen::CHighScoreInputScreen(bool won, HighScoreCalculation calc) + : CWindowObject(BORDERED), won(won), calc(calc) +{ + addUsedEvents(LCLICK | KEYBOARD); + + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + pos = center(Rect(0, 0, 800, 600)); + updateShadow(); + + background = std::make_shared(Rect(0, 0, pos.w, pos.h), Colors::BLACK); + + if(won) + { + int border = 100; + int textareaW = ((pos.w - 2 * border) / 4); + std::vector t = { "438", "439", "440", "441", "676" }; // time, score, difficulty, final score, rank + for (int i = 0; i < 5; i++) + texts.push_back(std::make_shared(Rect(textareaW * i + border - (textareaW / 2), 450, textareaW, 100), FONT_HIGH_SCORE, ETextAlignment::TOPCENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt." + t[i]))); + + std::string creatureName = (calc.calculate().cheater) ? CGI->generaltexth->translate("core.genrltxt.260") : (*CGI->creh)[HighScoreCalculation::getCreatureForPoints(calc.calculate().total, calc.isCampaign)]->getNameSingularTranslated(); + t = { std::to_string(calc.calculate().sumDays), std::to_string(calc.calculate().basic), CGI->generaltexth->translate("core.arraytxt." + std::to_string((142 + calc.parameters[0].difficulty))), std::to_string(calc.calculate().total), creatureName }; + for (int i = 0; i < 5; i++) + texts.push_back(std::make_shared(Rect(textareaW * i + border - (textareaW / 2), 530, textareaW, 100), FONT_HIGH_SCORE, ETextAlignment::TOPCENTER, Colors::WHITE, t[i])); + + CCS->musich->playMusic(AudioPath::builtin("music/Win Scenario"), true, true); + } + else + CCS->musich->playMusic(AudioPath::builtin("music/UltimateLose"), false, true); + + video = won ? "HSANIM.SMK" : "LOSEGAME.SMK"; +} + +int CHighScoreInputScreen::addEntry(std::string text) { + std::vector baseNode = persistentStorage["highscore"][calc.isCampaign ? "campaign" : "scenario"].Vector(); + + auto sortFunctor = [](const JsonNode & left, const JsonNode & right) + { + if(left["points"].Integer() == right["points"].Integer()) + return left["posFlag"].Integer() > right["posFlag"].Integer(); + return left["points"].Integer() > right["points"].Integer(); + }; + + JsonNode newNode = JsonNode(); + newNode["player"].String() = text; + if(calc.isCampaign) + newNode["campaignName"].String() = calc.calculate().cheater ? CGI->generaltexth->translate("core.genrltxt.260") : calc.parameters[0].campaignName; + else + newNode["scenarioName"].String() = calc.calculate().cheater ? CGI->generaltexth->translate("core.genrltxt.260") : calc.parameters[0].scenarioName; + newNode["days"].Integer() = calc.calculate().sumDays; + newNode["points"].Integer() = calc.calculate().cheater ? 0 : calc.calculate().total; + newNode["datetime"].String() = vstd::getFormattedDateTime(std::time(0)); + newNode["posFlag"].Bool() = true; + + baseNode.push_back(newNode); + boost::range::sort(baseNode, sortFunctor); + + int pos = -1; + for (int i = 0; i < baseNode.size(); i++) + { + if(!baseNode[i]["posFlag"].isNull()) + { + baseNode[i]["posFlag"].clear(); + pos = i; + } + } + + Settings s = persistentStorage.write["highscore"][calc.isCampaign ? "campaign" : "scenario"]; + s->Vector() = baseNode; + + return pos; +} + +void CHighScoreInputScreen::show(Canvas & to) +{ + CCS->videoh->update(pos.x, pos.y, to.getInternalSurface(), true, false, + [&]() + { + if(won) + { + CCS->videoh->close(); + video = "HSLOOP.SMK"; + CCS->videoh->open(VideoPath::builtin(video)); + } + else + close(); + }); + redraw(); + + CIntObject::show(to); +} + +void CHighScoreInputScreen::activate() +{ + if(!CCS->videoh->open(VideoPath::builtin(video))) + { + if(!won) + close(); + } + else + background = nullptr; + CIntObject::activate(); +} + +void CHighScoreInputScreen::deactivate() +{ + CCS->videoh->close(); + CIntObject::deactivate(); +} + +void CHighScoreInputScreen::clickPressed(const Point & cursorPosition) +{ + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + + if(!won) + { + close(); + return; + } + + if(!input) + { + input = std::make_shared(calc.parameters[0].playerName, + [&] (std::string text) + { + if(!text.empty()) + { + int pos = addEntry(text); + close(); + GH.windows().createAndPushWindow(calc.isCampaign ? CHighScoreScreen::HighScorePage::CAMPAIGN : CHighScoreScreen::HighScorePage::SCENARIO, pos); + } + else + close(); + }); + } +} + +void CHighScoreInputScreen::keyPressed(EShortcut key) +{ + clickPressed(Point()); +} + +CHighScoreInput::CHighScoreInput(std::string playerName, std::function readyCB) + : CWindowObject(0, ImagePath::builtin("HIGHNAME")), ready(readyCB) +{ + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + + pos = center(Rect(0, 0, 232, 212)); + updateShadow(); + + text = std::make_shared(Rect(15, 15, 202, 202), FONT_SMALL, ETextAlignment::TOPCENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.96")); + + buttonOk = std::make_shared(Point(26, 142), AnimationPath::builtin("MUBCHCK.DEF"), CGI->generaltexth->zelp[560], std::bind(&CHighScoreInput::okay, this), EShortcut::GLOBAL_ACCEPT); + buttonCancel = std::make_shared(Point(142, 142), AnimationPath::builtin("MUBCANC.DEF"), CGI->generaltexth->zelp[561], std::bind(&CHighScoreInput::abort, this), EShortcut::GLOBAL_CANCEL); + statusBar = CGStatusBar::create(std::make_shared(background->getSurface(), Rect(7, 186, 218, 18), 7, 186)); + textInput = std::make_shared(Rect(18, 104, 200, 25), FONT_SMALL, 0); + textInput->setText(playerName); +} + +void CHighScoreInput::okay() +{ + ready(textInput->getText()); +} + +void CHighScoreInput::abort() +{ + ready(""); +} \ No newline at end of file diff --git a/client/mainmenu/CHighScoreScreen.h b/client/mainmenu/CHighScoreScreen.h new file mode 100644 index 000000000..89e4bdf21 --- /dev/null +++ b/client/mainmenu/CHighScoreScreen.h @@ -0,0 +1,110 @@ +/* + * CHighScoreScreen.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 CButton; +class CLabel; +class CMultiLineLabel; +class CAnimImage; +class CTextInput; + +class TransparentFilledRectangle; + +class HighScoreParameter +{ +public: + int difficulty; + int day; + int townAmount; + bool usedCheat; + bool hasGrail; + bool allDefeated; + std::string campaignName; + std::string scenarioName; + std::string playerName; +}; + +class HighScoreCalculation +{ +public: + std::vector parameters = std::vector(); + bool isCampaign = false; + + auto calculate(); + static CreatureID getCreatureForPoints(int points, bool campaign); +}; + +class CHighScoreScreen : public CWindowObject +{ +public: + enum HighScorePage { SCENARIO, CAMPAIGN }; + +private: + void addButtons(); + void addHighScores(); + + void buttonCampaignClick(); + void buttonScenarioClick(); + void buttonResetClick(); + void buttonExitClick(); + + void showPopupWindow(const Point & cursorPosition) override; + + HighScorePage highscorepage; + + std::shared_ptr background; + std::vector> buttons; + std::vector> texts; + std::vector> images; + + const int screenRows = 11; + + int highlighted; +public: + CHighScoreScreen(HighScorePage highscorepage, int highlighted = -1); +}; + +class CHighScoreInput : public CWindowObject +{ + std::shared_ptr text; + std::shared_ptr buttonOk; + std::shared_ptr buttonCancel; + std::shared_ptr statusBar; + std::shared_ptr textInput; + + std::function ready; + + void okay(); + void abort(); +public: + CHighScoreInput(std::string playerName, std::function readyCB); +}; + +class CHighScoreInputScreen : public CWindowObject +{ + std::vector> texts; + std::shared_ptr input; + std::shared_ptr background; + + std::string video; + bool won; + HighScoreCalculation calc; +public: + CHighScoreInputScreen(bool won, HighScoreCalculation calc); + + int addEntry(std::string text); + + void show(Canvas & to) override; + void activate() override; + void deactivate() override; + void clickPressed(const Point & cursorPosition) override; + void keyPressed(EShortcut key) override; +}; \ No newline at end of file diff --git a/client/mainmenu/CMainMenu.cpp b/client/mainmenu/CMainMenu.cpp index 80021dbec..34bda33d8 100644 --- a/client/mainmenu/CMainMenu.cpp +++ b/client/mainmenu/CMainMenu.cpp @@ -12,6 +12,7 @@ #include "CCampaignScreen.h" #include "CreditsScreen.h" +#include "CHighScoreScreen.h" #include "../lobby/CBonusSelection.h" #include "../lobby/CSelectionBase.h" @@ -216,7 +217,7 @@ static std::function genCommand(CMenuScreen * menu, std::vectorgeneraltexth->translate("vcmi.mainMenu.highscoresNotImplemented"), std::vector>(), PlayerColor(1)); + return std::bind(CMainMenu::openHighScoreScreen); } } } @@ -395,6 +396,12 @@ void CMainMenu::startTutorial() CSH->startMapAfterConnection(mapInfo); } +void CMainMenu::openHighScoreScreen() +{ + GH.windows().createAndPushWindow(CHighScoreScreen::HighScorePage::SCENARIO); + return; +} + std::shared_ptr CMainMenu::create() { if(!CMM) diff --git a/client/mainmenu/CMainMenu.h b/client/mainmenu/CMainMenu.h index f0e246c60..ea9010797 100644 --- a/client/mainmenu/CMainMenu.h +++ b/client/mainmenu/CMainMenu.h @@ -152,6 +152,7 @@ public: static void openCampaignLobby(const std::string & campaignFileName, std::string campaignSet = ""); static void openCampaignLobby(std::shared_ptr campaign); static void startTutorial(); + static void openHighScoreScreen(); void openCampaignScreen(std::string name); static std::shared_ptr create(); diff --git a/config/highscoreCreatures.json b/config/highscoreCreatures.json new file mode 100644 index 000000000..b0feda288 --- /dev/null +++ b/config/highscoreCreatures.json @@ -0,0 +1,122 @@ +{ + "creatures": [ + { "min" : 1, "max" : 4, "creature": "imp" }, + { "min" : 5, "max" : 8, "creature": "gremlin" }, + { "min" : 9, "max" : 12, "creature": "gnoll" }, + { "min" : 13, "max" : 16, "creature": "troglodyte" }, + { "min" : 17, "max" : 20, "creature": "familiar" }, + { "min" : 21, "max" : 24, "creature": "skeleton" }, + { "min" : 25, "max" : 28, "creature": "goblin" }, + { "min" : 29, "max" : 32, "creature": "masterGremlin" }, + { "min" : 33, "max" : 36, "creature": "hobgoblin" }, + { "min" : 37, "max" : 40, "creature": "pikeman" }, + { "min" : 41, "max" : 44, "creature": "infernalTroglodyte" }, + { "min" : 45, "max" : 48, "creature": "skeletonWarrior" }, + { "min" : 49, "max" : 52, "creature": "gnollMarauder" }, + { "min" : 53, "max" : 56, "creature": "walkingDead" }, + { "min" : 57, "max" : 60, "creature": "centaur" }, + { "min" : 61, "max" : 64, "creature": "halberdier" }, + { "min" : 65, "max" : 68, "creature": "archer" }, + { "min" : 69, "max" : 72, "creature": "lizardman" }, + { "min" : 73, "max" : 76, "creature": "zombie" }, + { "min" : 77, "max" : 80, "creature": "goblinWolfRider" }, + { "min" : 81, "max" : 84, "creature": "centaurCaptain" }, + { "min" : 85, "max" : 88, "creature": "dwarf" }, + { "min" : 89, "max" : 92, "creature": "harpy" }, + { "min" : 93, "max" : 96, "creature": "lizardWarrior" }, + { "min" : 97, "max" : 100, "creature": "gog" }, + { "min" : 101, "max" : 104, "creature": "stoneGargoyle" }, + { "min" : 105, "max" : 108, "creature": "sharpshooter" }, + { "min" : 109, "max" : 112, "creature": "orc" }, + { "min" : 113, "max" : 116, "creature": "obsidianGargoyle" }, + { "min" : 117, "max" : 120, "creature": "hobgoblinWolfRider" }, + { "min" : 121, "max" : 124, "creature": "battleDwarf" }, + { "min" : 125, "max" : 128, "creature": "woodElf" }, + { "min" : 129, "max" : 132, "creature": "harpyHag" }, + { "min" : 133, "max" : 136, "creature": "magog" }, + { "min" : 137, "max" : 140, "creature": "orcChieftain" }, + { "min" : 141, "max" : 144, "creature": "stoneGolem" }, + { "min" : 145, "max" : 148, "creature": "wight" }, + { "min" : 149, "max" : 152, "creature": "serpentFly" }, + { "min" : 153, "max" : 156, "creature": "dragonFly" }, + { "min" : 157, "max" : 160, "creature": "wraith" }, + { "min" : 161, "max" : 164, "creature": "waterElemental" }, + { "min" : 165, "max" : 168, "creature": "earthElemental" }, + { "min" : 169, "max" : 172, "creature": "grandElf" }, + { "min" : 173, "max" : 176, "creature": "beholder" }, + { "min" : 177, "max" : 180, "creature": "fireElemental" }, + { "min" : 181, "max" : 184, "creature": "griffin" }, + { "min" : 185, "max" : 187, "creature": "airElemental" }, + { "min" : 188, "max" : 190, "creature": "hellHound" }, + { "min" : 191, "max" : 193, "creature": "evilEye" }, + { "min" : 194, "max" : 196, "creature": "cerberus" }, + { "min" : 197, "max" : 199, "creature": "ironGolem" }, + { "min" : 200, "max" : 202, "creature": "ogre" }, + { "min" : 203, "max" : 205, "creature": "swordsman" }, + { "min" : 206, "max" : 208, "creature": "demon" }, + { "min" : 209, "max" : 211, "creature": "royalGriffin" }, + { "min" : 212, "max" : 214, "creature": "hornedDemon" }, + { "min" : 215, "max" : 217, "creature": "monk" }, + { "min" : 218, "max" : 220, "creature": "dendroidGuard" }, + { "min" : 221, "max" : 223, "creature": "medusa" }, + { "min" : 224, "max" : 226, "creature": "pegasus" }, + { "min" : 227, "max" : 229, "creature": "silverPegasus" }, + { "min" : 230, "max" : 232, "creature": "basilisk" }, + { "min" : 233, "max" : 235, "creature": "vampire" }, + { "min" : 236, "max" : 238, "creature": "mage" }, + { "min" : 239, "max" : 241, "creature": "medusaQueen" }, + { "min" : 242, "max" : 244, "creature": "crusader" }, + { "min" : 245, "max" : 247, "creature": "goldGolem" }, + { "min" : 248, "max" : 250, "creature": "ogreMage" }, + { "min" : 251, "max" : 253, "creature": "archMage" }, + { "min" : 254, "max" : 256, "creature": "greaterBasilisk" }, + { "min" : 257, "max" : 259, "creature": "zealot" }, + { "min" : 260, "max" : 262, "creature": "pitFiend" }, + { "min" : 263, "max" : 265, "creature": "diamondGolem" }, + { "min" : 266, "max" : 268, "creature": "vampireLord" }, + { "min" : 269, "max" : 271, "creature": "dendroidSoldier" }, + { "min" : 272, "max" : 274, "creature": "minotaur" }, + { "min" : 275, "max" : 277, "creature": "lich" }, + { "min" : 278, "max" : 280, "creature": "genie" }, + { "min" : 281, "max" : 283, "creature": "gorgon" }, + { "min" : 284, "max" : 286, "creature": "masterGenie" }, + { "min" : 287, "max" : 289, "creature": "roc" }, + { "min" : 290, "max" : 292, "creature": "mightyGorgon" }, + { "min" : 293, "max" : 295, "creature": "minotaurKing" }, + { "min" : 296, "max" : 298, "creature": "powerLich" }, + { "min" : 299, "max" : 301, "creature": "thunderbird" }, + { "min" : 302, "max" : 304, "creature": "pitLord" }, + { "min" : 305, "max" : 307, "creature": "cyclop" }, + { "min" : 308, "max" : 310, "creature": "wyvern" }, + { "min" : 311, "max" : 313, "creature": "cyclopKing" }, + { "min" : 314, "max" : 316, "creature": "wyvernMonarch" }, + { "min" : 317, "max" : 319, "creature": "manticore" }, + { "min" : 320, "max" : 322, "creature": "scorpicore" }, + { "min" : 323, "max" : 325, "creature": "efreet" }, + { "min" : 326, "max" : 328, "creature": "unicorn" }, + { "min" : 329, "max" : 331, "creature": "efreetSultan" }, + { "min" : 332, "max" : 334, "creature": "cavalier" }, + { "min" : 335, "max" : 337, "creature": "naga" }, + { "min" : 338, "max" : 340, "creature": "warUnicorn" }, + { "min" : 341, "max" : 343, "creature": "blackKnight" }, + { "min" : 344, "max" : 346, "creature": "champion" }, + { "min" : 347, "max" : 349, "creature": "dreadKnight" }, + { "min" : 350, "max" : 352, "creature": "nagaQueen" }, + { "min" : 353, "max" : 355, "creature": "behemoth" }, + { "min" : 356, "max" : 358, "creature": "boneDragon" }, + { "min" : 359, "max" : 361, "creature": "giant" }, + { "min" : 362, "max" : 364, "creature": "hydra" }, + { "min" : 365, "max" : 367, "creature": "ghostDragon" }, + { "min" : 368, "max" : 370, "creature": "redDragon" }, + { "min" : 371, "max" : 373, "creature": "greenDragon" }, + { "min" : 374, "max" : 376, "creature": "angel" }, + { "min" : 377, "max" : 379, "creature": "devil" }, + { "min" : 380, "max" : 382, "creature": "chaosHydra" }, + { "min" : 383, "max" : 385, "creature": "ancientBehemoth" }, + { "min" : 386, "max" : 388, "creature": "archDevil" }, + { "min" : 389, "max" : 391, "creature": "titan" }, + { "min" : 392, "max" : 394, "creature": "goldDragon" }, + { "min" : 395, "max" : 397, "creature": "blackDragon" }, + { "min" : 398, "max" : 500, "creature": "archangel" } + ] +} \ No newline at end of file diff --git a/lib/CPlayerState.cpp b/lib/CPlayerState.cpp index b521b5d13..b59de08fa 100644 --- a/lib/CPlayerState.cpp +++ b/lib/CPlayerState.cpp @@ -17,7 +17,7 @@ VCMI_LIB_NAMESPACE_BEGIN PlayerState::PlayerState() - : color(-1), human(false), enteredWinningCheatCode(false), + : color(-1), human(false), cheated(false), enteredWinningCheatCode(false), enteredLosingCheatCode(false), status(EPlayerStatus::INGAME) { setNodeType(PLAYER); @@ -29,6 +29,7 @@ PlayerState::PlayerState(PlayerState && other) noexcept: human(other.human), team(other.team), resources(other.resources), + cheated(other.cheated), enteredWinningCheatCode(other.enteredWinningCheatCode), enteredLosingCheatCode(other.enteredLosingCheatCode), status(other.status), diff --git a/lib/CPlayerState.h b/lib/CPlayerState.h index b870359df..66c70619a 100644 --- a/lib/CPlayerState.h +++ b/lib/CPlayerState.h @@ -39,6 +39,7 @@ public: std::vector quests; //store info about all received quests std::vector battleBonuses; //additional bonuses to be added during battle with neutrals + bool cheated; bool enteredWinningCheatCode, enteredLosingCheatCode; //if true, this player has entered cheat codes for loss / victory EPlayerStatus status; std::optional daysWithoutCastle; @@ -83,6 +84,7 @@ public: h & visitedObjects; h & status; h & daysWithoutCastle; + h & cheated; h & battleBonuses; h & enteredLosingCheatCode; h & enteredWinningCheatCode; diff --git a/lib/NetPacksLib.cpp b/lib/NetPacksLib.cpp index c870b0093..dad9065c6 100644 --- a/lib/NetPacksLib.cpp +++ b/lib/NetPacksLib.cpp @@ -2514,6 +2514,7 @@ void PlayerCheated::applyGs(CGameState * gs) const gs->getPlayerState(player)->enteredLosingCheatCode = losingCheatCode; gs->getPlayerState(player)->enteredWinningCheatCode = winningCheatCode; + gs->getPlayerState(player)->cheated = true; } void PlayerStartsTurn::applyGs(CGameState * gs) const diff --git a/lib/constants/EntityIdentifiers.h b/lib/constants/EntityIdentifiers.h index 80fbd48d9..8e03b635c 100644 --- a/lib/constants/EntityIdentifiers.h +++ b/lib/constants/EntityIdentifiers.h @@ -204,8 +204,8 @@ class HeroTypeID : public Identifier public: using Identifier::Identifier; ///json serialization helpers - static si32 decode(const std::string & identifier); - static std::string encode(const si32 index); + DLL_LINKAGE static si32 decode(const std::string & identifier); + DLL_LINKAGE static std::string encode(const si32 index); static std::string entityType(); DLL_LINKAGE static const HeroTypeID NONE; @@ -603,8 +603,8 @@ public: static_assert (AFTER_LAST == BACKPACK_START, "incorrect number of artifact slots"); - static si32 decode(const std::string & identifier); - static std::string encode(const si32 index); + DLL_LINKAGE static si32 decode(const std::string & identifier); + DLL_LINKAGE static std::string encode(const si32 index); }; class ArtifactPosition : public IdentifierWithEnum @@ -643,8 +643,8 @@ public: using IdentifierWithEnum::IdentifierWithEnum; ///json serialization helpers - static si32 decode(const std::string & identifier); - static std::string encode(const si32 index); + DLL_LINKAGE static si32 decode(const std::string & identifier); + DLL_LINKAGE static std::string encode(const si32 index); static std::string entityType(); }; @@ -683,8 +683,8 @@ public: using IdentifierWithEnum::IdentifierWithEnum; ///json serialization helpers - static si32 decode(const std::string & identifier); - static std::string encode(const si32 index); + DLL_LINKAGE static si32 decode(const std::string & identifier); + DLL_LINKAGE static std::string encode(const si32 index); static std::string entityType(); }; @@ -801,8 +801,8 @@ public: using IdentifierWithEnum::IdentifierWithEnum; ///json serialization helpers - static si32 decode(const std::string & identifier); - static std::string encode(const si32 index); + DLL_LINKAGE static si32 decode(const std::string & identifier); + DLL_LINKAGE static std::string encode(const si32 index); static std::string entityType(); }; @@ -855,8 +855,8 @@ class TerrainId : public IdentifierWithEnum public: using IdentifierWithEnum::IdentifierWithEnum; - static si32 decode(const std::string & identifier); - static std::string encode(const si32 index); + DLL_LINKAGE static si32 decode(const std::string & identifier); + DLL_LINKAGE static std::string encode(const si32 index); static std::string entityType(); }; diff --git a/server/processors/PlayerMessageProcessor.cpp b/server/processors/PlayerMessageProcessor.cpp index 6f5358c9c..7cadfda29 100644 --- a/server/processors/PlayerMessageProcessor.cpp +++ b/server/processors/PlayerMessageProcessor.cpp @@ -109,11 +109,18 @@ bool PlayerMessageProcessor::handleHostCommand(PlayerColor player, const std::st } if(words.size() == 2 && words[1] == "cheaters") { - if (cheaters.empty()) - broadcastSystemMessage("No cheaters registered!"); + int playersCheated = 0; + for (const auto & player : gameHandler->gameState()->players) + { + if(player.second.cheated) + { + broadcastSystemMessage("Player " + player.first.toString() + " is cheater!"); + playersCheated++; + } + } - for (auto const & entry : cheaters) - broadcastSystemMessage("Player " + entry.toString() + " is cheater!"); + if (!playersCheated) + broadcastSystemMessage("No cheaters registered!"); return true; } @@ -411,7 +418,10 @@ bool PlayerMessageProcessor::handleCheatCode(const std::string & cheat, PlayerCo std::vector parameters = words; - cheaters.insert(i.first); + PlayerCheated pc; + pc.player = i.first; + gameHandler->sendAndApply(&pc); + playerTargetedCheat = true; parameters.erase(parameters.begin()); @@ -427,10 +437,13 @@ bool PlayerMessageProcessor::handleCheatCode(const std::string & cheat, PlayerCo executeCheatCode(cheatName, i.first, h->id, parameters); } + PlayerCheated pc; + pc.player = player; + gameHandler->sendAndApply(&pc); + if (!playerTargetedCheat) executeCheatCode(cheatName, player, currObj, words); - - cheaters.insert(player); + return true; } diff --git a/server/processors/PlayerMessageProcessor.h b/server/processors/PlayerMessageProcessor.h index d8f9e9878..47d2a8a4a 100644 --- a/server/processors/PlayerMessageProcessor.h +++ b/server/processors/PlayerMessageProcessor.h @@ -21,8 +21,6 @@ class CGameHandler; class PlayerMessageProcessor { - std::set cheaters; - void executeCheatCode(const std::string & cheatName, PlayerColor player, ObjectInstanceID currObj, const std::vector & arguments ); bool handleCheatCode(const std::string & cheatFullCommand, PlayerColor player, ObjectInstanceID currObj); bool handleHostCommand(PlayerColor player, const std::string & message); @@ -60,6 +58,5 @@ public: template void serialize(Handler &h, const int version) { - h & cheaters; } };