Bläddra i källkod

Merge pull request #1645 from rilian-la-te/infobox-pickups

Allow rewardables to show its rewards in info bar
Ivan Savenko 2 år sedan
förälder
incheckning
e764a7255d
50 ändrade filer med 724 tillägg och 374 borttagningar
  1. 2 2
      AI/Nullkiller/AIGateway.cpp
  2. 1 1
      AI/Nullkiller/AIGateway.h
  3. 2 1
      AI/VCAI/VCAI.cpp
  4. 1 1
      AI/VCAI/VCAI.h
  5. 2 0
      Mods/vcmi/config/vcmi/english.json
  6. 75 5
      Mods/vcmi/config/vcmi/russian.json
  7. 0 1
      Mods/vcmi/mod.json
  8. 0 7
      client/CMusicHandler.cpp
  9. 0 1
      client/CMusicHandler.h
  10. 36 15
      client/CPlayerInterface.cpp
  11. 1 2
      client/CPlayerInterface.h
  12. 0 1
      client/Client.h
  13. 0 1
      client/ClientNetPackVisitors.h
  14. 11 16
      client/NetPacksClient.cpp
  15. 3 1
      client/adventureMap/CAdvMapInt.cpp
  16. 199 12
      client/adventureMap/CInfoBar.cpp
  17. 54 4
      client/adventureMap/CInfoBar.h
  18. 44 23
      client/widgets/CComponent.cpp
  19. 17 4
      client/widgets/CComponent.h
  20. 1 1
      client/widgets/MiscWidgets.cpp
  21. 19 0
      client/windows/CMessage.cpp
  22. 6 0
      client/windows/CMessage.h
  23. 8 2
      client/windows/settings/AdventureOptionsTab.cpp
  24. 1 1
      config/objects/rewardableOncePerHero.json
  25. 5 0
      config/schemas/settings.json
  26. 12 1
      config/widgets/settings/adventureOptionsTab.json
  27. 0 2
      lib/IGameCallback.h
  28. 2 2
      lib/IGameEventsReceiver.h
  29. 0 1
      lib/NetPackVisitor.h
  30. 3 22
      lib/NetPacks.h
  31. 39 2
      lib/NetPacksBase.h
  32. 1 6
      lib/NetPacksLib.cpp
  33. 6 5
      lib/mapObjects/CBank.cpp
  34. 2 18
      lib/mapObjects/CGHeroInstance.cpp
  35. 2 12
      lib/mapObjects/CGMarket.cpp
  36. 14 28
      lib/mapObjects/CGPandoraBox.cpp
  37. 11 7
      lib/mapObjects/CGTownInstance.cpp
  38. 7 18
      lib/mapObjects/CObjectHandler.cpp
  39. 8 1
      lib/mapObjects/CObjectHandler.h
  40. 20 36
      lib/mapObjects/CQuest.cpp
  41. 8 3
      lib/mapObjects/CRewardableConstructor.cpp
  42. 28 24
      lib/mapObjects/CRewardableObject.cpp
  43. 7 0
      lib/mapObjects/CRewardableObject.h
  44. 48 61
      lib/mapObjects/MiscObjects.cpp
  45. 0 1
      lib/registerTypes/RegisterTypes.h
  46. 1 1
      lib/serializer/CSerializer.h
  47. 1 1
      lib/spells/AdventureSpellMechanics.cpp
  48. 16 18
      server/CGameHandler.cpp
  49. 0 1
      server/CGameHandler.h
  50. 0 1
      test/mock/mock_IGameCallback.h

+ 2 - 2
AI/Nullkiller/AIGateway.cpp

@@ -390,7 +390,7 @@ void AIGateway::advmapSpellCast(const CGHeroInstance * caster, int spellID)
 	NET_EVENT_HANDLER;
 }
 
