浏览代码

Merge branch 'develop' into feature/nullkiller2

Mircea TheHonestCTO 1 月之前
父节点
当前提交
00c93c7b6a
共有 55 个文件被更改,包括 478 次插入153 次删除
  1. 1 1
      .github/workflows/aab-from-build.yml
  2. 10 10
      .github/workflows/github.yml
  3. 19 3
      CI/wininstaller/installer.iss
  4. 22 22
      CMakePresets.json
  5. 2 0
      Mods/vcmi/Content/config/english.json
  6. 2 0
      Mods/vcmi/Content/config/german.json
  7. 2 2
      client/ClientCommandManager.cpp
  8. 1 0
      client/ClientNetPackVisitors.h
  9. 13 0
      client/NetPacksClient.cpp
  10. 1 28
      client/lobby/OptionsTab.cpp
  11. 2 5
      client/lobby/OptionsTab.h
  12. 21 4
      client/lobby/RandomMapTab.cpp
  13. 2 0
      client/lobby/RandomMapTab.h
  14. 1 0
      client/mapView/IMapRendererContext.h
  15. 20 2
      client/mapView/MapRenderer.cpp
  16. 5 1
      client/mapView/MapRenderer.h
  17. 10 0
      client/mapView/MapRendererContext.cpp
  18. 3 0
      client/mapView/MapRendererContext.h
  19. 1 0
      client/mapView/MapViewController.cpp
  20. 1 0
      client/netlag/PackRollbackGeneratorVisitor.h
  21. 80 1
      client/widgets/CTextInput.cpp
  22. 31 10
      client/widgets/CTextInput.h
  23. 20 10
      client/widgets/TextControls.cpp
  24. 2 0
      client/widgets/TextControls.h
  25. 10 1
      client/windows/CCastleInterface.cpp
  26. 2 1
      client/windows/CCastleInterface.h
  27. 1 1
      client/windows/CWindowObject.cpp
  28. 16 8
      client/windows/GUIClasses.cpp
  29. 1 1
      client/windows/GUIClasses.h
  30. 12 1
      docs/modders/Entities_Format/Hero_Type_Format.md
  31. 11 8
      docs/players/Cheat_Codes.md
  32. 6 0
      lib/callback/CCallback.cpp
  33. 1 0
      lib/callback/CCallback.h
  34. 1 0
      lib/callback/IGameActionCallback.h
  35. 5 0
      lib/gameState/GameStatePackVisitor.cpp
  36. 1 0
      lib/gameState/GameStatePackVisitor.h
  37. 6 1
      lib/mapObjects/CGTownInstance.cpp
  38. 4 0
      lib/mapObjects/CGTownInstance.h
  39. 1 1
      lib/mapObjects/ObjectTemplate.cpp
  40. 1 1
      lib/mapping/CMapOperation.cpp
  41. 6 0
      lib/mapping/MapFormatH3M.cpp
  42. 2 0
      lib/networkPacks/NetPackVisitor.h
  43. 10 0
      lib/networkPacks/NetPacksLib.cpp
  44. 14 0
      lib/networkPacks/PacksForClient.h
  45. 22 0
      lib/networkPacks/PacksForServer.h
  46. 2 1
      lib/serializer/ESerializationVersion.h
  47. 2 0
      lib/serializer/RegisterTypes.h
  48. 2 2
      lib/spells/adventure/TownPortalEffect.cpp
  49. 25 2
      server/CGameHandler.cpp
  50. 1 0
      server/CGameHandler.h
  51. 7 0
      server/NetPacksServer.cpp
  52. 1 0
      server/ServerNetPackVisitors.h
  53. 4 0
      test/bonus/BonusSystemTest.cpp
  54. 6 4
      test/events/ApplyDamageTest.cpp
  55. 23 21
      test/events/EventBusTest.cpp

+ 1 - 1
.github/workflows/aab-from-build.yml

@@ -42,7 +42,7 @@ jobs:
         echo "ANDROID_AAB_PATH=$ANDROID_AAB_PATH" >> $GITHUB_ENV
 
     - name: Artifact
-      uses: actions/upload-artifact@v4
+      uses: actions/upload-artifact@v5
       with:
         name: aab
         compression-level: 0

+ 10 - 10
.github/workflows/github.yml

@@ -332,7 +332,7 @@ jobs:
 
     - name: Upload Artifact
       id: upload_artifact
-      uses: actions/upload-artifact@v4
+      uses: actions/upload-artifact@v5
       with:
         name: ${{ env.VCMI_PACKAGE_FILE_NAME }} - ${{ matrix.platform }}
         compression-level: 9
@@ -342,7 +342,7 @@ jobs:
     - name: Upload AAB Artifact
       id: upload_aab
       if: ${{ startsWith(matrix.platform, 'android') && github.ref == 'refs/heads/master' }}
-      uses: actions/upload-artifact@v4
+      uses: actions/upload-artifact@v5
       with:
         name: ${{ env.VCMI_PACKAGE_FILE_NAME }} - ${{ matrix.platform }} - aab
         compression-level: 9
@@ -352,7 +352,7 @@ jobs:
     - name: Upload debug symbols
       id: upload_symbols
       if: ${{ startsWith(matrix.platform, 'msvc') }}
-      uses: actions/upload-artifact@v4
+      uses: actions/upload-artifact@v5
       with:
         name: ${{ env.VCMI_PACKAGE_FILE_NAME }} - ${{ matrix.platform }} - symbols
         compression-level: 9
@@ -381,7 +381,7 @@ jobs:
         python3 CI/emit_partial.py
 
     - name: Upload partial JSON with build informations
-      uses: actions/upload-artifact@v4
+      uses: actions/upload-artifact@v5
       with:
         name: partial-json-${{ matrix.platform }}
         path: .summary/${{ matrix.platform }}.json
@@ -412,7 +412,7 @@ jobs:
 
         - name: Upload source code archive
           id: upload_source
-          uses: actions/upload-artifact@v4
+          uses: actions/upload-artifact@v5
           with:
             name: ${{ env.VCMI_PACKAGE_FILE_NAME }}
             compression-level: 9
@@ -428,7 +428,7 @@ jobs:
             JSON
 
         - name: Upload partial JSON with source informations
-          uses: actions/upload-artifact@v4
+          uses: actions/upload-artifact@v5
           with:
             name: partial-json-source
             path: .summary/source.json
@@ -615,7 +615,7 @@ jobs:
         PULL_REQUEST: ${{ github.event.pull_request.number }}
 
     - name: Download Artifact
-      uses: actions/download-artifact@v5
+      uses: actions/download-artifact@v6
       with:
         name: ${{ env.VCMI_PACKAGE_FILE_NAME }} - ${{ matrix.platform }}
         path: ${{github.workspace}}/artifact
@@ -645,7 +645,7 @@ jobs:
 
     - name: Upload VCMI Installer Artifacts
       id: upload_installer
-      uses: actions/upload-artifact@v4
+      uses: actions/upload-artifact@v5
       with:
         name: ${{ env.VCMI_PACKAGE_FILE_NAME }} - ${{ matrix.platform }} - installer
         compression-level: 9
@@ -673,7 +673,7 @@ jobs:
         JSON
 
     - name: Upload partial JSON with installer informations
-      uses: actions/upload-artifact@v4
+      uses: actions/upload-artifact@v5
       with:
         name: partial-json-${{ matrix.platform }}-installer
         path: .summary/installer-${{ matrix.platform }}.json
@@ -720,7 +720,7 @@ jobs:
 
       - name: Download all partial JSON artifacts
         continue-on-error: true
-        uses: actions/download-artifact@v5
+        uses: actions/download-artifact@v6
         with:
           pattern: partial-json-*
           merge-multiple: true

+ 19 - 3
CI/wininstaller/installer.iss

@@ -166,12 +166,12 @@ Source: "{#UCRTFilesPath}\{#InstallerArch}\*"; DestDir: "{app}"; Flags: ignoreve
 
 
 [Icons]
-Name: "{group}\{cm:ShortcutLauncher}"; Filename: "{app}\VCMI_launcher.exe"; Comment: "{cm:ShortcutLauncherComment}";  Tasks: startmenu
-Name: "{group}\{cm:ShortcutMapEditor}"; Filename: "{app}\VCMI_mapeditor.exe"; Comment: "{cm:ShortcutMapEditorComment}";  Tasks: startmenu
+Name: "{group}\{cm:ShortcutLauncher}{code:GetBranchSuffix}"; Filename: "{app}\VCMI_launcher.exe"; Comment: "{cm:ShortcutLauncherComment}{code:GetBranchSuffix}";  Tasks: startmenu
+Name: "{group}\{cm:ShortcutMapEditor}{code:GetBranchSuffix}"; Filename: "{app}\VCMI_mapeditor.exe"; Comment: "{cm:ShortcutMapEditorComment}{code:GetBranchSuffix}";  Tasks: startmenu
 Name: "{group}\{cm:ShortcutWebPage}"; Filename: "{#VCMIHome}"; Comment: "{cm:ShortcutWebPageComment}";  Tasks: startmenu
 Name: "{group}\{cm:ShortcutDiscord}"; Filename: "{#VCMIContact}"; Comment: "{cm:ShortcutDiscordComment}";  Tasks: startmenu
 
-Name: "{code:GetUserDesktopFolder}\{cm:ShortcutLauncher}"; Filename: "{app}\VCMI_launcher.exe"; Comment: "{cm:ShortcutLauncherComment}"; Tasks: desktop
+Name: "{code:GetUserDesktopFolder}\{cm:ShortcutLauncher}{code:GetBranchSuffix}"; Filename: "{app}\VCMI_launcher.exe"; Comment: "{cm:ShortcutLauncherComment}{code:GetBranchSuffix}"; Tasks: desktop
 
 
 [Tasks]
