Parcourir la source

Merge pull request #2892 from Laserlicht/highscore_menu

Highscore Menu & end video
Ivan Savenko il y a 2 ans
Parent
commit
71a1ed816a

+ 0 - 1
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í...",

+ 0 - 1
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...",

+ 0 - 1
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...",

+ 0 - 1
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...",

+ 0 - 1
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...",

+ 0 - 1
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" : "Завершення...",

+ 2 - 0
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

+ 30 - 2
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<CHighScoreInputScreen>(won, highScoreCalc);
 			}
 		);
 	}

+ 18 - 3
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<CampaignState> cs)
+void CServerHandler::startCampaignScenario(HighScoreParameter param, std::shared_ptr<CampaignState> cs)
 {
 	std::shared_ptr<CampaignState> ourCampaign = cs;
 
 	if (!cs)
 		ourCampaign = si->campState;
 
-	GH.dispatchMainThread([ourCampaign]()
+	if(highScoreCalc == nullptr)
+	{
+		highScoreCalc = std::make_shared<HighScoreCalculation>();
+		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<CampaignState> cs)
 			if(!ourCampaign->isCampaignFinished())
 				CMM->openCampaignLobby(ourCampaign);
 			else
+			{
 				CMM->openCampaignScreen(ourCampaign->campaignSet);
+				GH.windows().createAndPushWindow<CHighScoreInputScreen>(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;

+ 6 - 1
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<std::string> myNames;
 
+	std::shared_ptr<HighScoreCalculation> 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<CampaignState> cs = {});
+	void startCampaignScenario(HighScoreParameter param, std::shared_ptr<CampaignState> 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

+ 3 - 1
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<void()> 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();

+ 2 - 2
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<void()> 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<void()> 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;

+ 2 - 0
client/VCMI_client.cbp

@@ -198,6 +198,8 @@
 		<Unit filename="mainmenu/CPrologEpilogVideo.h" />
 		<Unit filename="mainmenu/CreditsScreen.cpp" />
 		<Unit filename="mainmenu/CreditsScreen.h" />
+		<Unit filename="mainmenu/CHighScoreScreen.cpp" />
+		<Unit filename="mainmenu/CHighScoreScreen.h" />
 		<Unit filename="mapHandler.cpp" />
 		<Unit filename="mapHandler.h" />
 		<Unit filename="resource.h" />

+ 2 - 0
client/VCMI_client.vcxproj

@@ -217,6 +217,7 @@
     <ClCompile Include="mainmenu\CMainMenu.cpp" />
     <ClCompile Include="mainmenu\CPrologEpilogVideo.cpp" />
     <ClCompile Include="mainmenu\CreditsScreen.cpp" />
+    <ClCompile Include="mainmenu\CHighScoreScreen.cpp" />
     <ClCompile Include="mapHandler.cpp" />
     <ClCompile Include="NetPacksClient.cpp" />
     <ClCompile Include="NetPacksLobbyClient.cpp" />
@@ -286,6 +287,7 @@
     <ClInclude Include="mainmenu\CMainMenu.h" />
     <ClInclude Include="mainmenu\CPrologEpilogVideo.h" />
     <ClInclude Include="mainmenu\CreditsScreen.h" />
+    <ClInclude Include="mainmenu\CHighScoreScreen.h" />
     <ClInclude Include="mapHandler.h" />
     <ClInclude Include="resource.h" />
     <ClInclude Include="SDLRWwrapper.h" />

+ 2 - 0
client/VCMI_client.vcxproj.filters

@@ -132,6 +132,7 @@
     <ClCompile Include="mainmenu\CMainMenu.cpp" />
     <ClCompile Include="mainmenu\CPrologEpilogVideo.cpp" />
     <ClCompile Include="mainmenu\CreditsScreen.cpp" />
+    <ClCompile Include="mainmenu\CHighScoreScreen.cpp" />
   </ItemGroup>
   <ItemGroup>
     <ResourceCompile Include="VCMI_client.rc" />
@@ -290,5 +291,6 @@
     <ClInclude Include="mainmenu\CMainMenu.h" />
     <ClInclude Include="mainmenu\CPrologEpilogVideo.h" />
     <ClInclude Include="mainmenu\CreditsScreen.h" />
+    <ClInclude Include="mainmenu\CHighScoreScreen.h" />
   </ItemGroup>
 </Project>

+ 383 - 0
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<double, 5> 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<int>(tmp), static_cast<int>(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<CButton>(Point(31, 113), AnimationPath::builtin("HISCCAM.DEF"), CButton::tooltip(), [&](){ buttonCampaignClick(); }));
+	buttons.push_back(std::make_shared<CButton>(Point(31, 345), AnimationPath::builtin("HISCSTA.DEF"), CButton::tooltip(), [&](){ buttonScenarioClick(); }));
+	buttons.push_back(std::make_shared<CButton>(Point(726, 113), AnimationPath::builtin("HISCRES.DEF"), CButton::tooltip(), [&](){ buttonResetClick(); }));
+	buttons.push_back(std::make_shared<CButton>(Point(726, 345), AnimationPath::builtin("HISCEXT.DEF"), CButton::tooltip(), [&](){ buttonExitClick(); }));
+}
+
+void CHighScoreScreen::addHighScores()
+{
+	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
+
+	background = std::make_shared<CPicture>(ImagePath::builtin(highscorepage == HighScorePage::SCENARIO ? "HISCORE" : "HISCORE2"));
+
+	texts.clear();
+	images.clear();
+
+	// Header
+	texts.push_back(std::make_shared<CLabel>(115, 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.433"))); // rank
+	texts.push_back(std::make_shared<CLabel>(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<CLabel>(405, 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.435"))); // land
+		texts.push_back(std::make_shared<CLabel>(557, 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.436"))); // days
+		texts.push_back(std::make_shared<CLabel>(627, 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.75"))); // score
+	}
+	else
+	{
+		texts.push_back(std::make_shared<CLabel>(405, 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.672"))); // campaign
+		texts.push_back(std::make_shared<CLabel>(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<CLabel>(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<CLabel>(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<CLabel>(405, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, color, tmp));
+			texts.push_back(std::make_shared<CLabel>(557, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, color, std::to_string(curData["days"].Integer())));
+			texts.push_back(std::make_shared<CLabel>(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<CLabel>(405, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, color, tmp));
+			texts.push_back(std::make_shared<CLabel>(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<CAnimImage>(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<TransparentFilledRectangle>(Rect(0, 0, pos.w, pos.h), Colors::BLACK);
+
+	if(won)
+	{
+		int border = 100;
+		int textareaW = ((pos.w - 2 * border) / 4);
+		std::vector<std::string> 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<CMultiLineLabel>(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<CMultiLineLabel>(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<JsonNode> 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<CHighScoreInput>(calc.parameters[0].playerName,
+		[&] (std::string text) 
+		{
+			if(!text.empty())
+			{
+				int pos = addEntry(text);
+				close();
+				GH.windows().createAndPushWindow<CHighScoreScreen>(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<void(std::string text)> 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<CMultiLineLabel>(Rect(15, 15, 202, 202), FONT_SMALL, ETextAlignment::TOPCENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.96"));
+
+	buttonOk = std::make_shared<CButton>(Point(26, 142), AnimationPath::builtin("MUBCHCK.DEF"), CGI->generaltexth->zelp[560], std::bind(&CHighScoreInput::okay, this), EShortcut::GLOBAL_ACCEPT);
+	buttonCancel = std::make_shared<CButton>(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<CPicture>(background->getSurface(), Rect(7, 186, 218, 18), 7, 186));
+	textInput = std::make_shared<CTextInput>(Rect(18, 104, 200, 25), FONT_SMALL, 0);
+	textInput->setText(playerName);
+}
+
+void CHighScoreInput::okay()
+{
+	ready(textInput->getText());
+}
+
+void CHighScoreInput::abort()
+{
+	ready("");
+}

+ 110 - 0
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<HighScoreParameter> parameters = std::vector<HighScoreParameter>();
+	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<CPicture> background;
+	std::vector<std::shared_ptr<CButton>> buttons;
+	std::vector<std::shared_ptr<CLabel>> texts;
+	std::vector<std::shared_ptr<CAnimImage>> images;
+
+	const int screenRows = 11;
+
+	int highlighted;
+public:
+	CHighScoreScreen(HighScorePage highscorepage, int highlighted = -1);
+};
+
+class CHighScoreInput : public CWindowObject
+{
+	std::shared_ptr<CMultiLineLabel> text;
+	std::shared_ptr<CButton> buttonOk;
+	std::shared_ptr<CButton> buttonCancel;
+	std::shared_ptr<CGStatusBar> statusBar;
+	std::shared_ptr<CTextInput> textInput;
+
+	std::function<void(std::string text)> ready;
+	
+	void okay();
+	void abort();
+public:
+	CHighScoreInput(std::string playerName, std::function<void(std::string text)> readyCB);
+};
+
+class CHighScoreInputScreen : public CWindowObject
+{
+	std::vector<std::shared_ptr<CMultiLineLabel>> texts;
+	std::shared_ptr<CHighScoreInput> input;
+	std::shared_ptr<TransparentFilledRectangle> 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;
+};

+ 8 - 1
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<void()> genCommand(CMenuScreen * menu, std::vector<std::str
 			break;
 			case 5: //highscores
 			{
-				return std::bind(CInfoWindow::showInfoDialog, CGI->generaltexth->translate("vcmi.mainMenu.highscoresNotImplemented"), std::vector<std::shared_ptr<CComponent>>(), PlayerColor(1));
+				return std::bind(CMainMenu::openHighScoreScreen);
 			}
 			}
 		}
@@ -395,6 +396,12 @@ void CMainMenu::startTutorial()
 	CSH->startMapAfterConnection(mapInfo);
 }
 
+void CMainMenu::openHighScoreScreen()
+{
+	GH.windows().createAndPushWindow<CHighScoreScreen>(CHighScoreScreen::HighScorePage::SCENARIO);
+	return;
+}
+
 std::shared_ptr<CMainMenu> CMainMenu::create()
 {
 	if(!CMM)

+ 1 - 0
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<CampaignState> campaign);
 	static void startTutorial();
+	static void openHighScoreScreen();
 	void openCampaignScreen(std::string name);
 
 	static std::shared_ptr<CMainMenu> create();

+ 122 - 0
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" }
+    ]
+}

+ 2 - 1
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),

+ 2 - 0
lib/CPlayerState.h

@@ -39,6 +39,7 @@ public:
 	std::vector<QuestInfo> quests; //store info about all received quests
 	std::vector<Bonus> 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<ui8> daysWithoutCastle;
@@ -83,6 +84,7 @@ public:
 		h & visitedObjects;
 		h & status;
 		h & daysWithoutCastle;
+		h & cheated;
 		h & battleBonuses;
 		h & enteredLosingCheatCode;
 		h & enteredWinningCheatCode;

+ 1 - 0
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

+ 12 - 12
lib/constants/EntityIdentifiers.h

@@ -204,8 +204,8 @@ class HeroTypeID : public Identifier<HeroTypeID>
 public:
 	using Identifier<HeroTypeID>::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<ArtifactPosition, ArtifactPositionBase>
@@ -643,8 +643,8 @@ public:
 	using IdentifierWithEnum<ArtifactID, ArtifactIDBase>::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<CreatureID, CreatureIDBase>::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<SpellID, SpellIDBase>::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<TerrainId, TerrainIdBase>
 public:
 	using IdentifierWithEnum<TerrainId, TerrainIdBase>::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();
 };
 

+ 20 - 7
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<std::string> 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;
 }
 

+ 0 - 3
server/processors/PlayerMessageProcessor.h

@@ -21,8 +21,6 @@ class CGameHandler;
 
 class PlayerMessageProcessor
 {
-	std::set<PlayerColor> cheaters;
-
 	void executeCheatCode(const std::string & cheatName, PlayerColor player, ObjectInstanceID currObj, const std::vector<std::string> & 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 <typename Handler> void serialize(Handler &h, const int version)
 	{
-		h & cheaters;
 	}
 };