-void AIGateway::showInfoDialog(const std::string & text, const std::vector<Component> & components, int soundID)
+void AIGateway::showInfoDialog(EInfoWindowMode type, const std::string & text, const std::vector<Component> & components, int soundID)
 {
 	LOG_TRACE_PARAMS(logAi, "soundID '%i'", soundID);
 	NET_EVENT_HANDLER;
@@ -632,7 +632,7 @@ void AIGateway::showBlockingDialog(const std::string & text, const std::vector<C
 		// TODO: Find better way to understand it is Chest of Treasures
 		if(hero.validAndSet()
 			&& components.size() == 2
-			&& components.front().id == Component::RESOURCE
+			&& components.front().id == Component::EComponentType::RESOURCE
 			&& (nullkiller->heroManager->getHeroRole(hero) != HeroRole::MAIN
 			|| nullkiller->buildAnalyzer->getGoldPreasure() > MAX_GOLD_PEASURE))
 		{

+ 1 - 1
AI/Nullkiller/AIGateway.h

@@ -153,7 +153,7 @@ public:
 	void playerBonusChanged(const Bonus & bonus, bool gain) override;
 	void heroCreated(const CGHeroInstance *) override;
 	void advmapSpellCast(const CGHeroInstance * caster, int spellID) override;
-	void showInfoDialog(const std::string & text, const std::vector<Component> & components, int soundID) override;
+	void showInfoDialog(EInfoWindowMode type, const std::string & text, const std::vector<Component> & components, int soundID) override;
 	void requestRealized(PackageApplied * pa) override;
 	void receivedResource() override;
 	void objectRemoved(const CGObjectInstance * obj) override;

+ 2 - 1
AI/VCAI/VCAI.cpp

@@ -20,6 +20,7 @@
 #include "../../lib/CHeroHandler.h"
 #include "../../lib/CModHandler.h"
 #include "../../lib/CGameState.h"
+#include "../../lib/NetPacksBase.h"
 #include "../../lib/NetPacks.h"
 #include "../../lib/serializer/CTypeList.h"
 #include "../../lib/serializer/BinarySerializer.h"
@@ -475,7 +476,7 @@ void VCAI::advmapSpellCast(const CGHeroInstance * caster, int spellID)
 	NET_EVENT_HANDLER;
 }
 
-void VCAI::showInfoDialog(const std::string & text, const std::vector<Component> & components, int soundID)
+void VCAI::showInfoDialog(EInfoWindowMode type, const std::string & text, const std::vector<Component> & components, int soundID)
 {
 	LOG_TRACE_PARAMS(logAi, "soundID '%i'", soundID);
 	NET_EVENT_HANDLER;

+ 1 - 1
AI/VCAI/VCAI.h

@@ -186,7 +186,7 @@ public:
 	void playerBonusChanged(const Bonus & bonus, bool gain) override;
 	void heroCreated(const CGHeroInstance *) override;
 	void advmapSpellCast(const CGHeroInstance * caster, int spellID) override;
-	void showInfoDialog(const std::string & text, const std::vector<Component> & components, int soundID) override;
+	void showInfoDialog(EInfoWindowMode type, const std::string & text, const std::vector<Component> & components, int soundID) override;
 	void requestRealized(PackageApplied * pa) override;
 	void receivedResource() override;
 	void objectRemoved(const CGObjectInstance * obj) override;

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

@@ -47,6 +47,8 @@
 	"vcmi.systemOptions.framerateButton.hover"  : "Show FPS",
 	"vcmi.systemOptions.framerateButton.help"   : "{Show FPS}\n\n Toggles visibility of Frames Per Second counter in corner of game window.",
 
+	"vcmi.adventureOptions.infoBarPick.hover" : "Show Messages in Info Panel",
+	"vcmi.adventureOptions.infoBarPick.help" : "{Show Messages in Info Panel}\n\nWhenever possible, game messages from visiting map objects will be shown in info bar instead of showing up as popup windows",
 	"vcmi.adventureOptions.numericQuantities.hover" : "Numeric Creature Quantities",
 	"vcmi.adventureOptions.numericQuantities.help" : "{Numeric Creature Quantities}\n\n Shows approximate enemy creatures quantities in numeric A-B format.",
 	"vcmi.adventureOptions.forceMovementInfo.hover" : "Always Show Movement Cost",

+ 75 - 5
Mods/vcmi/config/vcmi/russian.json

@@ -25,13 +25,69 @@
 	"vcmi.server.errors.modsIncompatibility" : "Требуемые моды для загрузки игры:",
 	"vcmi.server.confirmReconnect"          : "Подключиться к предыдущей сессии?",
 
+	"vcmi.settingsMainWindow.generalTab.hover" : "Общее",
+	"vcmi.settingsMainWindow.generalTab.help"     : "Переключиться на вкладку \"Общее\", содержащее общие настройки клиента игры",
+	"vcmi.settingsMainWindow.battleTab.hover" : "Бой",
+	"vcmi.settingsMainWindow.battleTab.help"     : "Переключиться на вкладку \"Бой\", содержащее настройки поведения клиента игры в ходе битв",
+	"vcmi.settingsMainWindow.adventureTab.hover" : "Карта",
+	"vcmi.settingsMainWindow.adventureTab.help"  : "Переключиться на вкладку \"Карта\", содержащее настройки поведения клиента на карте - там, где перемещаются герои",
+
+	"vcmi.systemOptions.videoGroup" : "Видео",
+	"vcmi.systemOptions.audioGroup" : "Аудио",
+	"vcmi.systemOptions.otherGroup" : "Иное", // unused right now
+	"vcmi.systemOptions.townsGroup" : "Экран города",
+
 	"vcmi.systemOptions.fullscreenButton.hover" : "Полный экран",
 	"vcmi.systemOptions.fullscreenButton.help"  : "{Полный экран}\n\n Если выбрано, то VCMI будет работать в полноэкранном режиме, если нет - в окне",
-	"vcmi.systemOptions.resolutionButton.hover" : "Разрешение экрана",
+	"vcmi.systemOptions.resolutionButton.hover" : "Разрешение %wx%h",
 	"vcmi.systemOptions.resolutionButton.help"  : "{Разрешение экрана}\n\n Изменение разрешения экрана. Для применения нового разрешения требуется перезапуск игры.",
 	"vcmi.systemOptions.resolutionMenu.hover"   : "Выбрать разрешения экрана",
 	"vcmi.systemOptions.resolutionMenu.help"    : "Изменение разрешения экрана в игре.",
 	"vcmi.systemOptions.fullscreenFailed"       : "{Полный экран}\n\n Невозможно переключиться в полноэкранный режим - выбранное разрешение не поддерживается дисплеем!",
+	"vcmi.systemOptions.framerateButton.hover"  : "Показывать частоту кадров",
+	"vcmi.systemOptions.framerateButton.help"   : "{Показывать частоту кадров}\n\n Включить счетчик частоты кадров в углу игрового клиента",
+
+	"vcmi.adventureOptions.infoBarPick.hover" : "Сообщения в информационной панели",
+	"vcmi.adventureOptions.infoBarPick.help" : "{Сообщения в информационной панели}\n\n Если сообщения помещаются, то показывать их в информационной панели (только на интерфейсе карты).",
+	"vcmi.adventureOptions.numericQuantities.hover" : "Приблизительное число существ",
+	"vcmi.adventureOptions.numericQuantities.help" : "{Приблизительное число существ}\n\n Показывать приблизительное число существ в формате A-B вместо словесный обозначений",
+	"vcmi.adventureOptions.forceMovementInfo.hover" : "Всегда показывать стоимость перемещения",
+	"vcmi.adventureOptions.forceMovementInfo.help" : "{Всегда показывать стоимость перемещения}\n\n Заменить информацию в статусной строке на информацию о перемещении без необходимости нажатия {ALT}",
+	"vcmi.adventureOptions.showGrid.hover" : "Сетка",
+	"vcmi.adventureOptions.showGrid.help" : "{Сетка}\n\n Показывать сетку на видимой части карты.",
+	"vcmi.adventureOptions.mapSwipe.hover" : "Перемещение карты жестами",
+	"vcmi.adventureOptions.mapSwipe.help" : "{Перемещение карты жестами}\n\n Включает перемещение карты жестами на системах с сенсорным экраном. Сейчас также активируется левой кнопкой мыши.",
+	"vcmi.adventureOptions.mapScrollSpeed1.hover": "",
+	"vcmi.adventureOptions.mapScrollSpeed5.hover": "",
+	"vcmi.adventureOptions.mapScrollSpeed6.hover": "",
+	"vcmi.adventureOptions.mapScrollSpeed1.help": "Очень медленная прокрутка карты.",
+	"vcmi.adventureOptions.mapScrollSpeed5.help": "Очень быстрая прокрутка карты.",
+	"vcmi.adventureOptions.mapScrollSpeed6.help": "Мгновенная прокрутка карты.",
+
+	"vcmi.battleOptions.queueSizeLabel.hover": "Показывать очередь хода существ",
+	"vcmi.battleOptions.queueSizeNoneButton.hover": "ВЫКЛ",
+	"vcmi.battleOptions.queueSizeAutoButton.hover": "АВТО",
+	"vcmi.battleOptions.queueSizeSmallButton.hover": "МАЛ",
+	"vcmi.battleOptions.queueSizeBigButton.hover": "БОЛ",
+	"vcmi.battleOptions.queueSizeNoneButton.help": "Полностью выключает видимость очереди хода существ в битве",
+	"vcmi.battleOptions.queueSizeAutoButton.help": "Размер очереди существ зависит от разрешения (малая при высоте экрана меньше 700 пикселей, иначе большая)",
+	"vcmi.battleOptions.queueSizeSmallButton.help": "Уменьшенный размер очереди существ",
+	"vcmi.battleOptions.queueSizeBigButton.help": "Увеличенный размер очереди существ (не поддерживается при разрешении менее 700 пикселей в высоту)",
+	"vcmi.battleOptions.animationsSpeed1.hover": "",
+	"vcmi.battleOptions.animationsSpeed5.hover": "",
+	"vcmi.battleOptions.animationsSpeed6.hover": "",
+	"vcmi.battleOptions.animationsSpeed1.help": "Очень медленная скорость анимации.",
+	"vcmi.battleOptions.animationsSpeed5.help": "Очень быстрая скорость анимации.",
+	"vcmi.battleOptions.animationsSpeed6.help": "Мгновенная скорость анимации.",
+	"vcmi.battleOptions.skipBattleIntroMusic.hover": "Пропускать вступительную музыку",
+	"vcmi.battleOptions.skipBattleIntroMusic.help": "{Пропускать вступительную музыку}\n\n Пропускать музыку, которая проигрывается в начале каждой битвы. Также может быть пропущена по нажатию {ESC}",
+
+	"vcmi.otherOptions.availableCreaturesAsDwellingLabel.hover" : "Показывать доступных существ",
+	"vcmi.otherOptions.availableCreaturesAsDwellingLabel.help" : "{Показывать доступных существ}\n\n Показывать число доступных существ вместо прироста на экране города (в левом нижнем углу).",
+	"vcmi.otherOptions.creatureGrowthAsDwellingLabel.hover" : "Показывать прирост существ",
+	"vcmi.otherOptions.creatureGrowthAsDwellingLabel.help" : "{Показывать прирост существ}\n\n Показывать прирост существ вместо числа доступных существ на экране города (в левом нижнем углу).",
+	"vcmi.otherOptions.compactTownCreatureInfo.hover": "Компактное окно информации о существах",
+	"vcmi.otherOptions.compactTownCreatureInfo.help": "{Компактное окно информации о существах}\n\n Уменьшить окно информации о существах в информации о городе",
 
 	"vcmi.townHall.missingBase"             : "Сначала необходимо построить: %s",
 	"vcmi.townHall.noCreaturesToRecruit"    : "Нет существ для найма!",
@@ -66,10 +122,24 @@
 	"vcmi.questLog.hideComplete.hover" : "Скрыть завершенное",
 	"vcmi.questLog.hideComplete.help"  : "Скрыть все завершенные квесты",
 
-	"vcmi.randomMapTab.widgets.defaultTemplate"      : "default",
-	"vcmi.randomMapTab.widgets.templateLabel"        : "Template",
-	"vcmi.randomMapTab.widgets.teamAlignmentsButton" : "Setup...",
-	"vcmi.randomMapTab.widgets.teamAlignmentsLabel"  : "Team alignments",
+	"vcmi.randomMapTab.widgets.defaultTemplate"      : "(по умолчанию)",
+	"vcmi.randomMapTab.widgets.templateLabel"        : "Шаблон",
+	"vcmi.randomMapTab.widgets.teamAlignmentsButton" : "Настройка...",
+	"vcmi.randomMapTab.widgets.teamAlignmentsLabel"  : "Распределение команд",
+	"vcmi.randomMapTab.widgets.roadTypesLabel"       : "Виды дорог",
+
+	"mapObject.core.creatureBank.cyclopsStockpile.name" : "Хранилище циклопов",
+	"mapObject.core.creatureBank.dragonFlyHive.name" : "Улей летучих змиев",
+	"mapObject.core.creatureBank.dwarvenTreasury.name" : "Сокровищница гномов",
+	"mapObject.core.creatureBank.griffinConservatory.name" : "Консерватория грифонов",
+	"mapObject.core.creatureBank.inpCache.name" : "Яма бесов",
+	"mapObject.core.creatureBank.medusaStore.name" : "Склады медуз",
+	"mapObject.core.creatureBank.nagaBank.name" : "Хранилище наг",
+	"mapObject.core.crypt.crypt.name" : "Склеп",
+	"mapObject.core.derelictShip.derelictShip.name" : "Заброшенный корабль",
+	"mapObject.core.dragonUtopia.dragonUtopia.name" : "Утопия драконов",
+	"mapObject.core.pyramid.pyramid.name" : "Пирамида",
+	"mapObject.core.shipwreck.shipwreck.name" : "Кораблекрушение",
 
 	// few strings from WoG used by vcmi
 	"vcmi.stackExperience.description" : "» О п ы т   с у щ е с т в «\n\nТип существа ................... : %s\nРанг опыта ................. : %s (%i)\nОчки опыта ............... : %i\nДо следующего .. : %i\nМаксимум за битву ... : %i%% (%i)\nЧисло в отряде .... : %i\nМаксимум новичков\n без потери ранга .... : %i\nМножитель опыта ........... : %.2f\nМножитель улучшения .......... : %.2f\nОпыт после 10 ранга ........ : %i\nМаксимум новичков для сохранения\n ранга 10 при максимальном опыте : %i",

+ 0 - 1
Mods/vcmi/mod.json

@@ -32,7 +32,6 @@
 		"author" : "Команда VCMI",
 		"modType" : "Графический",
 
-		"skipValidation" : true,
 		"translations" : [
 			"config/vcmi/russian.json"
 		]

+ 0 - 7
client/CMusicHandler.cpp

@@ -77,13 +77,6 @@ CSoundHandler::CSoundHandler():
 {
 	listener(std::bind(&CSoundHandler::onVolumeChange, this, _1));
 
-	// Vectors for helper(s)
-	pickupSounds =
-	{
-		soundBase::pickup01, soundBase::pickup02, soundBase::pickup03,
-		soundBase::pickup04, soundBase::pickup05, soundBase::pickup06, soundBase::pickup07
-	};
-
 	battleIntroSounds =
 	{
 		soundBase::battle00, soundBase::battle01,

+ 0 - 1
client/CMusicHandler.h

@@ -78,7 +78,6 @@ public:
 	void ambientStopAllChannels();
 
 	// Sets
-	std::vector<soundBase::soundID> pickupSounds;
 	std::vector<soundBase::soundID> battleIntroSounds;
 };
 

+ 36 - 15
client/CPlayerInterface.cpp

@@ -324,6 +324,7 @@ void CPlayerInterface::heroMoved(const TryMoveHero & details, bool verbose)
 	if (!hero)
 		return;
 
+	adventureInt->infoBar->requestPopAll();
 	if (details.result == TryMoveHero::EMBARK || details.result == TryMoveHero::DISEMBARK)
 	{
 		if (hero->getRemovalSound())
@@ -1031,18 +1032,23 @@ void CPlayerInterface::yourTacticPhase(int distance)
 		boost::this_thread::sleep(boost::posix_time::millisec(1));
 }
 
-void CPlayerInterface::showComp(const Component &comp, std::string message)
+void CPlayerInterface::showInfoDialog(EInfoWindowMode type, const std::string &text, const std::vector<Component> & components, int soundID)
 {
 	EVENT_HANDLER_CALLED_BY_CLIENT;
-	waitWhileDialog(); //Fix for mantis #98
 
-	CCS->soundh->playSoundFromSet(CCS->soundh->pickupSounds);
-	adventureInt->infoBar->showComponent(comp, message);
-}
+	bool autoTryHover = settings["gameTweaks"]["infoBarPick"].Bool() && type == EInfoWindowMode::AUTO;
+	auto timer = type == EInfoWindowMode::INFO ? 3000 : 4500; //Implement long info windows like in HD mod
+
+	if(autoTryHover || type == EInfoWindowMode::INFO)
+	{
+		waitWhileDialog(); //Fix for mantis #98
+		adventureInt->infoBar->pushComponents(components, text, timer);
+
+		if (makingTurn && GH.listInt.size() && LOCPLINT == this)
+			CCS->soundh->playSound(static_cast<soundBase::soundID>(soundID));
+		return;
+	}
 
-void CPlayerInterface::showInfoDialog(const std::string &text, const std::vector<Component> & components, int soundID)
-{
-	EVENT_HANDLER_CALLED_BY_CLIENT;
 	if (settings["session"]["autoSkip"].Bool() && !GH.isKeyboardShiftDown())
 	{
 		return;
@@ -1093,7 +1099,7 @@ void CPlayerInterface::showInfoDialogAndWait(std::vector<Component> & components
 	std::string str;
 	text.toString(str);
 
-	showInfoDialog(str, components, 0);
+	showInfoDialog(EInfoWindowMode::MODAL, str, components, 0);
 	waitWhileDialog();
 }
 
@@ -1715,7 +1721,7 @@ void CPlayerInterface::acceptTurn()
 		auto playerColor = *cb->getPlayerID();
 
 		std::vector<Component> components;
-		components.push_back(Component(Component::FLAG, playerColor.getNum(), 0, 0));
+		components.emplace_back(Component::EComponentType::FLAG, playerColor.getNum(), 0, 0);
 		MetaString text;
 
 		const auto & optDaysWithoutCastle = cb->getPlayerState(playerColor)->daysWithoutCastle;
@@ -1773,6 +1779,16 @@ void CPlayerInterface::tryDiggging(const CGHeroInstance * h)
 
 void CPlayerInterface::updateInfo(const CGObjectInstance * specific)
 {
+	bool isHero = dynamic_cast<const CGHeroInstance *>(specific) != nullptr;
+	bool changedHero = dynamic_cast<const CGHeroInstance *>(specific) != adventureInt->curHero();
+	bool isTown = dynamic_cast<const CGTownInstance *>(specific) != nullptr;
+
+	bool update = (isHero && changedHero) || (isTown);
+	// If infobar is showing components and we request an update to hero
+	// do not force infobar tick here, it will prevents us to show components just picked up
+	if(adventureInt->infoBar->showingComponents() && !update)
+		return;
+
 	adventureInt->infoBar->showSelection();
 }
 
@@ -1883,14 +1899,16 @@ void CPlayerInterface::askToAssembleArtifact(const ArtifactLocation &al)
 void CPlayerInterface::artifactPut(const ArtifactLocation &al)
 {
 	EVENT_HANDLER_CALLED_BY_CLIENT;
-	adventureInt->infoBar->showSelection();
+	auto hero = boost::apply_visitor(HeroObjectRetriever(), al.artHolder);
+	updateInfo(hero);
 	askToAssembleArtifact(al);
 }
 
 void CPlayerInterface::artifactRemoved(const ArtifactLocation &al)
 {
 	EVENT_HANDLER_CALLED_BY_CLIENT;
-	adventureInt->infoBar->showSelection();
+	auto hero = boost::apply_visitor(HeroObjectRetriever(), al.artHolder);
+	updateInfo(hero);
 	for(auto isa : GH.listInt)
 	{
 		auto artWin = dynamic_cast<CArtifactHolder*>(isa.get());
@@ -1904,7 +1922,8 @@ void CPlayerInterface::artifactRemoved(const ArtifactLocation &al)
 void CPlayerInterface::artifactMoved(const ArtifactLocation &src, const ArtifactLocation &dst)
 {
 	EVENT_HANDLER_CALLED_BY_CLIENT;
-	adventureInt->infoBar->showSelection();
+	auto hero = boost::apply_visitor(HeroObjectRetriever(), dst.artHolder);
+	updateInfo(hero);
 
 	bool redraw = true;
 	// If a bulk transfer has arrived, then redrawing only the last art movement.
@@ -1932,7 +1951,8 @@ void CPlayerInterface::bulkArtMovementStart(size_t numOfArts)
 void CPlayerInterface::artifactAssembled(const ArtifactLocation &al)
 {
 	EVENT_HANDLER_CALLED_BY_CLIENT;
-	adventureInt->infoBar->showSelection();
+	auto hero = boost::apply_visitor(HeroObjectRetriever(), al.artHolder);
+	updateInfo(hero);
 	for(auto isa : GH.listInt)
 	{
 		auto artWin = dynamic_cast<CArtifactHolder*>(isa.get());
@@ -1944,7 +1964,8 @@ void CPlayerInterface::artifactAssembled(const ArtifactLocation &al)
 void CPlayerInterface::artifactDisassembled(const ArtifactLocation &al)
 {
 	EVENT_HANDLER_CALLED_BY_CLIENT;
-	adventureInt->infoBar->showSelection();
+	auto hero = boost::apply_visitor(HeroObjectRetriever(), al.artHolder);
+	updateInfo(hero);
 	for(auto isa : GH.listInt)
 	{
 		auto artWin = dynamic_cast<CArtifactHolder*>(isa.get());

+ 1 - 2
client/CPlayerInterface.h

@@ -168,7 +168,7 @@ public:
 	void heroMovePointsChanged(const CGHeroInstance * hero) override;
 	void heroVisitsTown(const CGHeroInstance* hero, const CGTownInstance * town) override;
 	void receivedResource() override;
-	void showInfoDialog(const std::string & text, const std::vector<Component> & components, int soundID) override;
+	void showInfoDialog(EInfoWindowMode type, const std::string & text, const std::vector<Component> & components, int soundID) override;
 	void showRecruitmentDialog(const CGDwelling *dwelling, const CArmedInstance *dst, int level) override;
 	void showShipyardDialog(const IShipyard *obj) override; //obj may be town or shipyard;
 	void showBlockingDialog(const std::string &text, const std::vector<Component> &components, QueryID askID, const int soundID, bool selection, bool cancel) override; //Show a dialog, player must take decision. If selection then he has to choose between one of given components, if cancel he is allowed to not choose. After making choice, CCallback::selectionMade should be called with number of selected component (1 - n) or 0 for cancel (if allowed) and askID.
@@ -201,7 +201,6 @@ public:
 	void playerBlocked(int reason, bool start) override;
 	void gameOver(PlayerColor player, const EVictoryLossCheckResult & victoryLossCheckResult) override;
 	void playerStartsTurn(PlayerColor player) override; //called before yourTurn on active itnerface
-	void showComp(const Component &comp, std::string message) override; //display component in the advmapint infobox
 	void saveGame(BinarySerializer & h, const int version) override; //saving
 	void loadGame(BinaryDeserializer & h, const int version) override; //loading
 	void showWorldViewEx(const std::vector<ObjectPosInfo> & objectPositions, bool showTerrain) override;

+ 0 - 1
client/Client.h

@@ -217,7 +217,6 @@ public:
 	void removeArtifact(const ArtifactLocation & al) override {};
 	bool moveArtifact(const ArtifactLocation & al1, const ArtifactLocation & al2) override {return false;};
 
-	void showCompInfo(ShowInInfobox * comp) override {};
 	void heroVisitCastle(const CGTownInstance * obj, const CGHeroInstance * hero) override {};
 	void visitCastleObjects(const CGTownInstance * obj, const CGHeroInstance * hero) override {};
 	void stopHeroVisitCastle(const CGTownInstance * obj, const CGHeroInstance * hero) override {};

+ 0 - 1
client/ClientNetPackVisitors.h

@@ -96,7 +96,6 @@ public:
 	void visitYourTurn(YourTurn & pack) override;
 	void visitSaveGameClient(SaveGameClient & pack) override;
 	void visitPlayerMessageClient(PlayerMessageClient & pack) override;
-	void visitShowInInfobox(ShowInInfobox & pack) override;
 	void visitAdvmapSpellCast(AdvmapSpellCast & pack) override;
 	void visitShowWorldViewEx(ShowWorldViewEx & pack) override;	
 	void visitOpenWindow(OpenWindow & pack) override;

+ 11 - 16
client/NetPacksClient.cpp

@@ -592,7 +592,7 @@ void ApplyClientNetPackVisitor::visitInfoWindow(InfoWindow & pack)
 	std::string str;
 	pack.text.toString(str);
 
-	if(!callInterfaceIfPresent(cl, pack.player, &CGameInterface::showInfoDialog, str, pack.components,(soundBase::soundID)pack.soundID))
+	if(!callInterfaceIfPresent(cl, pack.player, &CGameInterface::showInfoDialog, pack.type, str, pack.components,(soundBase::soundID)pack.soundID))
 		logNetwork->warn("We received InfoWindow for not our player...");
 }
 
@@ -855,11 +855,6 @@ void ApplyClientNetPackVisitor::visitPlayerMessageClient(PlayerMessageClient & p
 		LOCPLINT->cingconsole->print(str.str());
 }
 
-void ApplyClientNetPackVisitor::visitShowInInfobox(ShowInInfobox & pack)
-{
-	callInterfaceIfPresent(cl, pack.player, &IGameEventsReceiver::showComp, pack.c, pack.text.toString());
-}
-
 void ApplyClientNetPackVisitor::visitAdvmapSpellCast(AdvmapSpellCast & pack)
 {
 	cl.invalidatePaths();
@@ -880,28 +875,28 @@ void ApplyClientNetPackVisitor::visitOpenWindow(OpenWindow & pack)
 {
 	switch(pack.window)
 	{
-	case OpenWindow::RECRUITMENT_FIRST:
-	case OpenWindow::RECRUITMENT_ALL:
+	case EOpenWindowMode::RECRUITMENT_FIRST:
+	case EOpenWindowMode::RECRUITMENT_ALL:
 		{
 			const CGDwelling *dw = dynamic_cast<const CGDwelling*>(cl.getObj(ObjectInstanceID(pack.id1)));
 			const CArmedInstance *dst = dynamic_cast<const CArmedInstance*>(cl.getObj(ObjectInstanceID(pack.id2)));
-			callInterfaceIfPresent(cl, dst->tempOwner, &IGameEventsReceiver::showRecruitmentDialog, dw, dst, pack.window == OpenWindow::RECRUITMENT_FIRST ? 0 : -1);
+			callInterfaceIfPresent(cl, dst->tempOwner, &IGameEventsReceiver::showRecruitmentDialog, dw, dst, pack.window == EOpenWindowMode::RECRUITMENT_FIRST ? 0 : -1);
 		}
 		break;
-	case OpenWindow::SHIPYARD_WINDOW:
+	case EOpenWindowMode::SHIPYARD_WINDOW:
 		{
 			const IShipyard *sy = IShipyard::castFrom(cl.getObj(ObjectInstanceID(pack.id1)));
 			callInterfaceIfPresent(cl, sy->o->tempOwner, &IGameEventsReceiver::showShipyardDialog, sy);
 		}
 		break;
-	case OpenWindow::THIEVES_GUILD:
+	case EOpenWindowMode::THIEVES_GUILD:
 		{
 			//displays Thieves' Guild window (when hero enters Den of Thieves)
 			const CGObjectInstance *obj = cl.getObj(ObjectInstanceID(pack.id2));
 			callInterfaceIfPresent(cl, PlayerColor(pack.id1), &IGameEventsReceiver::showThievesGuildWindow, obj);
 		}
 		break;
-	case OpenWindow::UNIVERSITY_WINDOW:
+	case EOpenWindowMode::UNIVERSITY_WINDOW:
 		{
 			//displays University window (when hero enters University on adventure map)
 			const IMarket *market = IMarket::castFrom(cl.getObj(ObjectInstanceID(pack.id1)));
@@ -909,7 +904,7 @@ void ApplyClientNetPackVisitor::visitOpenWindow(OpenWindow & pack)
 			callInterfaceIfPresent(cl, hero->tempOwner, &IGameEventsReceiver::showUniversityWindow, market, hero);
 		}
 		break;
-	case OpenWindow::MARKET_WINDOW:
+	case EOpenWindowMode::MARKET_WINDOW:
 		{
 			//displays Thieves' Guild window (when hero enters Den of Thieves)
 			const CGObjectInstance *obj = cl.getObj(ObjectInstanceID(pack.id1));
@@ -918,7 +913,7 @@ void ApplyClientNetPackVisitor::visitOpenWindow(OpenWindow & pack)
 			callInterfaceIfPresent(cl, cl.getTile(obj->visitablePos())->visitableObjects.back()->tempOwner, &IGameEventsReceiver::showMarketWindow, market, hero);
 		}
 		break;
-	case OpenWindow::HILL_FORT_WINDOW:
+	case EOpenWindowMode::HILL_FORT_WINDOW:
 		{
 			//displays Hill fort window
 			const CGObjectInstance *obj = cl.getObj(ObjectInstanceID(pack.id1));
@@ -926,12 +921,12 @@ void ApplyClientNetPackVisitor::visitOpenWindow(OpenWindow & pack)
 			callInterfaceIfPresent(cl, cl.getTile(obj->visitablePos())->visitableObjects.back()->tempOwner, &IGameEventsReceiver::showHillFortWindow, obj, hero);
 		}
 		break;
-	case OpenWindow::PUZZLE_MAP:
+	case EOpenWindowMode::PUZZLE_MAP:
 		{
 			callInterfaceIfPresent(cl, PlayerColor(pack.id1), &IGameEventsReceiver::showPuzzleMap);
 		}
 		break;
-	case OpenWindow::TAVERN_WINDOW:
+	case EOpenWindowMode::TAVERN_WINDOW:
 		const CGObjectInstance *obj1 = cl.getObj(ObjectInstanceID(pack.id1)),
 								*obj2 = cl.getObj(ObjectInstanceID(pack.id2));
 		callInterfaceIfPresent(cl, obj1->tempOwner, &IGameEventsReceiver::showTavernWindow, obj2);

+ 3 - 1
client/adventureMap/CAdvMapInt.cpp

@@ -86,7 +86,7 @@ CAdvMapInt::CAdvMapInt():
 	statusbar(CGStatusBar::create(ADVOPT.statusbarX,ADVOPT.statusbarY,ADVOPT.statusbarG)),
 	heroList(new CHeroList(ADVOPT.hlistSize, Point(ADVOPT.hlistX, ADVOPT.hlistY), ADVOPT.hlistAU, ADVOPT.hlistAD)),
 	townList(new CTownList(ADVOPT.tlistSize, Point(ADVOPT.tlistX, ADVOPT.tlistY), ADVOPT.tlistAU, ADVOPT.tlistAD)),
-	infoBar(new CInfoBar(Rect(ADVOPT.infoboxX, ADVOPT.infoboxY, 192, 192))),
+	infoBar(new CInfoBar(Point(ADVOPT.infoboxX, ADVOPT.infoboxY))),
 	resdatabar(new CResDataBar),
 	mapAudio(new MapAudioPlayer()),
 	terrain(new MapView(Point(ADVOPT.advmapX, ADVOPT.advmapY), Point(ADVOPT.advmapW, ADVOPT.advmapH))),
@@ -869,6 +869,8 @@ boost::optional<Point> CAdvMapInt::keyToMoveDirection(const SDL_Keycode & key)
 void CAdvMapInt::select(const CArmedInstance *sel, bool centerView)
 {
 	assert(sel);
+	if(selection != sel)
+		infoBar->popAll();
 	selection = sel;
 	mapAudio->onSelectionChanged(sel);
 	if(centerView)

+ 199 - 12
client/adventureMap/CInfoBar.cpp

@@ -15,6 +15,7 @@
 
 #include "../widgets/CComponent.h"
 #include "../widgets/Images.h"
+#include "../windows/CMessage.h"
 #include "../widgets/TextControls.h"
 #include "../widgets/MiscWidgets.h"
 #include "../windows/InfoWindows.h"
@@ -29,7 +30,7 @@
 #include "../../lib/mapObjects/CGTownInstance.h"
 
 CInfoBar::CVisibleInfo::CVisibleInfo()
-	: CIntObject(0, Point(8, 12))
+	: CIntObject(0, Point(offset_x, offset_y))
 {
 }
 
@@ -163,16 +164,67 @@ CInfoBar::VisibleGameStatusInfo::VisibleGameStatusInfo()
 	}
 }
 
-CInfoBar::VisibleComponentInfo::VisibleComponentInfo(const Component & compToDisplay, std::string message)
+CInfoBar::VisibleComponentInfo::VisibleComponentInfo(const std::vector<Component> & compsToDisplay, std::string message, int textH, bool tiny)
 {
 	OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE);
 
 	background = std::make_shared<CPicture>("ADSTATOT", 1, 0);
+	auto fullRect = Rect(0, 0, data_width, data_height);
+	auto textRect = fullRect;
+	auto imageRect = fullRect;
+	auto font = FONT_SMALL;
+	auto maxComponents = 2; 
 
-	comp = std::make_shared<CComponent>(compToDisplay);
-	comp->moveTo(Point(pos.x+47, pos.y+50));
+	if(!compsToDisplay.empty())
+	{
+		auto size = CComponent::large;
+		if(compsToDisplay.size() > 2)
+		{
+			size = CComponent::medium;
+			font = FONT_TINY;
+		}
+		if(!message.empty())
+		{
+			textRect = Rect(CInfoBar::offset,
+							CInfoBar::offset,
+							data_width - 2 * CInfoBar::offset,
+							textH);
+			imageRect = Rect(CInfoBar::offset,
+							 textH,
+							 data_width - 2 * CInfoBar::offset,
+							 CInfoBar::data_height - 2* CInfoBar::offset - textH);
+		}
+		
+		if(compsToDisplay.size() > 4) {
+			maxComponents = 3; 
+			size = CComponent::small;
+		}
+		if(compsToDisplay.size() > 6)
+			maxComponents = 4;
+
+		std::vector<std::shared_ptr<CComponent>> vect;
 
-	text = std::make_shared<CTextBox>(message, Rect(10, 4, 160, 50), 0, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE);
+		for(const auto & c : compsToDisplay)
+			vect.emplace_back(std::make_shared<CComponent>(c, size, font));
+
+		comps = std::make_shared<CComponentBox>(vect, imageRect, 4, 4, 1, maxComponents);
+	}
+	else
+		font = tiny ? FONT_TINY : font;
+
+	if(!message.empty())
+		text = std::make_shared<CTextBox>(message, textRect, 0, font, ETextAlignment::CENTER, Colors::WHITE);
+}
+
+int CInfoBar::getEstimatedComponentHeight(int numComps) const
+{
+	if (numComps > 8) //Bigger than 8 components - return invalid value
+		return std::numeric_limits<int>::max();
+	else if (numComps > 2)
+		return 160; // 32px * 1 row + 20 to offset
+	else if (numComps)
+		return 118; // 118 px to offset
+	return 0;
 }
 
 void CInfoBar::playNewDaySound()
@@ -216,7 +268,7 @@ void CInfoBar::tick()
 {
 	removeUsedEvents(TIME);
 	if(GH.topInt() == adventureInt)
-		showSelection();
+		popComponents(true);
 }
 
 void CInfoBar::clickLeft(tribool down, bool previousState)
@@ -228,7 +280,7 @@ void CInfoBar::clickLeft(tribool down, bool previousState)
 		else if(state == GAME)
 			showDate();
 		else
-			showSelection();
+			popComponents(true);
 	}
 }
 
@@ -256,6 +308,10 @@ CInfoBar::CInfoBar(const Rect & position)
 	reset();
 }
 
+CInfoBar::CInfoBar(const Point & position): CInfoBar(Rect(position.x, position.y, width, height))
+{
+}
+
 void CInfoBar::showDate()
 {
 	OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE);
@@ -266,13 +322,144 @@ void CInfoBar::showDate()
 	redraw();
 }
 
-void CInfoBar::showComponent(const Component & comp, std::string message)
+
+void CInfoBar::pushComponents(const std::vector<Component> & components, std::string message, int timer)
+{
+	auto actualPush = [&](const std::vector<Component> & components, std::string message, int timer, size_t max){
+		std::vector<Component> vect = components; //I do not know currently how to avoid copy here
+		while(!vect.empty())
+		{
+			std::vector<Component> sender =  {vect.begin(), vect.begin() + std::min(vect.size(), max)};
+			prepareComponents(sender, message, timer);
+			vect.erase(vect.begin(), vect.begin() + std::min(vect.size(), max));
+		};
+	};
+	if(shouldPopAll)
+		popAll();
+	if(components.empty())
+		prepareComponents(components, message, timer);
+	else
+	{
+		std::array<std::pair<std::vector<Component>, int>, 10> reward_map;
+		for(const auto & c : components)
+		{
+			switch(c.id)
+			{
+				case Component::EComponentType::PRIM_SKILL:
+				case Component::EComponentType::EXPERIENCE: 
+					reward_map.at(0).first.push_back(c);
+					reward_map.at(0).second = 8; //At most 8, cannot be more
+					break;
+				case Component::EComponentType::SEC_SKILL:
+					reward_map.at(1).first.push_back(c);
+					reward_map.at(1).second = 4; //At most 4
+					break;
+				case Component::EComponentType::SPELL: 
+					reward_map.at(2).first.push_back(c);
+					reward_map.at(2).second = 4; //At most 4
+					break;
+				case Component::EComponentType::ARTIFACT:
+					reward_map.at(3).first.push_back(c);
+					reward_map.at(3).second = 4; //At most 4, too long names
+					break;
+				case Component::EComponentType::CREATURE:
+					reward_map.at(4).first.push_back(c);
+					reward_map.at(4).second = 4; //At most 4, too long names
+					break;
+				case Component::EComponentType::RESOURCE:
+					reward_map.at(5).first.push_back(c);
+					reward_map.at(5).second = 7; //At most 7
+					break;
+				case Component::EComponentType::MORALE: 
+				case Component::EComponentType::LUCK:
+					reward_map.at(6).first.push_back(c);
+					reward_map.at(6).second = 2; //At most 2 - 1 for morale + 1 for luck
+					break;
+				case Component::EComponentType::BUILDING:
+					reward_map.at(7).first.push_back(c);
+					reward_map.at(7).second = 1; //At most 1 - only large icons available AFAIK
+					break;
+				case Component::EComponentType::HERO_PORTRAIT:
+					reward_map.at(8).first.push_back(c);
+					reward_map.at(8).second = 1; //I do not think than we even can get more than 1 hero
+					break;
+				case Component::EComponentType::FLAG:
+					reward_map.at(9).first.push_back(c);
+					reward_map.at(9).second = 1; //I do not think than we even can get more than 1 player in notification
+					break;
+				default:
+					logGlobal->warn("Invalid component received!");
+			}
+		}
+		
+		for(const auto & kv : reward_map)
+			if(!kv.first.empty())
+				actualPush(kv.first, message, timer, kv.second);
+	}
+	popComponents();
+}
+
+void CInfoBar::prepareComponents(const std::vector<Component> & components, std::string message, int timer)
+{
+	auto imageH = getEstimatedComponentHeight(components.size()) + (components.empty() ? 0 : 2 * CInfoBar::offset);
+	auto textH = CMessage::guessHeight(message,CInfoBar::data_width - 2 * CInfoBar::offset, FONT_SMALL);
+	auto tinyH = CMessage::guessHeight(message,CInfoBar::data_width - 2 * CInfoBar::offset, FONT_TINY);
+	auto header = CMessage::guessHeader(message);
+	auto headerH = CMessage::guessHeight(header, CInfoBar::data_width - 2 * CInfoBar::offset, FONT_SMALL);
+	auto headerTinyH = CMessage::guessHeight(header, CInfoBar::data_width - 2 * CInfoBar::offset, FONT_TINY);
+
+	// Order matters - priority form should be chosen first
+	if(imageH + textH < CInfoBar::data_height)
+		pushComponents(components, message, textH, false, timer);
+	else if(!imageH && tinyH < CInfoBar::data_height)
+		pushComponents(components, message, tinyH, true, timer);
+	else if(imageH + headerH < CInfoBar::data_height)
+		pushComponents(components, header, headerH, false, timer);
+	else if(imageH + headerTinyH < CInfoBar::data_height - 2 * CInfoBar::offset)
+		pushComponents(components, header, headerTinyH, true, timer);
+	else
+		pushComponents(components, "", 0, false, timer);
+
+	return;
+}
+
+void CInfoBar::requestPopAll()
+{
+	shouldPopAll = true;
+}
+
+void CInfoBar::popAll()
+{
+	componentsQueue = {};
+	shouldPopAll = false;
+}
+
+void CInfoBar::popComponents(bool remove)
 {
 	OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE);
-	state = COMPONENT;
-	visibleInfo = std::make_shared<VisibleComponentInfo>(comp, message);
-	setTimer(3000);
-	redraw();
+	if(remove && !componentsQueue.empty())
+		componentsQueue.pop();
+	if(!componentsQueue.empty())
+	{
+		state = COMPONENT;
+		const auto & extracted = componentsQueue.front();
+		visibleInfo = std::make_shared<VisibleComponentInfo>(extracted.first);
+		setTimer(extracted.second);
+		redraw();
+		return;
+	}
+	showSelection();
+}
+
+void CInfoBar::pushComponents(const std::vector<Component> & comps, std::string message, int textH, bool tiny, int timer)
+{
+	OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE);
+	componentsQueue.emplace(VisibleComponentInfo::Cache(comps, message, textH, tiny), timer);
+}
+
+bool CInfoBar::showingComponents()
+{
+	return state == COMPONENT;
 }
 
 void CInfoBar::startEnemyTurn(PlayerColor color)

+ 54 - 4
client/adventureMap/CInfoBar.h

@@ -23,6 +23,7 @@ VCMI_LIB_NAMESPACE_END
 class CAnimImage;
 class CShowableAnim;
 class CComponent;
+class CComponentBox;
 class CHeroTooltip;
 class CTownTooltip;
 class CLabel;
@@ -31,10 +32,20 @@ class CTextBox;
 /// Info box which shows next week/day information, hold the current date
 class CInfoBar : public CIntObject
 {
+private:
+	/// Infobar actually have a fixed size
+	/// Declare before to compute correct size of widgets
+	static constexpr int width = 192;
+	static constexpr int height = 192;
+	static constexpr int offset = 4;
+
 	//all visible information located in one object - for ease of replacing
 	class CVisibleInfo : public CIntObject
 	{
 	public:
+		static constexpr int offset_x = 8;
+		static constexpr int offset_y = 12;
+
 		void show(SDL_Surface * to) override;
 
 	protected:
@@ -44,6 +55,9 @@ class CInfoBar : public CIntObject
 		CVisibleInfo();
 	};
 
+	static constexpr int data_width = width - 2 * CVisibleInfo::offset_x;
+	static constexpr int data_height = height - 2 * CVisibleInfo::offset_y;
+
 	class EmptyVisibleInfo : public CVisibleInfo
 	{
 	public:
@@ -97,10 +111,24 @@ class CInfoBar : public CIntObject
 
 	class VisibleComponentInfo : public CVisibleInfo
 	{
-		std::shared_ptr<CComponent> comp;
+		std::shared_ptr<CComponentBox> comps;
 		std::shared_ptr<CTextBox> text;
 	public:
-		VisibleComponentInfo(const Component & compToDisplay, std::string message);
+		struct Cache 
+		{
+			std::vector<Component> compsToDisplay;
+			std::string message;
+			int textH;
+			bool tiny;
+			Cache(std::vector<Component> comps, std::string msg, int textH, bool tiny):
+				compsToDisplay(std::move(comps)),
+				message(std::move(msg)),
+				textH(textH),
+				tiny(tiny)
+			{}
+		};
+		VisibleComponentInfo(const Cache & c): VisibleComponentInfo(c.compsToDisplay, c.message, c.textH, c.tiny) {}
+		VisibleComponentInfo(const std::vector<Component> & compsToDisplay, std::string message, int textH, bool tiny);
 	};
 
 	enum EState
@@ -110,6 +138,15 @@ class CInfoBar : public CIntObject
 
 	std::shared_ptr<CVisibleInfo> visibleInfo;
 	EState state;
+	bool shouldPopAll = false;
+
+	std::queue<std::pair<VisibleComponentInfo::Cache, int>> componentsQueue;
+
+	//private helper for showing components
+	void showComponents(const std::vector<Component> & comps, std::string message, int textH, bool tiny, int timer);
+	void pushComponents(const std::vector<Component> & comps, std::string message, int textH, bool tiny, int timer);
+	void prepareComponents(const std::vector<Component> & comps, std::string message, int timer);
+	void popComponents(bool remove = false);
 
 	//removes all information about current state, deactivates timer (if any)
 	void reset();
@@ -123,12 +160,19 @@ class CInfoBar : public CIntObject
 	void playNewDaySound();
 public:
 	CInfoBar(const Rect & pos);
+	CInfoBar(const Point & pos);
 
 	/// show new day/week animation
 	void showDate();
 
-	/// show component for 3 seconds. Used to display picked up resources
-	void showComponent(const Component & comp, std::string message);
+	/// show components for 3 seconds. Used to display picked up resources. Can display up to 8 components
+	void pushComponents(const std::vector<Component> & comps, std::string message, int timer = 3000);
+
+	/// Remove all queued components
+	void popAll();
+
+	/// Request infobar to pop all after next InfoWindow arrives.
+	void requestPopAll();
 
 	/// print enemy turn progress
 	void startEnemyTurn(PlayerColor color);
@@ -142,5 +186,11 @@ public:
 
 	/// for 3 seconds shows amount of town halls and players status
 	void showGameStatus();
+
+	/// check if infobar is showed something about pickups
+	bool showingComponents();
+
+	/// get estimated component height for InfoBar
+	int getEstimatedComponentHeight(int numComps) const;
 };
 

+ 44 - 23
client/widgets/CComponent.cpp

@@ -33,22 +33,22 @@
 #include "../../lib/NetPacksBase.h"
 #include "../../lib/CArtHandler.h"
 
-CComponent::CComponent(Etype Type, int Subtype, int Val, ESize imageSize)
-	: perDay(false)
+CComponent::CComponent(Etype Type, int Subtype, int Val, ESize imageSize, EFonts font):
+	perDay(false)
 {
-	init(Type, Subtype, Val, imageSize);
+	init(Type, Subtype, Val, imageSize, font);
 }
 
-CComponent::CComponent(const Component & c, ESize imageSize)
+CComponent::CComponent(const Component & c, ESize imageSize, EFonts font)
 	: perDay(false)
 {
-	if(c.id == Component::RESOURCE && c.when==-1)
+	if(c.id == Component::EComponentType::RESOURCE && c.when==-1)
 		perDay = true;
 
-	init((Etype)c.id, c.subtype, c.val, imageSize);
+	init((Etype)c.id, c.subtype, c.val, imageSize, font);
 }
 
-void CComponent::init(Etype Type, int Subtype, int Val, ESize imageSize)
+void CComponent::init(Etype Type, int Subtype, int Val, ESize imageSize, EFonts fnt)
 {
 	OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE);
 
@@ -58,6 +58,7 @@ void CComponent::init(Etype Type, int Subtype, int Val, ESize imageSize)
 	subtype = Subtype;
 	val = Val;
 	size = imageSize;
+	font = fnt;
 
 	assert(compType < typeInvalid);
 	assert(size < sizeInvalid);
@@ -67,13 +68,20 @@ void CComponent::init(Etype Type, int Subtype, int Val, ESize imageSize)
 	pos.w = image->pos.w;
 	pos.h = image->pos.h;
 
-	EFonts font = FONT_SMALL;
 	if (imageSize < small)
-		font = FONT_TINY; //other sizes?
+		font = FONT_TINY; //for tiny images - tiny font
 
 	pos.h += 4; //distance between text and image
 
-	std::vector<std::string> textLines = CMessage::breakText(getSubtitle(), std::max<int>(80, pos.w), font);
+	auto max = 80;
+	if (size < large)
+		max = 72;
+	if (size < medium)
+		max = 40;
+	if (size < small)
+		max = 30;
+
+	std::vector<std::string> textLines = CMessage::breakText(getSubtitle(), std::max<int>(max, pos.w), font);
 	for(auto & line : textLines)
 	{
 		int height = static_cast<int>(graphics->fonts[font]->getLineHeight());
@@ -93,13 +101,13 @@ const std::vector<std::string> CComponent::getFileName()
 {
 	static const std::string  primSkillsArr [] = {"PSKIL32",        "PSKIL32",        "PSKIL42",        "PSKILL"};
 	static const std::string  secSkillsArr [] =  {"SECSK32",        "SECSK32",        "SECSKILL",       "SECSK82"};
-	static const std::string  resourceArr [] =   {"SMALRES",        "RESOURCE",       "RESOUR82",       "RESOUR82"};
-	static const std::string  creatureArr [] =   {"CPRSMALL",       "CPRSMALL",       "TWCRPORT",       "TWCRPORT"};
+	static const std::string  resourceArr [] =   {"SMALRES",        "RESOURCE",       "RESOURCE",       "RESOUR82"};
+	static const std::string  creatureArr [] =   {"CPRSMALL",       "CPRSMALL",       "CPRSMALL",       "TWCRPORT"};
 	static const std::string  artifactArr[]  =   {"Artifact",       "Artifact",       "Artifact",       "Artifact"};
-	static const std::string  spellsArr [] =     {"SpellInt",       "SpellInt",       "SPELLSCR",       "SPELLSCR"};
+	static const std::string  spellsArr [] =     {"SpellInt",       "SpellInt",       "SpellInt",       "SPELLSCR"};
 	static const std::string  moraleArr [] =     {"IMRL22",         "IMRL30",         "IMRL42",         "imrl82"};
 	static const std::string  luckArr [] =       {"ILCK22",         "ILCK30",         "ILCK42",         "ilck82"};
-	static const std::string  heroArr [] =       {"PortraitsSmall", "PortraitsSmall", "PortraitsLarge", "PortraitsLarge"};
+	static const std::string  heroArr [] =       {"PortraitsSmall", "PortraitsSmall", "PortraitsSmall", "PortraitsLarge"};
 	static const std::string  flagArr [] =       {"CREST58",        "CREST58",        "CREST58",        "CREST58"};
 
 	auto gen = [](const std::string * arr)
@@ -136,7 +144,7 @@ size_t CComponent::getIndex()
 	case creature:   return CGI->creatures()->getByIndex(subtype)->getIconIndex();
 	case artifact:   return CGI->artifacts()->getByIndex(subtype)->getIconIndex();
 	case experience: return 4;
-	case spell:      return subtype;
+	case spell:      return (size < large) ? subtype + 1 : subtype;
 	case morale:     return val+3;
 	case luck:       return val+3;
 	case building:   return val;
@@ -336,9 +344,6 @@ Point CComponentBox::getOrTextPos(CComponent *left, CComponent *right)
 
 int CComponentBox::getDistance(CComponent *left, CComponent *right)
 {
-	static const int betweenImagesMin = 20;
-	static const int betweenSubtitlesMin = 10;
-
 	int leftSubtitle  = ( left->pos.w -  left->image->pos.w) / 2;
 	int rightSubtitle = (right->pos.w - right->image->pos.w) / 2;
 	int subtitlesOffset = leftSubtitle + rightSubtitle;
@@ -348,8 +353,6 @@ int CComponentBox::getDistance(CComponent *left, CComponent *right)
 
 void CComponentBox::placeComponents(bool selectable)
 {
-	static const int betweenRows = 22;
-
 	OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE);
 	if (components.empty())
 		return;
@@ -384,7 +387,7 @@ void CComponentBox::placeComponents(bool selectable)
 
 		//start next row
 		if ((pos.w != 0 && rows.back().width + comp->pos.w + distance > pos.w) // row is full
-			|| rows.back().comps >= 4) // no more than 4 comps per row
+			|| rows.back().comps >= componentsInRow)
 		{
 			prevComp = nullptr;
 			rows.push_back (RowData (0,0,0));
@@ -450,7 +453,16 @@ void CComponentBox::placeComponents(bool selectable)
 }
 
 CComponentBox::CComponentBox(std::vector<std::shared_ptr<CComponent>> _components, Rect position):
-	components(_components)
+	CComponentBox(_components, position, defaultBetweenImagesMin, defaultBetweenSubtitlesMin, defaultBetweenRows, defaultComponentsInRow)
+{
+}
+
+CComponentBox::CComponentBox(std::vector<std::shared_ptr<CComponent>> _components, Rect position, int betweenImagesMin, int betweenSubtitlesMin, int betweenRows, int componentsInRow):
+	components(_components),
+	betweenImagesMin(betweenImagesMin),
+	betweenSubtitlesMin(betweenSubtitlesMin),
+	betweenRows(betweenRows),
+	componentsInRow(componentsInRow)
 {
 	type |= REDRAW_PARENT;
 	pos = position + pos.topLeft();
@@ -458,8 +470,17 @@ CComponentBox::CComponentBox(std::vector<std::shared_ptr<CComponent>> _component
 }
 
 CComponentBox::CComponentBox(std::vector<std::shared_ptr<CSelectableComponent>> _components, Rect position, std::function<void(int newID)> _onSelect):
+	CComponentBox(_components, position, _onSelect, defaultBetweenImagesMin, defaultBetweenSubtitlesMin, defaultBetweenRows, defaultComponentsInRow)
+{
+}
+
+CComponentBox::CComponentBox(std::vector<std::shared_ptr<CSelectableComponent>> _components, Rect position, std::function<void(int newID)> _onSelect, int betweenImagesMin, int betweenSubtitlesMin, int betweenRows, int componentsInRow):
 	components(_components.begin(), _components.end()),
-	onSelect(_onSelect)
+	onSelect(_onSelect),
+	betweenImagesMin(betweenImagesMin),
+	betweenSubtitlesMin(betweenSubtitlesMin),
+	betweenRows(betweenRows),
+	componentsInRow(componentsInRow)
 {
 	type |= REDRAW_PARENT;
 	pos = position + pos.topLeft();

+ 17 - 4
client/widgets/CComponent.h

@@ -47,13 +47,14 @@ private:
 	void setSurface(std::string defName, int imgPos);
 	std::string getSubtitleInternal();
 
-	void init(Etype Type, int Subtype, int Val, ESize imageSize);
+	void init(Etype Type, int Subtype, int Val, ESize imageSize, EFonts font = FONT_SMALL);
 
 public:
 	std::shared_ptr<CAnimImage> image;
 
 	Etype compType; //component type
 	ESize size; //component size.
+	EFonts font; //Font size of label
 	int subtype; //type-dependant subtype. See getSomething methods for details
 	int val; // value \ strength \ amount of component. See getSomething methods for details
 	bool perDay; // add "per day" text to subtitle
@@ -61,8 +62,8 @@ public:
 	std::string getDescription();
 	std::string getSubtitle();
 
-	CComponent(Etype Type, int Subtype, int Val = 0, ESize imageSize=large);
-	CComponent(const Component &c, ESize imageSize=large);
+	CComponent(Etype Type, int Subtype, int Val = 0, ESize imageSize=large, EFonts font = FONT_SMALL);
+	CComponent(const Component &c, ESize imageSize=large, EFonts font = FONT_SMALL);
 
 	void clickRight(tribool down, bool previousState) override; //call-in
 };
@@ -94,6 +95,16 @@ class CComponentBox : public CIntObject
 	std::shared_ptr<CSelectableComponent> selected;
 	std::function<void(int newID)> onSelect;
 
+	static constexpr int defaultBetweenImagesMin = 20;
+	static constexpr int defaultBetweenSubtitlesMin = 10;
+	static constexpr int defaultBetweenRows = 22;
+	static constexpr int defaultComponentsInRow = 4;
+
+	int betweenImagesMin;
+	int betweenSubtitlesMin;
+	int betweenRows;
+	int componentsInRow;
+
 	void selectionChanged(std::shared_ptr<CSelectableComponent> newSelection);
 
 	//get position of "or" text between these comps
@@ -108,11 +119,13 @@ public:
 	/// return index of selected item
 	int selectedIndex();
 
-	/// constructor for non-selectable components
+	/// constructors for non-selectable components
 	CComponentBox(std::vector<std::shared_ptr<CComponent>> components, Rect position);
+	CComponentBox(std::vector<std::shared_ptr<CComponent>> components, Rect position, int betweenImagesMin, int betweenSubtitlesMin, int betweenRows, int componentsInRow);
 
 	/// constructor for selectable components
 	/// will also create "or" labels between components
 	/// onSelect - optional function that will be called every time on selection change
 	CComponentBox(std::vector<std::shared_ptr<CSelectableComponent>> components, Rect position, std::function<void(int newID)> onSelect = nullptr);
+	CComponentBox(std::vector<std::shared_ptr<CSelectableComponent>> components, Rect position, std::function<void(int newID)> onSelect, int betweenImagesMin, int betweenSubtitlesMin, int betweenRows, int componentsInRow);
 };

+ 1 - 1
client/widgets/MiscWidgets.cpp

@@ -270,7 +270,7 @@ void CArmyTooltip::init(const InfoAboutArmy &army)
 			}
 		}
 
-		subtitles.push_back(std::make_shared<CLabel>(slotsPos[slot.first.getNum()].x + 17, slotsPos[slot.first.getNum()].y + 41, FONT_TINY, ETextAlignment::CENTER, Colors::WHITE, subtitle));
+		subtitles.push_back(std::make_shared<CLabel>(slotsPos[slot.first.getNum()].x + 17, slotsPos[slot.first.getNum()].y + 39, FONT_TINY, ETextAlignment::CENTER, Colors::WHITE, subtitle));
 	}
 
 }

+ 19 - 0
client/windows/CMessage.cpp

@@ -202,6 +202,25 @@ std::vector<std::string> CMessage::breakText( std::string text, size_t maxLineWi
 	return ret;
 }
 
+std::string CMessage::guessHeader(const std::string & msg)
+{
+	size_t begin = 0;
+	std::string delimeters = "{}";
+	size_t start = msg.find_first_of(delimeters[0], begin);
+	size_t end = msg.find_first_of(delimeters[1], start);
+	if(start > msg.size() || end > msg.size())
+		return "";
+	return msg.substr(begin, end);
+}
+
+int CMessage::guessHeight(const std::string & txt, int width, EFonts font)
+{
+	const auto f = graphics->fonts[font];
+	auto lines = CMessage::breakText(txt, width, font);
+	int lineHeight = static_cast<int>(f->getLineHeight());
+	return lineHeight * (int)lines.size();
+}
+
 void CMessage::drawIWindow(CInfoWindow * ret, std::string text, PlayerColor player)
 {
 	bool blitOr = false;

+ 6 - 0
client/windows/CMessage.h

@@ -32,6 +32,12 @@ public:
 	/// split text in lines
 	static std::vector<std::string> breakText(std::string text, size_t maxLineWidth, EFonts font);
 
+	/// Try to guess a header of a message
+	static std::string guessHeader(const std::string & msg);
+
+	/// For convenience
+	static int guessHeight(const std::string & string, int width, EFonts fnt);
+
 	/// constructor
 	static void init();
 	/// destructor

+ 8 - 2
client/windows/settings/AdventureOptionsTab.cpp

@@ -105,11 +105,15 @@ AdventureOptionsTab::AdventureOptionsTab()
 	addCallback("mapSwipeChanged", [](bool value)
 	{
 #if defined(VCMI_MOBILE)
-		setBoolSetting("general", "swipe", value);
+		return setBoolSetting("general", "swipe", value);
 #else
-		setBoolSetting("general", "swipeDesktop", value);
+		return setBoolSetting("general", "swipeDesktop", value);
 #endif
 	});
+	addCallback("infoBarPickChanged", [](bool value)
+	{
+		return setBoolSetting("gameTweaks", "infoBarPick", value);
+	});
 	build(config);
 
 	std::shared_ptr<CToggleGroup> playerHeroSpeedToggle = widget<CToggleGroup>("heroMovementSpeedPicker");
@@ -142,4 +146,6 @@ AdventureOptionsTab::AdventureOptionsTab()
 #else
 	mapSwipeCheckbox->setSelected(settings["general"]["swipeDesktop"].Bool());
 #endif
+	std::shared_ptr<CToggleButton> infoBarPickCheckbox = widget<CToggleButton>("infoBarPickCheckbox");
+	infoBarPickCheckbox->setSelected(settings["gameTweaks"]["infoBarPick"].Bool());
 }

+ 1 - 1
config/objects/rewardableOncePerHero.json

@@ -125,7 +125,7 @@
 								{ "heroLevel" :  4, "secondary" : { "diplomacy" : 3 } }
 							]
 						},
-						"message" : 59,
+						"message" : 66,
 						"primary" : { 
 							"attack" : 2,
 							"defence" : 2,

+ 5 - 0
config/schemas/settings.json

@@ -503,6 +503,7 @@
 				"numericCreaturesQuantities",
 				"availableCreaturesAsDwellingLabel",
 				"compactTownCreatureInfo",
+				"infoBarPick",
 				"skipBattleIntroMusic"
 			],
 			"properties": {
@@ -526,6 +527,10 @@
 					"type" : "boolean",
 					"default" : false
 				},
+				"infoBarPick" : {
+					"type" : "boolean",
+					"default" : false
+				},
 				"skipBattleIntroMusic" : {
 					"type" : "boolean",
 					"default" : false

+ 12 - 1
config/widgets/settings/adventureOptionsTab.json

@@ -364,6 +364,10 @@
 				{
 					"position": {"x": 45, "y": 385},
 					"text": "vcmi.adventureOptions.mapSwipe.hover"
+				},
+				{
+					"position": {"x": 45, "y": 415},
+					"text": "vcmi.adventureOptions.infoBarPick.hover"
 				}
 			]
 		},
@@ -399,6 +403,13 @@
 			"position": {"x": 10, "y": 383},
 			"callback": "mapSwipeChanged"
 		},
-
+		{
+			"name": "infoBarPickCheckbox",
+			"type": "toggleButton",
+			"image": "sysopchk.def",
+			"help": "vcmi.adventureOptions.infoBarPick",
+			"position": {"x": 10, "y": 413},
+			"callback": "infoBarPickChanged"
+		}
 	]
 }