@@ -371,6 +371,22 @@ begin
 end;
 
 
+function GetBranchSuffix(Param: string): string;
+var
+  Branch: string;
+begin
+  Branch := UpperCase(ExpandConstant('{#VCMIFolder}'));
+
+  if Pos('(BRANCH BETA)', Branch) > 0 then
+    Result := ' (Beta)'
+  else
+  if Pos('(BRANCH DEVELOP)', Branch) > 0 then
+    Result := ' (Develop)'
+  else
+    Result := '';
+end;
+
+
 function GetCommonProgramFilesDir: String;
 begin
   if IsARM64 then

+ 22 - 22
CMakePresets.json

@@ -462,19 +462,19 @@
             "inherits": "default-release"
         },
         {
-        	"name": "windows-msvc-ninja-release",
-        	"configurePreset": "windows-msvc-ninja-release",
-        	"inherits": "default-release"
+            "name": "windows-msvc-ninja-release",
+            "configurePreset": "windows-msvc-ninja-release",
+            "inherits": "default-release"
         },
         {
-        	"name": "windows-msvc-ninja-release-x86",
-        	"configurePreset": "windows-msvc-ninja-release-x86",
-        	"inherits": "default-release"
+            "name": "windows-msvc-ninja-release-x86",
+            "configurePreset": "windows-msvc-ninja-release-x86",
+            "inherits": "default-release"
         },
         {
-        	"name": "windows-msvc-ninja-release-arm64",
-        	"configurePreset": "windows-msvc-ninja-release-arm64",
-        	"inherits": "default-release"
+            "name": "windows-msvc-ninja-release-arm64",
+            "configurePreset": "windows-msvc-ninja-release-arm64",
+            "inherits": "default-release"
         },
         {
             "name": "ios-release-conan",
@@ -518,34 +518,34 @@
             }
         },
         {
-          "name": "linux-clang-release",
-          "configurePreset": "linux-clang-release",
-          "inherits": "default-release"
+            "name": "linux-clang-debug",
+            "configurePreset": "linux-clang-debug",
+            "inherits": "default-release"
         },
         {
-            "name": "linux-gcc-release",
-            "configurePreset": "linux-gcc-release",
+            "name": "linux-clang-release",
+            "configurePreset": "linux-clang-release",
             "inherits": "default-release"
         },
         {
-            "name": "linux-clang-debug",
+            "name": "linux-clang-test",
             "configurePreset": "linux-clang-test",
             "inherits": "default-release"
         },
         {
             "name": "linux-gcc-debug",
-            "configurePreset": "linux-gcc-test",
+            "configurePreset": "linux-gcc-debug",
             "inherits": "default-release"
         },
         {
-          "name": "linux-gcc-test",
-          "configurePreset": "linux-gcc-test",
-          "inherits": "default-release"
+            "name": "linux-gcc-release",
+            "configurePreset": "linux-gcc-release",
+            "inherits": "default-release"
         },
         {
-          "name": "linux-clang-test",
-          "configurePreset": "linux-clang-test",
-          "inherits": "default-release"
+            "name": "linux-gcc-test",
+            "configurePreset": "linux-gcc-test",
+            "inherits": "default-release"
         },
         {
             "name": "macos-xcode-release",

+ 2 - 0
Mods/vcmi/Content/config/english.json

@@ -138,6 +138,8 @@
 	"vcmi.lobby.deleteFile" : "Do you want to delete following file?",
 	"vcmi.lobby.deleteFolder" : "Do you want to delete following folder?",
 	"vcmi.lobby.deleteMode" : "Switch to delete mode and back",
+	"vcmi.lobby.templatesSelect.hover" : "Templates",
+	"vcmi.lobby.templatesSelect.help" : "Search and select template",
 
 	"vcmi.broadcast.failedLoadGame" : "Failed to load game",
 	"vcmi.broadcast.command" : "Use '!help' to list available commands",

+ 2 - 0
Mods/vcmi/Content/config/german.json

@@ -138,6 +138,8 @@
 	"vcmi.lobby.deleteFile" : "Möchtet Ihr folgende Datei löschen?",
 	"vcmi.lobby.deleteFolder" : "Möchtet Ihr folgenden Ordner löschen?",
 	"vcmi.lobby.deleteMode" : "In den Löschmodus wechseln und zurück",
+	"vcmi.lobby.templatesSelect.hover" : "Templates",
+	"vcmi.lobby.templatesSelect.help" : "Suche und wähle Template aus",
 
 	"vcmi.broadcast.failedLoadGame" : "Spiel konnte nicht geladen werden",
 	"vcmi.broadcast.command" : "Benutze '!help' um alle verfügbaren Befehle aufzulisten",

+ 2 - 2
client/ClientCommandManager.cpp

@@ -239,7 +239,7 @@ void ClientCommandManager::handleTranslateMapsCommand()
 		try
 		{
 			// load and drop loaded map - we only need loader to run over all maps
-			loadedMaps.push_back(mapService.loadMap(mapName, nullptr));
+			loadedMaps.push_back(mapService.loadMap(mapName, GAME->interface()->cb.get()));
 		}
 		catch(std::exception & e)
 		{
@@ -260,7 +260,7 @@ void ClientCommandManager::handleTranslateMapsCommand()
 		{
 			loadedCampaigns.push_back(CampaignHandler::getCampaign(campaignName.getName()));
 			for (auto const & part : loadedCampaigns.back()->allScenarios())
-				loadedCampaigns.back()->getMap(part, nullptr);
+				loadedCampaigns.back()->getMap(part, GAME->interface()->cb.get());
 		}
 		catch(std::exception & e)
 		{

+ 1 - 0
client/ClientNetPackVisitors.h

@@ -104,6 +104,7 @@ public:
 	void visitSetAvailableArtifacts(SetAvailableArtifacts & pack) override;
 	void visitEntitiesChanged(EntitiesChanged & pack) override;
 	void visitPlayerCheated(PlayerCheated & pack) override;
+	void visitChangeTownName(ChangeTownName & pack) override;
 };
 
 class ApplyFirstClientNetPackVisitor : public VCMI_LIB_WRAP_NAMESPACE(ICPackVisitor)

+ 13 - 0
client/NetPacksClient.cpp

@@ -1074,3 +1074,16 @@ void ApplyClientNetPackVisitor::visitPlayerCheated(PlayerCheated & pack)
 	if(pack.colorScheme != ColorScheme::KEEP && vstd::contains(cl.playerint, pack.player))
 		cl.playerint[pack.player]->setColorScheme(pack.colorScheme);
 }
+
+void ApplyClientNetPackVisitor::visitChangeTownName(ChangeTownName & pack)
+{
+	if(!adventureInt)
+		return;
+
+	const CGTownInstance *town = gs.getTown(pack.tid);
+	if(town)
+	{
+		adventureInt->onTownChanged(town);
+		ENGINE->windows().totalRedraw();
+	}
+}

+ 1 - 28
client/lobby/OptionsTab.cpp

@@ -1035,10 +1035,7 @@ OptionsTab::PlayerOptionsEntry::PlayerOptionsEntry(const PlayerSettings & S, con
 	if(s->isControlledByAI() || GAME->server().isGuest())
 		labelPlayerName = std::make_shared<CLabel>(55, 10, EFonts::FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, name, 95);
 	else
-	{
-		labelPlayerNameEdit = std::make_shared<CTextInput>(Rect(6, 3, 95, 15), EFonts::FONT_SMALL, ETextAlignment::CENTER, false);
-		labelPlayerNameEdit->setText(name);
-	}
+		labelPlayerNameEdit = std::make_shared<CTextInputWithConfirm>(Rect(6, 3, 95, 15), EFonts::FONT_SMALL, ETextAlignment::CENTER, name, false, [this](){ updateName(); });
 
 	labelWhoCanPlay = std::make_shared<CMultiLineLabel>(Rect(6, 21, 45, 26), EFonts::FONT_TINY, ETextAlignment::CENTER, Colors::WHITE, LIBRARY->generaltexth->arraytxt[206 + whoCanPlay]);
 
@@ -1114,28 +1111,6 @@ OptionsTab::PlayerOptionsEntry::PlayerOptionsEntry(const PlayerSettings & S, con
 	bonus = std::make_shared<SelectedBox>(Point(271, 2), *s, BONUS);
 }
 
-bool OptionsTab::PlayerOptionsEntry::captureThisKey(EShortcut key)
-{
-	return labelPlayerNameEdit && labelPlayerNameEdit->hasFocus() && key == EShortcut::GLOBAL_ACCEPT;
-}
-
-void OptionsTab::PlayerOptionsEntry::keyPressed(EShortcut key)
-{
-	if(labelPlayerNameEdit && key == EShortcut::GLOBAL_ACCEPT)
-		updateName();
-}
-
-bool OptionsTab::PlayerOptionsEntry::receiveEvent(const Point & position, int eventType) const
-{
-	return eventType == AEventsReceiver::LCLICK; // capture all left clicks (not only within control)
-}
-
-void OptionsTab::PlayerOptionsEntry::clickReleased(const Point & cursorPosition)
-{
-	if(labelPlayerNameEdit && !labelPlayerNameEdit->pos.isInside(cursorPosition))
-		updateName();
-}
-
 void OptionsTab::PlayerOptionsEntry::updateName() {
 	if(labelPlayerNameEdit->getText() != name)
 	{
@@ -1146,8 +1121,6 @@ void OptionsTab::PlayerOptionsEntry::updateName() {
 			set->String() = labelPlayerNameEdit->getText();
 		}
 	}
-
-	labelPlayerNameEdit->removeFocus();
 	name = labelPlayerNameEdit->getText();
 }
 

+ 2 - 5
client/lobby/OptionsTab.h

@@ -28,6 +28,7 @@ class CTextBox;
 class CButton;
 class CSlider;
 class LRClickableArea;
+class CTextInputWithConfirm;
 
 class FilledTexturePlayerColored;
 class TransparentFilledRectangle;
@@ -196,7 +197,7 @@ private:
 		std::unique_ptr<PlayerInfo> pi;
 		std::unique_ptr<PlayerSettings> s;
 		std::shared_ptr<CLabel> labelPlayerName;
-		std::shared_ptr<CTextInput> labelPlayerNameEdit;
+		std::shared_ptr<CTextInputWithConfirm> labelPlayerNameEdit;
 		std::shared_ptr<CMultiLineLabel> labelWhoCanPlay;
 		std::shared_ptr<CPicture> background;
 		std::shared_ptr<CButton> buttonTownLeft;
@@ -215,10 +216,6 @@ private:
 
 		PlayerOptionsEntry(const PlayerSettings & S, const OptionsTab & parentTab);
 		void hideUnavailableButtons();
-		bool captureThisKey(EShortcut key) override;
-		void keyPressed(EShortcut key) override;
-		void clickReleased(const Point & cursorPosition) override;
-		bool receiveEvent(const Point & position, int eventType) const override;
 
 	private:
 		const OptionsTab & parentTab;

+ 21 - 4
client/lobby/RandomMapTab.cpp

@@ -43,7 +43,8 @@
 #include "../../lib/serializer/JsonDeserializer.h"
 
 RandomMapTab::RandomMapTab():
-	InterfaceObjectConfigurable()
+	InterfaceObjectConfigurable(),
+	templateIndex(0)
 {
 	recActions = 0;
 	mapGenOptions = std::make_shared<CMapGenOptions>();
@@ -140,16 +141,18 @@ RandomMapTab::RandomMapTab():
 	//set combo box callbacks
 	if(auto w = widget<ComboBox>("templateList"))
 	{
-		w->onConstructItems = [](std::vector<const void *> & curItems){
+		auto getTemplates = [](){
 			auto templates = LIBRARY->tplh->getTemplates();
-		
 			boost::range::sort(templates, [](const CRmgTemplate * a, const CRmgTemplate * b){
 				return a->getName() < b->getName();
 			});
+			return templates;
+		};
 
+		w->onConstructItems = [getTemplates](std::vector<const void *> & curItems){
 			curItems.push_back(nullptr); //default template
 			
-			for(auto & t : templates)
+			for(auto & t : getTemplates())
 				curItems.push_back(t);
 		};
 		
@@ -164,6 +167,20 @@ RandomMapTab::RandomMapTab():
 				return readText(variables["randomTemplate"]);
 			return std::string("");
 		};
+
+		w->addCallback([this, getTemplates]() // no real dropdown... - instead open dialog
+		{
+			std::vector<std::string> texts;
+			texts.push_back(readText(variables["randomTemplate"]));
+			for(auto & t : getTemplates())
+				texts.push_back(t->getName());
+
+			ENGINE->windows().popWindows(1);
+			ENGINE->windows().createAndPushWindow<CObjectListWindow>(texts, nullptr, LIBRARY->generaltexth->translate("vcmi.lobby.templatesSelect.hover"), LIBRARY->generaltexth->translate("vcmi.lobby.templatesSelect.help"), [this](int index){
+				widget<ComboBox>("templateList")->setItem(index);
+				templateIndex = index;
+			}, templateIndex, std::vector<std::shared_ptr<IImage>>(), true);
+		});
 	}
 	
 	loadOptions();

+ 2 - 0
client/lobby/RandomMapTab.h

@@ -55,6 +55,8 @@ private:
 	std::set<int> playerTeamsAllowed;
 	std::set<int> compCountAllowed;
 	std::set<int> compTeamsAllowed;
+
+	int templateIndex;
 };
 
 class TeamAlignmentsWidget: public InterfaceObjectConfigurable

+ 1 - 0
client/mapView/IMapRendererContext.h

@@ -99,6 +99,7 @@ public:
 	virtual bool showGrid() const = 0;
 	virtual bool showVisitable() const = 0;
 	virtual bool showBlocked() const = 0;
+	virtual bool showInvisible() const = 0;
 
 	/// if true, spell range for teleport / scuttle boat will be visible
 	virtual bool showSpellRange(const int3 & position) const = 0;

+ 20 - 2
client/mapView/MapRenderer.cpp

@@ -14,6 +14,10 @@
 #include "IMapRendererContext.h"
 #include "mapHandler.h"
 
+#include "../CPlayerInterface.h"
+#include "../CServerHandler.h"
+#include "../GameInstance.h"
+#include "../Client.h"
 #include "../GameEngine.h"
 #include "../render/CAnimation.h"
 #include "../render/Canvas.h"
@@ -23,12 +27,15 @@
 #include "../render/Graphics.h"
 
 #include "../../lib/CConfigHandler.h"
+#include "../../lib/callback/CCallback.h"
+#include "../../lib/gameState/CGameState.h"
 #include "../../lib/RiverHandler.h"
 #include "../../lib/RoadHandler.h"
 #include "../../lib/TerrainHandler.h"
 #include "../../lib/mapObjects/CGHeroInstance.h"
 #include "../../lib/mapObjects/MiscObjects.h"
 #include "../../lib/mapObjects/ObjectTemplate.h"
+#include "../../lib/mapping/CMap.h"
 #include "../../lib/mapping/TerrainTile.h"
 #include "../../lib/pathfinder/CGPathNode.h"
 
@@ -592,8 +599,10 @@ MapRendererOverlay::MapRendererOverlay()
 	, imageBlocked(ENGINE->renderHandler().loadImage(ImagePath::builtin("debug/blocked"), EImageBlitMode::COLORKEY))
 	, imageVisitable(ENGINE->renderHandler().loadImage(ImagePath::builtin("debug/visitable"), EImageBlitMode::COLORKEY))
 	, imageSpellRange(ENGINE->renderHandler().loadImage(ImagePath::builtin("debug/spellRange"), EImageBlitMode::COLORKEY))
+	, imageEvent(ENGINE->renderHandler().loadAnimation(AnimationPath::builtin("AVZevnt0"), EImageBlitMode::COLORKEY)->getImage(0))
+	, imageGrail(ENGINE->renderHandler().loadAnimation(AnimationPath::builtin("AVZgrail"), EImageBlitMode::COLORKEY)->getImage(0))
+	, grailPos(GAME->server().client->gameState().getMap().grailPos)
 {
-
 }
 
 void MapRendererOverlay::renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates)
@@ -601,7 +610,7 @@ void MapRendererOverlay::renderTile(IMapRendererContext & context, Canvas & targ
 	if(context.showGrid())
 		target.draw(imageGrid, Point(0,0));
 
-	if(context.showVisitable() || context.showBlocked())
+	if(GAME->interface()->cb->getStartInfo()->extraOptionsInfo.cheatsAllowed && (context.showVisitable() || context.showBlocked() || context.showInvisible()))
 	{
 		bool blocking = false;
 		bool visitable = false;
@@ -610,6 +619,12 @@ void MapRendererOverlay::renderTile(IMapRendererContext & context, Canvas & targ
 		{
 			const auto * object = context.getObject(objectID);
 
+			if(object->ID == Obj::EVENT && context.showInvisible())
+				target.draw(imageEvent, Point(0,0));
+			
+			if(grailPos == coordinates && context.showInvisible())
+				target.draw(imageGrail, Point(0,0));
+
 			if(context.objectTransparency(objectID, coordinates) > 0 && !context.isActiveHero(object))
 			{
 				visitable |= object->visitableAt(coordinates);
@@ -643,6 +658,9 @@ uint8_t MapRendererOverlay::checksum(IMapRendererContext & context, const int3 &
 	if (context.showSpellRange(coordinates))
 		result += 8;
 
+	if (context.showInvisible())
+		result += 16;
+
 	return result;
 }
 

+ 5 - 1
client/mapView/MapRenderer.h

@@ -9,11 +9,11 @@
  */
 #pragma once
 
+#include "../../lib/int3.h"
 #include "../../lib/filesystem/ResourcePath.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 
-class int3;
 class ObjectInstanceID;
 class CGObjectInstance;
 
@@ -139,6 +139,10 @@ class MapRendererOverlay
 	std::shared_ptr<IImage> imageVisitable;
 	std::shared_ptr<IImage> imageBlocked;
 	std::shared_ptr<IImage> imageSpellRange;
+	std::shared_ptr<IImage> imageEvent;
+	std::shared_ptr<IImage> imageGrail;
+
+	int3 grailPos;
 public:
 	MapRendererOverlay();
 

+ 10 - 0
client/mapView/MapRendererContext.cpp

@@ -232,6 +232,11 @@ bool MapRendererBaseContext::showBlocked() const
 	return false;
 }
 
+bool MapRendererBaseContext::showInvisible() const
+{
+	return false;
+}
+
 bool MapRendererBaseContext::showSpellRange(const int3 & position) const
 {
 	return false;
@@ -362,6 +367,11 @@ bool MapRendererAdventureContext::showBlocked() const
 	return settingShowBlocked;
 }
 
+bool MapRendererAdventureContext::showInvisible() const
+{
+	return settingShowInvisible;
+}
+
 bool MapRendererAdventureContext::showTextOverlay() const
 {
 	return settingTextOverlay;

+ 3 - 0
client/mapView/MapRendererContext.h

@@ -62,6 +62,7 @@ public:
 	bool showGrid() const override;
 	bool showVisitable() const override;
 	bool showBlocked() const override;
+	bool showInvisible() const override;
 	bool showSpellRange(const int3 & position) const override;
 };
 
@@ -72,6 +73,7 @@ public:
 	bool settingShowGrid = false;
 	bool settingShowVisitable = false;
 	bool settingShowBlocked = false;
+	bool settingShowInvisible = false;
 	bool settingTextOverlay = false;
 	bool settingsAdventureObjectAnimation = true;
 	bool settingsAdventureTerrainAnimation = true;
@@ -88,6 +90,7 @@ public:
 	bool showGrid() const override;
 	bool showVisitable() const override;
 	bool showBlocked() const override;
+	bool showInvisible() const override;
 	bool showTextOverlay() const override;
 
 	bool showSpellRange(const int3 & position) const override;

+ 1 - 0
client/mapView/MapViewController.cpp

@@ -233,6 +233,7 @@ void MapViewController::updateState()
 		adventureContext->settingShowGrid = settings["gameTweaks"]["showGrid"].Bool();
 		adventureContext->settingShowVisitable = settings["session"]["showVisitable"].Bool();
 		adventureContext->settingShowBlocked = settings["session"]["showBlocked"].Bool();
+		adventureContext->settingShowInvisible = settings["session"]["showInvisible"].Bool();
 		adventureContext->settingTextOverlay = (ENGINE->isKeyboardAltDown() || ENGINE->input().getNumTouchFingers() == 2) && settings["general"]["enableOverlay"].Bool();
 	}
 }

+ 1 - 0
client/netlag/PackRollbackGeneratorVisitor.h

@@ -85,6 +85,7 @@ private:
 	//void visitSetCommanderProperty(SetCommanderProperty & pack) override;
 	//void visitAddQuest(AddQuest & pack) override;
 	//void visitChangeFormation(ChangeFormation & pack) override;
+	//void visitChangeTownName(ChangeTownName & pack) override;
 	//void visitChangeSpells(ChangeSpells & pack) override;
 	//void visitSetAvailableHero(SetAvailableHero & pack) override;
 	//void visitChangeObjectVisitors(ChangeObjectVisitors & pack) override;

+ 80 - 1
client/widgets/CTextInput.cpp

@@ -27,6 +27,85 @@
 std::list<CFocusable *> CFocusable::focusables;
 CFocusable * CFocusable::inputWithFocus;
 
+CTextInputWithConfirm::CTextInputWithConfirm(const Rect & Pos, EFonts font, ETextAlignment alignment, std::string text, bool limitToRect, std::function<void()> confirmCallback)
+	: CTextInput(Pos, font, alignment, false), confirmCb(confirmCallback), limitToRect(limitToRect), initialText(text)
+{
+	setText(text);
+}
+
+bool CTextInputWithConfirm::captureThisKey(EShortcut key)
+{
+	return hasFocus() && (key == EShortcut::GLOBAL_ACCEPT || key == EShortcut::GLOBAL_CANCEL || key == EShortcut::GLOBAL_BACKSPACE);
+}
+
+void CTextInputWithConfirm::keyPressed(EShortcut key)
+{
+	if(!hasFocus())
+		return;
+
+	if(key == EShortcut::GLOBAL_ACCEPT)
+		confirm();
+	else if(key == EShortcut::GLOBAL_CANCEL)
+	{
+		setText(initialText);
+		removeFocus();
+	}
+	
+	CTextInput::keyPressed(key);
+}
+
+bool CTextInputWithConfirm::receiveEvent(const Point & position, int eventType) const
+{
+	return eventType == AEventsReceiver::LCLICK; // capture all left clicks (not only within control)
+}
+
+void CTextInputWithConfirm::clickReleased(const Point & cursorPosition)
+{
+	if(!pos.isInside(cursorPosition)) // clicked outside
+		confirm();
+}
+
+void CTextInputWithConfirm::clickPressed(const Point & cursorPosition)
+{
+	if(pos.isInside(cursorPosition)) // clickPressed should respect control area (receiveEvent also affects this)
+		CTextInput::clickPressed(cursorPosition);
+}
+
+void CTextInputWithConfirm::onFocusGot()
+{
+	initialText = getText();
+
+	CTextInput::onFocusGot();
+}
+
+void CTextInputWithConfirm::textInputted(const std::string & enteredText)
+{
+	if(!hasFocus())
+		return;
+
+	std::string visibleText = getVisibleText() + enteredText;
+	const auto & font = ENGINE->renderHandler().loadFont(label->font);
+	if(!limitToRect || (font->getStringWidth(visibleText) - CLabel::getDelimitersWidth(label->font, visibleText)) < pos.w)
+		CTextInput::textInputted(enteredText);
+}
+
+void CTextInputWithConfirm::deactivate()
+{
+	removeUsedEvents(LCLICK);
+
+	CTextInput::deactivate();
+}
+
+void CTextInputWithConfirm::confirm()
+{
+	if(getText().empty())
+		setText(initialText);
+
+	if(confirmCb && initialText != getText())
+		confirmCb();
+	removeFocus();
+}
+
 CTextInput::CTextInput(const Rect & Pos)
 	:originalAlignment(ETextAlignment::CENTERLEFT)
 {
@@ -196,7 +275,7 @@ void CTextInput::updateLabel()
 	label->alignment = originalAlignment;
 	const auto & font = ENGINE->renderHandler().loadFont(label->font);
 
-	while (font->getStringWidth(visibleText) > pos.w)
+	while ((font->getStringWidth(visibleText) - CLabel::getDelimitersWidth(label->font, visibleText)) > pos.w)
 	{
 		label->alignment = ETextAlignment::CENTERRIGHT;
 		visibleText = visibleText.substr(TextOperations::getUnicodeCharacterSize(visibleText[0]));

+ 31 - 10
client/widgets/CTextInput.h

@@ -45,8 +45,9 @@ public:
 };
 
 /// Text input box where players can enter text
-class CTextInput final : public CFocusable
+class CTextInput : public CFocusable
 {
+protected:
 	using TextEditedCallback = std::function<void(const std::string &)>;
 	using TextFilterCallback = std::function<void(std::string &, const std::string &)>;
 
@@ -71,12 +72,12 @@ class CTextInput final : public CFocusable
 	void createLabel(bool giveFocusToInput);
 	void updateLabel();
 
-	void clickPressed(const Point & cursorPosition) final;
-	void textInputted(const std::string & enteredText) final;
-	void textEdited(const std::string & enteredText) final;
-	void onFocusGot() final;
-	void onFocusLost() final;
-	void showPopupWindow(const Point & cursorPosition) final;
+	void clickPressed(const Point & cursorPosition) override;
+	void textInputted(const std::string & enteredText) override;
+	void textEdited(const std::string & enteredText) override;
+	void onFocusGot() override;
+	void onFocusLost() override;
+	void showPopupWindow(const Point & cursorPosition) override;
 
 	CTextInput(const Rect & Pos);
 public:
@@ -105,7 +106,27 @@ public:
 	void setAlignment(ETextAlignment alignment);
 
 	// CIntObject interface impl
-	void keyPressed(EShortcut key) final;
-	void activate() final;
-	void deactivate() final;
+	void keyPressed(EShortcut key) override;
+	void activate() override;
+	void deactivate() override;
+};
+
+class CTextInputWithConfirm final : public CTextInput
+{
+	std::string initialText;
+	std::function<void()> confirmCb;
+	bool limitToRect;
+
+	void confirm();
+public:
+	CTextInputWithConfirm(const Rect & Pos, EFonts font, ETextAlignment alignment, std::string text, bool limitToRect, std::function<void()> confirmCallback);
+
+	bool captureThisKey(EShortcut key) override;
+	void keyPressed(EShortcut key) override;
+	void clickReleased(const Point & cursorPosition) override;
+	void clickPressed(const Point & cursorPosition) override;
+	bool receiveEvent(const Point & position, int eventType) const override;
+	void onFocusGot() override;
+	void textInputted(const std::string & enteredText) override;
+	void deactivate() override;
 };

+ 20 - 10
client/widgets/TextControls.cpp

@@ -182,29 +182,39 @@ std::vector<std::string> CMultiLineLabel::getLines()
 	return lines;
 }
 
-void CTextContainer::blitLine(Canvas & to, Rect destRect, std::string what)
+const std::string delimiters = "{}";
+
+int CTextContainer::getDelimitersWidth(EFonts font, std::string text)
 {
 	const auto f = ENGINE->renderHandler().loadFont(font);
-	Point where = destRect.topLeft();
-	const std::string delimiters = "{}";
-	auto delimitersCount = std::count_if(what.cbegin(), what.cend(), [&delimiters](char c)
+	auto delimitersWidth = std::count_if(text.cbegin(), text.cend(), [](char c)
 	{
 		return delimiters.find(c) != std::string::npos;
 	});
 	//We should count delimiters length from string to correct centering later.
-	delimitersCount *= f->getStringWidth(delimiters)/2;
+	delimitersWidth *= f->getStringWidth(delimiters)/2;
 
 	std::smatch match;
 	std::regex expr("\\{(.*?)\\|");
-	std::string::const_iterator searchStart( what.cbegin() );
-	while(std::regex_search(searchStart, what.cend(), match, expr))
+	std::string::const_iterator searchStart( text.cbegin() );
+	while(std::regex_search(searchStart, text.cend(), match, expr))
 	{
 		std::string colorText = match[1].str();
 		if(auto c = Colors::parseColor(colorText))
-			delimitersCount += f->getStringWidth(colorText + "|");
+			delimitersWidth += f->getStringWidth(colorText + "|");
 		searchStart = match.suffix().first;
 	}
 
+	return delimitersWidth;
+}
+
+void CTextContainer::blitLine(Canvas & to, Rect destRect, std::string what)
+{
+	const auto f = ENGINE->renderHandler().loadFont(font);
+	Point where = destRect.topLeft();
+
+	int delimitersWidth = getDelimitersWidth(font, what);
+
 	// input is rect in which given text should be placed
 	// calculate proper position for top-left corner of the text
 
@@ -212,10 +222,10 @@ void CTextContainer::blitLine(Canvas & to, Rect destRect, std::string what)
 		where.x += getBorderSize().x;
 
 	if(alignment == ETextAlignment::CENTER || alignment == ETextAlignment::TOPCENTER || alignment == ETextAlignment::BOTTOMCENTER)
-		where.x += (destRect.w - (static_cast<int>(f->getStringWidth(what)) - delimitersCount)) / 2;
+		where.x += (destRect.w - (static_cast<int>(f->getStringWidth(what)) - delimitersWidth)) / 2;
 
 	if(alignment == ETextAlignment::TOPRIGHT || alignment == ETextAlignment::BOTTOMRIGHT || alignment == ETextAlignment::CENTERRIGHT)
-		where.x += getBorderSize().x + destRect.w - (static_cast<int>(f->getStringWidth(what)) - delimitersCount);
+		where.x += getBorderSize().x + destRect.w - (static_cast<int>(f->getStringWidth(what)) - delimitersWidth);
 
 	if(alignment == ETextAlignment::TOPLEFT || alignment == ETextAlignment::TOPCENTER || alignment == ETextAlignment::TOPRIGHT)
 		where.y += getBorderSize().y;

+ 2 - 0
client/widgets/TextControls.h

@@ -31,6 +31,8 @@ protected:
 	CTextContainer(ETextAlignment alignment, EFonts font, ColorRGBA color);
 
 public:
+	static int getDelimitersWidth(EFonts font, std::string text);
+
 	ETextAlignment alignment;
 	EFonts font;
 	ColorRGBA color; // default font color. Can be overridden by placing "{}" into the string

+ 10 - 1
client/windows/CCastleInterface.cpp

@@ -30,6 +30,7 @@
 #include "../widgets/MiscWidgets.h"
 #include "../widgets/CComponent.h"
 #include "../widgets/CGarrisonInt.h"
+#include "../widgets/CTextInput.h"
 #include "../widgets/Buttons.h"
 #include "../widgets/TextControls.h"
 #include "../widgets/RadialMenu.h"
@@ -1435,7 +1436,15 @@ CCastleInterface::CCastleInterface(const CGTownInstance * Town, const CGTownInst
 	garr->setRedrawParent(true);
 
 	heroes = std::make_shared<HeroSlots>(town, Point(241, 387), Point(241, 483), garr, true);
-	title = std::make_shared<CLabel>(85, 387, FONT_MEDIUM, ETextAlignment::TOPLEFT, Colors::WHITE, town->getNameTranslated());
+	title = std::make_shared<CTextInputWithConfirm>(Rect(83, 386, 140, 20), FONT_MEDIUM, ETextAlignment::TOPLEFT, town->getNameTranslated(), true, [this](){ 
+		std::string name = title->getText();
+		std::string originalName = LIBRARY->generaltexth->translate(town->getNameTextID());
+		if(name == originalName)
+			name = ""; // use textID again
+		GAME->interface()->cb->setTownName(town, name);
+	});
+	if(town->tempOwner != GAME->interface()->playerID) // disable changing for allied towns
+		title->deactivate();
 	income = std::make_shared<CLabel>(195, 443, FONT_SMALL, ETextAlignment::CENTER);
 	icon = std::make_shared<CAnimImage>(AnimationPath::builtin("ITPT"), 0, 0, 15, 387);
 

+ 2 - 1
client/windows/CCastleInterface.h

@@ -37,6 +37,7 @@ class CGarrisonInt;
 class CComponent;
 class CComponentBox;
 class LRClickableArea;
+class CTextInputWithConfirm;
 
 /// Building "button"
 class CBuildingRect : public CShowableAnim
@@ -225,7 +226,7 @@ public:
 /// Class which manages the castle window
 class CCastleInterface final : public CStatusbarWindow, public IGarrisonHolder, public IArtifactsHolder
 {
-	std::shared_ptr<CLabel> title;
+	std::shared_ptr<CTextInputWithConfirm> title;
 	std::shared_ptr<CLabel> income;
 	std::shared_ptr<CAnimImage> icon;
 

+ 1 - 1
client/windows/CWindowObject.cpp

@@ -86,7 +86,7 @@ std::shared_ptr<CPicture> CWindowObject::createBg(const ImagePath & imageName, b
 		return nullptr;
 
 	auto image = std::make_shared<CPicture>(imageName, Point(0,0), EImageBlitMode::OPAQUE);
-	if(playerColored)
+	if(playerColored && GAME->interface())
 		image->setPlayerColor(GAME->interface()->playerID);
 	return image;
 }

+ 16 - 8
client/windows/GUIClasses.cpp

@@ -1596,7 +1596,7 @@ CObjectListWindow::CObjectListWindow(const std::vector<int> & _items, std::share
 	for(int id : _items)
 	{
 		std::string objectName = GAME->interface()->cb->getObjInstance(ObjectInstanceID(id))->getObjectName();
-		trimTextIfTooWide(objectName);
+		trimTextIfTooWide(objectName, false);
 		items.emplace_back(id, objectName);
 	}
 	itemsVisible = items;
@@ -1620,7 +1620,7 @@ CObjectListWindow::CObjectListWindow(const std::vector<std::string> & _items, st
 	for(size_t i = 0; i < _items.size(); i++)
 	{
 		std::string objectName = _items[i];
-		trimTextIfTooWide(objectName);
+		trimTextIfTooWide(objectName, true);
 		items.emplace_back(static_cast<int>(i), objectName);
 	}
 	itemsVisible = items;
@@ -1664,16 +1664,24 @@ void CObjectListWindow::init(std::shared_ptr<CIntObject> titleWidget_, std::stri
 	searchBox->setCallback(std::bind(&CObjectListWindow::itemsSearchCallback, this, std::placeholders::_1));
 }
 
-void CObjectListWindow::trimTextIfTooWide(std::string & text) const
+void CObjectListWindow::trimTextIfTooWide(std::string & text, bool preserveCountSuffix) const
 {
+	std::string suffix = "...";
 	int maxWidth = pos.w - 60;	// 60 px for scrollbar and borders
-	auto posBrace = text.find('(');
-	auto posClosing = text.find(')');
-	std::string objCount = text.substr(posBrace, posClosing - posBrace) + ')';
+
 	if(text[0] == '{')
-		objCount = '}' + objCount;
+		suffix += "}";
+
+	if (preserveCountSuffix)
+	{
+		auto posBrace = text.find_last_of("(");
+		auto posClosing = text.find_last_of(")");
+		std::string objCount = text.substr(posBrace, posClosing - posBrace) + ')';
+		suffix += " ";
+		suffix += objCount;
+	}
+
 	const auto & font = ENGINE->renderHandler().loadFont(FONT_SMALL);
-	std::string suffix = "... " + objCount;
 
 	if(font->getStringWidth(text) >= maxWidth)
 	{

+ 1 - 1
client/windows/GUIClasses.h

@@ -206,7 +206,7 @@ class CObjectListWindow : public CWindowObject
 	std::vector< std::pair<int, std::string> > itemsVisible; //visible items present in list
 
 	void init(std::shared_ptr<CIntObject> titleWidget_, std::string _title, std::string _descr, bool searchBoxEnabled);
-	void trimTextIfTooWide(std::string & text) const; // trim item's text to fit within window's width
+	void trimTextIfTooWide(std::string & text, bool preserveCountSuffix) const; // trim item's text to fit within window's width
 	void itemsSearchCallback(const std::string & text);
 	void exitPressed();
 public:

+ 12 - 1
docs/modders/Entities_Format/Hero_Type_Format.md

@@ -130,10 +130,21 @@ In order to make functional hero you also need:
 			"anotherOne" : {Bonus Format}
 		},
 		// Shortcut for defining creature specialty, using standard H3 rules
+		// Can be combined with bonuses-based specialty if desired
 		"creature" : "griffin",
 
 		// Shortcut for defining specialty in secondary skill, using standard H3 rules
-		"secondary" : "offence"
+		// Can be combined with bonuses-based specialty if desired
+		"secondary" : "offence",
+		
+		// Optional, only applicable to creature specialties
+		// Overrides creature level to specific value for purposes of computing growth of h3-like creature specialty
+		"creatureLevel" : 5
+		
+		// Optional, only applicable to creature and secondary skill specialties
+		// Overrides default (5% for vanilla H3 specialties) growth of specialties per level to a specified value
+		// Default value can be modified globally using specialtySecondarySkillGrowth and specialtyCreatureGrowth game settings
+		"stepSize" : 5
 	}
 }
 ```

+ 11 - 8
docs/players/Cheat_Codes.md

@@ -162,16 +162,19 @@ Below a list of supported commands, with their arguments wrapped in `<>`
 
 #### Settings
 
-- `set <command> <on/off>` - sets special temporary settings that reset on game quit. Below some of the most notable commands:  
-- `autoskip` - identical to `autoskip` option  
-- `onlyAI` - run without human player, all players will be *default AI*  
-- `headless` - run without GUI, implies `onlyAI` is set  
-- `showGrid` - display a square grid overlay on top of adventure map  
-- `showBlocked` - show blocked tiles on map  
-- `showVisitable` - show visitable tiles on map  
-- `hideSystemMessages` - suppress server messages in chat  
+- `set <command> <on/off>` - sets special temporary settings that reset on game quit. Below some of the most notable commands:
+- `autoskip` - identical to `autoskip` option
+- `onlyAI` - run without human player, all players will be *default AI*
+- `headless` - run without GUI, implies `onlyAI` is set
+- `showGrid` - display a square grid overlay on top of adventure map
+- `showBlocked` - show blocked tiles on map
+- `showVisitable` - show visitable tiles on map
+- `showInvisible` - show invisible tiles (events, grail) on map
+- `hideSystemMessages` - suppress server messages in chat
 - `antilag` - toggles network lag compensation in multiplayer on or off
 
+`showBlocked`, `showVisitable` and `showInvisible` only works if cheats are enabled.
+
 #### Developer Commands
 
 - `crash` - force a game crash. It is sometimes useful to generate memory dump file in certain situations, for example game freeze  

+ 6 - 0
lib/callback/CCallback.cpp

@@ -286,6 +286,12 @@ void CCallback::setFormation(const CGHeroInstance * hero, EArmyFormation mode)
 	sendRequest(pack);
 }
 
+void CCallback::setTownName(const CGTownInstance * town, std::string & name)
+{
+	SetTownName pack(town->id, name);
+	sendRequest(pack);
+}
+
 void CCallback::recruitHero(const CGObjectInstance *townOrTavern, const CGHeroInstance *hero, const HeroTypeID & nextHero)
 {
 	assert(townOrTavern);

+ 1 - 0
lib/callback/CCallback.h

@@ -76,6 +76,7 @@ public:
 	void trade(const ObjectInstanceID marketId, EMarketMode mode, TradeItemSell id1, TradeItemBuy id2, ui32 val1, const CGHeroInstance * hero = nullptr) override;
 	void trade(const ObjectInstanceID marketId, EMarketMode mode, const std::vector<TradeItemSell> & id1, const std::vector<TradeItemBuy> & id2, const std::vector<ui32> & val1, const CGHeroInstance * hero = nullptr) override;
 	void setFormation(const CGHeroInstance * hero, EArmyFormation mode) override;
+	void setTownName(const CGTownInstance * town, std::string & name) override;
 	void recruitHero(const CGObjectInstance *townOrTavern, const CGHeroInstance *hero, const HeroTypeID & nextHero=HeroTypeID::NONE) override;
 	void save(const std::string &fname) override;
 	void sendMessage(const std::string &mess, const CGObjectInstance * currentObject = nullptr) override;

+ 1 - 0
lib/callback/IGameActionCallback.h

@@ -70,6 +70,7 @@ public:
 	virtual void endTurn()=0;
 	virtual void buyArtifact(const CGHeroInstance *hero, ArtifactID aid)=0; //used to buy artifacts in towns (including spell book in the guild and war machines in blacksmith)
 	virtual void setFormation(const CGHeroInstance * hero, EArmyFormation mode)=0;
+	virtual void setTownName(const CGTownInstance * town, std::string & name)=0;
 
 	virtual void save(const std::string &fname) = 0;
 	virtual void sendMessage(const std::string &mess, const CGObjectInstance * currentObject = nullptr) = 0;

+ 5 - 0
lib/gameState/GameStatePackVisitor.cpp

@@ -123,6 +123,11 @@ void GameStatePackVisitor::visitChangeFormation(ChangeFormation & pack)
 	gs.getHero(pack.hid)->setFormation(pack.formation);
 }
 
+void GameStatePackVisitor::visitChangeTownName(ChangeTownName & pack)
+{
+	gs.getTown(pack.tid)->setCustomName(pack.name);
+}
+
 void GameStatePackVisitor::visitHeroVisitCastle(HeroVisitCastle & pack)
 {
 	CGHeroInstance *h = gs.getHero(pack.hid);

+ 1 - 0
lib/gameState/GameStatePackVisitor.h

@@ -87,6 +87,7 @@ public:
 	void visitSetCommanderProperty(SetCommanderProperty & pack) override;
 	void visitAddQuest(AddQuest & pack) override;
 	void visitChangeFormation(ChangeFormation & pack) override;
+	void visitChangeTownName(ChangeTownName & pack) override;
 	void visitChangeSpells(ChangeSpells & pack) override;
 	void visitSetAvailableHero(SetAvailableHero & pack) override;
 	void visitChangeObjectVisitors(ChangeObjectVisitors & pack) override;

+ 6 - 1
lib/mapObjects/CGTownInstance.cpp

@@ -860,7 +860,7 @@ CBonusSystemNode & CGTownInstance::whatShouldBeAttached()
 
 std::string CGTownInstance::getNameTranslated() const
 {
-	return LIBRARY->generaltexth->translate(nameTextId);
+	return customName.empty() ? LIBRARY->generaltexth->translate(nameTextId) : customName;
 }
 
 std::string CGTownInstance::getNameTextID() const
@@ -873,6 +873,11 @@ void CGTownInstance::setNameTextId( const std::string & newName )
 	nameTextId = newName;
 }
 
+void CGTownInstance::setCustomName( const std::string & newName )
+{
+	customName = newName;
+}
+
 const CArmedInstance * CGTownInstance::getUpperArmy() const
 {
 	if(getGarrisonHero())

+ 4 - 0
lib/mapObjects/CGTownInstance.h

@@ -46,6 +46,7 @@ class DLL_LINKAGE CGTownInstance : public CGDwelling, public IShipyard, public I
 {
 	friend class CTownInstanceConstructor;
 	std::string nameTextId; // name of town
+	std::string customName;
 
 	std::map<BuildingID, TownRewardableBuildingInstance*> convertOldBuildings(std::vector<TownRewardableBuildingInstance*> oldVector);
 	std::set<BuildingID> builtBuildings;
@@ -75,6 +76,8 @@ public:
 	{
 		h & static_cast<CGDwelling&>(*this);
 		h & nameTextId;
+		if (h.version >= Handler::Version::CUSTOM_NAMES)
+			h & customName;
 		h & built;
 		h & destroyed;
 		h & identifier;
@@ -128,6 +131,7 @@ public:
 	std::string getNameTranslated() const;
 	std::string getNameTextID() const;
 	void setNameTextId(const std::string & newName);
+	void setCustomName(const std::string & newName);
 
 	//////////////////////////////////////////////////////////////////////////
 

+ 1 - 1
lib/mapObjects/ObjectTemplate.cpp

@@ -66,7 +66,7 @@ void ObjectTemplate::afterLoadFixup()
 	if(id == Obj::EVENT)
 	{
 		setSize(1,1);
-		usedTiles[0][0] = VISITABLE;
+		usedTiles[0][0] = VISITABLE | VISIBLE;
 		visitDir = 0xFF;
 	}
 }

+ 1 - 1
lib/mapping/CMapOperation.cpp

@@ -311,7 +311,7 @@ void CDrawTerrainOperation::updateTerrainViews()
 		if(!pattern.diffImages)
 		{
 			tile.terView = gen->nextInt(mapping.first, mapping.second);
-			tile.extTileFlags = valRslt.flip;
+			tile.extTileFlags = (tile.extTileFlags & 0b11111100) | valRslt.flip;
 		}
 		else
 		{

+ 6 - 0
lib/mapping/MapFormatH3M.cpp

@@ -1100,6 +1100,8 @@ void CMapLoaderH3M::readBoxContent(CGPandoraBox * object, const int3 & mapPositi
 			SpellID scrollSpell = reader->readSpell16();
 			if (grantedArtifact == ArtifactID::SPELL_SCROLL)
 				reward.grantedScrolls.push_back(scrollSpell);
+			else
+				reward.grantedArtifacts.push_back(grantedArtifact);
 		}
 		else
 			reward.grantedArtifacts.push_back(grantedArtifact);
@@ -2306,6 +2308,8 @@ void CMapLoaderH3M::readSeerHutQuest(CGSeerHut * hut, const int3 & position, con
 					SpellID scrollSpell = reader->readSpell16();
 					if (grantedArtifact == ArtifactID::SPELL_SCROLL)
 						reward.grantedScrolls.push_back(scrollSpell);
+					else
+						reward.grantedArtifacts.push_back(grantedArtifact);
 				}
 				else
 					reward.grantedArtifacts.push_back(grantedArtifact);
@@ -2381,6 +2385,8 @@ EQuestMission CMapLoaderH3M::readQuest(IQuestObject * guard, const int3 & positi
 					SpellID scrollSpell = reader->readSpell16();
 					if (requiredArtifact == ArtifactID::SPELL_SCROLL)
 						guard->getQuest().mission.scrolls.push_back(scrollSpell);
+					else
+						guard->getQuest().mission.artifacts.push_back(requiredArtifact);
 				}
 				else
 					guard->getQuest().mission.artifacts.push_back(requiredArtifact);

+ 2 - 0
lib/networkPacks/NetPackVisitor.h

@@ -58,6 +58,7 @@ public:
 	virtual void visitSetCommanderProperty(SetCommanderProperty & pack) {}
 	virtual void visitAddQuest(AddQuest & pack) {}
 	virtual void visitChangeFormation(ChangeFormation & pack) {}
+	virtual void visitChangeTownName(ChangeTownName & pack) {}
 	virtual void visitRemoveObject(RemoveObject & pack) {}
 	virtual void visitTryMoveHero(TryMoveHero & pack) {}
 	virtual void visitNewStructures(NewStructures & pack) {}
@@ -144,6 +145,7 @@ public:
 	virtual void visitBuyArtifact(BuyArtifact & pack) {}
 	virtual void visitTradeOnMarketplace(TradeOnMarketplace & pack) {}
 	virtual void visitSetFormation(SetFormation & pack) {}
+	virtual void visitSetTownName(SetTownName & pack) {}
 	virtual void visitHireHero(HireHero & pack) {}
 	virtual void visitBuildBoat(BuildBoat & pack) {}
 	virtual void visitQueryReply(QueryReply & pack) {}

+ 10 - 0
lib/networkPacks/NetPacksLib.cpp

@@ -213,6 +213,11 @@ void ChangeFormation::visitTyped(ICPackVisitor & visitor)
 	visitor.visitChangeFormation(*this);
 }
 
+void ChangeTownName::visitTyped(ICPackVisitor & visitor)
+{
+	visitor.visitChangeTownName(*this);
+}
+
 void RemoveObject::visitTyped(ICPackVisitor & visitor)
 {
 	visitor.visitRemoveObject(*this);
@@ -643,6 +648,11 @@ void SetFormation::visitTyped(ICPackVisitor & visitor)
 	visitor.visitSetFormation(*this);
 }
 
+void SetTownName::visitTyped(ICPackVisitor & visitor)
+{
+	visitor.visitSetTownName(*this);
+}
+
 void HireHero::visitTyped(ICPackVisitor & visitor)
 {
 	visitor.visitHireHero(*this);

+ 14 - 0
lib/networkPacks/PacksForClient.h

@@ -614,6 +614,20 @@ struct DLL_LINKAGE ChangeFormation : public CPackForClient
 	}
 };
 
+struct DLL_LINKAGE ChangeTownName : public CPackForClient
+{
+	ObjectInstanceID tid;
+	std::string name;
+
+	void visitTyped(ICPackVisitor & visitor) override;
+
+	template <typename Handler> void serialize(Handler & h)
+	{
+		h & tid;
+		h & name;
+	}
+};
+
 struct DLL_LINKAGE RemoveObject : public CPackForClient
 {
 	RemoveObject() = default;

+ 22 - 0
lib/networkPacks/PacksForServer.h

@@ -609,6 +609,28 @@ struct DLL_LINKAGE SetFormation : public CPackForServer
 	}
 };
 
+struct DLL_LINKAGE SetTownName : public CPackForServer
+{
+	SetTownName() = default;
+	;
+	SetTownName(const ObjectInstanceID & TID, std::string Name)
+		: tid(TID)
+		, name(Name)
+	{
+	}
+	ObjectInstanceID tid;
+	std::string name;
+
+	void visitTyped(ICPackVisitor & visitor) override;
+
+	template <typename Handler> void serialize(Handler & h)
+	{
+		h & static_cast<CPackForServer &>(*this);
+		h & tid;
+		h & name;
+	}
+};
+
 struct DLL_LINKAGE HireHero : public CPackForServer
 {
 	HireHero() = default;

+ 2 - 1
lib/serializer/ESerializationVersion.h

@@ -50,8 +50,9 @@ enum class ESerializationVersion : int32_t
 	BONUS_HIDDEN, // hidden bonus
 	MORE_MAP_LAYERS, // more map layers
 	CONFIGURABLE_RESOURCES, // configurable resources
+	CUSTOM_NAMES, // custom names
 
-	CURRENT = CONFIGURABLE_RESOURCES,
+	CURRENT = CUSTOM_NAMES,
 };
 
 static_assert(ESerializationVersion::MINIMAL <= ESerializationVersion::CURRENT, "Invalid serialization version definition!");

+ 2 - 0
lib/serializer/RegisterTypes.h

@@ -291,6 +291,8 @@ void registerTypes(Serializer &s)
 	s.template registerType<TimesStackSizeUpdater>(249);
 	s.template registerType<TimesArmySizeUpdater>(250);
 	s.template registerType<PackageReceived>(251);
+	s.template registerType<ChangeTownName>(252);
+	s.template registerType<SetTownName>(253);
 }
 
 VCMI_LIB_NAMESPACE_END

+ 2 - 2
lib/spells/adventure/TownPortalEffect.cpp

@@ -140,7 +140,7 @@ void TownPortalEffect::endCast(SpellCastEnvironment * env, const AdventureSpellC
 {
 	const CGTownInstance * destination = nullptr;
 
-	if(parameters.caster->getSpellSchoolLevel(owner) < 2)
+	if(!allowTownSelection)
 	{
 		std::vector<const CGTownInstance *> pool = getPossibleTowns(env, parameters);
 		destination = findNearestTown(env, parameters, pool);
@@ -194,7 +194,7 @@ ESpellCastResult TownPortalEffect::beginCast(SpellCastEnvironment * env, const A
 		return ESpellCastResult::CANCEL;
 	}
 
-	if(!parameters.pos.isValid() && parameters.caster->getSpellSchoolLevel(owner) >= 2)
+	if(!parameters.pos.isValid() && allowTownSelection)
 	{
 		auto queryCallback = [&mechanics, env, parameters](std::optional<int32_t> reply) -> void
 		{

+ 25 - 2
server/CGameHandler.cpp

@@ -3221,6 +3221,23 @@ bool CGameHandler::setFormation(ObjectInstanceID hid, EArmyFormation formation)
 	return true;
 }
 
+bool CGameHandler::setTownName(ObjectInstanceID tid, std::string & name)
+{
+	const CGTownInstance *t = gameInfo().getTown(tid);
+	if (!t)
+	{
+		logGlobal->error("Town doesn't exist!");
+		return false;
+	}
+
+	ChangeTownName ctn;
+	ctn.tid = tid;
+	ctn.name = name;
+	sendAndApply(ctn);
+
+	return true;
+}
+
 bool CGameHandler::queryReply(QueryID qid, std::optional<int32_t> answer, PlayerColor player)
 {
 	logGlobal->trace("Player %s attempts answering query %d with answer:", player, qid);
@@ -3883,8 +3900,14 @@ void CGameHandler::castSpell(const spells::Caster * caster, SpellID spellID, con
 	const CSpell * s = spellID.toSpell();
 	s->adventureCast(spellEnv.get(), p);
 
-	if(const auto * hero = caster->getHeroCaster())
-		useChargeBasedSpell(hero->id, spellID);
+	// FIXME: hack to avoid attempts to use charges when spell is casted externally
+	// For example, town gates map object in hota/wog
+	// Proper fix would be to instead spend charges similar to existing caster::spendMana call
+	if (dynamic_cast<const spells::ExternalCaster*>(caster) == nullptr)
+	{
+		if(const auto * hero = caster->getHeroCaster())
+			useChargeBasedSpell(hero->id, spellID);
+	}
 }
 
 bool CGameHandler::swapStacks(const StackLocation & sl1, const StackLocation & sl2)

+ 1 - 0
server/CGameHandler.h

@@ -212,6 +212,7 @@ public:
 	bool queryReply( QueryID qid, std::optional<int32_t> reply, PlayerColor player );
 	bool buildBoat( ObjectInstanceID objid, PlayerColor player );
 	bool setFormation( ObjectInstanceID hid, EArmyFormation formation );
+	bool setTownName( ObjectInstanceID tid, std::string & name );
 	bool tradeResources(const IMarket *market, ui32 amountToSell, PlayerColor player, GameResID toSell, GameResID toBuy);
 	bool sacrificeCreatures(const IMarket * market, const CGHeroInstance * hero, const std::vector<SlotID> & slot, const std::vector<ui32> & count);
 	bool sendResources(ui32 val, PlayerColor player, GameResID r1, PlayerColor r2);

+ 7 - 0
server/NetPacksServer.cpp

@@ -362,6 +362,13 @@ void ApplyGhNetPackVisitor::visitSetFormation(SetFormation & pack)
 	result = gh.setFormation(pack.hid, pack.formation);
 }
 
+void ApplyGhNetPackVisitor::visitSetTownName(SetTownName & pack)
+{
+	gh.throwIfWrongOwner(connection, &pack, pack.tid);
+
+	result = gh.setTownName(pack.tid, pack.name);
+}
+
 void ApplyGhNetPackVisitor::visitHireHero(HireHero & pack)
 {
 	gh.throwIfWrongPlayer(connection, &pack);

+ 1 - 0
server/ServerNetPackVisitors.h

@@ -58,6 +58,7 @@ public:
 	void visitBuyArtifact(BuyArtifact & pack) override;
 	void visitTradeOnMarketplace(TradeOnMarketplace & pack) override;
 	void visitSetFormation(SetFormation & pack) override;
+	void visitSetTownName(SetTownName & pack) override;
 	void visitHireHero(HireHero & pack) override;
 	void visitBuildBoat(BuildBoat & pack) override;
 	void visitQueryReply(QueryReply & pack) override;

+ 4 - 0
test/bonus/BonusSystemTest.cpp

@@ -189,6 +189,8 @@ TEST_F(BonusSystemTest, battlewidePropagationToAll)
 
 	EXPECT_TRUE(heroAine.hasBonusOfType(BonusType::BLOCK_ALL_MAGIC));
 	EXPECT_TRUE(heroBron.hasBonusOfType(BonusType::BLOCK_ALL_MAGIC));
+
+	heroAine.detachFromSource(orb);
 }
 
 TEST_F(BonusSystemTest, battlewidePropagationToEnemies)
@@ -297,6 +299,8 @@ TEST_F(BonusSystemTest, legionPieces)
 
 	heroAine.detachFrom(townAndVisitor);
 	EXPECT_EQ(town.valOfBonuses(BonusType::CREATURE_GROWTH, BonusCustomSubtype::creatureLevel(3)), 0);
+
+	heroAine.detachFromSource(legion);
 }
 
 }

+ 6 - 4
test/events/ApplyDamageTest.cpp

@@ -22,9 +22,11 @@ namespace test
 using namespace ::testing;
 using namespace ::events;
 
-class ListenerMock
+class ApplyDamageListenerMock
 {
 public:
+	virtual ~ApplyDamageListenerMock() = default;
+
 	MOCK_METHOD1(beforeEvent, void(ApplyDamage &));
 	MOCK_METHOD1(afterEvent, void(const ApplyDamage &));
 };
@@ -33,7 +35,7 @@ class ApplyDamageTest : public Test
 {
 public:
 	EventBus eventBus;
-	ListenerMock listener;
+	ApplyDamageListenerMock listener;
 	StrictMock<EnvironmentMock> environmentMock;
 
 	std::shared_ptr<StrictMock<UnitMock>> targetMock;
@@ -47,8 +49,8 @@ protected:
 //this should be the only subscription test for events, just in case cross-binary subscription breaks
 TEST_F(ApplyDamageTest, Subscription)
 {
-	auto subscription1 = eventBus.subscribeBefore<ApplyDamage>(std::bind(&ListenerMock::beforeEvent, &listener, _1));
-	auto subscription2 = eventBus.subscribeAfter<ApplyDamage>(std::bind(&ListenerMock::afterEvent, &listener, _1));
+	auto subscription1 = eventBus.subscribeBefore<ApplyDamage>(std::bind(&ApplyDamageListenerMock::beforeEvent, &listener, _1));
+	auto subscription2 = eventBus.subscribeAfter<ApplyDamage>(std::bind(&ApplyDamageListenerMock::afterEvent, &listener, _1));
 
 	EXPECT_CALL(listener, beforeEvent(_)).Times(1);
 	EXPECT_CALL(listener, afterEvent(_)).Times(1);

+ 23 - 21
test/events/EventBusTest.cpp

@@ -35,9 +35,11 @@ public:
 	friend class SubscriptionRegistry<EventExample>;
 };
 
-class ListenerMock
+class EventBusListenerMock
 {
 public:
+	virtual ~EventBusListenerMock() = default;
+
 	MOCK_METHOD1(beforeEvent, void(EventExample &));
 	MOCK_METHOD1(onEvent, void(EventExample &));
 	MOCK_METHOD1(afterEvent, void(const EventExample &));
@@ -51,9 +53,9 @@ public:
 	EventBus subject1;
 	EventBus subject2;
 
-	StrictMock<ListenerMock> listener;
-	StrictMock<ListenerMock> listener1;
-	StrictMock<ListenerMock> listener2;
+	StrictMock<EventBusListenerMock> listener;
+	StrictMock<EventBusListenerMock> listener1;
+	StrictMock<EventBusListenerMock> listener2;
 };
 
 TEST_F(EventBusTest, ExecuteNoListeners)
@@ -67,15 +69,15 @@ TEST_F(EventBusTest, ExecuteNoListenersWithHandler)
 	EXPECT_CALL(event1, isEnabled()).WillRepeatedly(Return(true));
 	EXPECT_CALL(listener, onEvent(Ref(event1))).Times(1);
 
-	subject1.executeEvent(event1, std::bind(&ListenerMock::onEvent, &listener, _1));
+	subject1.executeEvent(event1, std::bind(&EventBusListenerMock::onEvent, &listener, _1));
 }
 
 TEST_F(EventBusTest, ExecuteIgnoredSubscription)
 {
 	EXPECT_CALL(event1, isEnabled()).WillRepeatedly(Return(true));
 
-	subject1.subscribeBefore<EventExample>(std::bind(&ListenerMock::beforeEvent, &listener, _1));
-	subject1.subscribeAfter<EventExample>(std::bind(&ListenerMock::afterEvent, &listener, _1));
+	subject1.subscribeBefore<EventExample>(std::bind(&EventBusListenerMock::beforeEvent, &listener, _1));
+	subject1.subscribeAfter<EventExample>(std::bind(&EventBusListenerMock::afterEvent, &listener, _1));
 
 	EXPECT_CALL(listener, beforeEvent(_)).Times(0);
 	EXPECT_CALL(listener, afterEvent(_)).Times(0);
@@ -87,10 +89,10 @@ TEST_F(EventBusTest, ExecuteSequence)
 {
 	EXPECT_CALL(event1, isEnabled()).WillRepeatedly(Return(true));
 
-	auto subscription1 = subject1.subscribeBefore<EventExample>(std::bind(&ListenerMock::beforeEvent, &listener1, _1));
-	auto subscription2 = subject1.subscribeAfter<EventExample>(std::bind(&ListenerMock::afterEvent, &listener1, _1));
-	auto subscription3 = subject1.subscribeBefore<EventExample>(std::bind(&ListenerMock::beforeEvent, &listener2, _1));
-	auto subscription4 = subject1.subscribeAfter<EventExample>(std::bind(&ListenerMock::afterEvent, &listener2, _1));
+	auto subscription1 = subject1.subscribeBefore<EventExample>(std::bind(&EventBusListenerMock::beforeEvent, &listener1, _1));
+	auto subscription2 = subject1.subscribeAfter<EventExample>(std::bind(&EventBusListenerMock::afterEvent, &listener1, _1));
+	auto subscription3 = subject1.subscribeBefore<EventExample>(std::bind(&EventBusListenerMock::beforeEvent, &listener2, _1));
+	auto subscription4 = subject1.subscribeAfter<EventExample>(std::bind(&EventBusListenerMock::afterEvent, &listener2, _1));
 
 	{
 		InSequence sequence;
@@ -101,17 +103,17 @@ TEST_F(EventBusTest, ExecuteSequence)
 		EXPECT_CALL(listener2, afterEvent(Ref(event1))).Times(1);
 	}
 
-	subject1.executeEvent(event1, std::bind(&ListenerMock::onEvent, &listener, _1));
+	subject1.executeEvent(event1, std::bind(&EventBusListenerMock::onEvent, &listener, _1));
 }
 
 TEST_F(EventBusTest, BusesAreIndependent)
 {
 	EXPECT_CALL(event1, isEnabled()).WillRepeatedly(Return(true));
 
-	auto subscription1 = subject1.subscribeBefore<EventExample>(std::bind(&ListenerMock::beforeEvent, &listener1, _1));
-	auto subscription2 = subject1.subscribeAfter<EventExample>(std::bind(&ListenerMock::afterEvent, &listener1, _1));
-	auto subscription3 = subject2.subscribeBefore<EventExample>(std::bind(&ListenerMock::beforeEvent, &listener2, _1));
-	auto subscription4 = subject2.subscribeAfter<EventExample>(std::bind(&ListenerMock::afterEvent, &listener2, _1));
+	auto subscription1 = subject1.subscribeBefore<EventExample>(std::bind(&EventBusListenerMock::beforeEvent, &listener1, _1));
+	auto subscription2 = subject1.subscribeAfter<EventExample>(std::bind(&EventBusListenerMock::afterEvent, &listener1, _1));
+	auto subscription3 = subject2.subscribeBefore<EventExample>(std::bind(&EventBusListenerMock::beforeEvent, &listener2, _1));
+	auto subscription4 = subject2.subscribeAfter<EventExample>(std::bind(&EventBusListenerMock::afterEvent, &listener2, _1));
 
 	EXPECT_CALL(listener1, beforeEvent(_)).Times(1);
 	EXPECT_CALL(listener2, beforeEvent(_)).Times(0);
@@ -125,7 +127,7 @@ TEST_F(EventBusTest, DisabledTestDontExecute)
 {
 	EXPECT_CALL(event1, isEnabled()).Times(AtLeast(1)).WillRepeatedly(Return(false));
 	EXPECT_CALL(listener, onEvent(Ref(event1))).Times(0);
-	subject1.executeEvent(event1, std::bind(&ListenerMock::onEvent, &listener, _1));
+	subject1.executeEvent(event1, std::bind(&EventBusListenerMock::onEvent, &listener, _1));
 }
 
 TEST_F(EventBusTest, DisabledTestDontExecutePostHandler)
@@ -134,10 +136,10 @@ TEST_F(EventBusTest, DisabledTestDontExecutePostHandler)
 	EXPECT_CALL(listener, onEvent(Ref(event1))).WillRepeatedly(Return());
 	EXPECT_CALL(listener1, afterEvent(Ref(event1))).Times(0);
 
-	auto subscription1 = subject1.subscribeAfter<EventExample>(std::bind(&ListenerMock::afterEvent, &listener1, _1));
+	auto subscription1 = subject1.subscribeAfter<EventExample>(std::bind(&EventBusListenerMock::afterEvent, &listener1, _1));
 
 
-	subject1.executeEvent(event1, std::bind(&ListenerMock::onEvent, &listener, _1));
+	subject1.executeEvent(event1, std::bind(&EventBusListenerMock::onEvent, &listener, _1));
 }
 
 TEST_F(EventBusTest, DisabledTestExecutePreHandler)
@@ -146,9 +148,9 @@ TEST_F(EventBusTest, DisabledTestExecutePreHandler)
 	EXPECT_CALL(listener, onEvent(Ref(event1))).WillRepeatedly(Return());
 	EXPECT_CALL(listener1, beforeEvent(Ref(event1))).Times(1);
 
-	auto subscription1 = subject1.subscribeBefore<EventExample>(std::bind(&ListenerMock::beforeEvent, &listener1, _1));
+	auto subscription1 = subject1.subscribeBefore<EventExample>(std::bind(&EventBusListenerMock::beforeEvent, &listener1, _1));
 
-	subject1.executeEvent(event1, std::bind(&ListenerMock::onEvent, &listener, _1));
+	subject1.executeEvent(event1, std::bind(&EventBusListenerMock::onEvent, &listener, _1));
 }