+ 0 - 2
lib/IGameCallback.h

@@ -21,7 +21,6 @@ struct GiveBonus;
 struct BlockingDialog;
 struct TeleportDialog;
 struct MetaString;
-struct ShowInInfobox;
 struct StackLocation;
 struct ArtifactLocation;
 class CCreatureSet;
@@ -113,7 +112,6 @@ public:
 	virtual void removeArtifact(const ArtifactLocation &al) = 0;
 	virtual bool moveArtifact(const ArtifactLocation &al1, const ArtifactLocation &al2) = 0;
 
-	virtual void showCompInfo(ShowInInfobox * comp)=0;
 	virtual void heroVisitCastle(const CGTownInstance * obj, const CGHeroInstance * hero)=0;
 	virtual void visitCastleObjects(const CGTownInstance * obj, const CGHeroInstance * hero)=0;
 	virtual void stopHeroVisitCastle(const CGTownInstance * obj, const CGHeroInstance * hero)=0;

+ 2 - 2
lib/IGameEventsReceiver.h

@@ -10,6 +10,7 @@
 #pragma once
 
 
+#include "NetPacksBase.h"
 #include "battle/BattleHex.h"
 #include "GameConstants.h"
 #include "int3.h"
@@ -103,7 +104,7 @@ public:
 	virtual void heroMovePointsChanged(const CGHeroInstance * hero){} //not called at the beginning of turn and after movement
 	virtual void heroVisitsTown(const CGHeroInstance* hero, const CGTownInstance * town){};
 	virtual void receivedResource(){};
-	virtual void showInfoDialog(const std::string & text, const std::vector<Component> & components, int soundID){};
+	virtual void showInfoDialog(EInfoWindowMode type, const std::string & text, const std::vector<Component> & components, int soundID){};
 	virtual void showRecruitmentDialog(const CGDwelling *dwelling, const CArmedInstance *dst, int level){}
 	virtual void showShipyardDialog(const IShipyard *obj){} //obj may be town or shipyard; state: 0 - can buid, 1 - lack of resources, 2 - dest tile is blocked, 3 - no water
 
@@ -132,7 +133,6 @@ public:
 	virtual void playerBlocked(int reason, bool start){}; //reason: 0 - upcoming battle
 	virtual void gameOver(PlayerColor player, const EVictoryLossCheckResult & victoryLossCheckResult) {}; //player lost or won the game
 	virtual void playerStartsTurn(PlayerColor player){};
-	virtual void showComp(const Component &comp, std::string message) {}; //display component in the advmapint infobox
 
 	//TODO shouldn't be moved down the tree?
 	virtual void heroExchangeStarted(ObjectInstanceID hero1, ObjectInstanceID hero2, QueryID queryID){};

+ 0 - 1
lib/NetPackVisitor.h

@@ -106,7 +106,6 @@ public:
 	virtual void visitBattleSetStackProperty(BattleSetStackProperty & pack) {}
 	virtual void visitBattleTriggerEffect(BattleTriggerEffect & pack) {}
 	virtual void visitBattleUpdateGateState(BattleUpdateGateState & pack) {}
-	virtual void visitShowInInfobox(ShowInInfobox & pack) {}
 	virtual void visitAdvmapSpellCast(AdvmapSpellCast & pack) {}
 	virtual void visitShowWorldViewEx(ShowWorldViewEx & pack) {}
 	virtual void visitEndTurn(EndTurn & pack) {}

+ 3 - 22
lib/NetPacks.h

@@ -718,12 +718,7 @@ struct DLL_LINKAGE GiveHero : public CPackForClient
 
 struct DLL_LINKAGE OpenWindow : public CPackForClient
 {
-	enum EWindow
-	{
-		EXCHANGE_WINDOW, RECRUITMENT_FIRST, RECRUITMENT_ALL, SHIPYARD_WINDOW, THIEVES_GUILD,
-		UNIVERSITY_WINDOW, HILL_FORT_WINDOW, MARKET_WINDOW, PUZZLE_MAP, TAVERN_WINDOW
-	};
-	ui8 window;
+	EOpenWindowMode window;
 	si32 id1 = -1;
 	si32 id2 = -1;
 
@@ -1150,6 +1145,7 @@ struct DLL_LINKAGE NewTurn : public CPackForClient
 
 struct DLL_LINKAGE InfoWindow : public CPackForClient //103  - displays simple info window
 {
+	EInfoWindowMode type = EInfoWindowMode::MODAL;
 	MetaString text;
 	std::vector<Component> components;
 	PlayerColor player;
@@ -1159,6 +1155,7 @@ struct DLL_LINKAGE InfoWindow : public CPackForClient //103  - displays simple i
 
 	template <typename Handler> void serialize(Handler & h, const int version)
 	{
+		h & type;
 		h & text;
 		h & components;
 		h & player;
@@ -1873,22 +1870,6 @@ protected:
 	virtual void visitTyped(ICPackVisitor & visitor) override;
 };
 
-struct DLL_LINKAGE ShowInInfobox : public CPackForClient
-{
-	PlayerColor player;
-	Component c;
-	MetaString text;
-	template <typename Handler> void serialize(Handler & h, const int version)
-	{
-		h & player;
-		h & c;
-		h & text;
-	}
-
-protected:
-	virtual void visitTyped(ICPackVisitor & visitor) override;
-};
-
 struct DLL_LINKAGE AdvmapSpellCast : public CPackForClient
 {
 	ObjectInstanceID casterID;

+ 39 - 2
lib/NetPacksBase.h

@@ -35,6 +35,27 @@ struct ArtSlotInfo;
 
 class ICPackVisitor;
 
+enum class EInfoWindowMode : uint8_t
+{
+	AUTO,
+	MODAL,
+	INFO
+};
+
+enum class EOpenWindowMode : uint8_t
+{
+	EXCHANGE_WINDOW,
+	RECRUITMENT_FIRST,
+	RECRUITMENT_ALL,
+	SHIPYARD_WINDOW,
+	THIEVES_GUILD,
+	UNIVERSITY_WINDOW,
+	HILL_FORT_WINDOW,
+	MARKET_WINDOW,
+	PUZZLE_MAP,
+	TAVERN_WINDOW
+};
+
 struct DLL_LINKAGE CPack
 {
 	std::shared_ptr<CConnection> c; // Pointer to connection that pack received from
@@ -176,8 +197,24 @@ public:
 
 struct Component
 {
-	enum EComponentType {PRIM_SKILL, SEC_SKILL, RESOURCE, CREATURE, ARTIFACT, EXPERIENCE, SPELL, MORALE, LUCK, BUILDING, HERO_PORTRAIT, FLAG};
-	ui16 id = 0, subtype = 0; //id uses ^^^ enums, when id==EXPPERIENCE subtype==0 means exp points and subtype==1 levels)
+	enum class EComponentType : uint8_t 
+	{
+		PRIM_SKILL,
+		SEC_SKILL,
+		RESOURCE,
+		CREATURE,
+		ARTIFACT,
+		EXPERIENCE,
+		SPELL,
+		MORALE,
+		LUCK,
+		BUILDING,
+		HERO_PORTRAIT,
+		FLAG,
+		INVALID //should be last
+	};
+	EComponentType id = EComponentType::INVALID;
+	ui16 subtype = 0; //id==EXPPERIENCE subtype==0 means exp points and subtype==1 levels
 	si32 val = 0; // + give; - take
 	si16 when = 0; // 0 - now; +x - within x days; -x - per x days
 

+ 1 - 6
lib/NetPacksLib.cpp

@@ -492,11 +492,6 @@ void BattleUpdateGateState::visitTyped(ICPackVisitor & visitor)
 	visitor.visitBattleUpdateGateState(*this);
 }
 
-void ShowInInfobox::visitTyped(ICPackVisitor & visitor)
-{
-	visitor.visitShowInInfobox(*this);
-}
-
 void AdvmapSpellCast::visitTyped(ICPackVisitor & visitor)
 {
 	visitor.visitAdvmapSpellCast(*this);
@@ -2503,7 +2498,7 @@ void YourTurn::applyGs(CGameState * gs) const
 }
 
 Component::Component(const CStackBasicDescriptor & stack)
-	: id(CREATURE)
+	: id(EComponentType::CREATURE)
 	, subtype(stack.type->idNumber)
 	, val(stack.count)
 {

+ 6 - 5
lib/mapObjects/CBank.cpp

@@ -132,6 +132,7 @@ void CBank::doVisit(const CGHeroInstance * hero) const
 {
 	int textID = -1;
 	InfoWindow iw;
+	iw.type = EInfoWindowMode::AUTO;
 	iw.player = hero->getOwner();
 	MetaString loot;
 
@@ -189,7 +190,7 @@ void CBank::doVisit(const CGHeroInstance * hero) const
 				break;
 			}
 			cb->giveHeroBonus(&gbonus);
-			iw.components.emplace_back(Component::MORALE, 0, -1, 0);
+			iw.components.emplace_back(Component::EComponentType::MORALE, 0, -1, 0);
 			iw.soundID = soundBase::GRAVEYARD;
 			break;
 		}
@@ -200,7 +201,7 @@ void CBank::doVisit(const CGHeroInstance * hero) const
 			gb.id = hero->id.getNum();
 			cb->giveHeroBonus(&gb);
 			textID = 107;
-			iw.components.emplace_back(Component::LUCK, 0, -2, 0);
+			iw.components.emplace_back(Component::EComponentType::LUCK, 0, -2, 0);
 			break;
 		}
 		case Obj::CREATURE_BANK:
@@ -224,7 +225,7 @@ void CBank::doVisit(const CGHeroInstance * hero) const
 		{
 			if (bc->resources[it] != 0)
 			{
-				iw.components.emplace_back(Component::RESOURCE, it, bc->resources[it], 0);
+				iw.components.emplace_back(Component::EComponentType::RESOURCE, it, bc->resources[it], 0);
 				loot << "%d %s";
 				loot.addReplacement(iw.components.back().val);
 				loot.addReplacement(MetaString::RES_NAMES, iw.components.back().subtype);
@@ -234,7 +235,7 @@ void CBank::doVisit(const CGHeroInstance * hero) const
 		//grant artifacts
 		for (auto & elem : bc->artifacts)
 		{
-			iw.components.emplace_back(Component::ARTIFACT, elem, 0, 0);
+			iw.components.emplace_back(Component::EComponentType::ARTIFACT, elem, 0, 0);
 			loot << "%s";
 			loot.addReplacement(MetaString::ART_NAMES, elem);
 			cb->giveHeroNewArtifact(hero, VLC->arth->objects[elem], ArtifactPosition::FIRST_AVAILABLE);
@@ -278,7 +279,7 @@ void CBank::doVisit(const CGHeroInstance * hero) const
 					if(hero->canLearnSpell(spell))
 					{
 						spells.insert(spellId);
-						iw.components.emplace_back(Component::SPELL, spellId, 0, 0);
+						iw.components.emplace_back(Component::EComponentType::SPELL, spellId, 0, 0);
 					}
 				}
 				else

+ 2 - 18
lib/mapObjects/CGHeroInstance.cpp

@@ -36,23 +36,6 @@
 
 VCMI_LIB_NAMESPACE_BEGIN
 
-
-///helpers
-static void showInfoDialog(const PlayerColor & playerID, const ui32 txtID, const ui16 soundID = 0)
-{
-	InfoWindow iw;
-	iw.soundID = soundID;
-	iw.player = playerID;
-	iw.text.addTxt(MetaString::ADVOB_TXT,txtID);
-	IObjectInterface::cb->sendAndApply(&iw);
-}
-
-static void showInfoDialog(const CGHeroInstance* h, const ui32 txtID, const ui16 soundID = 0)
-{
-	const PlayerColor playerID = h->getOwner();
-	showInfoDialog(playerID,txtID,soundID);
-}
-
 static int lowestSpeed(const CGHeroInstance * chi)
 {
 	static const CSelector selectorSTACKS_SPEED = Selector::type()(Bonus::STACKS_SPEED);
@@ -462,7 +445,7 @@ void CGHeroInstance::onHeroVisit(const CGHeroInstance * h) const
 			txt_id = 103;
 		}
 
-		showInfoDialog(h,txt_id);
+		h->showInfoDialog(txt_id);
 	}
 }
 
@@ -876,6 +859,7 @@ CStackBasicDescriptor CGHeroInstance::calculateNecromancy (const BattleResult &b
 void CGHeroInstance::showNecromancyDialog(const CStackBasicDescriptor &raisedStack, CRandomGenerator & rand) const
 {
 	InfoWindow iw;
+	iw.type = EInfoWindowMode::AUTO;
 	iw.soundID = soundBase::pickup01 + rand.nextInt(6);
 	iw.player = tempOwner;
 	iw.components.emplace_back(raisedStack);

+ 2 - 12
lib/mapObjects/CGMarket.cpp

@@ -22,16 +22,6 @@
 
 VCMI_LIB_NAMESPACE_BEGIN
 
-///helpers
-static void openWindow(const OpenWindow::EWindow type, const int id1, const int id2 = -1)
-{
-	OpenWindow ow;
-	ow.window = type;
-	ow.id1 = id1;
-	ow.id2 = id2;
-	IObjectInterface::cb->sendAndApply(&ow);
-}
-
 bool IMarket::getOffer(int id1, int id2, int &val1, int &val2, EMarketMode::EMarketMode mode) const
 {
 	switch(mode)
@@ -205,7 +195,7 @@ std::vector<EMarketMode::EMarketMode> IMarket::availableModes() const
 
 void CGMarket::onHeroVisit(const CGHeroInstance * h) const
 {
-	openWindow(OpenWindow::MARKET_WINDOW,id.getNum(),h->id.getNum());
+	openWindow(EOpenWindowMode::MARKET_WINDOW,id.getNum(),h->id.getNum());
 }
 
 int CGMarket::getMarketEfficiency() const
@@ -339,7 +329,7 @@ std::vector<int> CGUniversity::availableItemsIds(EMarketMode::EMarketMode mode)
 
 void CGUniversity::onHeroVisit(const CGHeroInstance * h) const
 {
-	openWindow(OpenWindow::UNIVERSITY_WINDOW,id.getNum(),h->id.getNum());
+	openWindow(EOpenWindowMode::UNIVERSITY_WINDOW,id.getNum(),h->id.getNum());
 }
 
 VCMI_LIB_NAMESPACE_END

+ 14 - 28
lib/mapObjects/CGPandoraBox.cpp

@@ -25,22 +25,6 @@
 
 VCMI_LIB_NAMESPACE_BEGIN
 
-///helpers
-static void showInfoDialog(const PlayerColor & playerID, const ui32 txtID, const ui16 soundID)
-{
-	InfoWindow iw;
-	iw.soundID = soundID;
-	iw.player = playerID;
-	iw.text.addTxt(MetaString::ADVOB_TXT,txtID);
-	IObjectInterface::cb->sendAndApply(&iw);
-}
-
-static void showInfoDialog(const CGHeroInstance* h, const ui32 txtID, const ui16 soundID)
-{
-	const PlayerColor playerID = h->getOwner();
-	showInfoDialog(playerID,txtID,soundID);
-}
-
 void CGPandoraBox::initObj(CRandomGenerator & rand)
 {
 	blockVisit = (ID==Obj::PANDORAS_BOX); //block only if it's really pandora's box (events also derive from that class)
@@ -60,6 +44,7 @@ void CGPandoraBox::giveContentsUpToExp(const CGHeroInstance *h) const
 	afterSuccessfulVisit();
 
 	InfoWindow iw;
+	iw.type = EInfoWindowMode::AUTO;
 	iw.player = h->getOwner();
 
 	bool changesPrimSkill = false;
@@ -98,14 +83,14 @@ void CGPandoraBox::giveContentsUpToExp(const CGHeroInstance *h) const
 		iw.text.addReplacement(h->getNameTranslated());
 
 		if(expVal)
-			iw.components.emplace_back(Component::EXPERIENCE, 0, static_cast<si32>(expVal), 0);
+			iw.components.emplace_back(Component::EComponentType::EXPERIENCE, 0, static_cast<si32>(expVal), 0);
 
 		for(int i=0; i<primskills.size(); i++)
 			if(primskills[i])
-				iw.components.emplace_back(Component::PRIM_SKILL, i, primskills[i], 0);
+				iw.components.emplace_back(Component::EComponentType::PRIM_SKILL, i, primskills[i], 0);
 
 		for(const auto & abilityData : unpossessedAbilities)
-			iw.components.emplace_back(Component::SEC_SKILL, abilityData.first, abilityData.second, 0);
+			iw.components.emplace_back(Component::EComponentType::SEC_SKILL, abilityData.first, abilityData.second, 0);
 
 		cb->showInfoDialog(&iw);
 
@@ -139,6 +124,7 @@ void CGPandoraBox::giveContentsAfterExp(const CGHeroInstance *h) const
 
 	std::string msg = message; //in case box is removed in the meantime
 	InfoWindow iw;
+	iw.type = EInfoWindowMode::AUTO;
 	iw.player = h->getOwner();
 
 	//TODO: reuse this code for Scholar skill
@@ -158,7 +144,7 @@ void CGPandoraBox::giveContentsAfterExp(const CGHeroInstance *h) const
 				const auto * spell = (*i).toSpell(VLC->spells());
 				if(h->canLearnSpell(spell))
 				{
-					iw.components.emplace_back(Component::SPELL, *i, 0, 0);
+					iw.components.emplace_back(Component::EComponentType::SPELL, *i, 0, 0);
 					spellsToGive.insert(*i);
 				}
 				if(spellsToGive.size() == 8) //display up to 8 spells at once
@@ -186,7 +172,7 @@ void CGPandoraBox::giveContentsAfterExp(const CGHeroInstance *h) const
 	if(manaDiff)
 	{
 		getText(iw,hadGuardians,manaDiff,176,177,h);
-		iw.components.emplace_back(Component::PRIM_SKILL, 5, manaDiff, 0);
+		iw.components.emplace_back(Component::EComponentType::PRIM_SKILL, 5, manaDiff, 0);
 		cb->showInfoDialog(&iw);
 		cb->setManaPoints(h->id, h->mana + manaDiff);
 	}
@@ -194,7 +180,7 @@ void CGPandoraBox::giveContentsAfterExp(const CGHeroInstance *h) const
 	if(moraleDiff)
 	{
 		getText(iw,hadGuardians,moraleDiff,178,179,h);
-		iw.components.emplace_back(Component::MORALE, 0, moraleDiff, 0);
+		iw.components.emplace_back(Component::EComponentType::MORALE, 0, moraleDiff, 0);
 		cb->showInfoDialog(&iw);
 		GiveBonus gb;
 		gb.bonus = Bonus(Bonus::ONE_BATTLE,Bonus::MORALE,Bonus::OBJECT,moraleDiff,id.getNum(),"");
@@ -205,7 +191,7 @@ void CGPandoraBox::giveContentsAfterExp(const CGHeroInstance *h) const
 	if(luckDiff)
 	{
 		getText(iw,hadGuardians,luckDiff,180,181,h);
-		iw.components.emplace_back(Component::LUCK, 0, luckDiff, 0);
+		iw.components.emplace_back(Component::EComponentType::LUCK, 0, luckDiff, 0);
 		cb->showInfoDialog(&iw);
 		GiveBonus gb;
 		gb.bonus = Bonus(Bonus::ONE_BATTLE,Bonus::LUCK,Bonus::OBJECT,luckDiff,id.getNum(),"");
@@ -218,7 +204,7 @@ void CGPandoraBox::giveContentsAfterExp(const CGHeroInstance *h) const
 	for(int i=0; i<resources.size(); i++)
 	{
 		if(resources[i] < 0)
-			iw.components.emplace_back(Component::RESOURCE, i, resources[i], 0);
+			iw.components.emplace_back(Component::EComponentType::RESOURCE, i, resources[i], 0);
 	}
 	if(!iw.components.empty())
 	{
@@ -231,7 +217,7 @@ void CGPandoraBox::giveContentsAfterExp(const CGHeroInstance *h) const
 	for(int i=0; i<resources.size(); i++)
 	{
 		if(resources[i] > 0)
-			iw.components.emplace_back(Component::RESOURCE, i, resources[i], 0);
+			iw.components.emplace_back(Component::EComponentType::RESOURCE, i, resources[i], 0);
 	}
 	if(!iw.components.empty())
 	{
@@ -245,7 +231,7 @@ void CGPandoraBox::giveContentsAfterExp(const CGHeroInstance *h) const
 	iw.text.addReplacement(h->getNameTranslated());
 	for(const auto & elem : artifacts)
 	{
-		iw.components.emplace_back(Component::ARTIFACT, elem, 0, 0);
+		iw.components.emplace_back(Component::EComponentType::ARTIFACT, elem, 0, 0);
 		if(iw.components.size() >= 14)
 		{
 			cb->showInfoDialog(&iw);
@@ -339,7 +325,7 @@ void CGPandoraBox::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answe
 	{
 		if(stacksCount() > 0) //if pandora's box is protected by army
 		{
-			showInfoDialog(hero,16,0);
+			hero->showInfoDialog(16, 0, EInfoWindowMode::MODAL);
 			cb->startBattleI(hero, this); //grants things after battle
 		}
 		else if(message.empty() && resources.empty()
@@ -348,7 +334,7 @@ void CGPandoraBox::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answe
 			&& spells.empty() && creatures.stacksCount() > 0
 			&& gainedExp == 0 && manaDiff == 0 && moraleDiff == 0 && luckDiff == 0) //if it gives nothing without battle
 		{
-			showInfoDialog(hero,15,0);
+			hero->showInfoDialog(15);
 			cb->removeObject(this);
 		}
 		else //if it gives something without battle

+ 11 - 7
lib/mapObjects/CGTownInstance.cpp

@@ -173,6 +173,7 @@ void CGDwelling::onHeroVisit( const CGHeroInstance * h ) const
 	if(ID == Obj::REFUGEE_CAMP && !creatures[0].first) //Refugee Camp, no available cres
 	{
 		InfoWindow iw;
+		iw.type = EInfoWindowMode::AUTO;
 		iw.player = h->tempOwner;
 		iw.text.addTxt(MetaString::ADVOB_TXT, 44); //{%s} \n\n The camp is deserted.  Perhaps you should try next week.
 		iw.text.addReplacement(MetaString::OBJ_NAMES, ID);
@@ -328,6 +329,7 @@ void CGDwelling::heroAcceptsCreatures( const CGHeroInstance *h) const
 			if(!slot.validSlot()) //no available slot
 			{
 				InfoWindow iw;
+				iw.type = EInfoWindowMode::AUTO;
 				iw.player = h->tempOwner;
 				iw.text.addTxt(MetaString::GENERAL_TXT, 425);//The %s would join your hero, but there aren't enough provisions to support them.
 				iw.text.addReplacement(MetaString::CRE_PL_NAMES, crid);
@@ -342,6 +344,7 @@ void CGDwelling::heroAcceptsCreatures( const CGHeroInstance *h) const
 
 
 				InfoWindow iw;
+				iw.type = EInfoWindowMode::AUTO;
 				iw.player = h->tempOwner;
 				iw.text.addTxt(MetaString::GENERAL_TXT, 423); //%d %s join your army.
 				iw.text.addReplacement(count);
@@ -355,6 +358,7 @@ void CGDwelling::heroAcceptsCreatures( const CGHeroInstance *h) const
 		else //there no creatures
 		{
 			InfoWindow iw;
+			iw.type = EInfoWindowMode::AUTO;
 			iw.text.addTxt(MetaString::GENERAL_TXT, 422); //There are no %s here to recruit.
 			iw.text.addReplacement(MetaString::CRE_PL_NAMES, crid);
 			iw.player = h->tempOwner;
@@ -379,8 +383,8 @@ void CGDwelling::heroAcceptsCreatures( const CGHeroInstance *h) const
 		ow.id1 = id.getNum();
 		ow.id2 = h->id.getNum();
 		ow.window = (ID == Obj::CREATURE_GENERATOR1 || ID == Obj::REFUGEE_CAMP)
-			? OpenWindow::RECRUITMENT_FIRST
-			: OpenWindow::RECRUITMENT_ALL;
+			? EOpenWindowMode::RECRUITMENT_FIRST
+			: EOpenWindowMode::RECRUITMENT_ALL;
 		cb->sendAndApply(&ow);
 	}
 }
@@ -1731,31 +1735,31 @@ void CTownBonus::onHeroVisit (const CGHeroInstance * h) const
 		case BuildingSubID::KNOWLEDGE_VISITING_BONUS: //wall of knowledge
 			what = PrimarySkill::KNOWLEDGE;
 			val = 1;
-			iw.components.emplace_back(Component::PRIM_SKILL, 3, 1, 0);
+			iw.components.emplace_back(Component::EComponentType::PRIM_SKILL, 3, 1, 0);
 			break;
 
 		case BuildingSubID::SPELL_POWER_VISITING_BONUS: //order of fire
 			what = PrimarySkill::SPELL_POWER;
 			val = 1;
-			iw.components.emplace_back(Component::PRIM_SKILL, 2, 1, 0);
+			iw.components.emplace_back(Component::EComponentType::PRIM_SKILL, 2, 1, 0);
 			break;
 
 		case BuildingSubID::ATTACK_VISITING_BONUS: //hall of Valhalla
 			what = PrimarySkill::ATTACK;
 			val = 1;
-			iw.components.emplace_back(Component::PRIM_SKILL, 0, 1, 0);
+			iw.components.emplace_back(Component::EComponentType::PRIM_SKILL, 0, 1, 0);
 			break;
 
 		case BuildingSubID::EXPERIENCE_VISITING_BONUS: //academy of battle scholars
 			what = PrimarySkill::EXPERIENCE;
 			val = static_cast<int>(h->calculateXp(1000));
-			iw.components.emplace_back(Component::EXPERIENCE, 0, val, 0);
+			iw.components.emplace_back(Component::EComponentType::EXPERIENCE, 0, val, 0);
 			break;
 
 		case BuildingSubID::DEFENSE_VISITING_BONUS: //cage of warlords
 			what = PrimarySkill::DEFENSE;
 			val = 1;
-			iw.components.emplace_back(Component::PRIM_SKILL, 1, 1, 0);
+			iw.components.emplace_back(Component::EComponentType::PRIM_SKILL, 1, 1, 0);
 			break;
 
 		case BuildingSubID::CUSTOM_VISITING_BONUS:

+ 7 - 18
lib/mapObjects/CObjectHandler.cpp

@@ -32,7 +32,7 @@ VCMI_LIB_NAMESPACE_BEGIN
 IGameCallback * IObjectInterface::cb = nullptr;
 
 ///helpers
-static void openWindow(const OpenWindow::EWindow type, const int id1, const int id2 = -1)
+void IObjectInterface::openWindow(const EOpenWindowMode type, const int id1, const int id2)
 {
 	OpenWindow ow;
 	ow.window = type;
@@ -41,27 +41,16 @@ static void openWindow(const OpenWindow::EWindow type, const int id1, const int
 	IObjectInterface::cb->sendAndApply(&ow);
 }
 
-static void showInfoDialog(const PlayerColor & playerID, const ui32 txtID, const ui16 soundID)
+void IObjectInterface::showInfoDialog(const ui32 txtID, const ui16 soundID, EInfoWindowMode mode) const
 {
 	InfoWindow iw;
 	iw.soundID = soundID;
-	iw.player = playerID;
+	iw.player = getOwner();
+	iw.type = mode;
 	iw.text.addTxt(MetaString::ADVOB_TXT,txtID);
 	IObjectInterface::cb->sendAndApply(&iw);
 }
 
-/*static void showInfoDialog(const ObjectInstanceID heroID, const ui32 txtID, const ui16 soundID)
-{
-	const PlayerColor playerID = IObjectInterface::cb->getOwner(heroID);
-	showInfoDialog(playerID,txtID,soundID);
-}*/
-
-static void showInfoDialog(const CGHeroInstance* h, const ui32 txtID, const ui16 soundID = 0)
-{
-	const PlayerColor playerID = h->getOwner();
-	showInfoDialog(playerID,txtID,soundID);
-}
-
 ///IObjectInterface
 void IObjectInterface::onHeroVisit(const CGHeroInstance * h) const
 {}
@@ -340,18 +329,18 @@ void CGObjectInstance::onHeroVisit( const CGHeroInstance * h ) const
 	{
 	case Obj::HILL_FORT:
 		{
-			openWindow(OpenWindow::HILL_FORT_WINDOW,id.getNum(),h->id.getNum());
+			openWindow(EOpenWindowMode::HILL_FORT_WINDOW,id.getNum(),h->id.getNum());
 		}
 		break;
 	case Obj::SANCTUARY:
 		{
 			//You enter the sanctuary and immediately feel as if a great weight has been lifted off your shoulders.  You feel safe here.
-			showInfoDialog(h, 114);
+			h->showInfoDialog(114);
 		}
 		break;
 	case Obj::TAVERN:
 		{
-			openWindow(OpenWindow::TAVERN_WINDOW,h->id.getNum(),id.getNum());
+			openWindow(EOpenWindowMode::TAVERN_WINDOW,h->id.getNum(),id.getNum());
 		}
 		break;
 	}

+ 8 - 1
lib/mapObjects/CObjectHandler.h

@@ -13,6 +13,7 @@
 
 #include "../int3.h"
 #include "../HeroBonus.h"
+#include "../NetPacksBase.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 
@@ -57,7 +58,13 @@ public:
 	virtual void garrisonDialogClosed(const CGHeroInstance *hero) const;
 	virtual void heroLevelUpDone(const CGHeroInstance *hero) const;
 
-//unified interface, AI helpers
+	//unified helper to show info dialog for object owner
+	virtual void showInfoDialog(const ui32 txtID, const ui16 soundID = 0, EInfoWindowMode mode = EInfoWindowMode::AUTO) const;
+
+	//unified helper to show a specific window
+	static void openWindow(const EOpenWindowMode type, const int id1, const int id2 = -1);
+
+	//unified interface, AI helpers
 	virtual bool wasVisited (PlayerColor player) const;
 	virtual bool wasVisited (const CGHeroInstance * h) const;
 

+ 20 - 36
lib/mapObjects/CQuest.cpp

@@ -50,22 +50,6 @@ CQuest::CQuest():
 {
 }
 
-///helpers
-static void showInfoDialog(const PlayerColor & playerID, const ui32 txtID, const ui16 soundID = 0)
-{
-	InfoWindow iw;
-	iw.soundID = soundID;
-	iw.player = playerID;
-	iw.text.addTxt(MetaString::ADVOB_TXT,txtID);
-	IObjectInterface::cb->sendAndApply(&iw);
-}
-
-static void showInfoDialog(const CGHeroInstance* h, const ui32 txtID, const ui16 soundID = 0)
-{
-	const PlayerColor playerID = h->getOwner();
-	showInfoDialog(playerID,txtID,soundID);
-}
-
 static std::string visitedTxt(const bool visited)
 {
 	int id = visited ? 352 : 353;
@@ -204,7 +188,7 @@ void CQuest::getVisitText(MetaString &iwText, std::vector<Component> &components
 	switch (missionType)
 	{
 		case MISSION_LEVEL:
-			components.emplace_back(Component::EXPERIENCE, 0, m13489val, 0);
+			components.emplace_back(Component::EComponentType::EXPERIENCE, 0, m13489val, 0);
 			if(!isCustom)
 				iwText.addReplacement(m13489val);
 			break;
@@ -215,7 +199,7 @@ void CQuest::getVisitText(MetaString &iwText, std::vector<Component> &components
 			{
 				if(m2stats[i])
 				{
-					components.emplace_back(Component::PRIM_SKILL, i, m2stats[i], 0);
+					components.emplace_back(Component::EComponentType::PRIM_SKILL, i, m2stats[i], 0);
 					loot << "%d %s";
 					loot.addReplacement(m2stats[i]);
 					loot.addReplacement(VLC->generaltexth->primarySkillNames[i]);
@@ -226,13 +210,13 @@ void CQuest::getVisitText(MetaString &iwText, std::vector<Component> &components
 		}
 			break;
 		case MISSION_KILL_HERO:
-			components.emplace_back(Component::HERO_PORTRAIT, heroPortrait, 0, 0);
+			components.emplace_back(Component::EComponentType::HERO_PORTRAIT, heroPortrait, 0, 0);
 			if(!isCustom)
 				addReplacements(iwText, text);
 			break;
 		case MISSION_HERO:
 			//FIXME: portrait may not match hero, if custom portrait was set in map editor
-			components.emplace_back(Component::HERO_PORTRAIT, VLC->heroh->objects[m13489val]->imageIndex, 0, 0);
+			components.emplace_back(Component::EComponentType::HERO_PORTRAIT, VLC->heroh->objects[m13489val]->imageIndex, 0, 0);
 			if(!isCustom)
 				iwText.addReplacement(VLC->heroh->objects[m13489val]->getNameTranslated());
 			break;
@@ -250,7 +234,7 @@ void CQuest::getVisitText(MetaString &iwText, std::vector<Component> &components
 			MetaString loot;
 			for(const auto & elem : m5arts)
 			{
-				components.emplace_back(Component::ARTIFACT, elem, 0, 0);
+				components.emplace_back(Component::EComponentType::ARTIFACT, elem, 0, 0);
 				loot << "%s";
 				loot.addReplacement(MetaString::ART_NAMES, elem);
 			}
@@ -278,7 +262,7 @@ void CQuest::getVisitText(MetaString &iwText, std::vector<Component> &components
 			{
 				if(m7resources[i])
 				{
-					components.emplace_back(Component::RESOURCE, i, m7resources[i], 0);
+					components.emplace_back(Component::EComponentType::RESOURCE, i, m7resources[i], 0);
 					loot << "%d %s";
 					loot.addReplacement(m7resources[i]);
 					loot.addReplacement(MetaString::RES_NAMES, i);
@@ -289,7 +273,7 @@ void CQuest::getVisitText(MetaString &iwText, std::vector<Component> &components
 		}
 			break;
 		case MISSION_PLAYER:
-			components.emplace_back(Component::FLAG, m13489val, 0, 0);
+			components.emplace_back(Component::EComponentType::FLAG, m13489val, 0, 0);
 			if(!isCustom)
 				iwText.addReplacement(VLC->generaltexth->colors[m13489val]);
 			break;
@@ -662,34 +646,34 @@ void CGSeerHut::getCompletionText(MetaString &text, std::vector<Component> &comp
 	switch(rewardType)
 	{
 	case EXPERIENCE:
-		components.emplace_back(Component::EXPERIENCE, 0, static_cast<si32>(h->calculateXp(rVal)), 0);
+		components.emplace_back(Component::EComponentType::EXPERIENCE, 0, static_cast<si32>(h->calculateXp(rVal)), 0);
 		break;
 	case MANA_POINTS:
-		components.emplace_back(Component::PRIM_SKILL, 5, rVal, 0);
+		components.emplace_back(Component::EComponentType::PRIM_SKILL, 5, rVal, 0);
 		break;
 	case MORALE_BONUS:
-		components.emplace_back(Component::MORALE, 0, rVal, 0);
+		components.emplace_back(Component::EComponentType::MORALE, 0, rVal, 0);
 		break;
 	case LUCK_BONUS:
-		components.emplace_back(Component::LUCK, 0, rVal, 0);
+		components.emplace_back(Component::EComponentType::LUCK, 0, rVal, 0);
 		break;
 	case RESOURCES:
-		components.emplace_back(Component::RESOURCE, rID, rVal, 0);
+		components.emplace_back(Component::EComponentType::RESOURCE, rID, rVal, 0);
 		break;
 	case PRIMARY_SKILL:
-		components.emplace_back(Component::PRIM_SKILL, rID, rVal, 0);
+		components.emplace_back(Component::EComponentType::PRIM_SKILL, rID, rVal, 0);
 		break;
 	case SECONDARY_SKILL:
-		components.emplace_back(Component::SEC_SKILL, rID, rVal, 0);
+		components.emplace_back(Component::EComponentType::SEC_SKILL, rID, rVal, 0);
 		break;
 	case ARTIFACT:
-		components.emplace_back(Component::ARTIFACT, rID, 0, 0);
+		components.emplace_back(Component::EComponentType::ARTIFACT, rID, 0, 0);
 		break;
 	case SPELL:
-		components.emplace_back(Component::SPELL, rID, 0, 0);
+		components.emplace_back(Component::EComponentType::SPELL, rID, 0, 0);
 		break;
 	case CREATURE:
-		components.emplace_back(Component::CREATURE, rID, rVal, 0);
+		components.emplace_back(Component::EComponentType::CREATURE, rID, rVal, 0);
 		break;
 	}
 }
@@ -1146,7 +1130,7 @@ void CGKeymasterTent::onHeroVisit( const CGHeroInstance * h ) const
 	}
 	else
 		txt_id=20;
-	showInfoDialog(h, txt_id);
+	h->showInfoDialog(txt_id);
 }
 
 void CGBorderGuard::initObj(CRandomGenerator & rand)
@@ -1182,7 +1166,7 @@ void CGBorderGuard::onHeroVisit(const CGHeroInstance * h) const
 	}
 	else
 	{
-		showInfoDialog(h, 18);
+		h->showInfoDialog(18);
 
 		AddQuest aq;
 		aq.quest = QuestInfo (quest, this, visitablePos());
@@ -1207,7 +1191,7 @@ void CGBorderGate::onHeroVisit(const CGHeroInstance * h) const //TODO: passabili
 {
 	if (!wasMyColorVisited (h->getOwner()) )
 	{
-		showInfoDialog(h,18,0);
+		h->showInfoDialog(18);
 
 		AddQuest aq;
 		aq.quest = QuestInfo (quest, this, visitablePos());

+ 8 - 3
lib/mapObjects/CRewardableConstructor.cpp

@@ -116,9 +116,9 @@ void CRandomRewardObjectInfo::configureReward(CRewardableObject * object, CRando
 		bonus.sid = object->ID;
 		//TODO: bonus.description = object->getObjectName();
 		if (bonus.type == Bonus::MORALE)
-			reward.extraComponents.emplace_back(Component::MORALE, 0, bonus.val, 0);
+			reward.extraComponents.emplace_back(Component::EComponentType::MORALE, 0, bonus.val, 0);
 		if (bonus.type == Bonus::LUCK)
-			reward.extraComponents.emplace_back(Component::LUCK, 0, bonus.val, 0);
+			reward.extraComponents.emplace_back(Component::EComponentType::LUCK, 0, bonus.val, 0);
 	}
 
 	reward.primary = JsonRandom::loadPrimary(source["primary"], rng);
@@ -137,7 +137,7 @@ void CRandomRewardObjectInfo::configureReward(CRewardableObject * object, CRando
 		CreatureID from (VLC->modh->identifiers.getIdentifier (node.second.meta, "creature", node.first) .get());
 		CreatureID dest (VLC->modh->identifiers.getIdentifier (node.second.meta, "creature", node.second.String()).get());
 
-		reward.extraComponents.emplace_back(Component::CREATURE, dest.getNum(), 0, 0);
+		reward.extraComponents.emplace_back(Component::EComponentType::CREATURE, dest.getNum(), 0, 0);
 
 		reward.creaturesChange[from] = dest;
 	}
@@ -230,6 +230,11 @@ void CRandomRewardObjectInfo::configureObject(CRewardableObject * object, CRando
 	configureResetInfo(object, rng, object->resetParameters, parameters["resetParameters"]);
 
 	object->canRefuse = parameters["canRefuse"].Bool();
+
+	if(parameters["showInInfobox"].isNull())
+		object->infoWindowType = EInfoWindowMode::AUTO;
+	else
+		object->infoWindowType = parameters["showInInfobox"].Bool() ? EInfoWindowMode::INFO : EInfoWindowMode::MODAL;
 	
 	auto visitMode = parameters["visitMode"].String();
 	for(int i = 0; i < Rewardable::VisitModeString.size(); ++i)

+ 28 - 24
lib/mapObjects/CRewardableObject.cpp

@@ -134,13 +134,14 @@ void CRewardableObject::onHeroVisit(const CGHeroInstance *h) const
 	{
 		auto vi = info[index];
 		logGlobal->debug("Granting reward %d. Message says: %s", index, vi.message.toString());
-		// show message only if it is not empty
-		if (!vi.message.toString().empty())
+ 		// show message only if it is not empty or in infobox
+		if (infoWindowType != EInfoWindowMode::MODAL || !vi.message.toString().empty())
 		{
 			InfoWindow iw;
 			iw.player = h->tempOwner;
 			iw.text = vi.message;
 			vi.reward.loadComponents(iw.components, h);
+			iw.type = infoWindowType;
 			cb->showInfoDialog(&iw);
 		}
 		// grant reward afterwards. Note that it may remove object
@@ -297,19 +298,7 @@ void CRewardableObject::grantRewardBeforeLevelup(const CRewardVisitInfo & info,
 void CRewardableObject::grantRewardAfterLevelup(const CRewardVisitInfo & info, const CGHeroInstance * hero) const
 {
 	if(info.reward.manaDiff || info.reward.manaPercentage >= 0)
-	{
-		si32 manaScaled = hero->mana;
-		if (info.reward.manaPercentage >= 0)
-			manaScaled = hero->manaLimit() * info.reward.manaPercentage / 100;
-
-		si32 manaMissing   = std::max(0, hero->manaLimit() - manaScaled);
-		si32 manaGranted   = std::min(manaMissing, info.reward.manaDiff);
-		si32 manaOverflow  = info.reward.manaDiff - manaGranted;
-		si32 manaOverLimit = manaOverflow * info.reward.manaOverflowFactor / 100;
-		si32 manaOutput    = manaScaled + manaGranted + manaOverLimit;
-
-		cb->setManaPoints(hero->id, manaOutput);
-	}
+		cb->setManaPoints(hero->id, info.reward.calculateManaPoints(hero));
 
 	if(info.reward.movePoints || info.reward.movePercentage >= 0)
 	{
@@ -442,39 +431,54 @@ void CRewardInfo::loadComponents(std::vector<Component> & comps,
 
 	if (heroExperience)
 	{
-		comps.emplace_back(Component::EXPERIENCE, 0, static_cast<si32>(h->calculateXp(heroExperience)), 0);
+		comps.emplace_back(Component::EComponentType::EXPERIENCE, 0, static_cast<si32>(h->calculateXp(heroExperience)), 0);
 	}
 	if (heroLevel)
-		comps.emplace_back(Component::EXPERIENCE, 1, heroLevel, 0);
+		comps.emplace_back(Component::EComponentType::EXPERIENCE, 1, heroLevel, 0);
 
 	if (manaDiff || manaPercentage >= 0)
-		comps.emplace_back(Component::PRIM_SKILL, 5, manaDiff, 0);
+		comps.emplace_back(Component::EComponentType::PRIM_SKILL, 5, calculateManaPoints(h) - h->mana, 0);
 
 	for (size_t i=0; i<primary.size(); i++)
 	{
 		if (primary[i] != 0)
-			comps.emplace_back(Component::PRIM_SKILL, static_cast<ui16>(i), primary[i], 0);
+			comps.emplace_back(Component::EComponentType::PRIM_SKILL, static_cast<ui16>(i), primary[i], 0);
 	}
 
 	for(const auto & entry : secondary)
-		comps.emplace_back(Component::SEC_SKILL, entry.first, entry.second, 0);
+		comps.emplace_back(Component::EComponentType::SEC_SKILL, entry.first, entry.second, 0);
 
 	for(const auto & entry : artifacts)
-		comps.emplace_back(Component::ARTIFACT, entry, 1, 0);
+		comps.emplace_back(Component::EComponentType::ARTIFACT, entry, 1, 0);
 
 	for(const auto & entry : spells)
-		comps.emplace_back(Component::SPELL, entry, 1, 0);
+		comps.emplace_back(Component::EComponentType::SPELL, entry, 1, 0);
 
 	for(const auto & entry : creatures)
-		comps.emplace_back(Component::CREATURE, entry.type->idNumber, entry.count, 0);
+		comps.emplace_back(Component::EComponentType::CREATURE, entry.type->idNumber, entry.count, 0);
 
 	for (size_t i=0; i<resources.size(); i++)
 	{
 		if (resources[i] !=0)
-			comps.emplace_back(Component::RESOURCE, static_cast<ui16>(i), resources[i], 0);
+			comps.emplace_back(Component::EComponentType::RESOURCE, static_cast<ui16>(i), resources[i], 0);
 	}
 }
 
+si32 CRewardInfo::calculateManaPoints(const CGHeroInstance * hero) const
+{
+	si32 manaScaled = hero->mana;
+	if (manaPercentage >= 0)
+		manaScaled = hero->manaLimit() * manaPercentage / 100;
+
+	si32 manaMissing   = std::max(0, hero->manaLimit() - manaScaled);
+	si32 manaGranted   = std::min(manaMissing, manaDiff);
+	si32 manaOverflow  = manaDiff - manaGranted;
+	si32 manaOverLimit = manaOverflow * manaOverflowFactor / 100;
+	si32 manaOutput    = manaScaled + manaGranted + manaOverLimit;
+
+	return manaOutput;
+}
+
 Component CRewardInfo::getDisplayedComponent(const CGHeroInstance * h) const
 {
 	std::vector<Component> comps;

+ 7 - 0
lib/mapObjects/CRewardableObject.h

@@ -178,6 +178,8 @@ public:
 	                            const CGHeroInstance * h) const;
 	Component getDisplayedComponent(const CGHeroInstance * h) const;
 
+	si32 calculateManaPoints(const CGHeroInstance * h) const;
+
 	CRewardInfo() :
 		heroExperience(0),
 		heroLevel(0),
@@ -304,6 +306,9 @@ protected:
 	/// if true - player can refuse visiting an object (e.g. Tomb)
 	bool canRefuse;
 
+	/// if true - object info will shown in infobox (like resource pickup)
+	EInfoWindowMode infoWindowType = EInfoWindowMode::AUTO;
+
 	/// return true if this object was "cleared" before and no longer has rewards applicable to selected hero
 	/// unlike wasVisited, this method uses information not available to player owner, for example, if object was cleared by another player before
 	bool wasVisitedBefore(const CGHeroInstance * contextHero) const;
@@ -349,6 +354,8 @@ public:
 		h & selectMode;
 		h & selectedReward;
 		h & onceVisitableObjectCleared;
+		if (version >= 817)
+			h & infoWindowType;
 	}
 
 	// for configuration/object setup

+ 48 - 61
lib/mapObjects/MiscObjects.cpp

@@ -34,31 +34,6 @@ ui8 CGObelisk::obeliskCount = 0; //how many obelisks are on map
 std::map<TeamID, ui8> CGObelisk::visited; //map: team_id => how many obelisks has been visited
 
 ///helpers
-static void openWindow(const OpenWindow::EWindow type, const int id1, const int id2 = -1)
-{
-	OpenWindow ow;
-	ow.window = type;
-	ow.id1 = id1;
-	ow.id2 = id2;
-	IObjectInterface::cb->sendAndApply(&ow);
-}
-
-static void showInfoDialog(const PlayerColor & playerID, const ui32 txtID, const ui16 soundID = 0)
-{
-	InfoWindow iw;
-	if(soundID)
-		iw.soundID = soundID;
-	iw.player = playerID;
-	iw.text.addTxt(MetaString::ADVOB_TXT,txtID);
-	IObjectInterface::cb->sendAndApply(&iw);
-}
-
-static void showInfoDialog(const CGHeroInstance* h, const ui32 txtID, const ui16 soundID = 0)
-{
-	const PlayerColor playerID = h->getOwner();
-	showInfoDialog(playerID,txtID,soundID);
-}
-
 static std::string visitedTxt(const bool visited)
 {
 	int id = visited ? 352 : 353;
@@ -385,7 +360,7 @@ void CGCreature::joinDecision(const CGHeroInstance *h, int cost, ui32 accept) co
 		}
 		else //they fight
 		{
-			showInfoDialog(h,87,0);//Insulted by your refusal of their offer, the monsters attack!
+			h->showInfoDialog(87, 0, EInfoWindowMode::MODAL);//Insulted by your refusal of their offer, the monsters attack!
 			fight(h);
 		}
 	}
@@ -587,18 +562,19 @@ void CGCreature::giveReward(const CGHeroInstance * h) const
 		for(int i = 0; i < resources.size(); i++)
 		{
 			if(resources[i] > 0)
-				iw.components.emplace_back(Component::RESOURCE, i, resources[i], 0);
+				iw.components.emplace_back(Component::EComponentType::RESOURCE, i, resources[i], 0);
 		}
 	}
 
 	if(gainedArtifact != ArtifactID::NONE)
 	{
 		cb->giveHeroNewArtifact(h, VLC->arth->objects[gainedArtifact], ArtifactPosition::FIRST_AVAILABLE);
-		iw.components.emplace_back(Component::ARTIFACT, gainedArtifact, 0, 0);
+		iw.components.emplace_back(Component::EComponentType::ARTIFACT, gainedArtifact, 0, 0);
 	}
 
 	if(!iw.components.empty())
 	{
+		iw.type = EInfoWindowMode::AUTO;
 		iw.text.addTxt(MetaString::ADVOB_TXT, 183); // % has found treasure
 		iw.text.addReplacement(h->getNameTranslated());
 		cb->showInfoDialog(&iw);
@@ -740,10 +716,11 @@ void CGMine::flagMine(const PlayerColor & player) const
 	cb->setOwner(this, player); //not ours? flag it!
 
 	InfoWindow iw;
+	iw.type = EInfoWindowMode::AUTO;
 	iw.soundID = soundBase::FLAGMINE;
 	iw.text.addTxt(MetaString::MINE_EVNTS,producedResource); //not use subID, abandoned mines uses default mine texts
 	iw.player = player;
-	iw.components.emplace_back(Component::RESOURCE, producedResource, producedQuantity, -1);
+	iw.components.emplace_back(Component::EComponentType::RESOURCE, producedResource, producedQuantity, -1);
 	cb->showInfoDialog(&iw);
 }
 
@@ -767,7 +744,7 @@ void CGMine::battleFinished(const CGHeroInstance *hero, const BattleResult &resu
 	{
 		if(isAbandoned())
 		{
-			showInfoDialog(hero->tempOwner, 85, 0);
+			hero->showInfoDialog(85);
 		}
 		flagMine(hero->tempOwner);
 	}
@@ -881,27 +858,28 @@ void CGResource::onHeroVisit( const CGHeroInstance * h ) const
 		}
 	}
 	else
-	{
-		if(message.length())
-		{
-			InfoWindow iw;
-			iw.player = h->tempOwner;
-			iw.text << message;
-			cb->showInfoDialog(&iw);
-		}
 		collectRes(h->getOwner());
-	}
 }
 
 void CGResource::collectRes(const PlayerColor & player) const
 {
 	cb->giveResource(player, static_cast<Res::ERes>(subID), amount);
-	ShowInInfobox sii;
+	InfoWindow sii;
 	sii.player = player;
-	sii.c = Component(Component::RESOURCE,subID,amount,0);
-	sii.text.addTxt(MetaString::ADVOB_TXT,113);
-	sii.text.addReplacement(MetaString::RES_NAMES, subID);
-	cb->showCompInfo(&sii);
+	if(!message.empty())
+	{
+		sii.type = EInfoWindowMode::AUTO;
+		sii.text << message;
+	}
+	else
+	{
+		sii.type = EInfoWindowMode::INFO;
+		sii.text.addTxt(MetaString::ADVOB_TXT,113);
+		sii.text.addReplacement(MetaString::RES_NAMES, subID);
+	}
+	sii.components.emplace_back(Component::EComponentType::RESOURCE,subID,amount,0);
+	sii.soundID = soundBase::pickup01 + CRandomGenerator::getDefault().nextInt(6);
+	cb->showInfoDialog(&sii);
 	cb->removeObject(this);
 }
 
@@ -1077,7 +1055,7 @@ void CGMonolith::onHeroVisit( const CGHeroInstance * h ) const
 			logGlobal->debug("All exits blocked for monolith %d at %s", id.getNum(), pos.toString());
 	}
 	else
-		showInfoDialog(h, 70, 0);
+		h->showInfoDialog(70);
 
 	cb->showTeleportDialog(&td);
 }
@@ -1133,7 +1111,7 @@ void CGSubterraneanGate::onHeroVisit( const CGHeroInstance * h ) const
 	TeleportDialog td(h->tempOwner, channel);
 	if(cb->isTeleportChannelImpassable(channel))
 	{
-		showInfoDialog(h,153,0);//Just inside the entrance you find a large pile of rubble blocking the tunnel. You leave discouraged.
+		h->showInfoDialog(153);//Just inside the entrance you find a large pile of rubble blocking the tunnel. You leave discouraged.
 		logGlobal->debug("Cannot find exit subterranean gate for  %d at %s", id.getNum(), pos.toString());
 		td.impassable = true;
 	}
@@ -1236,9 +1214,10 @@ void CGWhirlpool::onHeroVisit( const CGHeroInstance * h ) const
 		vstd::amax(countToTake, 1);
 
 		InfoWindow iw;
+		iw.type = EInfoWindowMode::AUTO;
 		iw.player = h->tempOwner;
 		iw.text.addTxt(MetaString::ADVOB_TXT, 168);
-		iw.components.emplace_back(CStackBasicDescriptor(h->getCreature(targetstack), countToTake));
+		iw.components.emplace_back(CStackBasicDescriptor(h->getCreature(targetstack), -countToTake));
 		cb->showInfoDialog(&iw);
 		cb->changeStackCount(StackLocation(h, targetstack), -countToTake);
 	}
@@ -1318,12 +1297,13 @@ void CGArtifact::onHeroVisit(const CGHeroInstance * h) const
 	if(!stacksCount())
 	{
 		InfoWindow iw;
+		iw.type = EInfoWindowMode::AUTO;
 		iw.player = h->tempOwner;
 		switch(ID)
 		{
 		case Obj::ARTIFACT:
 			{
-				iw.components.emplace_back(Component::ARTIFACT, subID, 0, 0);
+				iw.components.emplace_back(Component::EComponentType::ARTIFACT, subID, 0, 0);
 				if(message.length())
 					iw.text << message;
 				else
@@ -1333,7 +1313,7 @@ void CGArtifact::onHeroVisit(const CGHeroInstance * h) const
 		case Obj::SPELL_SCROLL:
 			{
 				int spellID = storedArtifact->getGivenSpellID();
-				iw.components.emplace_back(Component::SPELL, spellID, 0, 0);
+				iw.components.emplace_back(Component::EComponentType::SPELL, spellID, 0, 0);
 				if(message.length())
 					iw.text << message;
 				else
@@ -1446,6 +1426,7 @@ void CGWitchHut::initObj(CRandomGenerator & rand)
 void CGWitchHut::onHeroVisit( const CGHeroInstance * h ) const
 {
 	InfoWindow iw;
+	iw.type = EInfoWindowMode::AUTO;
 	iw.player = h->getOwner();
 	if(!wasVisited(h->tempOwner))
 		cb->setObjProperty(id, CGWitchHut::OBJPROP_VISITED, h->tempOwner.getNum());
@@ -1460,7 +1441,7 @@ void CGWitchHut::onHeroVisit( const CGHeroInstance * h ) const
 	}
 	else //give sec skill
 	{
-		iw.components.emplace_back(Component::SEC_SKILL, ability, 1, 0);
+		iw.components.emplace_back(Component::EComponentType::SEC_SKILL, ability, 1, 0);
 		txt_id = 171;
 		cb->changeSecSkill(h, SecondarySkill(ability), 1, true);
 	}
@@ -1520,6 +1501,7 @@ void CGWitchHut::serializeJsonOptions(JsonSerializeFormat & handler)
 void CGObservatory::onHeroVisit( const CGHeroInstance * h ) const
 {
 	InfoWindow iw;
+	iw.type = EInfoWindowMode::AUTO;
 	iw.player = h->tempOwner;
 	switch (ID)
 	{
@@ -1562,6 +1544,7 @@ void CGShrine::onHeroVisit( const CGHeroInstance * h ) const
 		cb->setObjProperty(id, CGShrine::OBJPROP_VISITED, h->tempOwner.getNum());
 
 	InfoWindow iw;
+	iw.type = EInfoWindowMode::AUTO;
 	iw.player = h->getOwner();
 	iw.text.addTxt(MetaString::ADVOB_TXT,127 + ID - 88);
 	iw.text.addTxt(MetaString::SPELL_NAME,spell);
@@ -1585,7 +1568,7 @@ void CGShrine::onHeroVisit( const CGHeroInstance * h ) const
 		spells.insert(spell);
 		cb->changeSpells(h, true, spells);
 
-		iw.components.emplace_back(Component::SPELL, spell, 0, 0);
+		iw.components.emplace_back(Component::EComponentType::SPELL, spell, 0, 0);
 	}
 
 	cb->showInfoDialog(&iw);
@@ -1679,6 +1662,7 @@ void CGScholar::onHeroVisit( const CGHeroInstance * h ) const
 	}
 
 	InfoWindow iw;
+	iw.type = EInfoWindowMode::AUTO;
 	iw.player = h->getOwner();
 	iw.text.addTxt(MetaString::ADVOB_TXT,115);
 
@@ -1686,18 +1670,18 @@ void CGScholar::onHeroVisit( const CGHeroInstance * h ) const
 	{
 	case PRIM_SKILL:
 		cb->changePrimSkill(h,static_cast<PrimarySkill::PrimarySkill>(bid),+1);
-		iw.components.emplace_back(Component::PRIM_SKILL, bid, +1, 0);
+		iw.components.emplace_back(Component::EComponentType::PRIM_SKILL, bid, +1, 0);
 		break;
 	case SECONDARY_SKILL:
 		cb->changeSecSkill(h,SecondarySkill(bid),+1);
-		iw.components.emplace_back(Component::SEC_SKILL, bid, ssl + 1, 0);
+		iw.components.emplace_back(Component::EComponentType::SEC_SKILL, bid, ssl + 1, 0);
 		break;
 	case SPELL:
 		{
 			std::set<SpellID> hlp;
 			hlp.insert(SpellID(bid));
 			cb->changeSpells(h,true,hlp);
-			iw.components.emplace_back(Component::SPELL, bid, 0, 0);
+			iw.components.emplace_back(Component::EComponentType::SPELL, bid, 0, 0);
 		}
 		break;
 	default:
@@ -1851,7 +1835,7 @@ void CGMagi::onHeroVisit(const CGHeroInstance * h) const
 {
 	if (ID == Obj::HUT_OF_MAGI)
 	{
-		showInfoDialog(h, 61);
+		h->showInfoDialog(61);
 
 		if (!eyelist[subID].empty())
 		{
@@ -1881,7 +1865,7 @@ void CGMagi::onHeroVisit(const CGHeroInstance * h) const
 	}
 	else if (ID == Obj::EYE_OF_MAGI)
 	{
-		showInfoDialog(h, 48);
+		h->showInfoDialog(48);
 	}
 
 }
@@ -1906,6 +1890,7 @@ void CGSirens::onHeroVisit( const CGHeroInstance * h ) const
 	iw.player = h->tempOwner;
 	if(h->hasBonusFrom(Bonus::OBJECT,ID)) //has already visited Sirens
 	{
+		iw.type = EInfoWindowMode::AUTO;
 		iw.text.addTxt(MetaString::ADVOB_TXT,133);
 	}
 	else
@@ -1971,13 +1956,14 @@ void CGShipyard::onHeroVisit( const CGHeroInstance * h ) const
 	if(s != IBoatGenerator::GOOD)
 	{
 		InfoWindow iw;
+		iw.type = EInfoWindowMode::AUTO;
 		iw.player = tempOwner;
 		getProblemText(iw.text, h);
 		cb->showInfoDialog(&iw);
 	}
 	else
 	{
-		openWindow(OpenWindow::SHIPYARD_WINDOW,id.getNum(),h->id.getNum());
+		openWindow(EOpenWindowMode::SHIPYARD_WINDOW,id.getNum(),h->id.getNum());
 	}
 }
 
@@ -2017,12 +2003,12 @@ void CCartographer::onHeroVisit( const CGHeroInstance * h ) const
 		}
 		else //if he cannot afford
 		{
-			showInfoDialog(h, 28);
+			h->showInfoDialog(28);
 		}
 	}
 	else //if he already visited carographer
 	{
-		showInfoDialog(h, 24);
+		h->showInfoDialog(24);
 	}
 }
 
@@ -2067,6 +2053,7 @@ void CGDenOfthieves::onHeroVisit (const CGHeroInstance * h) const
 void CGObelisk::onHeroVisit( const CGHeroInstance * h ) const
 {
 	InfoWindow iw;
+	iw.type = EInfoWindowMode::AUTO;
 	iw.player = h->tempOwner;
 	TeamState *ts = cb->gameState()->getPlayerTeam(h->tempOwner);
 	assert(ts);
@@ -2080,7 +2067,7 @@ void CGObelisk::onHeroVisit( const CGHeroInstance * h ) const
 		// increment general visited obelisks counter
 		cb->setObjProperty(id, CGObelisk::OBJPROP_INC, team.getNum());
 
-		openWindow(OpenWindow::PUZZLE_MAP, h->tempOwner.getNum());
+		openWindow(EOpenWindowMode::PUZZLE_MAP, h->tempOwner.getNum());
 
 		// mark that particular obelisk as visited for all players in the team
 		for(const auto & color : ts->players)
@@ -2141,7 +2128,7 @@ void CGLighthouse::onHeroVisit( const CGHeroInstance * h ) const
 	{
 		PlayerColor oldOwner = tempOwner;
 		cb->setOwner(this,h->tempOwner); //not ours? flag it!
-		showInfoDialog(h, 69);
+		h->showInfoDialog(69);
 		giveBonusTo(h->tempOwner);
 
 		if(oldOwner < PlayerColor::PLAYER_LIMIT) //remove bonus from old owner

+ 0 - 1
lib/registerTypes/RegisterTypes.h

@@ -243,7 +243,6 @@ void registerTypesClientPacks1(Serializer &s)
 	s.template registerType<CPackForClient, NewTurn>();
 	s.template registerType<CPackForClient, InfoWindow>();
 	s.template registerType<CPackForClient, SetObjectProperty>();
-	s.template registerType<CPackForClient, ShowInInfobox>();
 	s.template registerType<CPackForClient, AdvmapSpellCast>();
 	s.template registerType<CPackForClient, OpenWindow>();
 	s.template registerType<CPackForClient, NewObject>();

+ 1 - 1
lib/serializer/CSerializer.h

@@ -14,7 +14,7 @@
 
 VCMI_LIB_NAMESPACE_BEGIN
 
-const ui32 SERIALIZATION_VERSION = 816;
+const ui32 SERIALIZATION_VERSION = 817;
 const ui32 MINIMAL_SERIALIZATION_VERSION = 813;
 const std::string SAVEGAME_MAGIC = "VCMISVG";
 

+ 1 - 1
lib/spells/AdventureSpellMechanics.cpp

@@ -505,7 +505,7 @@ ESpellCastResult TownPortalMechanics::beginCast(SpellCastEnvironment * env, cons
 		request.player = parameters.caster->getOwner();
 		request.title.addTxt(MetaString::JK_TXT, 40);
 		request.description.addTxt(MetaString::JK_TXT, 41);
-		request.icon.id = Component::SPELL;
+		request.icon.id = Component::EComponentType::SPELL;
 		request.icon.subtype = owner->id.toEnum();
 
 		env->genericQuery(&request, request.player, queryCallback);

+ 16 - 18
server/CGameHandler.cpp

@@ -837,9 +837,9 @@ void CGameHandler::endBattle(int3 tile, const CGHeroInstance * heroAttacker, con
 
 		for (auto art : arts) //TODO; separate function to display loot for various ojects?
 		{
-			iw.components.push_back(Component(
-				Component::ARTIFACT, art->artType->getId(),
-				art->artType->getId() == ArtifactID::SPELL_SCROLL? art->getGivenSpellID() : 0, 0));
+			iw.components.emplace_back(
+				Component::EComponentType::ARTIFACT, art->artType->getId(),
+				art->artType->getId() == ArtifactID::SPELL_SCROLL? art->getGivenSpellID() : 0, 0);
 			if (iw.components.size() >= 14)
 			{
 				sendAndApply(&iw);
@@ -881,7 +881,7 @@ void CGameHandler::endBattle(int3 tile, const CGHeroInstance * heroAttacker, con
 			iw.text.addReplacement(MetaString::SPELL_NAME, it->toEnum());
 			if (i == cs.spells.size() - 2) //we just added pre-last name
 				iw.text.addReplacement(MetaString::GENERAL_TXT, 141); // " and "
-			iw.components.push_back(Component(Component::SPELL, *it, 0, 0));
+			iw.components.emplace_back(Component::EComponentType::SPELL, *it, 0, 0);
 		}
 		sendAndApply(&iw);
 		sendAndApply(&cs);
@@ -2657,10 +2657,6 @@ void CGameHandler::takeCreatures(ObjectInstanceID objid, const std::vector<CStac
 	}
 }
 
-void CGameHandler::showCompInfo(ShowInInfobox * comp)
-{
-	sendToAllClients(comp);
-}
 void CGameHandler::heroVisitCastle(const CGTownInstance * obj, const CGHeroInstance * hero)
 {
 	HeroVisitCastle vc;
@@ -2822,7 +2818,7 @@ void CGameHandler::useScholarSkill(ObjectInstanceID fromHero, ObjectInstanceID t
 		                                 h2->getSecSkillLevel(SecondarySkill::SCHOLAR));
 		InfoWindow iw;
 		iw.player = h1->tempOwner;
-		iw.components.push_back(Component(Component::SEC_SKILL, 18, ScholarSkillLevel, 0));
+		iw.components.emplace_back(Component::EComponentType::SEC_SKILL, 18, ScholarSkillLevel, 0);
 
 		iw.text.addTxt(MetaString::GENERAL_TXT, 139);//"%s, who has studied magic extensively,
 		iw.text.addReplacement(h1->getNameTranslated());
@@ -2833,7 +2829,7 @@ void CGameHandler::useScholarSkill(ObjectInstanceID fromHero, ObjectInstanceID t
 			int size = static_cast<int>(cs2.spells.size());
 			for (auto it : cs2.spells)
 			{
-				iw.components.push_back(Component(Component::SPELL, it, 1, 0));
+				iw.components.emplace_back(Component::EComponentType::SPELL, it, 1, 0);
 				iw.text.addTxt(MetaString::SPELL_NAME, it.toEnum());
 				switch (size--)
 				{
@@ -2858,7 +2854,7 @@ void CGameHandler::useScholarSkill(ObjectInstanceID fromHero, ObjectInstanceID t
 			int size = static_cast<int>(cs1.spells.size());
 			for (auto it : cs1.spells)
 			{
-				iw.components.push_back(Component(Component::SPELL, it, 1, 0));
+				iw.components.emplace_back(Component::EComponentType::SPELL, it, 1, 0);
 				iw.text.addTxt(MetaString::SPELL_NAME, it.toEnum());
 				switch (size--)
 				{
@@ -5582,7 +5578,7 @@ void CGameHandler::handleTimeEvents()
 				for (int i=0; i<ev.resources.size(); i++)
 				{
 					if (ev.resources.at(i)) //if resource is changed, we add it to the dialog
-						iw.components.push_back(Component(Component::RESOURCE,i,ev.resources.at(i),0));
+						iw.components.emplace_back(Component::EComponentType::RESOURCE,i,ev.resources.at(i),0);
 				}
 
 				sendAndApply(&iw); //show dialog
@@ -5640,7 +5636,7 @@ void CGameHandler::handleTownEvents(CGTownInstance * town, NewTurn &n)
 
 				for (int i=0; i<ev.resources.size(); i++)
 					if (ev.resources.at(i) && pinfo->resources.at(i) != n.res.at(player).at(i)) //if resource had changed, we add it to the dialog
-						iw.components.push_back(Component(Component::RESOURCE,i,n.res.at(player).at(i)-was.at(i),0));
+						iw.components.emplace_back(Component::EComponentType::RESOURCE,i,n.res.at(player).at(i)-was.at(i),0);
 
 			}
 
@@ -5649,7 +5645,7 @@ void CGameHandler::handleTownEvents(CGTownInstance * town, NewTurn &n)
 				if (!town->hasBuilt(i))
 				{
 					buildStructure(town->id, i, true);
-					iw.components.push_back(Component(Component::BUILDING, town->subID, i, 0));
+					iw.components.emplace_back(Component::EComponentType::BUILDING, town->subID, i, 0);
 				}
 			}
 
@@ -5665,8 +5661,8 @@ void CGameHandler::handleTownEvents(CGTownInstance * town, NewTurn &n)
 				if (!town->creatures.at(i).second.empty() && ev.creatures.at(i) > 0)//there is dwelling
 				{
 					sac.creatures[i].first += ev.creatures.at(i);
-					iw.components.push_back(Component(Component::CREATURE,
-							town->creatures.at(i).second.back(), ev.creatures.at(i), 0));
+					iw.components.emplace_back(Component::EComponentType::CREATURE,
+							town->creatures.at(i).second.back(), ev.creatures.at(i), 0);
 				}
 			}
 			sendAndApply(&iw); //show dialog
@@ -5725,7 +5721,7 @@ void CGameHandler::showGarrisonDialog(ObjectInstanceID upobj, ObjectInstanceID h
 void CGameHandler::showThievesGuildWindow(PlayerColor player, ObjectInstanceID requestingObjId)
 {
 	OpenWindow ow;
-	ow.window = OpenWindow::THIEVES_GUILD;
+	ow.window = EOpenWindowMode::THIEVES_GUILD;
 	ow.id1 = player.getNum();
 	ow.id2 = requestingObjId.getNum();
 	sendAndApply(&ow);
@@ -6028,7 +6024,7 @@ void CGameHandler::getVictoryLossMessage(PlayerColor player, const EVictoryLossC
 	if (victoryLossCheckResult.messageToSelf.find("%s") != std::string::npos)
 		out.text.addReplacement(MetaString::COLOR, player.getNum());
 
-	out.components.push_back(Component(Component::FLAG, player.getNum(), 0, 0));
+	out.components.emplace_back(Component::EComponentType::FLAG, player.getNum(), 0, 0);
 }
 
 bool CGameHandler::dig(const CGHeroInstance *h)
@@ -6050,6 +6046,7 @@ bool CGameHandler::dig(const CGHeroInstance *h)
 	sendAndApply(&smp);
 
 	InfoWindow iw;
+	iw.type = EInfoWindowMode::AUTO;
 	iw.player = h->tempOwner;
 	if (gs->map->grailPos == h->visitablePos())
 	{
@@ -6060,6 +6057,7 @@ bool CGameHandler::dig(const CGHeroInstance *h)
 		sendAndApply(&iw);
 
 		iw.soundID = soundBase::invalid;
+		iw.components.emplace_back(Component::EComponentType::ARTIFACT, ArtifactID::GRAIL, 0, 0);
 		iw.text.clear();
 		iw.text.addTxt(MetaString::ART_DESCR, ArtifactID::GRAIL);
 		sendAndApply(&iw);

+ 0 - 1
server/CGameHandler.h

@@ -184,7 +184,6 @@ public:
 	bool bulkMoveArtifacts(ObjectInstanceID srcHero, ObjectInstanceID dstHero, bool swap);
 	void synchronizeArtifactHandlerLists();
 
-	void showCompInfo(ShowInInfobox * comp) override;
 	void heroVisitCastle(const CGTownInstance * obj, const CGHeroInstance * hero) override;
 	void stopHeroVisitCastle(const CGTownInstance * obj, const CGHeroInstance * hero) override;
 	void startBattlePrimary(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool creatureBank = false, const CGTownInstance *town = nullptr) override; //use hero=nullptr for no hero

+ 0 - 1
test/mock/mock_IGameCallback.h

@@ -70,7 +70,6 @@ public:
 	void removeArtifact(const ArtifactLocation &al) override {}
 	bool moveArtifact(const ArtifactLocation &al1, const ArtifactLocation &al2) override {return false;}
 
-	void showCompInfo(ShowInInfobox * comp) override {}
 	void heroVisitCastle(const CGTownInstance * obj, const CGHeroInstance * hero) override {}
 	void stopHeroVisitCastle(const CGTownInstance * obj, const CGHeroInstance * hero) override {}
 	void visitCastleObjects(const CGTownInstance * obj, const CGHeroInstance * hero) override {}