Ver Fonte

Merge pull request #2235 from IvanSavenko/hota_h3m_support

Hota h3m support
Ivan Savenko há 2 anos atrás
pai
commit
69dc95c3c8
100 ficheiros alterados com 2255 adições e 1659 exclusões
  1. 1 1
      AI/Nullkiller/AIGateway.cpp
  2. 1 0
      AI/VCAI/FuzzyHelper.cpp
  3. 1 1
      AI/VCAI/VCAI.cpp
  4. 10 1
      Mods/vcmi/config/vcmi/english.json
  5. 10 1
      Mods/vcmi/config/vcmi/ukrainian.json
  6. 9 10
      client/CPlayerInterface.cpp
  7. 2 4
      client/NetPacksClient.cpp
  8. 3 0
      client/gui/EventDispatcher.cpp
  9. 2 2
      client/lobby/CSelectionBase.cpp
  10. 2 2
      client/lobby/SelectionTab.cpp
  11. 4 2
      client/render/CDefFile.cpp
  12. 5 5
      client/windows/CQuestLog.cpp
  13. 4 0
      cmake_modules/VCMI_lib.cmake
  14. 1 1
      config/factions/castle.json
  15. 1 1
      config/factions/conflux.json
  16. 1 0
      config/factions/dungeon.json
  17. 1 1
      config/factions/fortress.json
  18. 1 0
      config/factions/inferno.json
  19. 1 1
      config/factions/necropolis.json
  20. 1 0
      config/factions/rampart.json
  21. 1 0
      config/factions/stronghold.json
  22. 1 0
      config/factions/tower.json
  23. 341 175
      config/mapOverrides.json
  24. 5 0
      config/schemas/creature.json
  25. 6 5
      config/schemas/faction.json
  26. 2 0
      include/vcmi/Faction.h
  27. 1 1
      include/vcmi/spells/Caster.h
  28. 1 1
      include/vcmi/spells/Magic.h
  29. 2 9
      lib/CArtHandler.cpp
  30. 10 3
      lib/CCreatureHandler.cpp
  31. 0 2
      lib/CCreatureHandler.h
  32. 34 233
      lib/CGameState.cpp
  33. 1 1
      lib/CGameState.h
  34. 6 5
      lib/CGameStateFwd.h
  35. 14 29
      lib/CTownHandler.cpp
  36. 7 3
      lib/CTownHandler.h
  37. 2 2
      lib/GameConstants.h
  38. 1 1
      lib/IGameCallback.h
  39. 1 1
      lib/IGameEventsReceiver.h
  40. 388 0
      lib/MetaString.cpp
  41. 127 0
      lib/MetaString.h
  42. 1 0
      lib/NetPacks.h
  43. 0 80
      lib/NetPacksBase.h
  44. 4 3
      lib/NetPacksLib.cpp
  45. 2 2
      lib/battle/CUnitState.cpp
  46. 8 7
      lib/battle/Unit.cpp
  47. 3 2
      lib/battle/Unit.h
  48. 5 0
      lib/mapObjectConstructors/AObjectTypeHandler.cpp
  49. 1 0
      lib/mapObjectConstructors/AObjectTypeHandler.h
  50. 1 0
      lib/mapObjectConstructors/CObjectClassesHandler.cpp
  51. 2 2
      lib/mapObjectConstructors/ShrineInstanceConstructor.cpp
  52. 26 26
      lib/mapObjects/CBank.cpp
  53. 574 0
      lib/mapObjects/CGCreature.cpp
  54. 91 0
      lib/mapObjects/CGCreature.h
  55. 21 21
      lib/mapObjects/CGDwelling.cpp
  56. 17 12
      lib/mapObjects/CGHeroInstance.cpp
  57. 1 0
      lib/mapObjects/CGHeroInstance.h
  58. 15 11
      lib/mapObjects/CGObjectInstance.cpp
  59. 25 25
      lib/mapObjects/CGPandoraBox.cpp
  60. 4 4
      lib/mapObjects/CGTownBuilding.cpp
  61. 2 2
      lib/mapObjects/CGTownInstance.cpp
  62. 74 68
      lib/mapObjects/CQuest.cpp
  63. 5 5
      lib/mapObjects/IObjectInterface.cpp
  64. 1 0
      lib/mapObjects/IObjectInterface.h
  65. 37 596
      lib/mapObjects/MiscObjects.cpp
  66. 1 76
      lib/mapObjects/MiscObjects.h
  67. 8 9
      lib/mapping/CMap.cpp
  68. 6 6
      lib/mapping/CMapHeader.cpp
  69. 6 5
      lib/mapping/CMapHeader.h
  70. 3 5
      lib/mapping/MapFeaturesH3M.cpp
  71. 98 76
      lib/mapping/MapFormatH3M.cpp
  72. 0 2
      lib/mapping/MapFormatH3M.h
  73. 8 8
      lib/mapping/MapFormatJson.cpp
  74. 26 0
      lib/mapping/MapIdentifiersH3M.cpp
  75. 3 0
      lib/mapping/MapIdentifiersH3M.h
  76. 12 1
      lib/mapping/MapReaderH3M.cpp
  77. 1 0
      lib/mapping/MapReaderH3M.h
  78. 1 0
      lib/registerTypes/RegisterTypes.h
  79. 2 1
      lib/rewardable/Configuration.h
  80. 4 4
      lib/rewardable/Info.cpp
  81. 1 0
      lib/rmg/modificators/ConnectionsPlacer.cpp
  82. 1 0
      lib/rmg/modificators/ObjectManager.cpp
  83. 13 13
      lib/spells/AdventureSpellMechanics.cpp
  84. 1 1
      lib/spells/BattleSpellMechanics.cpp
  85. 4 4
      lib/spells/BonusCaster.cpp
  86. 6 6
      lib/spells/ISpellMechanics.cpp
  87. 0 1
      lib/spells/Problem.cpp
  88. 1 1
      lib/spells/Problem.h
  89. 14 14
      lib/spells/effects/Damage.cpp
  90. 1 1
      lib/spells/effects/Dispel.cpp
  91. 4 4
      lib/spells/effects/Heal.cpp
  92. 2 2
      lib/spells/effects/Obstacle.cpp
  93. 5 5
      lib/spells/effects/Summon.cpp
  94. 3 3
      lib/spells/effects/Timed.cpp
  95. 1 1
      lib/spells/effects/Timed.h
  96. 49 6
      mapeditor/Animation.cpp
  97. 1 1
      mapeditor/Animation.h
  98. 1 0
      mapeditor/BitmapHandler.cpp
  99. 1 0
      mapeditor/inspector/inspector.h
  100. 35 33
      mapeditor/mapsettings.cpp

+ 1 - 1
AI/Nullkiller/AIGateway.cpp

@@ -188,7 +188,7 @@ void AIGateway::showShipyardDialog(const IShipyard * obj)
 
 void AIGateway::gameOver(PlayerColor player, const EVictoryLossCheckResult & victoryLossCheckResult)
 {
-	LOG_TRACE_PARAMS(logAi, "victoryLossCheckResult '%s'", victoryLossCheckResult.messageToSelf);
+	LOG_TRACE_PARAMS(logAi, "victoryLossCheckResult '%s'", victoryLossCheckResult.messageToSelf.toString());
 	NET_EVENT_HANDLER;
 	logAi->debug("Player %d (%s): I heard that player %d (%s) %s.", playerID, playerID.getStr(), player, player.getStr(), (victoryLossCheckResult.victory() ? "won" : "lost"));
 

+ 1 - 0
AI/VCAI/FuzzyHelper.cpp

@@ -17,6 +17,7 @@
 #include "../../lib/mapObjectConstructors/CObjectClassesHandler.h"
 #include "../../lib/mapObjectConstructors/CBankInstanceConstructor.h"
 #include "../../lib/mapObjects/CBank.h"
+#include "../../lib/mapObjects/CGCreature.h"
 #include "../../lib/mapObjects/CGDwelling.h"
 
 FuzzyHelper * fh;

+ 1 - 1
AI/VCAI/VCAI.cpp

@@ -202,7 +202,7 @@ void VCAI::showShipyardDialog(const IShipyard * obj)
 
 void VCAI::gameOver(PlayerColor player, const EVictoryLossCheckResult & victoryLossCheckResult)
 {
-	LOG_TRACE_PARAMS(logAi, "victoryLossCheckResult '%s'", victoryLossCheckResult.messageToSelf);
+	LOG_TRACE_PARAMS(logAi, "victoryLossCheckResult '%s'", victoryLossCheckResult.messageToSelf.toString());
 	NET_EVENT_HANDLER;
 	logAi->debug("Player %d (%s): I heard that player %d (%s) %s.", playerID, playerID.getStr(), player, player.getStr(), (victoryLossCheckResult.victory() ? "won" : "lost"));
 	if(player == playerID)

+ 10 - 1
Mods/vcmi/config/vcmi/english.json

@@ -173,7 +173,16 @@
 	"vcmi.randomMapTab.widgets.teamAlignmentsButton" : "Setup...",
 	"vcmi.randomMapTab.widgets.teamAlignmentsLabel"  : "Team Alignments",
 	"vcmi.randomMapTab.widgets.roadTypesLabel"       : "Road Types",
-	
+
+	// Custom victory conditions for H3 campaigns and HotA maps
+	"vcmi.map.victoryCondition.daysPassed.toOthers" : "The enemy has managed to survive till this day. Victory is theirs!",
+	"vcmi.map.victoryCondition.daysPassed.toSelf" : "Congratulations! You have managed to survive. Victory is yours!",
+	"vcmi.map.victoryCondition.eliminateMonsters.toOthers" : "The enemy has defeated all of the monsters plaguing this land and claims victory!",
+	"vcmi.map.victoryCondition.eliminateMonsters.toSelf" : "Congratulations! You have defeated all of the monsters plaguing this land and can claim victory!",
+	"vcmi.map.victoryCondition.collectArtifacts.message" : "Acquire Three Artifacts",
+	"vcmi.map.victoryCondition.angelicAlliance.toSelf" : "Congratulations! All your enemies have been defeated and you have Angelic Alliance! Victory is yours!",
+	"vcmi.map.victoryCondition.angelicAlliance.message" : "Defeat All Enemies and create Angelic Alliance",
+
 	// few strings from WoG used by vcmi
 	"vcmi.stackExperience.description" : "» S t a c k   E x p e r i e n c e   D e t a i l s «\n\nCreature Type ................... : %s\nExperience Rank ................. : %s (%i)\nExperience Points ............... : %i\nExperience Points to Next Rank .. : %i\nMaximum Experience per Battle ... : %i%% (%i)\nNumber of Creatures in stack .... : %i\nMaximum New Recruits\n without losing current Rank .... : %i\nExperience Multiplier ........... : %.2f\nUpgrade Multiplier .............. : %.2f\nExperience after Rank 10 ........ : %i\nMaximum New Recruits to remain at\n Rank 10 if at Maximum Experience : %i",
 	"vcmi.stackExperience.rank.0" : "Basic",

+ 10 - 1
Mods/vcmi/config/vcmi/ukrainian.json

@@ -156,7 +156,16 @@
 	"vcmi.randomMapTab.widgets.teamAlignmentsButton" : "Налаштувати...",
 	"vcmi.randomMapTab.widgets.teamAlignmentsLabel"  : "Розподіл команд",
 	"vcmi.randomMapTab.widgets.roadTypesLabel"       : "Види доріг",
-	
+
+	// Custom victory conditions for H3 campaigns and HotA maps
+	"vcmi.map.victoryCondition.daysPassed.toOthers" : "Ворогу вдалося вижити до сьогоднішнього дня. Він переміг!",
+	"vcmi.map.victoryCondition.daysPassed.toSelf" : "Вітаємо! Вам вдалося залишитися в живих. Перемога за вами!",
+	"vcmi.map.victoryCondition.eliminateMonsters.toOthers" : "Ворог переміг усіх звірів, що заполонили цю землю, і святкує перемогу!",
+	"vcmi.map.victoryCondition.eliminateMonsters.toSelf" : "Вітаємо! Ви перемогли усіх звірів, що заполонили цю землю, і можете святкувати перемогу!",
+	"vcmi.map.victoryCondition.collectArtifacts.message" : "Здобути три артефакти",
+	"vcmi.map.victoryCondition.angelicAlliance.toSelf" : "Вітаємо! Усі ваші вороги переможені, і ви маєте Альянс Ангелів! Перемога ваша!",
+	"vcmi.map.victoryCondition.angelicAlliance.message" : "Перемогти всіх ворогів і створити Альянс Ангелів",
+
 	"core.bonus.ADDITIONAL_ATTACK.name" : "Подвійний удар",
 	"core.bonus.ADDITIONAL_ATTACK.description" : "Атакує двічі",
 	"core.bonus.ADDITIONAL_RETALIATION.name" : "Додаткові відплати",

+ 9 - 10
client/CPlayerInterface.cpp

@@ -270,14 +270,14 @@ void CPlayerInterface::acceptTurn()
 			auto daysWithoutCastle = optDaysWithoutCastle.value();
 			if (daysWithoutCastle < 6)
 			{
-				text.addTxt(MetaString::ARRAY_TXT,128); //%s, you only have %d days left to capture a town or you will be banished from this land.
-				text.addReplacement(MetaString::COLOR, playerColor.getNum());
-				text.addReplacement(7 - daysWithoutCastle);
+				text.appendLocalString(EMetaText::ARRAY_TXT,128); //%s, you only have %d days left to capture a town or you will be banished from this land.
+				text.replaceLocalString(EMetaText::COLOR, playerColor.getNum());
+				text.replaceNumber(7 - daysWithoutCastle);
 			}
 			else if (daysWithoutCastle == 6)
 			{
-				text.addTxt(MetaString::ARRAY_TXT,129); //%s, this is your last day to capture a town or you will be banished from this land.
-				text.addReplacement(MetaString::COLOR, playerColor.getNum());
+				text.appendLocalString(EMetaText::ARRAY_TXT,129); //%s, this is your last day to capture a town or you will be banished from this land.
+				text.replaceLocalString(EMetaText::COLOR, playerColor.getNum());
 			}
 
 			showInfoDialogAndWait(components, text);
@@ -1048,8 +1048,7 @@ void CPlayerInterface::showInfoDialogAndWait(std::vector<Component> & components
 {
 	EVENT_HANDLER_CALLED_BY_CLIENT;
 
-	std::string str;
-	text.toString(str);
+	std::string str = text.toString();
 
 	showInfoDialog(EInfoWindowMode::MODAL, str, components, 0);
 	waitWhileDialog();
@@ -1586,9 +1585,9 @@ void CPlayerInterface::gameOver(PlayerColor player, const EVictoryLossCheckResul
 	{
 		if (victoryLossCheckResult.loss() && cb->getPlayerStatus(playerID) == EPlayerStatus::INGAME) //enemy has lost
 		{
-			std::string str = victoryLossCheckResult.messageToSelf;
-			boost::algorithm::replace_first(str, "%s", CGI->generaltexth->capColors[player.getNum()]);
-			showInfoDialog(str, std::vector<std::shared_ptr<CComponent>>(1, std::make_shared<CComponent>(CComponent::flag, player.getNum(), 0)));
+			MetaString message = victoryLossCheckResult.messageToSelf;
+			message.appendLocalString(EMetaText::COLOR, player.getNum());
+			showInfoDialog(message.toString(), std::vector<std::shared_ptr<CComponent>>(1, std::make_shared<CComponent>(CComponent::flag, player.getNum(), 0)));
 		}
 	}
 }

+ 2 - 4
client/NetPacksClient.cpp

@@ -600,8 +600,7 @@ void ApplyFirstClientNetPackVisitor::visitGiveHero(GiveHero & pack)
 
 void ApplyClientNetPackVisitor::visitInfoWindow(InfoWindow & pack)
 {
-	std::string str;
-	pack.text.toString(str);
+	std::string str = pack.text.toString();
 
 	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...");
@@ -643,8 +642,7 @@ void ApplyClientNetPackVisitor::visitCommanderLevelUp(CommanderLevelUp & pack)
 
 void ApplyClientNetPackVisitor::visitBlockingDialog(BlockingDialog & pack)
 {
-	std::string str;
-	pack.text.toString(str);
+	std::string str = pack.text.toString();
 
 	if(!callOnlyThatInterface(cl, pack.player, &CGameInterface::showBlockingDialog, str, pack.components, pack.queryID, (soundBase::soundID)pack.soundID, pack.selection(), pack.cancel()))
 		logNetwork->warn("We received YesNoDialog for not our player...");

+ 3 - 0
client/gui/EventDispatcher.cpp

@@ -304,6 +304,9 @@ void EventDispatcher::dispatchMouseMoved(const Point & position)
 	EventReceiversList miCopy = motioninterested;
 	for(auto & elem : miCopy)
 	{
+		if (!vstd::contains(motioninterested, elem))
+			continue;
+
 		if(elem->receiveEvent(position, AEventsReceiver::HOVER))
 		{
 			(elem)->mouseMoved(position);

+ 2 - 2
client/lobby/CSelectionBase.cpp

@@ -210,9 +210,9 @@ void InfoCard::changeSelection()
 	iconsMapSizes->setFrame(mapInfo->getMapSizeIconId());
 	const CMapHeader * header = mapInfo->mapHeader.get();
 	iconsVictoryCondition->setFrame(header->victoryIconIndex);
-	labelVictoryConditionText->setText(header->victoryMessage);
+	labelVictoryConditionText->setText(header->victoryMessage.toString());
 	iconsLossCondition->setFrame(header->defeatIconIndex);
-	labelLossConditionText->setText(header->defeatMessage);
+	labelLossConditionText->setText(header->defeatMessage.toString());
 	flagbox->recreate();
 	labelDifficulty->setText(CGI->generaltexth->arraytxt[142 + mapInfo->mapHeader->difficulty]);
 	iconDifficulty->setSelected(SEL->getCurrentDifficulty());

+ 2 - 2
client/lobby/SelectionTab.cpp

@@ -54,7 +54,7 @@ bool mapSorter::operator()(const std::shared_ptr<CMapInfo> aaa, const std::share
 			return (a->version < b->version);
 			break;
 		case _loscon: //by loss conditions
-			return (a->defeatMessage < b->defeatMessage);
+			return (a->defeatIconIndex < b->defeatIconIndex);
 			break;
 		case _playerAm: //by player amount
 			int playerAmntB, humenPlayersB, playerAmntA, humenPlayersA;
@@ -89,7 +89,7 @@ bool mapSorter::operator()(const std::shared_ptr<CMapInfo> aaa, const std::share
 			return (a->width < b->width);
 			break;
 		case _viccon: //by victory conditions
-			return (a->victoryMessage < b->victoryMessage);
+			return (a->victoryIconIndex < b->victoryIconIndex);
 			break;
 		case _name: //by name
 			return boost::ilexicographical_compare(a->name, b->name);

+ 4 - 2
client/render/CDefFile.cpp

@@ -145,11 +145,13 @@ CDefFile::CDefFile(std::string Name):
 		palette[i].a = SDL_ALPHA_OPAQUE;
 	}
 
-	// first color seems to be used unconditionally as 100% transparency
+	// these colors seems to be used unconditionally
 	palette[0] = targetPalette[0];
+	palette[1] = targetPalette[1];
+	palette[4] = targetPalette[4];
 
 	// rest of special colors are used only if their RGB values are close to H3
-	for (uint32_t i = 1; i < 8; ++i)
+	for (uint32_t i = 0; i < 8; ++i)
 	{
 		if (colorsSimilar(sourcePalette[i], palette[i]))
 			palette[i] = targetPalette[i];

+ 5 - 5
client/windows/CQuestLog.cpp

@@ -28,7 +28,7 @@
 #include "../../lib/CConfigHandler.h"
 #include "../../lib/CGameState.h"
 #include "../../lib/CGeneralTextHandler.h"
-#include "../../lib/NetPacksBase.h"
+#include "../../lib/MetaString.h"
 #include "../../lib/mapObjects/CQuest.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
@@ -167,12 +167,12 @@ void CQuestLog::recreateLabelList()
 			if (auto seersHut = dynamic_cast<const CGSeerHut *>(quests[i].obj))
 			{
 				MetaString toSeer;
-				toSeer << VLC->generaltexth->allTexts[347];
-				toSeer.addReplacement(seersHut->seerName);
-				text.addReplacement(toSeer.toString());
+				toSeer.appendRawString(VLC->generaltexth->allTexts[347]);
+				toSeer.replaceRawString(seersHut->seerName);
+				text.replaceRawString(toSeer.toString());
 			}
 			else
-				text.addReplacement(quests[i].obj->getObjectName()); //get name of the object
+				text.replaceRawString(quests[i].obj->getObjectName()); //get name of the object
 		}
 		auto label = std::make_shared<CQuestLabel>(Rect(13, 195, 149,31), FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, text.toString());
 		label->disable();

+ 4 - 0
cmake_modules/VCMI_lib.cmake

@@ -77,6 +77,7 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE)
 
 		${MAIN_LIB_DIR}/mapObjects/CArmedInstance.cpp
 		${MAIN_LIB_DIR}/mapObjects/CBank.cpp
+		${MAIN_LIB_DIR}/mapObjects/CGCreature.cpp
 		${MAIN_LIB_DIR}/mapObjects/CGDwelling.cpp
 		${MAIN_LIB_DIR}/mapObjects/CGHeroInstance.cpp
 		${MAIN_LIB_DIR}/mapObjects/CGMarket.cpp
@@ -243,6 +244,7 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE)
 		${MAIN_LIB_DIR}/LoadProgress.cpp
 		${MAIN_LIB_DIR}/LogicalExpression.cpp
 		${MAIN_LIB_DIR}/NetPacksLib.cpp
+		${MAIN_LIB_DIR}/MetaString.cpp
 		${MAIN_LIB_DIR}/ObstacleHandler.cpp
 		${MAIN_LIB_DIR}/StartInfo.cpp
 		${MAIN_LIB_DIR}/ResourceSet.cpp
@@ -395,6 +397,7 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE)
 
 		${MAIN_LIB_DIR}/mapObjects/CArmedInstance.h
 		${MAIN_LIB_DIR}/mapObjects/CBank.h
+		${MAIN_LIB_DIR}/mapObjects/CGCreature.h
 		${MAIN_LIB_DIR}/mapObjects/CGDwelling.h
 		${MAIN_LIB_DIR}/mapObjects/CGHeroInstance.h
 		${MAIN_LIB_DIR}/mapObjects/CGMarket.h
@@ -572,6 +575,7 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE)
 		${MAIN_LIB_DIR}/Languages.h
 		${MAIN_LIB_DIR}/LoadProgress.h
 		${MAIN_LIB_DIR}/LogicalExpression.h
+		${MAIN_LIB_DIR}/MetaString.h
 		${MAIN_LIB_DIR}/NetPacksBase.h
 		${MAIN_LIB_DIR}/NetPacks.h
 		${MAIN_LIB_DIR}/NetPacksLobby.h

+ 1 - 1
config/factions/castle.json

@@ -9,6 +9,7 @@
 			"120px" : "TPCASCAS",
 			"130px" : "CRBKGCAS"
 		},
+		"boat" : "boatCastle",
 		"puzzleMap" :
 		{
 			"prefix" : "PUZCAS",
@@ -148,7 +149,6 @@
 			"mageGuild" : 4,
 			"warMachine" : "ballista",
 			"moatAbility" : "castleMoat",
-			"boat" : "boatCastle",
 			// primaryResource not specified so town get both Wood and Ore for resource bonus
 
 			"buildings" :

+ 1 - 1
config/factions/conflux.json

@@ -9,6 +9,7 @@
 			"120px" : "TPCASELE",
 			"130px" : "CRBKGELE"
 		},
+		"boat" : "boatNecropolis",
 		"puzzleMap" :
 		{
 			"prefix" : "PUZELE",
@@ -153,7 +154,6 @@
 			"primaryResource" : "mercury",
 			"warMachine" : "ballista",
 			"moatAbility" : "castleMoat",
-			"boat" : "boatNecropolis",
 
 			"buildings" :
 			{

+ 1 - 0
config/factions/dungeon.json

@@ -10,6 +10,7 @@
 			"120px" : "TPCASDUN",
 			"130px" : "CRBKGDUN"
 		},
+		"boat" : "boatCastle",
 		"puzzleMap" :
 		{
 			"prefix" : "PUZDUN",

+ 1 - 1
config/factions/fortress.json

@@ -9,6 +9,7 @@
 			"120px" : "TPCASFOR",
 			"130px" : "CRBKGFOR"
 		},
+		"boat" : "boatFortress",
 		"puzzleMap" :
 		{
 			"prefix" : "PUZFOR",
@@ -148,7 +149,6 @@
 			"mageGuild" : 3,
 			"warMachine" : "firstAidTent",
 			"moatAbility" : "fortressMoat",
-			"boat" : "boatFortress",
 			// primaryResource not specified so town get both Wood and Ore for resource bonus
 
 			"buildings" :

+ 1 - 0
config/factions/inferno.json

@@ -10,6 +10,7 @@
 			"120px" : "TPCASINF",
 			"130px" : "CRBKGINF"
 		},
+		"boat" : "boatCastle",
 		"puzzleMap" :
 		{
 			"prefix" : "PUZINF",

+ 1 - 1
config/factions/necropolis.json

@@ -10,6 +10,7 @@
 			"120px" : "TPCASNEC",
 			"130px" : "CRBKGNEC"
 		},
+		"boat" : "boatNecropolis",
 		"puzzleMap" :
 		{
 			"prefix" : "PUZNEC",
@@ -153,7 +154,6 @@
 			"mageGuild" : 5,
 			"warMachine" : "firstAidTent",
 			"moatAbility" : "necropolisMoat",
-			"boat" : "boatNecropolis",
 			// primaryResource not specified so town get both Wood and Ore for resource bonus
 
 			"buildings" :

+ 1 - 0
config/factions/rampart.json

@@ -9,6 +9,7 @@
 			"120px" : "TPCASRAM",
 			"130px" : "CRBKGRAM"
 		},
+		"boat" : "boatCastle",
 		"puzzleMap" :
 		{
 			"prefix" : "PUZRAM",

+ 1 - 0
config/factions/stronghold.json

@@ -9,6 +9,7 @@
 			"120px" : "TPCASSTR",
 			"130px" : "CRBKGSTR"
 		},
+		"boat" : "boatCastle",
 		"puzzleMap" :
 		{
 			"prefix" : "PUZSTR",

+ 1 - 0
config/factions/tower.json

@@ -9,6 +9,7 @@
 			"120px" : "TPCASTOW",
 			"130px" : "CRBKGTOW"
 		},
+		"boat" : "boatCastle",
 		"puzzleMap" :
 		{
 			"prefix" : "PUZTOW",

Diff do ficheiro suprimidas por serem muito extensas
+ 341 - 175
config/mapOverrides.json


+ 5 - 0
config/schemas/creature.json

@@ -144,6 +144,11 @@
 					"description" : ".def file with animation of this creature on adventure map",
 					"format" : "defFile"
 				},
+				"mapMask" : {
+					"type" : "array",
+					"items" : { "type" : "string" },
+					"description" : "Object mask that describes on which tiles object is visible/blocked/activatable"
+				},
 				"iconLarge" : {
 					"type" : "string",
 					"description" : "Large icon for this creature, used for example in town screen",

+ 6 - 5
config/schemas/faction.json

@@ -32,7 +32,7 @@
 	"description" : "Json format for defining new faction (aka towns) in VCMI",
 	"required" : [ "name", "alignment", "nativeTerrain", "creatureBackground" ],
 	"dependencies" : {
-		"town" : [ "puzzleMap" ]
+		"town" : [ "puzzleMap", "boat" ]
 	},
 	"additionalProperties" : false,
 	"properties" : {
@@ -49,6 +49,11 @@
 			"type" : "string",
 			"description" : "Native terrain for creatures. Creatures fighting on native terrain receive several bonuses"
 		},
+		"boat" : {
+			"type" : "string",
+			"description" : "Identifier of boat type that is produced by shipyard in town, if any"
+		},
+
 		"preferUndergroundPlacement" : {
 			"type" : "boolean",
 			"description" : "Random map generator places player/cpu-owned towns underground if true is specified and on the ground otherwise. Parameter is unused for maps without underground. False by default."
@@ -124,10 +129,6 @@
 					"type" : "string",
 					"description" : "Identifier of war machine produced by blacksmith in town"
 				},
-				"boat" : {
-					"type" : "string",
-					"description" : "Identifier of boat type that is produced by shipyard in town, if any"
-				},
 				"horde" : {
 					"type" : "array",
 					"maxItems" : 2,

+ 2 - 0
include/vcmi/Faction.h

@@ -16,12 +16,14 @@ VCMI_LIB_NAMESPACE_BEGIN
 
 class FactionID;
 enum class EAlignment : uint8_t;
+enum class EBoatId : int32_t;
 
 class DLL_LINKAGE Faction : public EntityT<FactionID>, public INativeTerrainProvider
 {
 public:
 	virtual bool hasTown() const = 0;
 	virtual EAlignment getAlignment() const = 0;
+	virtual EBoatId getBoatType() const = 0;
 };
 
 VCMI_LIB_NAMESPACE_END

+ 1 - 1
include/vcmi/spells/Caster.h

@@ -13,7 +13,7 @@
 VCMI_LIB_NAMESPACE_BEGIN
 
 class PlayerColor;
-struct MetaString;
+class MetaString;
 class ServerCallback;
 class CGHeroInstance;
 

+ 1 - 1
include/vcmi/spells/Magic.h

@@ -12,7 +12,7 @@
 
 VCMI_LIB_NAMESPACE_BEGIN
 
-struct MetaString;
+class MetaString;
 
 namespace battle
 {

+ 2 - 9
lib/CArtHandler.cpp

@@ -709,16 +709,9 @@ void CArtHandler::initAllowedArtifactsList(const std::vector<bool> &allowed)
 	majors.clear();
 	relics.clear();
 
-	for (ArtifactID i=ArtifactID::SPELLBOOK; i<ArtifactID::ART_SELECTION; i.advance(1))
+	for (ArtifactID i=ArtifactID::SPELLBOOK; i < ArtifactID(static_cast<si32>(objects.size())); i.advance(1))
 	{
-		//check artifacts allowed on a map
-		//TODO: This line will be different when custom map format is implemented
-		if (allowed[i] && legalArtifact(i))
-			allowedArtifacts.push_back(objects[i]);
-	}
-	for(ArtifactID i = ArtifactID::ART_SELECTION; i < ArtifactID(static_cast<si32>(objects.size())); i.advance(1)) //try to allow all artifacts added by mods
-	{
-		if (legalArtifact(ArtifactID(i)))
+		if (allowed[i] && legalArtifact(ArtifactID(i)))
 			allowedArtifacts.push_back(objects[i]);
 			 //keep im mind that artifact can be worn by more than one type of bearer
 	}

+ 10 - 3
lib/CCreatureHandler.cpp

@@ -625,17 +625,25 @@ CCreature * CCreatureHandler::loadFromJson(const std::string & scope, const Json
 			registerObject(scope, type_name, extraName.String(), cre->getIndex());
 	}
 
+	JsonNode advMapFile = node["graphics"]["map"];
+	JsonNode advMapMask = node["graphics"]["mapMask"];
+
 	VLC->modh->identifiers.requestIdentifier(scope, "object", "monster", [=](si32 index)
 	{
 		JsonNode conf;
 		conf.setMeta(scope);
 
 		VLC->objtypeh->loadSubObject(cre->identifier, conf, Obj::MONSTER, cre->getId().num);
-		if (!cre->advMapDef.empty())
+		if (!advMapFile.isNull())
 		{
 			JsonNode templ;
-			templ["animation"].String() = cre->advMapDef;
+			templ["animation"] = advMapFile;
+			if (!advMapMask.isNull())
+				templ["mask"] = advMapMask;
 			templ.setMeta(scope);
+
+			// if creature has custom advMapFile, reset any potentially imported H3M templates and use provided file instead
+			VLC->objtypeh->getHandlerFor(Obj::MONSTER, cre->getId().num)->clearTemplates();
 			VLC->objtypeh->getHandlerFor(Obj::MONSTER, cre->getId().num)->addTemplate(templ);
 		}
 
@@ -871,7 +879,6 @@ void CCreatureHandler::loadJsonAnimation(CCreature * cre, const JsonNode & graph
 	cre->animation.attackClimaxFrame = static_cast<int>(missile["attackClimaxFrame"].Float());
 	cre->animation.missleFrameAngles = missile["frameAngles"].convertTo<std::vector<double> >();
 
-	cre->advMapDef = graphics["map"].String();
 	cre->smallIconName = graphics["iconSmall"].String();
 	cre->largeIconName = graphics["iconLarge"].String();
 }

+ 0 - 2
lib/CCreatureHandler.h

@@ -58,7 +58,6 @@ public:
 	std::set<CreatureID> upgrades; // IDs of creatures to which this creature can be upgraded
 
 	std::string animDefName; // creature animation used during battles
-	std::string advMapDef; //for new creatures only, image for adventure map
 
 	si32 iconIndex = -1; // index of icon in files like twcrport, used in tests now.
 	/// names of files with appropriate icons. Used only during loading
@@ -230,7 +229,6 @@ public:
 		h & ammMax;
 		h & level;
 		h & animDefName;
-		h & advMapDef;
 		h & iconIndex;
 		h & smallIconName;
 		h & largeIconName;

+ 34 - 233
lib/CGameState.cpp

@@ -70,234 +70,6 @@ public:
 	}
 };
 
-void MetaString::getLocalString(const std::pair<ui8, ui32> & txt, std::string & dst) const
-{
-	int type = txt.first;
-	int ser = txt.second;
-
-	if(type == ART_NAMES)
-	{
-		const auto * art = ArtifactID(ser).toArtifact(VLC->artifacts());
-		if(art)
-			dst = art->getNameTranslated();
-		else
-			dst = "#!#";
-	}
-	else if(type == ART_DESCR)
-	{
-		const auto * art = ArtifactID(ser).toArtifact(VLC->artifacts());
-		if(art)
-			dst = art->getDescriptionTranslated();
-		else
-			dst = "#!#";
-	}
-	else if (type == ART_EVNTS)
-	{
-		const auto * art = ArtifactID(ser).toArtifact(VLC->artifacts());
-		if(art)
-			dst = art->getEventTranslated();
-		else
-			dst = "#!#";
-	}
-	else if(type == CRE_PL_NAMES)
-	{
-		const auto * cre = CreatureID(ser).toCreature(VLC->creatures());
-		if(cre)
-			dst = cre->getNamePluralTranslated();
-		else
-			dst = "#!#";
-	}
-	else if(type == CRE_SING_NAMES)
-	{
-		const auto * cre = CreatureID(ser).toCreature(VLC->creatures());
-		if(cre)
-			dst = cre->getNameSingularTranslated();
-		else
-			dst = "#!#";
-	}
-	else if(type == MINE_NAMES)
-	{
-		dst = VLC->generaltexth->translate("core.minename", ser);
-	}
-	else if(type == MINE_EVNTS)
-	{
-		dst = VLC->generaltexth->translate("core.mineevnt", ser);
-	}
-	else if(type == SPELL_NAME)
-	{
-		const auto * spell = SpellID(ser).toSpell(VLC->spells());
-		if(spell)
-			dst = spell->getNameTranslated();
-		else
-			dst = "#!#";
-	}
-	else if(type == OBJ_NAMES)
-	{
-		dst = VLC->objtypeh->getObjectName(ser, 0);
-	}
-	else if(type == SEC_SKILL_NAME)
-	{
-		dst = VLC->skillh->getByIndex(ser)->getNameTranslated();
-	}
-	else
-	{
-		switch(type)
-		{
-		case GENERAL_TXT:
-			dst = VLC->generaltexth->translate("core.genrltxt", ser);
-			break;
-		case RES_NAMES:
-			dst = VLC->generaltexth->translate("core.restypes", ser);
-			break;
-		case ARRAY_TXT:
-			dst = VLC->generaltexth->translate("core.arraytxt", ser);
-			break;
-		case CREGENS:
-			dst = VLC->objtypeh->getObjectName(Obj::CREATURE_GENERATOR1, ser);
-			break;
-		case CREGENS4:
-			dst = VLC->objtypeh->getObjectName(Obj::CREATURE_GENERATOR4, ser);
-			break;
-		case ADVOB_TXT:
-			dst = VLC->generaltexth->translate("core.advevent", ser);
-			break;
-		case COLOR:
-			dst = VLC->generaltexth->translate("vcmi.capitalColors", ser);
-			break;
-		case JK_TXT:
-			dst = VLC->generaltexth->translate("core.jktext", ser);
-			break;
-		default:
-			logGlobal->error("Failed string substitution because type is %d", type);
-			dst = "#@#";
-			return;
-		}
-	}
-}
-
-DLL_LINKAGE void MetaString::toString(std::string &dst) const
-{
-	size_t exSt = 0;
-	size_t loSt = 0;
-	size_t nums = 0;
-	dst.clear();
-
-	for(const auto & elem : message)
-	{//TEXACT_STRING, TLOCAL_STRING, TNUMBER, TREPLACE_ESTRING, TREPLACE_LSTRING, TREPLACE_NUMBER
-		switch(elem)
-		{
-		case TEXACT_STRING:
-			dst += exactStrings[exSt++];
-			break;
-		case TLOCAL_STRING:
-			{
-				std::string hlp;
-				getLocalString(localStrings[loSt++], hlp);
-				dst += hlp;
-			}
-			break;
-		case TNUMBER:
-			dst += std::to_string(numbers[nums++]);
-			break;
-		case TREPLACE_ESTRING:
-			boost::replace_first(dst, "%s", exactStrings[exSt++]);
-			break;
-		case TREPLACE_LSTRING:
-			{
-				std::string hlp;
-				getLocalString(localStrings[loSt++], hlp);
-				boost::replace_first(dst, "%s", hlp);
-			}
-			break;
-		case TREPLACE_NUMBER:
-			boost::replace_first(dst, "%d", std::to_string(numbers[nums++]));
-			break;
-		case TREPLACE_PLUSNUMBER:
-			boost::replace_first(dst, "%+d", '+' + std::to_string(numbers[nums++]));
-			break;
-		default:
-			logGlobal->error("MetaString processing error! Received message of type %d", int(elem));
-			break;
-		}
-	}
-}
-
-DLL_LINKAGE std::string MetaString::toString() const
-{
-	std::string ret;
-	toString(ret);
-	return ret;
-}
-
-DLL_LINKAGE std::string MetaString::buildList () const
-///used to handle loot from creature bank
-{
-
-	size_t exSt = 0;
-	size_t loSt = 0;
-	size_t nums = 0;
-	std::string lista;
-	for (int i = 0; i < message.size(); ++i)
-	{
-		if (i > 0 && (message[i] == TEXACT_STRING || message[i] == TLOCAL_STRING))
-		{
-			if (exSt == exactStrings.size() - 1)
-				lista += VLC->generaltexth->allTexts[141]; //" and "
-			else
-				lista += ", ";
-		}
-		switch (message[i])
-		{
-			case TEXACT_STRING:
-				lista += exactStrings[exSt++];
-				break;
-			case TLOCAL_STRING:
-			{
-				std::string hlp;
-				getLocalString (localStrings[loSt++], hlp);
-				lista += hlp;
-			}
-				break;
-			case TNUMBER:
-				lista += std::to_string(numbers[nums++]);
-				break;
-			case TREPLACE_ESTRING:
-				lista.replace (lista.find("%s"), 2, exactStrings[exSt++]);
-				break;
-			case TREPLACE_LSTRING:
-			{
-				std::string hlp;
-				getLocalString (localStrings[loSt++], hlp);
-				lista.replace (lista.find("%s"), 2, hlp);
-			}
-				break;
-			case TREPLACE_NUMBER:
-				lista.replace (lista.find("%d"), 2, std::to_string(numbers[nums++]));
-				break;
-			default:
-				logGlobal->error("MetaString processing error! Received message of type %d",int(message[i]));
-		}
-
-	}
-	return lista;
-}
-
-void MetaString::addCreReplacement(const CreatureID & id, TQuantity count) //adds sing or plural name;
-{
-	if (!count)
-		addReplacement (CRE_PL_NAMES, id); //no creatures - just empty name (eg. defeat Angels)
-	else if (count == 1)
-		addReplacement (CRE_SING_NAMES, id);
-	else
-		addReplacement (CRE_PL_NAMES, id);
-}
-
-void MetaString::addReplacement(const CStackBasicDescriptor & stack)
-{
-	assert(stack.type); //valid type
-	addCreReplacement(stack.type->getId(), stack.count);
-}
-
 static CGObjectInstance * createObject(const Obj & id, int subid, const int3 & pos, const PlayerColor & owner)
 {
 	CGObjectInstance * nobj;
@@ -651,7 +423,7 @@ void CGameState::randomizeObject(CGObjectInstance *cur)
 	std::pair<Obj,int> ran = pickObject(cur);
 	if(ran.first == Obj::NO_OBJ || ran.second<0) //this is not a random object, or we couldn't find anything
 	{
-		if(cur->ID==Obj::TOWN)
+		if(cur->ID==Obj::TOWN || cur->ID==Obj::MONSTER)
 			cur->setType(cur->ID, cur->subID); // update def, if necessary
 	}
 	else if(ran.first==Obj::HERO)//special code for hero
@@ -1480,6 +1252,31 @@ void CGameState::initHeroes()
 		map->allHeroes[hero->type->getIndex()] = hero;
 	}
 
+	// generate boats for all heroes on water
+	for(auto hero : map->heroesOnMap)
+	{
+		assert(map->isInTheMap(hero->visitablePos()));
+		const auto & tile = map->getTile(hero->visitablePos());
+		if (tile.terType->isWater())
+		{
+			auto handler = VLC->objtypeh->getHandlerFor(Obj::BOAT, hero->getBoatType().getNum());
+			CGBoat * boat = dynamic_cast<CGBoat*>(handler->create());
+			handler->configureObject(boat, gs->getRandomGenerator());
+
+			boat->ID = Obj::BOAT;
+			boat->subID = hero->getBoatType().getNum();
+			boat->pos = hero->pos;
+			boat->appearance = handler->getTemplates().front();
+			boat->id = ObjectInstanceID(static_cast<si32>(gs->map->objects.size()));
+
+			map->objects.emplace_back(boat);
+			map->addBlockVisTiles(boat);
+
+			boat->hero = hero;
+			hero->boat = boat;
+		}
+	}
+
 	for(auto obj : map->objects) //prisons
 	{
 		if(obj && obj->ID == Obj::PRISON)
@@ -2265,10 +2062,10 @@ bool CGameState::checkForVisitableDir(const int3 & src, const int3 & dst) const
 
 EVictoryLossCheckResult CGameState::checkForVictoryAndLoss(const PlayerColor & player) const
 {
-	const std::string & messageWonSelf = VLC->generaltexth->allTexts[659];
-	const std::string & messageWonOther = VLC->generaltexth->allTexts[5];
-	const std::string & messageLostSelf = VLC->generaltexth->allTexts[7];
-	const std::string & messageLostOther = VLC->generaltexth->allTexts[8];
+	const MetaString messageWonSelf = MetaString::createFromTextID("core.genrltxt.659");
+	const MetaString messageWonOther = MetaString::createFromTextID("core.genrltxt.5");
+	const MetaString messageLostSelf = MetaString::createFromTextID("core.genrltxt.7");
+	const MetaString messageLostOther = MetaString::createFromTextID("core.genrltxt.8");
 
 	auto evaluateEvent = [=](const EventCondition & condition)
 	{
@@ -2765,6 +2562,10 @@ void CGameState::buildBonusSystemTree()
 	}
 	// CStackInstance <-> CCreature, CStackInstance <-> CArmedInstance, CArtifactInstance <-> CArtifact
 	// are provided on initializing / deserializing
+
+	// NOTE: calling deserializationFix() might be more correct option, but might lead to side effects
+	for (auto hero : map->heroesOnMap)
+		hero->boatDeserializationFix();
 }
 
 void CGameState::deserializationFix()

+ 1 - 1
lib/CGameState.h

@@ -42,7 +42,7 @@ class CCreature;
 class CMap;
 struct StartInfo;
 struct SetObjectProperty;
-struct MetaString;
+class MetaString;
 struct CPack;
 class CSpell;
 struct TerrainTile;

+ 6 - 5
lib/CGameStateFwd.h

@@ -10,6 +10,7 @@
 #pragma once
 
 #include "CCreatureSet.h"
+#include "MetaString.h"
 #include "int3.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
@@ -101,12 +102,12 @@ struct DLL_LINKAGE InfoAboutTown : public InfoAboutArmy
 class DLL_LINKAGE EVictoryLossCheckResult
 {
 public:
-	static EVictoryLossCheckResult victory(std::string toSelf, std::string toOthers)
+	static EVictoryLossCheckResult victory(MetaString toSelf, MetaString toOthers)
 	{
 		return EVictoryLossCheckResult(VICTORY, toSelf, toOthers);
 	}
 
-	static EVictoryLossCheckResult defeat(std::string toSelf, std::string toOthers)
+	static EVictoryLossCheckResult defeat(MetaString toSelf, MetaString toOthers)
 	{
 		return EVictoryLossCheckResult(DEFEAT, toSelf, toOthers);
 	}
@@ -140,8 +141,8 @@ public:
 		return EVictoryLossCheckResult(-intValue, messageToOthers, messageToSelf);
 	}
 
-	std::string messageToSelf;
-	std::string messageToOthers;
+	MetaString messageToSelf;
+	MetaString messageToOthers;
 
 	template <typename Handler> void serialize(Handler &h, const int version)
 	{
@@ -157,7 +158,7 @@ private:
 		VICTORY= +1
 	};
 
-	EVictoryLossCheckResult(si32 intValue, std::string toSelf, std::string toOthers):
+	EVictoryLossCheckResult(si32 intValue, MetaString toSelf, MetaString toOthers):
 		messageToSelf(toSelf),
 		messageToOthers(toOthers),
 		intValue(intValue)

+ 14 - 29
lib/CTownHandler.cpp

@@ -174,6 +174,11 @@ EAlignment CFaction::getAlignment() const
 	return alignment;
 }
 
+EBoatId CFaction::getBoatType() const
+{
+	return boatType.toEnum();
+}
+
 TerrainId CFaction::getNativeTerrain() const
 {
 	return nativeTerrain;
@@ -893,16 +898,6 @@ void CTownHandler::loadTown(CTown * town, const JsonNode & source)
 
 	warMachinesToLoad[town] = source["warMachine"];
 
-
-	town->shipyardBoat = EBoatId::NONE;
-	if (!source["boat"].isNull())
-	{
-		VLC->modh->identifiers.requestIdentifier("core:boat", source["boat"], [=](int32_t boatTypeID)
-		{
-			town->shipyardBoat = BoatId(boatTypeID);
-		});
-	}
-
 	town->mageLevel = static_cast<ui32>(source["mageGuild"].Float());
 
 	town->namesCount = 0;
@@ -1028,6 +1023,15 @@ CFaction * CTownHandler::loadFromJson(const std::string & scope, const JsonNode
 	faction->creatureBg120 = source["creatureBackground"]["120px"].String();
 	faction->creatureBg130 = source["creatureBackground"]["130px"].String();
 
+	faction->boatType = EBoatId::NONE;
+	if (!source["boat"].isNull())
+	{
+		VLC->modh->identifiers.requestIdentifier("core:boat", source["boat"], [=](int32_t boatTypeID)
+		{
+			faction->boatType = BoatId(boatTypeID);
+		});
+	}
+
 	int alignment = vstd::find_pos(GameConstants::ALIGNMENT_NAMES, source["alignment"].String());
 	if (alignment == -1)
 		faction->alignment = EAlignment::NEUTRAL;
@@ -1158,25 +1162,6 @@ void CTownHandler::afterLoadFinalization()
 	initializeRequirements();
 	initializeOverridden();
 	initializeWarMachines();
-
-	for(auto & faction : objects)
-	{
-		if (!faction->town)
-			continue;
-
-		bool hasBoat = faction->town->shipyardBoat != EBoatId::NONE;
-		bool hasShipyard = faction->town->buildings.count(BuildingID::SHIPYARD);
-
-		if ( hasBoat && !hasShipyard )
-			logMod->warn("Town %s has boat but has no shipyard!", faction->getJsonKey());
-
-		if ( !hasBoat && hasShipyard )
-		{
-			logMod->warn("Town %s has shipyard but has no boat set!", faction->getJsonKey());
-			// Mod compatibility for 1.3
-			faction->town->shipyardBoat = EBoatId::CASTLE;
-		}
-	}
 }
 
 void CTownHandler::initializeRequirements()

+ 7 - 3
lib/CTownHandler.h

@@ -204,6 +204,11 @@ public:
 	EAlignment alignment = EAlignment::NEUTRAL;
 	bool preferUndergroundPlacement = false;
 
+	/// Boat that will be used by town shipyard (if any)
+	/// and for placing heroes directly on boat (in map editor, water prisons & taverns)
+	BoatId boatType;
+
+
 	CTown * town = nullptr; //NOTE: can be null
 
 	std::string creatureBg120;
@@ -226,6 +231,7 @@ public:
 	bool hasTown() const override;
 	TerrainId getNativeTerrain() const override;
 	EAlignment getAlignment() const override;
+	EBoatId getBoatType() const override;
 
 	void updateFrom(const JsonNode & data);
 	void serializeJson(JsonSerializeFormat & handler);
@@ -236,6 +242,7 @@ public:
 		h & identifier;
 		h & index;
 		h & nativeTerrain;
+		h & boatType;
 		h & alignment;
 		h & town;
 		h & creatureBg120;
@@ -282,8 +289,6 @@ public:
 	ArtifactID warMachine;
 	SpellID moatAbility;
 
-	/// boat that will be built by town shipyard, if exists
-	BoatId shipyardBoat;
 	// default chance for hero of specific class to appear in tavern, if field "tavern" was not set
 	// resulting chance = sqrt(town.chance * heroClass.chance)
 	ui32 defaultTavernChance;
@@ -349,7 +354,6 @@ public:
 		h & mageLevel;
 		h & primaryRes;
 		h & warMachine;
-		h & shipyardBoat;
 		h & clientInfo;
 		h & moatAbility;
 		h & defaultTavernChance;

+ 2 - 2
lib/GameConstants.h

@@ -75,7 +75,7 @@ namespace GameConstants
 
 	constexpr ui32 BASE_MOVEMENT_COST = 100; //default cost for non-diagonal movement
 
-	constexpr int HERO_PORTRAIT_SHIFT = 30;// 2 special frames + some extra portraits
+	constexpr int HERO_PORTRAIT_SHIFT = 9;// 2 special frames + 7 extra portraits
 
 	constexpr std::array<int, 11> POSSIBLE_TURNTIME = {1, 2, 4, 6, 8, 10, 15, 20, 25, 30, 0};
 }
@@ -1282,7 +1282,7 @@ class BattleField : public BaseForID<BattleField, si32>
 	DLL_LINKAGE static BattleField fromString(const std::string & identifier);
 };
 
-enum class EBoatId
+enum class EBoatId : int32_t
 {
 	NONE = -1,
 	NECROPOLIS = 0,

+ 1 - 1
lib/IGameCallback.h

@@ -20,7 +20,7 @@ struct SetMovePoints;
 struct GiveBonus;
 struct BlockingDialog;
 struct TeleportDialog;
-struct MetaString;
+class MetaString;
 struct StackLocation;
 struct ArtifactLocation;
 class CCreatureSet;

+ 1 - 1
lib/IGameEventsReceiver.h

@@ -49,7 +49,7 @@ struct BattleTriggerEffect;
 struct CObstacleInstance;
 struct CPackForServer;
 class EVictoryLossCheckResult;
-struct MetaString;
+class MetaString;
 class ObstacleChanges;
 class UnitChanges;
 

+ 388 - 0
lib/MetaString.cpp

@@ -0,0 +1,388 @@
+/*
+ * MetaString.cpp, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+#include "StdInc.h"
+#include "MetaString.h"
+
+#include "CArtHandler.h"
+#include "CCreatureHandler.h"
+#include "CCreatureSet.h"
+#include "CGeneralTextHandler.h"
+#include "CSkillHandler.h"
+#include "GameConstants.h"
+#include "VCMI_Lib.h"
+#include "mapObjectConstructors/CObjectClassesHandler.h"
+#include "spells/CSpellHandler.h"
+
+VCMI_LIB_NAMESPACE_BEGIN
+
+MetaString MetaString::createFromRawString(const std::string & value)
+{
+	MetaString result;
+	result.appendRawString(value);
+	return result;
+}
+
+MetaString MetaString::createFromTextID(const std::string & value)
+{
+	MetaString result;
+	result.appendTextID(value);
+	return result;
+}
+
+void MetaString::appendLocalString(EMetaText type, ui32 serial)
+{
+	message.push_back(EMessage::APPEND_LOCAL_STRING);
+	localStrings.emplace_back(type, serial);
+}
+
+void MetaString::appendRawString(const std::string & value)
+{
+	message.push_back(EMessage::APPEND_RAW_STRING);
+	exactStrings.push_back(value);
+}
+
+void MetaString::appendTextID(const std::string & value)
+{
+	message.push_back(EMessage::APPEND_TEXTID_STRING);
+	stringsTextID.push_back(value);
+}
+
+void MetaString::appendNumber(int64_t value)
+{
+	message.push_back(EMessage::APPEND_NUMBER);
+	numbers.push_back(value);
+}
+
+void MetaString::replaceLocalString(EMetaText type, ui32 serial)
+{
+	message.push_back(EMessage::REPLACE_LOCAL_STRING);
+	localStrings.emplace_back(type, serial);
+}
+
+void MetaString::replaceRawString(const std::string &txt)
+{
+	message.push_back(EMessage::REPLACE_RAW_STRING);
+	exactStrings.push_back(txt);
+}
+
+void MetaString::replaceTextID(const std::string & value)
+{
+	message.push_back(EMessage::REPLACE_TEXTID_STRING);
+	stringsTextID.push_back(value);
+}
+
+void MetaString::replaceNumber(int64_t txt)
+{
+	message.push_back(EMessage::REPLACE_NUMBER);
+	numbers.push_back(txt);
+}
+
+void MetaString::replacePositiveNumber(int64_t txt)
+{
+	message.push_back(EMessage::REPLACE_POSITIVE_NUMBER);
+	numbers.push_back(txt);
+}
+
+void MetaString::clear()
+{
+	exactStrings.clear();
+	localStrings.clear();
+	stringsTextID.clear();
+	message.clear();
+	numbers.clear();
+}
+
+bool MetaString::empty() const
+{
+	return message.empty();
+}
+
+std::string MetaString::getLocalString(const std::pair<EMetaText, ui32> & txt) const
+{
+	EMetaText type = txt.first;
+	int ser = txt.second;
+
+	switch(type)
+	{
+		case EMetaText::ART_NAMES:
+		{
+			const auto * art = ArtifactID(ser).toArtifact(VLC->artifacts());
+			if(art)
+				return art->getNameTranslated();
+			return "#!#";
+		}
+		case EMetaText::ART_DESCR:
+		{
+			const auto * art = ArtifactID(ser).toArtifact(VLC->artifacts());
+			if(art)
+				return art->getDescriptionTranslated();
+			return "#!#";
+		}
+		case EMetaText::ART_EVNTS:
+		{
+			const auto * art = ArtifactID(ser).toArtifact(VLC->artifacts());
+			if(art)
+				return art->getEventTranslated();
+			return "#!#";
+		}
+		case EMetaText::CRE_PL_NAMES:
+		{
+			const auto * cre = CreatureID(ser).toCreature(VLC->creatures());
+			if(cre)
+				return cre->getNamePluralTranslated();
+			return "#!#";
+		}
+		case EMetaText::CRE_SING_NAMES:
+		{
+			const auto * cre = CreatureID(ser).toCreature(VLC->creatures());
+			if(cre)
+				return cre->getNameSingularTranslated();
+			return "#!#";
+		}
+		case EMetaText::MINE_NAMES:
+		{
+			return VLC->generaltexth->translate("core.minename", ser);
+		}
+		case EMetaText::MINE_EVNTS:
+		{
+			return VLC->generaltexth->translate("core.mineevnt", ser);
+		}
+		case EMetaText::SPELL_NAME:
+		{
+			const auto * spell = SpellID(ser).toSpell(VLC->spells());
+			if(spell)
+				return spell->getNameTranslated();
+			return "#!#";
+		}
+		case EMetaText::OBJ_NAMES:
+			return VLC->objtypeh->getObjectName(ser, 0);
+		case EMetaText::SEC_SKILL_NAME:
+			return VLC->skillh->getByIndex(ser)->getNameTranslated();
+		case EMetaText::GENERAL_TXT:
+			return VLC->generaltexth->translate("core.genrltxt", ser);
+		case EMetaText::RES_NAMES:
+			return VLC->generaltexth->translate("core.restypes", ser);
+		case EMetaText::ARRAY_TXT:
+			return VLC->generaltexth->translate("core.arraytxt", ser);
+		case EMetaText::CREGENS:
+			return VLC->objtypeh->getObjectName(Obj::CREATURE_GENERATOR1, ser);
+		case EMetaText::CREGENS4:
+			return VLC->objtypeh->getObjectName(Obj::CREATURE_GENERATOR4, ser);
+		case EMetaText::ADVOB_TXT:
+			return VLC->generaltexth->translate("core.advevent", ser);
+		case EMetaText::COLOR:
+			return VLC->generaltexth->translate("vcmi.capitalColors", ser);
+		case EMetaText::JK_TXT:
+			return VLC->generaltexth->translate("core.jktext", ser);
+		default:
+			logGlobal->error("Failed string substitution because type is %d", static_cast<int>(type));
+			return "#@#";
+	}
+}
+
+DLL_LINKAGE std::string MetaString::toString() const
+{
+	std::string dst;
+
+	size_t exSt = 0;
+	size_t loSt = 0;
+	size_t nums = 0;
+	size_t textID = 0;
+	dst.clear();
+
+	for(const auto & elem : message)
+	{
+		switch(elem)
+		{
+			case EMessage::APPEND_RAW_STRING:
+				dst += exactStrings[exSt++];
+				break;
+			case EMessage::APPEND_LOCAL_STRING:
+				dst += getLocalString(localStrings[loSt++]);
+				break;
+			case EMessage::APPEND_TEXTID_STRING:
+				dst += VLC->generaltexth->translate(stringsTextID[textID++]);
+				break;
+			case EMessage::APPEND_NUMBER:
+				dst += std::to_string(numbers[nums++]);
+				break;
+			case EMessage::REPLACE_RAW_STRING:
+				boost::replace_first(dst, "%s", exactStrings[exSt++]);
+				break;
+			case EMessage::REPLACE_LOCAL_STRING:
+				boost::replace_first(dst, "%s", getLocalString(localStrings[loSt++]));
+				break;
+			case EMessage::REPLACE_TEXTID_STRING:
+				boost::replace_first(dst, "%s", VLC->generaltexth->translate(stringsTextID[textID++]));
+				break;
+			case EMessage::REPLACE_NUMBER:
+				boost::replace_first(dst, "%d", std::to_string(numbers[nums++]));
+				break;
+			case EMessage::REPLACE_POSITIVE_NUMBER:
+				boost::replace_first(dst, "%+d", '+' + std::to_string(numbers[nums++]));
+				break;
+			default:
+				logGlobal->error("MetaString processing error! Received message of type %d", static_cast<int>(elem));
+				assert(0);
+				break;
+		}
+	}
+	return dst;
+}
+
+DLL_LINKAGE std::string MetaString::buildList() const
+{
+	size_t exSt = 0;
+	size_t loSt = 0;
+	size_t nums = 0;
+	size_t textID = 0;
+	std::string lista;
+	for(int i = 0; i < message.size(); ++i)
+	{
+		if(i > 0 && (message[i] == EMessage::APPEND_RAW_STRING || message[i] == EMessage::APPEND_LOCAL_STRING))
+		{
+			if(exSt == exactStrings.size() - 1)
+				lista += VLC->generaltexth->allTexts[141]; //" and "
+			else
+				lista += ", ";
+		}
+		switch(message[i])
+		{
+			case EMessage::APPEND_RAW_STRING:
+				lista += exactStrings[exSt++];
+				break;
+			case EMessage::APPEND_LOCAL_STRING:
+				lista += getLocalString(localStrings[loSt++]);
+				break;
+			case EMessage::APPEND_TEXTID_STRING:
+				lista += VLC->generaltexth->translate(stringsTextID[textID++]);
+				break;
+			case EMessage::APPEND_NUMBER:
+				lista += std::to_string(numbers[nums++]);
+				break;
+			case EMessage::REPLACE_RAW_STRING:
+				lista.replace(lista.find("%s"), 2, exactStrings[exSt++]);
+				break;
+			case EMessage::REPLACE_LOCAL_STRING:
+				lista.replace(lista.find("%s"), 2, getLocalString(localStrings[loSt++]));
+				break;
+			case EMessage::REPLACE_TEXTID_STRING:
+				lista.replace(lista.find("%s"), 2, VLC->generaltexth->translate(stringsTextID[textID++]));
+				break;
+			case EMessage::REPLACE_NUMBER:
+				lista.replace(lista.find("%d"), 2, std::to_string(numbers[nums++]));
+				break;
+			default:
+				logGlobal->error("MetaString processing error! Received message of type %d", int(message[i]));
+		}
+	}
+	return lista;
+}
+
+void MetaString::replaceCreatureName(const CreatureID & id, TQuantity count) //adds sing or plural name;
+{
+	if (count == 1)
+		replaceLocalString (EMetaText::CRE_SING_NAMES, id);
+	else
+		replaceLocalString (EMetaText::CRE_PL_NAMES, id);
+}
+
+void MetaString::replaceCreatureName(const CStackBasicDescriptor & stack)
+{
+	assert(stack.type); //valid type
+	replaceCreatureName(stack.type->getId(), stack.count);
+}
+
+bool MetaString::operator == (const MetaString & other) const
+{
+	return message == other.message && localStrings == other.localStrings && exactStrings == other.exactStrings && stringsTextID == other.stringsTextID && numbers == other.numbers;
+}
+
+void MetaString::jsonSerialize(JsonNode & dest) const
+{
+	JsonNode jsonMessage;
+	JsonNode jsonLocalStrings;
+	JsonNode jsonExactStrings;
+	JsonNode jsonStringsTextID;
+	JsonNode jsonNumbers;
+
+	for (const auto & entry : message )
+	{
+		JsonNode value;
+		value.Float() = static_cast<int>(entry);
+		jsonMessage.Vector().push_back(value);
+	}
+
+	for (const auto & entry : localStrings )
+	{
+		JsonNode value;
+		value.Integer() = static_cast<int>(entry.first) * 10000 + entry.second;
+		jsonLocalStrings.Vector().push_back(value);
+	}
+
+	for (const auto & entry : exactStrings )
+	{
+		JsonNode value;
+		value.String() = entry;
+		jsonExactStrings.Vector().push_back(value);
+	}
+
+	for (const auto & entry : stringsTextID )
+	{
+		JsonNode value;
+		value.String() = entry;
+		jsonStringsTextID.Vector().push_back(value);
+	}
+
+	for (const auto & entry : numbers )
+	{
+		JsonNode value;
+		value.Integer() = entry;
+		jsonNumbers.Vector().push_back(value);
+	}
+
+	dest["message"] = jsonMessage;
+	dest["localStrings"] = jsonLocalStrings;
+	dest["exactStrings"] = jsonExactStrings;
+	dest["stringsTextID"] = jsonStringsTextID;
+	dest["numbers"] = jsonNumbers;
+}
+
+void MetaString::jsonDeserialize(const JsonNode & source)
+{
+	clear();
+
+	if (source.isString())
+	{
+		// compatibility with fields that were converted from string to MetaString
+		if(boost::starts_with(source.String(), "core.") || boost::starts_with(source.String(), "vcmi."))
+			appendTextID(source.String());
+		else
+			appendRawString(source.String());
+		return;
+	}
+
+	for (const auto & entry : source["message"].Vector() )
+		message.push_back(static_cast<EMessage>(entry.Integer()));
+
+	for (const auto & entry : source["localStrings"].Vector() )
+		localStrings.push_back({ static_cast<EMetaText>(entry.Integer() / 10000), entry.Integer() % 10000 });
+
+	for (const auto & entry : source["exactStrings"].Vector() )
+		exactStrings.push_back(entry.String());
+
+	for (const auto & entry : source["stringsTextID"].Vector() )
+		stringsTextID.push_back(entry.String());
+
+	for (const auto & entry : source["numbers"].Vector() )
+		numbers.push_back(entry.Integer());
+}
+
+VCMI_LIB_NAMESPACE_END

+ 127 - 0
lib/MetaString.h

@@ -0,0 +1,127 @@
+/*
+ * MetaString.h, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+#pragma once
+
+VCMI_LIB_NAMESPACE_BEGIN
+
+class JsonNode;
+class CreatureID;
+class CStackBasicDescriptor;
+using TQuantity = si32;
+
+/// Strings classes that can be used as replacement in MetaString
+enum class EMetaText : uint8_t
+{
+	GENERAL_TXT = 1,
+	OBJ_NAMES,
+	RES_NAMES,
+	ART_NAMES,
+	ARRAY_TXT,
+	CRE_PL_NAMES,
+	CREGENS,
+	MINE_NAMES,
+	MINE_EVNTS,
+	ADVOB_TXT,
+	ART_EVNTS,
+	SPELL_NAME,
+	SEC_SKILL_NAME,
+	CRE_SING_NAMES,
+	CREGENS4,
+	COLOR,
+	ART_DESCR,
+	JK_TXT
+};
+
+/// Class for string formatting tools that also support transfer over network with localization using language of local install
+/// Can be used to compose resulting text from multiple line segments and with placeholder replacement
+class DLL_LINKAGE MetaString
+{
+private:
+	enum class EMessage : uint8_t
+	{
+		APPEND_RAW_STRING,
+		APPEND_LOCAL_STRING,
+		APPEND_TEXTID_STRING,
+		APPEND_NUMBER,
+		REPLACE_RAW_STRING,
+		REPLACE_LOCAL_STRING,
+		REPLACE_TEXTID_STRING,
+		REPLACE_NUMBER,
+		REPLACE_POSITIVE_NUMBER
+	};
+
+	std::vector<EMessage> message;
+
+	std::vector<std::pair<EMetaText,ui32> > localStrings;
+	std::vector<std::string> exactStrings;
+	std::vector<std::string> stringsTextID;
+	std::vector<int64_t> numbers;
+
+	std::string getLocalString(const std::pair<EMetaText, ui32> & txt) const;
+
+public:
+	/// Creates MetaString and appends provided raw string to it
+	static MetaString createFromRawString(const std::string & value);
+	/// Creates MetaString and appends provided text ID string to it
+	static MetaString createFromTextID(const std::string & value);
+
+	/// Appends local string to resulting string
+	void appendLocalString(EMetaText type, ui32 serial);
+	/// Appends raw string, without translation to resulting string
+	void appendRawString(const std::string & value);
+	/// Appends text ID that will be translated in output
+	void appendTextID(const std::string & value);
+	/// Appends specified number to resulting string
+	void appendNumber(int64_t value);
+
+	/// Replaces first '%s' placeholder in string with specified local string
+	void replaceLocalString(EMetaText type, ui32 serial);
+	/// Replaces first '%s' placeholder in string with specified fixed, untranslated string
+	void replaceRawString(const std::string & txt);
+	/// Repalces first '%s' placeholder with string ID that will be translated in output
+	void replaceTextID(const std::string & value);
+	/// Replaces first '%d' placeholder in string with specified number
+	void replaceNumber(int64_t txt);
+	/// Replaces first '%+d' placeholder in string with specified number using '+' sign as prefix
+	void replacePositiveNumber(int64_t txt);
+
+	/// Replaces first '%s' placeholder with singular or plural name depending on creatures count
+	void replaceCreatureName(const CreatureID & id, TQuantity count);
+	/// Replaces first '%s' placeholder with singular or plural name depending on creatures count
+	void replaceCreatureName(const CStackBasicDescriptor & stack);
+
+	/// erases any existing content in the string
+	void clear();
+
+	///used to handle loot from creature bank
+	std::string buildList() const;
+
+	/// Convert all stored values into a single, user-readable string
+	std::string toString() const;
+
+	/// Returns true if current string is empty
+	bool empty() const;
+
+	bool operator == (const MetaString & other) const;
+
+	void jsonSerialize(JsonNode & dest) const;
+	void jsonDeserialize(const JsonNode & dest);
+
+	template <typename Handler> void serialize(Handler & h, const int version)
+	{
+		h & exactStrings;
+		h & localStrings;
+		h & stringsTextID;
+		h & message;
+		h & numbers;
+	}
+};
+
+VCMI_LIB_NAMESPACE_END

+ 1 - 0
lib/NetPacks.h

@@ -19,6 +19,7 @@
 #include "CGameStateFwd.h"
 #include "mapping/CMapDefines.h"
 #include "battle/CObstacleInstance.h"
+#include "MetaString.h"
 
 #include "spells/ViewSpellInt.h"
 

+ 0 - 80
lib/NetPacksBase.h

@@ -115,86 +115,6 @@ protected:
 	virtual void visitBasic(ICPackVisitor & cpackVisitor) override;
 };
 
-struct DLL_LINKAGE MetaString
-{
-private:
-	enum EMessage {TEXACT_STRING, TLOCAL_STRING, TNUMBER, TREPLACE_ESTRING, TREPLACE_LSTRING, TREPLACE_NUMBER, TREPLACE_PLUSNUMBER};
-public:
-	enum {GENERAL_TXT=1, OBJ_NAMES, RES_NAMES, ART_NAMES, ARRAY_TXT, CRE_PL_NAMES, CREGENS, MINE_NAMES,
-		MINE_EVNTS, ADVOB_TXT, ART_EVNTS, SPELL_NAME, SEC_SKILL_NAME, CRE_SING_NAMES, CREGENS4, COLOR, ART_DESCR, JK_TXT};
-
-	std::vector<ui8> message; //vector of EMessage
-
-	std::vector<std::pair<ui8,ui32> > localStrings;
-	std::vector<std::string> exactStrings;
-	std::vector<int64_t> numbers;
-
-	template <typename Handler> void serialize(Handler & h, const int version)
-	{
-		h & exactStrings;
-		h & localStrings;
-		h & message;
-		h & numbers;
-	}
-	void addTxt(ui8 type, ui32 serial)
-	{
-		message.push_back(TLOCAL_STRING);
-		localStrings.emplace_back(type, serial);
-	}
-	MetaString& operator<<(const std::pair<ui8,ui32> &txt)
-	{
-		message.push_back(TLOCAL_STRING);
-		localStrings.push_back(txt);
-		return *this;
-	}
-	MetaString& operator<<(const std::string &txt)
-	{
-		message.push_back(TEXACT_STRING);
-		exactStrings.push_back(txt);
-		return *this;
-	}
-	MetaString& operator<<(int64_t txt)
-	{
-		message.push_back(TNUMBER);
-		numbers.push_back(txt);
-		return *this;
-	}
-	void addReplacement(ui8 type, ui32 serial)
-	{
-		message.push_back(TREPLACE_LSTRING);
-		localStrings.emplace_back(type, serial);
-	}
-	void addReplacement(const std::string &txt)
-	{
-		message.push_back(TREPLACE_ESTRING);
-		exactStrings.push_back(txt);
-	}
-	void addReplacement(int64_t txt)
-	{
-		message.push_back(TREPLACE_NUMBER);
-		numbers.push_back(txt);
-	}
-	void addReplacement2(int64_t txt)
-	{
-		message.push_back(TREPLACE_PLUSNUMBER);
-		numbers.push_back(txt);
-	}
-	void addCreReplacement(const CreatureID & id, TQuantity count); //adds sing or plural name;
-	void addReplacement(const CStackBasicDescriptor &stack); //adds sing or plural name;
-	std::string buildList () const;
-	void clear()
-	{
-		exactStrings.clear();
-		localStrings.clear();
-		message.clear();
-		numbers.clear();
-	}
-	void toString(std::string &dst) const;
-	std::string toString() const;
-	void getLocalString(const std::pair<ui8, ui32> & txt, std::string & dst) const;
-
-};
-
 struct Component
 {
 	enum class EComponentType : uint8_t 

+ 4 - 3
lib/NetPacksLib.cpp

@@ -27,6 +27,7 @@
 #include "StartInfo.h"
 #include "CPlayerState.h"
 #include "TerrainHandler.h"
+#include "mapObjects/CGCreature.h"
 #include "mapObjects/CGMarket.h"
 #include "mapObjectConstructors/AObjectTypeHandler.h"
 #include "mapObjectConstructors/CObjectClassesHandler.h"
@@ -984,7 +985,7 @@ void GiveBonus::applyGs(CGameState *gs)
 
 	std::string &descr = b->description;
 
-	if(bdescr.message.empty() && (bonus.type == BonusType::LUCK || bonus.type == BonusType::MORALE))
+	if(bdescr.empty() && (bonus.type == BonusType::LUCK || bonus.type == BonusType::MORALE))
 	{
 		if (bonus.source == BonusSource::OBJECT)
 		{
@@ -997,12 +998,12 @@ void GiveBonus::applyGs(CGameState *gs)
 		}
 		else
 		{
-			bdescr.toString(descr);
+			descr = bdescr.toString();
 		}
 	}
 	else
 	{
-		bdescr.toString(descr);
+		descr = bdescr.toString();
 	}
 	// Some of(?) versions of H3 use %s here instead of %d. Try to replace both of them
 	boost::replace_first(descr, "%d", std::to_string(std::abs(bonus.val)));

+ 2 - 2
lib/battle/CUnitState.cpp

@@ -482,10 +482,10 @@ void CUnitState::getCasterName(MetaString & text) const
 
 void CUnitState::getCastDescription(const spells::Spell * spell, const std::vector<const Unit *> & attacked, MetaString & text) const
 {
-	text.addTxt(MetaString::GENERAL_TXT, 565);//The %s casts %s
+	text.appendLocalString(EMetaText::GENERAL_TXT, 565);//The %s casts %s
 	//todo: use text 566 for single creature
 	getCasterName(text);
-	text.addReplacement(MetaString::SPELL_NAME, spell->getIndex());
+	text.replaceLocalString(EMetaText::SPELL_NAME, spell->getIndex());
 }
 
 int32_t CUnitState::manaLimit() const

+ 8 - 7
lib/battle/Unit.cpp

@@ -13,6 +13,7 @@
 
 #include "../VCMI_Lib.h"
 #include "../CGeneralTextHandler.h"
+#include "../MetaString.h"
 #include "../NetPacksBase.h"
 
 #include "../serializer/JsonDeserializer.h"
@@ -172,7 +173,7 @@ BattleHex Unit::occupiedHex(BattleHex assumedPos, bool twoHex, ui8 side)
 	}
 }
 
-void Unit::addText(MetaString & text, ui8 type, int32_t serial, const boost::logic::tribool & plural) const
+void Unit::addText(MetaString & text, EMetaText type, int32_t serial, const boost::logic::tribool & plural) const
 {
 	if(boost::logic::indeterminate(plural))
 		serial = VLC->generaltexth->pluralText(serial, getCount());
@@ -181,17 +182,17 @@ void Unit::addText(MetaString & text, ui8 type, int32_t serial, const boost::log
 	else
 		serial = VLC->generaltexth->pluralText(serial, 1);
 
-	text.addTxt(type, serial);
+	text.appendLocalString(type, serial);
 }
 
 void Unit::addNameReplacement(MetaString & text, const boost::logic::tribool & plural) const
 {
 	if(boost::logic::indeterminate(plural))
-		text.addCreReplacement(creatureId(), getCount());
+		text.replaceCreatureName(creatureId(), getCount());
 	else if(plural)
-		text.addReplacement(MetaString::CRE_PL_NAMES, creatureIndex());
+		text.replaceLocalString(EMetaText::CRE_PL_NAMES, creatureIndex());
 	else
-		text.addReplacement(MetaString::CRE_SING_NAMES, creatureIndex());
+		text.replaceLocalString(EMetaText::CRE_SING_NAMES, creatureIndex());
 }
 
 std::string Unit::formatGeneralMessage(const int32_t baseTextId) const
@@ -199,8 +200,8 @@ std::string Unit::formatGeneralMessage(const int32_t baseTextId) const
 	const int32_t textId = VLC->generaltexth->pluralText(baseTextId, getCount());
 
 	MetaString text;
-	text.addTxt(MetaString::GENERAL_TXT, textId);
-	text.addCreReplacement(creatureId(), getCount());
+	text.appendLocalString(EMetaText::GENERAL_TXT, textId);
+	text.replaceCreatureName(creatureId(), getCount());
 
 	return text.toString();
 }

+ 3 - 2
lib/battle/Unit.h

@@ -21,7 +21,8 @@
 
 VCMI_LIB_NAMESPACE_BEGIN
 
-struct MetaString;
+enum class EMetaText : uint8_t;
+class MetaString;
 class JsonNode;
 class JsonSerializeFormat;
 
@@ -122,7 +123,7 @@ public:
 	static BattleHex occupiedHex(BattleHex assumedPos, bool twoHex, ui8 side);
 
 	///MetaStrings
-	void addText(MetaString & text, ui8 type, int32_t serial, const boost::logic::tribool & plural = boost::logic::indeterminate) const;
+	void addText(MetaString & text, EMetaText type, int32_t serial, const boost::logic::tribool & plural = boost::logic::indeterminate) const;
 	void addNameReplacement(MetaString & text, const boost::logic::tribool & plural = boost::logic::indeterminate) const;
 	std::string formatGeneralMessage(const int32_t baseTextId) const;
 

+ 5 - 0
lib/mapObjectConstructors/AObjectTypeHandler.cpp

@@ -148,6 +148,11 @@ SObjectSounds AObjectTypeHandler::getSounds() const
 	return sounds;
 }
 
+void AObjectTypeHandler::clearTemplates()
+{
+	templates.clear();
+}
+
 void AObjectTypeHandler::addTemplate(const std::shared_ptr<const ObjectTemplate> & templ)
 {
 	templates.push_back(templ);

+ 1 - 0
lib/mapObjectConstructors/AObjectTypeHandler.h

@@ -70,6 +70,7 @@ public:
 
 	void addTemplate(const std::shared_ptr<const ObjectTemplate> & templ);
 	void addTemplate(JsonNode config);
+	void clearTemplates();
 
 	/// returns all templates matching parameters
 	std::vector<std::shared_ptr<const ObjectTemplate>> getTemplates() const;

+ 1 - 0
lib/mapObjectConstructors/CObjectClassesHandler.cpp

@@ -28,6 +28,7 @@
 #include "../mapObjectConstructors/HillFortInstanceConstructor.h"
 #include "../mapObjectConstructors/ShipyardInstanceConstructor.h"
 #include "../mapObjectConstructors/ShrineInstanceConstructor.h"
+#include "../mapObjects/CGCreature.h"
 #include "../mapObjects/CGPandoraBox.h"
 #include "../mapObjects/CQuest.h"
 #include "../mapObjects/ObjectTemplate.h"

+ 2 - 2
lib/mapObjectConstructors/ShrineInstanceConstructor.cpp

@@ -26,9 +26,9 @@ void ShrineInstanceConstructor::randomizeObject(CGShrine * shrine, CRandomGenera
 	auto visitTextParameter = parameters["visitText"];
 
 	if (visitTextParameter.isNumber())
-		shrine->visitText.addTxt(MetaString::ADVOB_TXT, static_cast<ui32>(visitTextParameter.Float()));
+		shrine->visitText.appendLocalString(EMetaText::ADVOB_TXT, static_cast<ui32>(visitTextParameter.Float()));
 	else
-		shrine->visitText << visitTextParameter.String();
+		shrine->visitText.appendRawString(visitTextParameter.String());
 
 	if(shrine->spell == SpellID::NONE) // shrine has no predefined spell
 	{

+ 26 - 26
lib/mapObjects/CBank.cpp

@@ -123,9 +123,9 @@ void CBank::onHeroVisit(const CGHeroInstance * h) const
 	BlockingDialog bd(true, false);
 	bd.player = h->getOwner();
 	bd.soundID = soundBase::invalid; // Sound is handled in json files, else two sounds are played
-	bd.text.addTxt(MetaString::ADVOB_TXT, banktext);
+	bd.text.appendLocalString(EMetaText::ADVOB_TXT, banktext);
 	if (banktext == 32)
-		bd.text.addReplacement(getObjectName());
+		bd.text.replaceRawString(getObjectName());
 	cb->showBlockingDialog(&bd);
 }
 
@@ -179,15 +179,15 @@ void CBank::doVisit(const CGHeroInstance * hero) const
 			{
 			case Obj::SHIPWRECK:
 				textID = 123;
-				gbonus.bdescr << VLC->generaltexth->arraytxt[99];
+				gbonus.bdescr.appendRawString(VLC->generaltexth->arraytxt[99]);
 				break;
 			case Obj::DERELICT_SHIP:
 				textID = 42;
-				gbonus.bdescr << VLC->generaltexth->arraytxt[101];
+				gbonus.bdescr.appendRawString(VLC->generaltexth->arraytxt[101]);
 				break;
 			case Obj::CRYPT:
 				textID = 120;
-				gbonus.bdescr << VLC->generaltexth->arraytxt[98];
+				gbonus.bdescr.appendRawString(VLC->generaltexth->arraytxt[98]);
 				break;
 			}
 			cb->giveHeroBonus(&gbonus);
@@ -208,12 +208,12 @@ void CBank::doVisit(const CGHeroInstance * hero) const
 		case Obj::CREATURE_BANK:
 		case Obj::DRAGON_UTOPIA:
 		default:
-			iw.text << VLC->generaltexth->advobtxt[33];// This was X, now is completely empty
-			iw.text.addReplacement(getObjectName());
+			iw.text.appendRawString(VLC->generaltexth->advobtxt[33]);// This was X, now is completely empty
+			iw.text.replaceRawString(getObjectName());
 		}
 		if(textID != -1)
 		{
-			iw.text.addTxt(MetaString::ADVOB_TXT, textID);
+			iw.text.appendLocalString(EMetaText::ADVOB_TXT, textID);
 		}
 		cb->showInfoDialog(&iw);
 	}
@@ -227,9 +227,9 @@ void CBank::doVisit(const CGHeroInstance * hero) const
 			if (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);
+				loot.appendRawString("%d %s");
+				loot.replaceNumber(iw.components.back().val);
+				loot.replaceLocalString(EMetaText::RES_NAMES, iw.components.back().subtype);
 				cb->giveResource(hero->getOwner(), static_cast<EGameResID>(it), bc->resources[it]);
 			}
 		}
@@ -237,14 +237,14 @@ void CBank::doVisit(const CGHeroInstance * hero) const
 		for (auto & elem : bc->artifacts)
 		{
 			iw.components.emplace_back(Component::EComponentType::ARTIFACT, elem, 0, 0);
-			loot << "%s";
-			loot.addReplacement(MetaString::ART_NAMES, elem);
+			loot.appendRawString("%s");
+			loot.replaceLocalString(EMetaText::ART_NAMES, elem);
 			cb->giveHeroNewArtifact(hero, VLC->arth->objects[elem], ArtifactPosition::FIRST_AVAILABLE);
 		}
 		//display loot
 		if (!iw.components.empty())
 		{
-			iw.text.addTxt(MetaString::ADVOB_TXT, textID);
+			iw.text.appendLocalString(EMetaText::ADVOB_TXT, textID);
 			if (textID == 34)
 			{
 				const auto * strongest = boost::range::max_element(bc->guards, [](const CStackBasicDescriptor & a, const CStackBasicDescriptor & b)
@@ -252,8 +252,8 @@ void CBank::doVisit(const CGHeroInstance * hero) const
 					return a.type->getFightValue() < b.type->getFightValue();
 				})->type;
 
-				iw.text.addReplacement(MetaString::CRE_PL_NAMES, strongest->getId());
-				iw.text.addReplacement(loot.buildList());
+				iw.text.replaceLocalString(EMetaText::CRE_PL_NAMES, strongest->getId());
+				iw.text.replaceRawString(loot.buildList());
 			}
 			cb->showInfoDialog(&iw);
 		}
@@ -269,12 +269,12 @@ void CBank::doVisit(const CGHeroInstance * hero) const
 			bool noWisdom = false;
 			if(textID == 106)
 			{
-				iw.text.addTxt(MetaString::ADVOB_TXT, textID); //pyramid
+				iw.text.appendLocalString(EMetaText::ADVOB_TXT, textID); //pyramid
 			}
 			for(const SpellID & spellId : bc->spells)
 			{
 				const auto * spell = spellId.toSpell(VLC->spells());
-				iw.text.addTxt(MetaString::SPELL_NAME, spellId);
+				iw.text.appendLocalString(EMetaText::SPELL_NAME, spellId);
 				if(spell->getLevel() <= hero->maxSpellLevel())
 				{
 					if(hero->canLearnSpell(spell))
@@ -288,9 +288,9 @@ void CBank::doVisit(const CGHeroInstance * hero) const
 			}
 
 			if (!hero->getArt(ArtifactPosition::SPELLBOOK))
-				iw.text.addTxt(MetaString::ADVOB_TXT, 109); //no spellbook
+				iw.text.appendLocalString(EMetaText::ADVOB_TXT, 109); //no spellbook
 			else if(noWisdom)
-				iw.text.addTxt(MetaString::ADVOB_TXT, 108); //no expert Wisdom
+				iw.text.appendLocalString(EMetaText::ADVOB_TXT, 108); //no expert Wisdom
 
 			if(!iw.components.empty() || !iw.text.toString().empty())
 				cb->showInfoDialog(&iw);
@@ -312,19 +312,19 @@ void CBank::doVisit(const CGHeroInstance * hero) const
 		for(const auto & elem : ourArmy.Slots())
 		{
 			iw.components.emplace_back(*elem.second);
-			loot << "%s";
-			loot.addReplacement(*elem.second);
+			loot.appendRawString("%s");
+			loot.replaceCreatureName(*elem.second);
 		}
 
 		if(ourArmy.stacksCount())
 		{
 			if(ourArmy.stacksCount() == 1 && ourArmy.Slots().begin()->second->count == 1)
-				iw.text.addTxt(MetaString::ADVOB_TXT, 185);
+				iw.text.appendLocalString(EMetaText::ADVOB_TXT, 185);
 			else
-				iw.text.addTxt(MetaString::ADVOB_TXT, 186);
+				iw.text.appendLocalString(EMetaText::ADVOB_TXT, 186);
 
-			iw.text.addReplacement(loot.buildList());
-			iw.text.addReplacement(hero->getNameTranslated());
+			iw.text.replaceRawString(loot.buildList());
+			iw.text.replaceRawString(hero->getNameTranslated());
 			cb->showInfoDialog(&iw);
 			cb->giveCreatures(this, hero, ourArmy, false);
 		}

+ 574 - 0
lib/mapObjects/CGCreature.cpp

@@ -0,0 +1,574 @@
+/*
+ * CGCreature.cpp, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+
+#include "StdInc.h"
+#include "CGCreature.h"
+
+#include "../NetPacks.h"
+#include "../CGeneralTextHandler.h"
+#include "../CConfigHandler.h"
+#include "../GameSettings.h"
+#include "../IGameCallback.h"
+#include "../serializer/JsonSerializeFormat.h"
+
+VCMI_LIB_NAMESPACE_BEGIN
+
+std::string CGCreature::getHoverText(PlayerColor player) const
+{
+	if(stacks.empty())
+	{
+		//should not happen...
+		logGlobal->error("Invalid stack at tile %s: subID=%d; id=%d", pos.toString(), subID, id.getNum());
+		return "INVALID_STACK";
+	}
+
+	std::string hoverName;
+	MetaString ms;
+	CCreature::CreatureQuantityId monsterQuantityId = stacks.begin()->second->getQuantityID();
+	int quantityTextIndex = 172 + 3 * (int)monsterQuantityId;
+	if(settings["gameTweaks"]["numericCreaturesQuantities"].Bool())
+		ms.appendRawString(CCreature::getQuantityRangeStringForId(monsterQuantityId));
+	else
+		ms.appendLocalString(EMetaText::ARRAY_TXT, quantityTextIndex);
+	ms.appendRawString(" ");
+	ms.appendLocalString(EMetaText::CRE_PL_NAMES,subID);
+	hoverName = ms.toString();
+	return hoverName;
+}
+
+std::string CGCreature::getHoverText(const CGHeroInstance * hero) const
+{
+	std::string hoverName;
+	if(hero->hasVisions(this, 0))
+	{
+		MetaString ms;
+		ms.appendNumber(stacks.begin()->second->count);
+		ms.appendRawString(" ");
+		ms.appendLocalString(EMetaText::CRE_PL_NAMES,subID);
+
+		ms.appendRawString("\n");
+
+		int decision = takenAction(hero, true);
+
+		switch (decision)
+		{
+		case FIGHT:
+			ms.appendLocalString(EMetaText::GENERAL_TXT,246);
+			break;
+		case FLEE:
+			ms.appendLocalString(EMetaText::GENERAL_TXT,245);
+			break;
+		case JOIN_FOR_FREE:
+			ms.appendLocalString(EMetaText::GENERAL_TXT,243);
+			break;
+		default: //decision = cost in gold
+			ms.appendRawString(boost::to_string(boost::format(VLC->generaltexth->allTexts[244]) % decision));
+			break;
+		}
+
+		hoverName = ms.toString();
+	}
+	else
+	{
+		hoverName = getHoverText(hero->tempOwner);
+	}
+
+	hoverName += VLC->generaltexth->translate("vcmi.adventureMap.monsterThreat.title");
+
+	int choice;
+	double ratio = (static_cast<double>(getArmyStrength()) / hero->getTotalStrength());
+		 if (ratio < 0.1)  choice = 0;
+	else if (ratio < 0.25) choice = 1;
+	else if (ratio < 0.6)  choice = 2;
+	else if (ratio < 0.9)  choice = 3;
+	else if (ratio < 1.1)  choice = 4;
+	else if (ratio < 1.3)  choice = 5;
+	else if (ratio < 1.8)  choice = 6;
+	else if (ratio < 2.5)  choice = 7;
+	else if (ratio < 4)    choice = 8;
+	else if (ratio < 8)    choice = 9;
+	else if (ratio < 20)   choice = 10;
+	else                   choice = 11;
+
+	hoverName += VLC->generaltexth->translate("vcmi.adventureMap.monsterThreat.levels." + std::to_string(choice));
+	return hoverName;
+}
+
+void CGCreature::onHeroVisit( const CGHeroInstance * h ) const
+{
+	int action = takenAction(h);
+	switch( action ) //decide what we do...
+	{
+	case FIGHT:
+		fight(h);
+		break;
+	case FLEE:
+		{
+			flee(h);
+			break;
+		}
+	case JOIN_FOR_FREE: //join for free
+		{
+			BlockingDialog ynd(true,false);
+			ynd.player = h->tempOwner;
+			ynd.text.appendLocalString(EMetaText::ADVOB_TXT, 86);
+			ynd.text.replaceLocalString(EMetaText::CRE_PL_NAMES, subID);
+			cb->showBlockingDialog(&ynd);
+			break;
+		}
+	default: //join for gold
+		{
+			assert(action > 0);
+
+			//ask if player agrees to pay gold
+			BlockingDialog ynd(true,false);
+			ynd.player = h->tempOwner;
+			std::string tmp = VLC->generaltexth->advobtxt[90];
+			boost::algorithm::replace_first(tmp, "%d", std::to_string(getStackCount(SlotID(0))));
+			boost::algorithm::replace_first(tmp, "%d", std::to_string(action));
+			boost::algorithm::replace_first(tmp,"%s",VLC->creh->objects[subID]->getNamePluralTranslated());
+			ynd.text.appendRawString(tmp);
+			cb->showBlockingDialog(&ynd);
+			break;
+		}
+	}
+
+}
+
+void CGCreature::initObj(CRandomGenerator & rand)
+{
+	blockVisit = true;
+	switch(character)
+	{
+	case 0:
+		character = -4;
+		break;
+	case 1:
+		character = rand.nextInt(1, 7);
+		break;
+	case 2:
+		character = rand.nextInt(1, 10);
+		break;
+	case 3:
+		character = rand.nextInt(4, 10);
+		break;
+	case 4:
+		character = 10;
+		break;
+	}
+
+	stacks[SlotID(0)]->setType(CreatureID(subID));
+	TQuantity &amount = stacks[SlotID(0)]->count;
+	CCreature &c = *VLC->creh->objects[subID];
+	if(amount == 0)
+	{
+		amount = rand.nextInt(c.ammMin, c.ammMax);
+
+		if(amount == 0) //armies with 0 creatures are illegal
+		{
+			logGlobal->warn("Stack %s cannot have 0 creatures. Check properties of %s", nodeName(), c.nodeName());
+			amount = 1;
+		}
+	}
+
+	temppower = stacks[SlotID(0)]->count * static_cast<ui64>(1000);
+	refusedJoining = false;
+}
+
+void CGCreature::newTurn(CRandomGenerator & rand) const
+{//Works only for stacks of single type of size up to 2 millions
+	if (!notGrowingTeam)
+	{
+		if (stacks.begin()->second->count < VLC->settings()->getInteger(EGameSettings::CREATURES_WEEKLY_GROWTH_CAP) && cb->getDate(Date::DAY_OF_WEEK) == 1 && cb->getDate(Date::DAY) > 1)
+		{
+			ui32 power = static_cast<ui32>(temppower * (100 + VLC->settings()->getInteger(EGameSettings::CREATURES_WEEKLY_GROWTH_PERCENT)) / 100);
+			cb->setObjProperty(id, ObjProperty::MONSTER_COUNT, std::min<uint32_t>(power / 1000, VLC->settings()->getInteger(EGameSettings::CREATURES_WEEKLY_GROWTH_CAP))); //set new amount
+			cb->setObjProperty(id, ObjProperty::MONSTER_POWER, power); //increase temppower
+		}
+	}
+	if (VLC->settings()->getBoolean(EGameSettings::MODULE_STACK_EXPERIENCE))
+		cb->setObjProperty(id, ObjProperty::MONSTER_EXP, VLC->settings()->getInteger(EGameSettings::CREATURES_DAILY_STACK_EXPERIENCE)); //for testing purpose
+}
+void CGCreature::setPropertyDer(ui8 what, ui32 val)
+{
+	switch (what)
+	{
+		case ObjProperty::MONSTER_COUNT:
+			stacks[SlotID(0)]->count = val;
+			break;
+		case ObjProperty::MONSTER_POWER:
+			temppower = val;
+			break;
+		case ObjProperty::MONSTER_EXP:
+			giveStackExp(val);
+			break;
+		case ObjProperty::MONSTER_RESTORE_TYPE:
+			formation.basicType = val;
+			break;
+		case ObjProperty::MONSTER_REFUSED_JOIN:
+			refusedJoining = val;
+			break;
+	}
+}
+
+int CGCreature::takenAction(const CGHeroInstance *h, bool allowJoin) const
+{
+	//calculate relative strength of hero and creatures armies
+	double relStrength = static_cast<double>(h->getTotalStrength()) / getArmyStrength();
+
+	int powerFactor;
+	if(relStrength >= 7)
+		powerFactor = 11;
+
+	else if(relStrength >= 1)
+		powerFactor = static_cast<int>(2 * (relStrength - 1));
+
+	else if(relStrength >= 0.5)
+		powerFactor = -1;
+
+	else if(relStrength >= 0.333)
+		powerFactor = -2;
+	else
+		powerFactor = -3;
+
+	std::set<CreatureID> myKindCres; //what creatures are the same kind as we
+	const CCreature * myCreature = VLC->creh->objects[subID];
+	myKindCres.insert(myCreature->getId()); //we
+	myKindCres.insert(myCreature->upgrades.begin(), myCreature->upgrades.end()); //our upgrades
+
+	for(ConstTransitivePtr<CCreature> &crea : VLC->creh->objects)
+	{
+		if(vstd::contains(crea->upgrades, myCreature->getId())) //it's our base creatures
+			myKindCres.insert(crea->getId());
+	}
+
+	int count = 0; //how many creatures of similar kind has hero
+	int totalCount = 0;
+
+	for(const auto & elem : h->Slots())
+	{
+		if(vstd::contains(myKindCres,elem.second->type->getId()))
+			count += elem.second->count;
+		totalCount += elem.second->count;
+	}
+
+	int sympathy = 0; // 0 if hero have no similar creatures
+	if(count)
+		sympathy++; // 1 - if hero have at least 1 similar creature
+	if(count*2 > totalCount)
+		sympathy++; // 2 - hero have similar creatures more that 50%
+
+	int diplomacy = h->valOfBonuses(BonusType::WANDERING_CREATURES_JOIN_BONUS);
+	int charisma = powerFactor + diplomacy + sympathy;
+
+	if(charisma < character)
+		return FIGHT;
+
+	if (allowJoin)
+	{
+		if(diplomacy + sympathy + 1 >= character)
+			return JOIN_FOR_FREE;
+
+		else if(diplomacy * 2  +  sympathy  +  1 >= character)
+			return VLC->creatures()->getByIndex(subID)->getRecruitCost(EGameResID::GOLD) * getStackCount(SlotID(0)); //join for gold
+	}
+
+	//we are still here - creatures have not joined hero, flee or fight
+
+	if (charisma > character && !neverFlees)
+		return FLEE;
+	else
+		return FIGHT;
+}
+
+void CGCreature::fleeDecision(const CGHeroInstance *h, ui32 pursue) const
+{
+	if(refusedJoining)
+		cb->setObjProperty(id, ObjProperty::MONSTER_REFUSED_JOIN, false);
+
+	if(pursue)
+	{
+		fight(h);
+	}
+	else
+	{
+		cb->removeObject(this);
+	}
+}
+
+void CGCreature::joinDecision(const CGHeroInstance *h, int cost, ui32 accept) const
+{
+	if(!accept)
+	{
+		if(takenAction(h,false) == FLEE)
+		{
+			cb->setObjProperty(id, ObjProperty::MONSTER_REFUSED_JOIN, true);
+			flee(h);
+		}
+		else //they fight
+		{
+			h->showInfoDialog(87, 0, EInfoWindowMode::MODAL);//Insulted by your refusal of their offer, the monsters attack!
+			fight(h);
+		}
+	}
+	else //accepted
+	{
+		if (cb->getResource(h->tempOwner, EGameResID::GOLD) < cost) //player don't have enough gold!
+		{
+			InfoWindow iw;
+			iw.player = h->tempOwner;
+			iw.text.appendLocalString(EMetaText::GENERAL_TXT,29);  //You don't have enough gold
+			cb->showInfoDialog(&iw);
+
+			//act as if player refused
+			joinDecision(h,cost,false);
+			return;
+		}
+
+		//take gold
+		if(cost)
+			cb->giveResource(h->tempOwner,EGameResID::GOLD,-cost);
+
+		giveReward(h);
+		cb->tryJoiningArmy(this, h, true, true);
+	}
+}
+
+void CGCreature::fight( const CGHeroInstance *h ) const
+{
+	//split stacks
+	//TODO: multiple creature types in a stack?
+	int basicType = stacks.begin()->second->type->getId();
+	cb->setObjProperty(id, ObjProperty::MONSTER_RESTORE_TYPE, basicType); //store info about creature stack
+
+	int stacksCount = getNumberOfStacks(h);
+	//source: http://heroescommunity.com/viewthread.php3?TID=27539&PID=1266335#focus
+
+	int amount = getStackCount(SlotID(0));
+	int m = amount / stacksCount;
+	int b = stacksCount * (m + 1) - amount;
+	int a = stacksCount - b;
+
+	SlotID sourceSlot = stacks.begin()->first;
+	for (int slotID = 1; slotID < a; ++slotID)
+	{
+		int stackSize = m + 1;
+		cb->moveStack(StackLocation(this, sourceSlot), StackLocation(this, SlotID(slotID)), stackSize);
+	}
+	for (int slotID = a; slotID < stacksCount; ++slotID)
+	{
+		int stackSize = m;
+		if (slotID) //don't do this when a = 0 -> stack is single
+			cb->moveStack(StackLocation(this, sourceSlot), StackLocation(this, SlotID(slotID)), stackSize);
+	}
+	if (stacksCount > 1)
+	{
+		if (containsUpgradedStack()) //upgrade
+		{
+			SlotID slotID = SlotID(static_cast<si32>(std::floor(static_cast<float>(stacks.size()) / 2.0f)));
+			const auto & upgrades = getStack(slotID).type->upgrades;
+			if(!upgrades.empty())
+			{
+				auto it = RandomGeneratorUtil::nextItem(upgrades, CRandomGenerator::getDefault());
+				cb->changeStackType(StackLocation(this, slotID), VLC->creh->objects[*it]);
+			}
+		}
+	}
+
+	cb->startBattleI(h, this);
+
+}
+
+void CGCreature::flee( const CGHeroInstance * h ) const
+{
+	BlockingDialog ynd(true,false);
+	ynd.player = h->tempOwner;
+	ynd.text.appendLocalString(EMetaText::ADVOB_TXT,91);
+	ynd.text.replaceLocalString(EMetaText::CRE_PL_NAMES, subID);
+	cb->showBlockingDialog(&ynd);
+}
+
+void CGCreature::battleFinished(const CGHeroInstance *hero, const BattleResult &result) const
+{
+	if(result.winner == 0)
+	{
+		giveReward(hero);
+		cb->removeObject(this);
+	}
+	else if(result.winner > 1) // draw
+	{
+		// guarded reward is lost forever on draw
+		cb->removeObject(this);
+	}
+	else
+	{
+		//merge stacks into one
+		TSlots::const_iterator i;
+		CCreature * cre = VLC->creh->objects[formation.basicType];
+		for(i = stacks.begin(); i != stacks.end(); i++)
+		{
+			if(cre->isMyUpgrade(i->second->type))
+			{
+				cb->changeStackType(StackLocation(this, i->first), cre); //un-upgrade creatures
+			}
+		}
+
+		//first stack has to be at slot 0 -> if original one got killed, move there first remaining stack
+		if(!hasStackAtSlot(SlotID(0)))
+			cb->moveStack(StackLocation(this, stacks.begin()->first), StackLocation(this, SlotID(0)), stacks.begin()->second->count);
+
+		while(stacks.size() > 1) //hopefully that's enough
+		{
+			// TODO it's either overcomplicated (if we assume there'll be only one stack) or buggy (if we allow multiple stacks... but that'll also cause troubles elsewhere)
+			i = stacks.end();
+			i--;
+			SlotID slot = getSlotFor(i->second->type);
+			if(slot == i->first) //no reason to move stack to its own slot
+				break;
+			else
+				cb->moveStack(StackLocation(this, i->first), StackLocation(this, slot), i->second->count);
+		}
+
+		cb->setObjProperty(id, ObjProperty::MONSTER_POWER, stacks.begin()->second->count * 1000); //remember casualties
+	}
+}
+
+void CGCreature::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const
+{
+	auto action = takenAction(hero);
+	if(!refusedJoining && action >= JOIN_FOR_FREE) //higher means price
+		joinDecision(hero, action, answer);
+	else if(action != FIGHT)
+		fleeDecision(hero, answer);
+	else
+		assert(0);
+}
+
+bool CGCreature::containsUpgradedStack() const
+{
+	//source http://heroescommunity.com/viewthread.php3?TID=27539&PID=830557#focus
+
+	float a = 2992.911117f;
+	float b = 14174.264968f;
+	float c = 5325.181015f;
+	float d = 32788.727920f;
+
+	int val = static_cast<int>(std::floor(a * pos.x + b * pos.y + c * pos.z + d));
+	return ((val % 32768) % 100) < 50;
+}
+
+int CGCreature::getNumberOfStacks(const CGHeroInstance *hero) const
+{
+	//source http://heroescommunity.com/viewthread.php3?TID=27539&PID=1266094#focus
+
+	double strengthRatio = static_cast<double>(hero->getArmyStrength()) / getArmyStrength();
+	int split = 1;
+
+	if (strengthRatio < 0.5f)
+		split = 7;
+	else if (strengthRatio < 0.67f)
+		split = 6;
+	else if (strengthRatio < 1)
+		split = 5;
+	else if (strengthRatio < 1.5f)
+		split = 4;
+	else if (strengthRatio < 2)
+		split = 3;
+	else
+		split = 2;
+
+	ui32 a = 1550811371u;
+	ui32 b = 3359066809u;
+	ui32 c = 1943276003u;
+	ui32 d = 3174620878u;
+
+	ui32 R1 = a * static_cast<ui32>(pos.x) + b * static_cast<ui32>(pos.y) + c * static_cast<ui32>(pos.z) + d;
+	ui32 R2 = (R1 >> 16) & 0x7fff;
+
+	int R4 = R2 % 100 + 1;
+
+	if (R4 <= 20)
+		split -= 1;
+	else if (R4 >= 80)
+		split += 1;
+
+	vstd::amin(split, getStack(SlotID(0)).count); //can't divide into more stacks than creatures total
+	vstd::amin(split, 7); //can't have more than 7 stacks
+
+	return split;
+}
+
+void CGCreature::giveReward(const CGHeroInstance * h) const
+{
+	InfoWindow iw;
+	iw.player = h->tempOwner;
+
+	if(!resources.empty())
+	{
+		cb->giveResources(h->tempOwner, resources);
+		for(int i = 0; i < resources.size(); i++)
+		{
+			if(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::EComponentType::ARTIFACT, gainedArtifact, 0, 0);
+	}
+
+	if(!iw.components.empty())
+	{
+		iw.type = EInfoWindowMode::AUTO;
+		iw.text.appendLocalString(EMetaText::ADVOB_TXT, 183); // % has found treasure
+		iw.text.replaceRawString(h->getNameTranslated());
+		cb->showInfoDialog(&iw);
+	}
+}
+
+static const std::vector<std::string> CHARACTER_JSON  =
+{
+	"compliant", "friendly", "aggressive", "hostile", "savage"
+};
+
+void CGCreature::serializeJsonOptions(JsonSerializeFormat & handler)
+{
+	handler.serializeEnum("character", character, CHARACTER_JSON);
+
+	if(handler.saving)
+	{
+		if(hasStackAtSlot(SlotID(0)))
+		{
+			si32 amount = getStack(SlotID(0)).count;
+			handler.serializeInt("amount", amount, 0);
+		}
+	}
+	else
+	{
+		si32 amount = 0;
+		handler.serializeInt("amount", amount);
+		auto * hlp = new CStackInstance();
+		hlp->count = amount;
+		//type will be set during initialization
+		putStack(SlotID(0), hlp);
+	}
+
+	resources.serializeJson(handler, "rewardResources");
+
+	handler.serializeId("rewardArtifact", gainedArtifact, ArtifactID(ArtifactID::NONE));
+
+	handler.serializeBool("noGrowing", notGrowingTeam);
+	handler.serializeBool("neverFlees", neverFlees);
+	handler.serializeString("rewardMessage", message);
+}
+
+VCMI_LIB_NAMESPACE_END

+ 91 - 0
lib/mapObjects/CGCreature.h

@@ -0,0 +1,91 @@
+/*
+ * CGCreature.h, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+#pragma once
+
+#include "CArmedInstance.h"
+#include "../ResourceSet.h"
+
+VCMI_LIB_NAMESPACE_BEGIN
+
+class DLL_LINKAGE CGCreature : public CArmedInstance //creatures on map
+{
+public:
+	enum Action {
+		FIGHT = -2, FLEE = -1, JOIN_FOR_FREE = 0 //values > 0 mean gold price
+	};
+
+	enum Character {
+		COMPLIANT = 0, FRIENDLY = 1, AGRESSIVE = 2, HOSTILE = 3, SAVAGE = 4
+	};
+
+	ui32 identifier; //unique code for this monster (used in missions)
+	si8 character; //character of this set of creatures (0 - the most friendly, 4 - the most hostile) => on init changed to -4 (compliant) ... 10 value (savage)
+	std::string message; //message printed for attacking hero
+	TResources resources; // resources given to hero that has won with monsters
+	ArtifactID gainedArtifact; //ID of artifact gained to hero, -1 if none
+	bool neverFlees; //if true, the troops will never flee
+	bool notGrowingTeam; //if true, number of units won't grow
+	ui64 temppower; //used to handle fractional stack growth for tiny stacks
+
+	bool refusedJoining;
+
+	void onHeroVisit(const CGHeroInstance * h) const override;
+	std::string getHoverText(PlayerColor player) const override;
+	std::string getHoverText(const CGHeroInstance * hero) const override;
+	void initObj(CRandomGenerator & rand) override;
+	void newTurn(CRandomGenerator & rand) const override;
+	void battleFinished(const CGHeroInstance *hero, const BattleResult &result) const override;
+	void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override;
+
+	//stack formation depends on position,
+	bool containsUpgradedStack() const;
+	int getNumberOfStacks(const CGHeroInstance *hero) const;
+
+	struct DLL_LINKAGE formationInfo // info about merging stacks after battle back into one
+	{
+		si32 basicType;
+		ui8 upgrade; //random seed used to determine number of stacks and is there's upgraded stack
+		template <typename Handler> void serialize(Handler &h, const int version)
+		{
+			h & basicType;
+			h & upgrade;
+		}
+	} formation;
+
+	template <typename Handler> void serialize(Handler &h, const int version)
+	{
+		h & static_cast<CArmedInstance&>(*this);
+		h & identifier;
+		h & character;
+		h & message;
+		h & resources;
+		h & gainedArtifact;
+		h & neverFlees;
+		h & notGrowingTeam;
+		h & temppower;
+		h & refusedJoining;
+		h & formation;
+	}
+protected:
+	void setPropertyDer(ui8 what, ui32 val) override;
+	void serializeJsonOptions(JsonSerializeFormat & handler) override;
+
+private:
+	void fight(const CGHeroInstance *h) const;
+	void flee( const CGHeroInstance * h ) const;
+	void fleeDecision(const CGHeroInstance *h, ui32 pursue) const;
+	void joinDecision(const CGHeroInstance *h, int cost, ui32 accept) const;
+
+	int takenAction(const CGHeroInstance *h, bool allowJoin=true) const; //action on confrontation: -2 - fight, -1 - flee, >=0 - will join for given value of gold (may be 0)
+	void giveReward(const CGHeroInstance * h) const;
+
+};
+
+VCMI_LIB_NAMESPACE_END

+ 21 - 21
lib/mapObjects/CGDwelling.cpp

@@ -167,8 +167,8 @@ void CGDwelling::onHeroVisit( const CGHeroInstance * h ) const
 		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);
+		iw.text.appendLocalString(EMetaText::ADVOB_TXT, 44); //{%s} \n\n The camp is deserted.  Perhaps you should try next week.
+		iw.text.replaceLocalString(EMetaText::OBJ_NAMES, ID);
 		cb->sendAndApply(&iw);
 		return;
 	}
@@ -182,13 +182,13 @@ void CGDwelling::onHeroVisit( const CGHeroInstance * h ) const
 	{
 		BlockingDialog bd(true,false);
 		bd.player = h->tempOwner;
-		bd.text.addTxt(MetaString::GENERAL_TXT, 421); //Much to your dismay, the %s is guarded by %s %s. Do you wish to fight the guards?
-		bd.text.addReplacement(ID == Obj::CREATURE_GENERATOR1 ? MetaString::CREGENS : MetaString::CREGENS4, subID);
+		bd.text.appendLocalString(EMetaText::GENERAL_TXT, 421); //Much to your dismay, the %s is guarded by %s %s. Do you wish to fight the guards?
+		bd.text.replaceLocalString(ID == Obj::CREATURE_GENERATOR1 ? EMetaText::CREGENS : EMetaText::CREGENS4, subID);
 		if(settings["gameTweaks"]["numericCreaturesQuantities"].Bool())
-			bd.text.addReplacement(CCreature::getQuantityRangeStringForId(Slots().begin()->second->getQuantityID()));
+			bd.text.replaceRawString(CCreature::getQuantityRangeStringForId(Slots().begin()->second->getQuantityID()));
 		else
-			bd.text.addReplacement(MetaString::ARRAY_TXT, 173 + (int)Slots().begin()->second->getQuantityID()*3);
-		bd.text.addReplacement(*Slots().begin()->second);
+			bd.text.replaceLocalString(EMetaText::ARRAY_TXT, 173 + (int)Slots().begin()->second->getQuantityID()*3);
+		bd.text.replaceCreatureName(*Slots().begin()->second);
 		cb->showBlockingDialog(&bd);
 		return;
 	}
@@ -203,20 +203,20 @@ void CGDwelling::onHeroVisit( const CGHeroInstance * h ) const
 	bd.player = h->tempOwner;
 	if(ID == Obj::CREATURE_GENERATOR1 || ID == Obj::CREATURE_GENERATOR4)
 	{
-		bd.text.addTxt(MetaString::ADVOB_TXT, ID == Obj::CREATURE_GENERATOR1 ? 35 : 36); //{%s} Would you like to recruit %s? / {%s} Would you like to recruit %s, %s, %s, or %s?
-		bd.text.addReplacement(ID == Obj::CREATURE_GENERATOR1 ? MetaString::CREGENS : MetaString::CREGENS4, subID);
+		bd.text.appendLocalString(EMetaText::ADVOB_TXT, ID == Obj::CREATURE_GENERATOR1 ? 35 : 36); //{%s} Would you like to recruit %s? / {%s} Would you like to recruit %s, %s, %s, or %s?
+		bd.text.replaceLocalString(ID == Obj::CREATURE_GENERATOR1 ? EMetaText::CREGENS : EMetaText::CREGENS4, subID);
 		for(const auto & elem : creatures)
-			bd.text.addReplacement(MetaString::CRE_PL_NAMES, elem.second[0]);
+			bd.text.replaceLocalString(EMetaText::CRE_PL_NAMES, elem.second[0]);
 	}
 	else if(ID == Obj::REFUGEE_CAMP)
 	{
-		bd.text.addTxt(MetaString::ADVOB_TXT, 35); //{%s} Would you like to recruit %s?
-		bd.text.addReplacement(MetaString::OBJ_NAMES, ID);
+		bd.text.appendLocalString(EMetaText::ADVOB_TXT, 35); //{%s} Would you like to recruit %s?
+		bd.text.replaceLocalString(EMetaText::OBJ_NAMES, ID);
 		for(const auto & elem : creatures)
-			bd.text.addReplacement(MetaString::CRE_PL_NAMES, elem.second[0]);
+			bd.text.replaceLocalString(EMetaText::CRE_PL_NAMES, elem.second[0]);
 	}
 	else if(ID == Obj::WAR_MACHINE_FACTORY)
-		bd.text.addTxt(MetaString::ADVOB_TXT, 157); //{War Machine Factory} Would you like to purchase War Machines?
+		bd.text.appendLocalString(EMetaText::ADVOB_TXT, 157); //{War Machine Factory} Would you like to purchase War Machines?
 	else
 		throw std::runtime_error("Illegal dwelling!");
 
@@ -330,8 +330,8 @@ void CGDwelling::heroAcceptsCreatures( const CGHeroInstance *h) const
 				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);
+				iw.text.appendLocalString(EMetaText::GENERAL_TXT, 425);//The %s would join your hero, but there aren't enough provisions to support them.
+				iw.text.replaceLocalString(EMetaText::CRE_PL_NAMES, crid);
 				cb->showInfoDialog(&iw);
 			}
 			else //give creatures
@@ -345,9 +345,9 @@ 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);
-				iw.text.addReplacement(MetaString::CRE_PL_NAMES, crid);
+				iw.text.appendLocalString(EMetaText::GENERAL_TXT, 423); //%d %s join your army.
+				iw.text.replaceNumber(count);
+				iw.text.replaceLocalString(EMetaText::CRE_PL_NAMES, crid);
 
 				cb->showInfoDialog(&iw);
 				cb->sendAndApply(&sac);
@@ -358,8 +358,8 @@ void CGDwelling::heroAcceptsCreatures( const CGHeroInstance *h) const
 		{
 			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.text.appendLocalString(EMetaText::GENERAL_TXT, 422); //There are no %s here to recruit.
+			iw.text.replaceLocalString(EMetaText::CRE_PL_NAMES, crid);
 			iw.player = h->tempOwner;
 			cb->sendAndApply(&iw);
 		}

+ 17 - 12
lib/mapObjects/CGHeroInstance.cpp

@@ -453,9 +453,8 @@ void CGHeroInstance::onHeroVisit(const CGHeroInstance * h) const
 				//Create a new boat for hero
 				NewObject no;
 				no.ID = Obj::BOAT;
-				no.subID = BoatId(EBoatId::CASTLE);
-				no.pos = CGBoat::translatePos(boatPos);
-				
+				no.subID = getBoatType().getNum();
+
 				cb->sendAndApply(&no);
 
 				boatId = cb->getTopObj(boatPos)->id;
@@ -681,7 +680,7 @@ PlayerColor CGHeroInstance::getCasterOwner() const
 void CGHeroInstance::getCasterName(MetaString & text) const
 {
 	//FIXME: use local name, MetaString need access to gamestate as hero name is part of map object
-	text.addReplacement(getNameTranslated());
+	text.replaceRawString(getNameTranslated());
 }
 
 void CGHeroInstance::getCastDescription(const spells::Spell * spell, const std::vector<const battle::Unit *> & attacked, MetaString & text) const
@@ -689,9 +688,9 @@ void CGHeroInstance::getCastDescription(const spells::Spell * spell, const std::
 	const bool singleTarget = attacked.size() == 1;
 	const int textIndex = singleTarget ? 195 : 196;
 
-	text.addTxt(MetaString::GENERAL_TXT, textIndex);
+	text.appendLocalString(EMetaText::GENERAL_TXT, textIndex);
 	getCasterName(text);
-	text.addReplacement(MetaString::SPELL_NAME, spell->getIndex());
+	text.replaceLocalString(EMetaText::SPELL_NAME, spell->getIndex());
 	if(singleTarget)
 		attacked.at(0)->addNameReplacement(text, true);
 }
@@ -897,14 +896,14 @@ void CGHeroInstance::showNecromancyDialog(const CStackBasicDescriptor &raisedSta
 
 	if (raisedStack.count > 1) // Practicing the dark arts of necromancy, ... (plural)
 	{
-		iw.text.addTxt(MetaString::GENERAL_TXT, 145);
-		iw.text.addReplacement(raisedStack.count);
+		iw.text.appendLocalString(EMetaText::GENERAL_TXT, 145);
+		iw.text.replaceNumber(raisedStack.count);
 	}
 	else // Practicing the dark arts of necromancy, ... (singular)
 	{
-		iw.text.addTxt(MetaString::GENERAL_TXT, 146);
+		iw.text.appendLocalString(EMetaText::GENERAL_TXT, 146);
 	}
-	iw.text.addReplacement(raisedStack);
+	iw.text.replaceCreatureName(raisedStack);
 
 	cb->showInfoDialog(&iw);
 }
@@ -954,8 +953,7 @@ si32 CGHeroInstance::getManaNewTurn() const
 
 BoatId CGHeroInstance::getBoatType() const
 {
-	// hero can only generate boat via "Summon Boat" spell which always create same boat as in Necropolis shipyard
-	return EBoatId::NECROPOLIS;
+	return BoatId(VLC->townh->getById(type->heroClass->faction)->getBoatType());
 }
 
 void CGHeroInstance::getOutOffsets(std::vector<int3> &offsets) const
@@ -1106,6 +1104,13 @@ int CGHeroInstance::maxSpellLevel() const
 void CGHeroInstance::deserializationFix()
 {
 	artDeserializationFix(this);
+	boatDeserializationFix();
+}
+
+void CGHeroInstance::boatDeserializationFix()
+{
+	if (boat)
+		attachTo(const_cast<CGBoat&>(*boat));
 }
 
 CBonusSystemNode * CGHeroInstance::whereShouldBeAttachedOnSiege(const bool isBattleOutsideTown) const

+ 1 - 0
lib/mapObjects/CGHeroInstance.h

@@ -274,6 +274,7 @@ public:
 	void getCastDescription(const spells::Spell * spell, const std::vector<const battle::Unit *> & attacked, MetaString & text) const override;
 	void spendMana(ServerCallback * server, const int spellCost) const override;
 
+	void boatDeserializationFix();
 	void deserializationFix();
 
 	void initObj(CRandomGenerator & rand) override;

+ 15 - 11
lib/mapObjects/CGObjectInstance.cpp

@@ -105,7 +105,7 @@ std::set<int3> CGObjectInstance::getBlockedOffsets() const
 	return appearance->getBlockedOffsets();
 }
 
-void CGObjectInstance::setType(si32 ID, si32 subID)
+void CGObjectInstance::setType(si32 newID, si32 newSubID)
 {
 	auto position = visitablePos();
 	auto oldOffset = getVisitableOffset();
@@ -113,10 +113,10 @@ void CGObjectInstance::setType(si32 ID, si32 subID)
 
 	//recalculate blockvis tiles - new appearance might have different blockmap than before
 	cb->gameState()->map->removeBlockVisTiles(this, true);
-	auto handler = VLC->objtypeh->getHandlerFor(ID, subID);
+	auto handler = VLC->objtypeh->getHandlerFor(newID, newSubID);
 	if(!handler)
 	{
-		logGlobal->error("Unknown object type %d:%d at %s", ID, subID, visitablePos().toString());
+		logGlobal->error("Unknown object type %d:%d at %s", newID, newSubID, visitablePos().toString());
 		return;
 	}
 	if(!handler->getTemplates(tile.terType->getId()).empty())
@@ -125,22 +125,26 @@ void CGObjectInstance::setType(si32 ID, si32 subID)
 	}
 	else
 	{
-		logGlobal->warn("Object %d:%d at %s has no templates suitable for terrain %s", ID, subID, visitablePos().toString(), tile.terType->getNameTranslated());
+		logGlobal->warn("Object %d:%d at %s has no templates suitable for terrain %s", newID, newSubID, visitablePos().toString(), tile.terType->getNameTranslated());
 		appearance = handler->getTemplates()[0]; // get at least some appearance since alternative is crash
 	}
 
-	if(this->ID == Obj::PRISON && ID == Obj::HERO)
+	bool needToAdjustOffset = false;
+
+	// FIXME: potentially unused code - setType is NOT called when releasing hero from prison
+	// instead, appearance update & pos adjustment occurs in GiveHero::applyGs
+	needToAdjustOffset |= this->ID == Obj::PRISON && newID == Obj::HERO;
+	needToAdjustOffset |= newID == Obj::MONSTER;
+
+	if(needToAdjustOffset)
 	{
+		// adjust position since object visitable offset might have changed
 		auto newOffset = getVisitableOffset();
-		// FIXME: potentially unused code - setType is NOT called when releasing hero from prison
-		// instead, appearance update & pos adjustment occurs in GiveHero::applyGs
-
-		// adjust position since hero and prison may have different visitable offset
 		pos = pos - oldOffset + newOffset;
 	}
 
-	this->ID = Obj(ID);
-	this->subID = subID;
+	this->ID = Obj(newID);
+	this->subID = newSubID;
 
 	cb->gameState()->map->addBlockVisTiles(this);
 }

+ 25 - 25
lib/mapObjects/CGPandoraBox.cpp

@@ -35,7 +35,7 @@ void CGPandoraBox::onHeroVisit(const CGHeroInstance * h) const
 {
 		BlockingDialog bd (true, false);
 		bd.player = h->getOwner();
-		bd.text.addTxt (MetaString::ADVOB_TXT, 14);
+		bd.text.appendLocalString (EMetaText::ADVOB_TXT, 14);
 		cb->showBlockingDialog (&bd);
 }
 
@@ -79,8 +79,8 @@ void CGPandoraBox::giveContentsUpToExp(const CGHeroInstance *h) const
 	{
 		TExpType expVal = h->calculateXp(gainedExp);
 		//getText(iw,afterBattle,175,h); //wtf?
-		iw.text.addTxt(MetaString::ADVOB_TXT, 175); //%s learns something
-		iw.text.addReplacement(h->getNameTranslated());
+		iw.text.appendLocalString(EMetaText::ADVOB_TXT, 175); //%s learns something
+		iw.text.replaceRawString(h->getNameTranslated());
 
 		if(expVal)
 			iw.components.emplace_back(Component::EComponentType::EXPERIENCE, 0, static_cast<si32>(expVal), 0);
@@ -156,13 +156,13 @@ void CGPandoraBox::giveContentsAfterExp(const CGHeroInstance *h) const
 			{
 				if (spellsToGive.size() > 1)
 				{
-					iw.text.addTxt(MetaString::ADVOB_TXT, 188); //%s learns spells
+					iw.text.appendLocalString(EMetaText::ADVOB_TXT, 188); //%s learns spells
 				}
 				else
 				{
-					iw.text.addTxt(MetaString::ADVOB_TXT, 184); //%s learns a spell
+					iw.text.appendLocalString(EMetaText::ADVOB_TXT, 184); //%s learns a spell
 				}
-				iw.text.addReplacement(h->getNameTranslated());
+				iw.text.replaceRawString(h->getNameTranslated());
 				cb->changeSpells(h, true, spellsToGive);
 				cb->showInfoDialog(&iw);
 			}
@@ -227,8 +227,8 @@ void CGPandoraBox::giveContentsAfterExp(const CGHeroInstance *h) const
 
 	iw.components.clear();
 	// 	getText(iw,afterBattle,183,h);
-	iw.text.addTxt(MetaString::ADVOB_TXT, 183); //% has found treasure
-	iw.text.addReplacement(h->getNameTranslated());
+	iw.text.appendLocalString(EMetaText::ADVOB_TXT, 183); //% has found treasure
+	iw.text.replaceRawString(h->getNameTranslated());
 	for(const auto & elem : artifacts)
 	{
 		iw.components.emplace_back(Component::EComponentType::ARTIFACT, elem, 0, 0);
@@ -236,8 +236,8 @@ void CGPandoraBox::giveContentsAfterExp(const CGHeroInstance *h) const
 		{
 			cb->showInfoDialog(&iw);
 			iw.components.clear();
-			iw.text.addTxt(MetaString::ADVOB_TXT, 183); //% has found treasure - once more?
-			iw.text.addReplacement(h->getNameTranslated());
+			iw.text.appendLocalString(EMetaText::ADVOB_TXT, 183); //% has found treasure - once more?
+			iw.text.replaceRawString(h->getNameTranslated());
 		}
 	}
 	if(!iw.components.empty())
@@ -259,24 +259,24 @@ void CGPandoraBox::giveContentsAfterExp(const CGHeroInstance *h) const
 		for(const auto & elem : creatures.Slots())
 		{ //build list of joined creatures
 			iw.components.emplace_back(*elem.second);
-			loot << "%s";
-			loot.addReplacement(*elem.second);
+			loot.appendRawString("%s");
+			loot.replaceCreatureName(*elem.second);
 		}
 
 		if(creatures.stacksCount() == 1 && creatures.Slots().begin()->second->count == 1)
-			iw.text.addTxt(MetaString::ADVOB_TXT, 185);
+			iw.text.appendLocalString(EMetaText::ADVOB_TXT, 185);
 		else
-			iw.text.addTxt(MetaString::ADVOB_TXT, 186);
+			iw.text.appendLocalString(EMetaText::ADVOB_TXT, 186);
 
-		iw.text.addReplacement(loot.buildList());
-		iw.text.addReplacement(h->getNameTranslated());
+		iw.text.replaceRawString(loot.buildList());
+		iw.text.replaceRawString(h->getNameTranslated());
 
 		cb->showInfoDialog(&iw);
 		cb->giveCreatures(this, h, creatures, false);
 	}
 	if(!hasGuardians && !msg.empty())
 	{
-		iw.text << msg;
+		iw.text.appendRawString(msg);
 		cb->showInfoDialog(&iw);
 	}
 }
@@ -285,12 +285,12 @@ void CGPandoraBox::getText( InfoWindow &iw, bool &afterBattle, int text, const C
 {
 	if(afterBattle || message.empty())
 	{
-		iw.text.addTxt(MetaString::ADVOB_TXT,text);//%s has lost treasure.
-		iw.text.addReplacement(h->getNameTranslated());
+		iw.text.appendLocalString(EMetaText::ADVOB_TXT,text);//%s has lost treasure.
+		iw.text.replaceRawString(h->getNameTranslated());
 	}
 	else
 	{
-		iw.text << message;
+		iw.text.appendRawString(message);
 		afterBattle = true;
 	}
 }
@@ -301,12 +301,12 @@ void CGPandoraBox::getText( InfoWindow &iw, bool &afterBattle, int val, int nega
 	iw.text.clear();
 	if(afterBattle || message.empty())
 	{
-		iw.text.addTxt(MetaString::ADVOB_TXT,val < 0 ? negative : positive); //%s's luck takes a turn for the worse / %s's luck increases
-		iw.text.addReplacement(h->getNameTranslated());
+		iw.text.appendLocalString(EMetaText::ADVOB_TXT,val < 0 ? negative : positive); //%s's luck takes a turn for the worse / %s's luck increases
+		iw.text.replaceRawString(h->getNameTranslated());
 	}
 	else
 	{
-		iw.text << message;
+		iw.text.appendRawString(message);
 		afterBattle = true;
 	}
 }
@@ -461,9 +461,9 @@ void CGEvent::activated( const CGHeroInstance * h ) const
 		InfoWindow iw;
 		iw.player = h->tempOwner;
 		if(!message.empty())
-			iw.text << message;
+			iw.text.appendRawString(message);
 		else
-			iw.text.addTxt(MetaString::ADVOB_TXT, 16);
+			iw.text.appendLocalString(EMetaText::ADVOB_TXT, 16);
 		cb->showInfoDialog(&iw);
 		cb->startBattleI(h, this);
 	}

+ 4 - 4
lib/mapObjects/CGTownBuilding.cpp

@@ -156,7 +156,7 @@ void COPWBonus::onHeroVisit (const CGHeroInstance * h) const
 				mp.hid = heroID;
 				cb->setMovePoints(&mp);
 
-				iw.text << VLC->generaltexth->allTexts[580];
+				iw.text.appendRawString(VLC->generaltexth->allTexts[580]);
 				cb->showInfoDialog(&iw);
 			}
 			break;
@@ -168,7 +168,7 @@ void COPWBonus::onHeroVisit (const CGHeroInstance * h) const
 					cb->setManaPoints (heroID, 2 * h->manaLimit());
 				//TODO: investigate line below
 				//cb->setObjProperty (town->id, ObjProperty::VISITED, true);
-				iw.text << getVisitingBonusGreeting();
+				iw.text.appendRawString(getVisitingBonusGreeting());
 				cb->showInfoDialog(&iw);
 				//extra visit penalty if hero alredy had double mana points (or even more?!)
 				town->addHeroToStructureVisitors(h, indexOnTV);
@@ -246,7 +246,7 @@ void CTownBonus::onHeroVisit (const CGHeroInstance * h) const
 		if(what != PrimarySkill::NONE)
 		{
 			iw.player = cb->getOwner(heroID);
-				iw.text << getVisitingBonusGreeting();
+				iw.text.appendRawString(getVisitingBonusGreeting());
 			cb->showInfoDialog(&iw);
 			cb->changePrimSkill (cb->getHero(heroID), what, val);
 				town->addHeroToStructureVisitors(h, indexOnTV);
@@ -278,7 +278,7 @@ void CTownBonus::applyBonuses(CGHeroInstance * h, const BonusList & bonuses) con
 			addToVisitors = true;
 
 		iw.player = cb->getOwner(h->id);
-		iw.text << getCustomBonusGreeting(gb.bonus);
+		iw.text.appendRawString(getCustomBonusGreeting(gb.bonus));
 		cb->showInfoDialog(&iw);
 	}
 	if(addToVisitors)

+ 2 - 2
lib/mapObjects/CGTownInstance.cpp

@@ -320,7 +320,7 @@ void CGTownInstance::onHeroVisit(const CGHeroInstance * h) const
 		{
 			InfoWindow iw;
 			iw.player = h->tempOwner;
-			iw.text << h->commander->getName();
+			iw.text.appendRawString(h->commander->getName());
 			iw.components.emplace_back(*h->commander);
 			cb->showInfoDialog(&iw);
 		}
@@ -682,7 +682,7 @@ void CGTownInstance::clearArmy() const
 
 BoatId CGTownInstance::getBoatType() const
 {
-	return town->shipyardBoat;
+	return town->faction->boatType;
 }
 
 int CGTownInstance::getMarketEfficiency() const

+ 74 - 68
lib/mapObjects/CQuest.cpp

@@ -18,7 +18,7 @@
 #include "../CSoundBase.h"
 #include "../CGeneralTextHandler.h"
 #include "../CHeroHandler.h"
-#include "MiscObjects.h"
+#include "CGCreature.h"
 #include "../IGameCallback.h"
 #include "../CGameState.h"
 #include "../mapObjectConstructors/CObjectClassesHandler.h"
@@ -187,19 +187,21 @@ void CQuest::getVisitText(MetaString &iwText, std::vector<Component> &components
 	if(firstVisit)
 	{
 		isCustom = isCustomFirst;
-		iwText << (text = firstVisitText);
+		text = firstVisitText;
+		iwText.appendRawString(text);
 	}
 	else if(failRequirements)
 	{
 		isCustom = isCustomNext;
-		iwText << (text = nextVisitText);
+		text = nextVisitText;
+		iwText.appendRawString(text);
 	}
 	switch (missionType)
 	{
 		case MISSION_LEVEL:
 			components.emplace_back(Component::EComponentType::EXPERIENCE, 0, m13489val, 0);
 			if(!isCustom)
-				iwText.addReplacement(m13489val);
+				iwText.replaceNumber(m13489val);
 			break;
 		case MISSION_PRIMARY_STAT:
 		{
@@ -209,13 +211,13 @@ void CQuest::getVisitText(MetaString &iwText, std::vector<Component> &components
 				if(m2stats[i])
 				{
 					components.emplace_back(Component::EComponentType::PRIM_SKILL, i, m2stats[i], 0);
-					loot << "%d %s";
-					loot.addReplacement(m2stats[i]);
-					loot.addReplacement(VLC->generaltexth->primarySkillNames[i]);
+					loot.appendRawString("%d %s");
+					loot.replaceNumber(m2stats[i]);
+					loot.replaceRawString(VLC->generaltexth->primarySkillNames[i]);
 				}
 			}
 			if (!isCustom)
-				iwText.addReplacement(loot.buildList());
+				iwText.replaceRawString(loot.buildList());
 		}
 			break;
 		case MISSION_KILL_HERO:
@@ -227,7 +229,7 @@ void CQuest::getVisitText(MetaString &iwText, std::vector<Component> &components
 			//FIXME: portrait may not match hero, if custom portrait was set in map editor
 			components.emplace_back(Component::EComponentType::HERO_PORTRAIT, VLC->heroh->objects[m13489val]->imageIndex, 0, 0);
 			if(!isCustom)
-				iwText.addReplacement(VLC->heroh->objects[m13489val]->getNameTranslated());
+				iwText.replaceRawString(VLC->heroh->objects[m13489val]->getNameTranslated());
 			break;
 		case MISSION_KILL_CREATURE:
 			{
@@ -244,11 +246,11 @@ void CQuest::getVisitText(MetaString &iwText, std::vector<Component> &components
 			for(const auto & elem : m5arts)
 			{
 				components.emplace_back(Component::EComponentType::ARTIFACT, elem, 0, 0);
-				loot << "%s";
-				loot.addReplacement(MetaString::ART_NAMES, elem);
+				loot.appendRawString("%s");
+				loot.replaceLocalString(EMetaText::ART_NAMES, elem);
 			}
 			if(!isCustom)
-				iwText.addReplacement(loot.buildList());
+				iwText.replaceRawString(loot.buildList());
 		}
 			break;
 		case MISSION_ARMY:
@@ -257,11 +259,11 @@ void CQuest::getVisitText(MetaString &iwText, std::vector<Component> &components
 			for(const auto & elem : m6creatures)
 			{
 				components.emplace_back(elem);
-				loot << "%s";
-				loot.addReplacement(elem);
+				loot.appendRawString("%s");
+				loot.replaceCreatureName(elem);
 			}
 			if(!isCustom)
-				iwText.addReplacement(loot.buildList());
+				iwText.replaceRawString(loot.buildList());
 		}
 			break;
 		case MISSION_RESOURCES:
@@ -272,19 +274,19 @@ void CQuest::getVisitText(MetaString &iwText, std::vector<Component> &components
 				if(m7resources[i])
 				{
 					components.emplace_back(Component::EComponentType::RESOURCE, i, m7resources[i], 0);
-					loot << "%d %s";
-					loot.addReplacement(m7resources[i]);
-					loot.addReplacement(MetaString::RES_NAMES, i);
+					loot.appendRawString("%d %s");
+					loot.replaceNumber(m7resources[i]);
+					loot.replaceLocalString(EMetaText::RES_NAMES, i);
 				}
 			}
 			if(!isCustom)
-				iwText.addReplacement(loot.buildList());
+				iwText.replaceRawString(loot.buildList());
 		}
 			break;
 		case MISSION_PLAYER:
 			components.emplace_back(Component::EComponentType::FLAG, m13489val, 0, 0);
 			if(!isCustom)
-				iwText.addReplacement(VLC->generaltexth->colors[m13489val]);
+				iwText.replaceRawString(VLC->generaltexth->colors[m13489val]);
 			break;
 	}
 }
@@ -295,17 +297,17 @@ void CQuest::getRolloverText(MetaString &ms, bool onHover) const
 	assert(missionType != MISSION_NONE);
 
 	if(onHover)
-		ms << "\n\n";
+		ms.appendRawString("\n\n");
 
 	std::string questName = missionName(missionType);
 	std::string questState = missionState(onHover ? 3 : 4);
 
-	ms << VLC->generaltexth->translate("core.seerhut.quest", questName, questState,textOption);
+	ms.appendRawString(VLC->generaltexth->translate("core.seerhut.quest", questName, questState,textOption));
 
 	switch(missionType)
 	{
 		case MISSION_LEVEL:
-			ms.addReplacement(m13489val);
+			ms.replaceNumber(m13489val);
 			break;
 		case MISSION_PRIMARY_STAT:
 			{
@@ -314,29 +316,29 @@ void CQuest::getRolloverText(MetaString &ms, bool onHover) const
 				{
 					if (m2stats[i])
 					{
-						loot << "%d %s";
-						loot.addReplacement(m2stats[i]);
-						loot.addReplacement(VLC->generaltexth->primarySkillNames[i]);
+						loot.appendRawString("%d %s");
+						loot.replaceNumber(m2stats[i]);
+						loot.replaceRawString(VLC->generaltexth->primarySkillNames[i]);
 					}
 				}
-				ms.addReplacement(loot.buildList());
+				ms.replaceRawString(loot.buildList());
 			}
 			break;
 		case MISSION_KILL_HERO:
-			ms.addReplacement(heroName);
+			ms.replaceRawString(heroName);
 			break;
 		case MISSION_KILL_CREATURE:
-			ms.addReplacement(stackToKill);
+			ms.replaceCreatureName(stackToKill);
 			break;
 		case MISSION_ART:
 			{
 				MetaString loot;
 				for(const auto & elem : m5arts)
 				{
-					loot << "%s";
-					loot.addReplacement(MetaString::ART_NAMES, elem);
+					loot.appendRawString("%s");
+					loot.replaceLocalString(EMetaText::ART_NAMES, elem);
 				}
-				ms.addReplacement(loot.buildList());
+				ms.replaceRawString(loot.buildList());
 			}
 			break;
 		case MISSION_ARMY:
@@ -344,10 +346,10 @@ void CQuest::getRolloverText(MetaString &ms, bool onHover) const
 				MetaString loot;
 				for(const auto & elem : m6creatures)
 				{
-					loot << "%s";
-					loot.addReplacement(elem);
+					loot.appendRawString("%s");
+					loot.replaceCreatureName(elem);
 				}
-				ms.addReplacement(loot.buildList());
+				ms.replaceRawString(loot.buildList());
 			}
 			break;
 		case MISSION_RESOURCES:
@@ -357,19 +359,19 @@ void CQuest::getRolloverText(MetaString &ms, bool onHover) const
 				{
 					if (m7resources[i])
 					{
-						loot << "%d %s";
-						loot.addReplacement(m7resources[i]);
-						loot.addReplacement(MetaString::RES_NAMES, i);
+						loot.appendRawString("%d %s");
+						loot.replaceNumber(m7resources[i]);
+						loot.replaceLocalString(EMetaText::RES_NAMES, i);
 					}
 				}
-				ms.addReplacement(loot.buildList());
+				ms.replaceRawString(loot.buildList());
 			}
 			break;
 		case MISSION_HERO:
-			ms.addReplacement(VLC->heroh->objects[m13489val]->getNameTranslated());
+			ms.replaceRawString(VLC->heroh->objects[m13489val]->getNameTranslated());
 			break;
 		case MISSION_PLAYER:
-			ms.addReplacement(VLC->generaltexth->colors[m13489val]);
+			ms.replaceRawString(VLC->generaltexth->colors[m13489val]);
 			break;
 		default:
 			break;
@@ -378,12 +380,12 @@ void CQuest::getRolloverText(MetaString &ms, bool onHover) const
 
 void CQuest::getCompletionText(MetaString &iwText, std::vector<Component> &components, bool isCustom, const CGHeroInstance * h) const
 {
-	iwText << completedText;
+	iwText.appendRawString(completedText);
 	switch(missionType)
 	{
 		case CQuest::MISSION_LEVEL:
 			if (!isCustomComplete)
-				iwText.addReplacement(m13489val);
+				iwText.replaceNumber(m13489val);
 			break;
 		case CQuest::MISSION_PRIMARY_STAT:
 			if (vstd::contains (completedText,'%')) //there's one case when there's nothing to replace
@@ -393,13 +395,13 @@ void CQuest::getCompletionText(MetaString &iwText, std::vector<Component> &compo
 				{
 					if (m2stats[i])
 					{
-						loot << "%d %s";
-						loot.addReplacement(m2stats[i]);
-						loot.addReplacement(VLC->generaltexth->primarySkillNames[i]);
+						loot.appendRawString("%d %s");
+						loot.replaceNumber(m2stats[i]);
+						loot.replaceRawString(VLC->generaltexth->primarySkillNames[i]);
 					}
 				}
 				if (!isCustomComplete)
-					iwText.addReplacement(loot.buildList());
+					iwText.replaceRawString(loot.buildList());
 			}
 			break;
 		case CQuest::MISSION_ART:
@@ -407,11 +409,11 @@ void CQuest::getCompletionText(MetaString &iwText, std::vector<Component> &compo
 			MetaString loot;
 			for(const auto & elem : m5arts)
 			{
-				loot << "%s";
-				loot.addReplacement(MetaString::ART_NAMES, elem);
+				loot.appendRawString("%s");
+				loot.replaceLocalString(EMetaText::ART_NAMES, elem);
 			}
 			if (!isCustomComplete)
-				iwText.addReplacement(loot.buildList());
+				iwText.replaceRawString(loot.buildList());
 		}
 			break;
 		case CQuest::MISSION_ARMY:
@@ -419,11 +421,11 @@ void CQuest::getCompletionText(MetaString &iwText, std::vector<Component> &compo
 			MetaString loot;
 			for(const auto & elem : m6creatures)
 			{
-				loot << "%s";
-				loot.addReplacement(elem);
+				loot.appendRawString("%s");
+				loot.replaceCreatureName(elem);
 			}
 			if (!isCustomComplete)
-				iwText.addReplacement(loot.buildList());
+				iwText.replaceRawString(loot.buildList());
 		}
 			break;
 		case CQuest::MISSION_RESOURCES:
@@ -433,13 +435,13 @@ void CQuest::getCompletionText(MetaString &iwText, std::vector<Component> &compo
 			{
 				if (m7resources[i])
 				{
-					loot << "%d %s";
-					loot.addReplacement(m7resources[i]);
-					loot.addReplacement(MetaString::RES_NAMES, i);
+					loot.appendRawString("%d %s");
+					loot.replaceNumber(m7resources[i]);
+					loot.replaceLocalString(EMetaText::RES_NAMES, i);
 				}
 			}
 			if (!isCustomComplete)
-				iwText.addReplacement(loot.buildList());
+				iwText.replaceRawString(loot.buildList());
 		}
 			break;
 		case MISSION_KILL_HERO:
@@ -449,11 +451,11 @@ void CQuest::getCompletionText(MetaString &iwText, std::vector<Component> &compo
 			break;
 		case MISSION_HERO:
 			if (!isCustomComplete)
-				iwText.addReplacement(VLC->heroh->objects[m13489val]->getNameTranslated());
+				iwText.replaceRawString(VLC->heroh->objects[m13489val]->getNameTranslated());
 			break;
 		case MISSION_PLAYER:
 			if (!isCustomComplete)
-				iwText.addReplacement(VLC->generaltexth->colors[m13489val]);
+				iwText.replaceRawString(VLC->generaltexth->colors[m13489val]);
 			break;
 	}
 }
@@ -594,7 +596,7 @@ void CGSeerHut::getRolloverText(MetaString &text, bool onHover) const
 {
 	quest->getRolloverText (text, onHover);//TODO: simplify?
 	if(!onHover)
-		text.addReplacement(seerName);
+		text.replaceRawString(seerName);
 }
 
 std::string CGSeerHut::getHoverText(PlayerColor player) const
@@ -620,14 +622,14 @@ void CQuest::addReplacements(MetaString &out, const std::string &base) const
 	switch(missionType)
 	{
 	case MISSION_KILL_CREATURE:
-		out.addReplacement(stackToKill);
+		out.replaceCreatureName(stackToKill);
 		if (std::count(base.begin(), base.end(), '%') == 2) //say where is placed monster
 		{
-			out.addReplacement(VLC->generaltexth->arraytxt[147+stackDirection]);
+			out.replaceRawString(VLC->generaltexth->arraytxt[147+stackDirection]);
 		}
 		break;
 	case MISSION_KILL_HERO:
-		out.addReplacement(heroName);
+		out.replaceRawString(heroName);
 		break;
 	}
 }
@@ -747,9 +749,9 @@ void CGSeerHut::onHeroVisit(const CGHeroInstance * h) const
 	}
 	else
 	{
-		iw.text << VLC->generaltexth->seerEmpty[quest->completedOption];
+		iw.text.appendRawString(VLC->generaltexth->seerEmpty[quest->completedOption]);
 		if (ID == Obj::SEER_HUT)
-			iw.text.addReplacement(seerName);
+			iw.text.replaceRawString(seerName);
 		cb->showInfoDialog(&iw);
 	}
 }
@@ -1155,13 +1157,17 @@ void CGBorderGuard::initObj(CRandomGenerator & rand)
 
 void CGBorderGuard::getVisitText (MetaString &text, std::vector<Component> &components, bool isCustom, bool FirstVisit, const CGHeroInstance * h) const
 {
-	text << std::pair<ui8,ui32>(11,18);
+	text.appendLocalString(EMetaText::ADVOB_TXT,18);
 }
 
 void CGBorderGuard::getRolloverText (MetaString &text, bool onHover) const
 {
 	if (!onHover)
-		text << VLC->generaltexth->tentColors[subID] << " " << VLC->objtypeh->getObjectName(Obj::KEYMASTER, subID);
+	{
+		text.appendRawString(VLC->generaltexth->tentColors[subID]);
+		text.appendRawString(" ");
+		text.appendRawString(VLC->objtypeh->getObjectName(Obj::KEYMASTER, subID));
+	}
 }
 
 bool CGBorderGuard::checkQuest(const CGHeroInstance * h) const
@@ -1175,7 +1181,7 @@ void CGBorderGuard::onHeroVisit(const CGHeroInstance * h) const
 	{
 		BlockingDialog bd (true, false);
 		bd.player = h->getOwner();
-		bd.text.addTxt (MetaString::ADVOB_TXT, 17);
+		bd.text.appendLocalString (EMetaText::ADVOB_TXT, 17);
 		cb->showBlockingDialog (&bd);
 	}
 	else

+ 5 - 5
lib/mapObjects/IObjectInterface.cpp

@@ -38,7 +38,7 @@ void IObjectInterface::showInfoDialog(const ui32 txtID, const ui16 soundID, EInf
 	iw.soundID = soundID;
 	iw.player = getOwner();
 	iw.type = mode;
-	iw.text.addTxt(MetaString::ADVOB_TXT,txtID);
+	iw.text.appendLocalString(EMetaText::ADVOB_TXT,txtID);
 	IObjectInterface::cb->sendAndApply(&iw);
 }
 
@@ -122,16 +122,16 @@ void IBoatGenerator::getProblemText(MetaString &out, const CGHeroInstance *visit
 	switch(shipyardStatus())
 	{
 	case BOAT_ALREADY_BUILT:
-		out.addTxt(MetaString::GENERAL_TXT, 51);
+		out.appendLocalString(EMetaText::GENERAL_TXT, 51);
 		break;
 	case TILE_BLOCKED:
 		if(visitor)
 		{
-			out.addTxt(MetaString::GENERAL_TXT, 134);
-			out.addReplacement(visitor->getNameTranslated());
+			out.appendLocalString(EMetaText::GENERAL_TXT, 134);
+			out.replaceRawString(visitor->getNameTranslated());
 		}
 		else
-			out.addTxt(MetaString::ADVOB_TXT, 189);
+			out.appendLocalString(EMetaText::ADVOB_TXT, 189);
 		break;
 	case NO_WATER:
 		logGlobal->error("Shipyard without water at tile %s! ", getObject()->getPosition().toString());

+ 1 - 0
lib/mapObjects/IObjectInterface.h

@@ -20,6 +20,7 @@ class CRandomGenerator;
 class IGameCallback;
 class ResourceSet;
 class int3;
+class MetaString;
 
 class DLL_LINKAGE IObjectInterface
 {

+ 37 - 596
lib/mapObjects/MiscObjects.cpp

@@ -15,16 +15,13 @@
 #include "../NetPacks.h"
 #include "../CGeneralTextHandler.h"
 #include "../CSoundBase.h"
-#include "../CConfigHandler.h"
 #include "../CModHandler.h"
-#include "../CHeroHandler.h"
 #include "../CSkillHandler.h"
 #include "../spells/CSpellHandler.h"
 #include "../IGameCallback.h"
 #include "../CGameState.h"
 #include "../mapping/CMap.h"
 #include "../CPlayerState.h"
-#include "../GameSettings.h"
 #include "../serializer/JsonSerializeFormat.h"
 #include "../mapObjectConstructors/AObjectTypeHandler.h"
 #include "../mapObjectConstructors/CObjectClassesHandler.h"
@@ -68,557 +65,6 @@ bool CTeamVisited::wasVisited(const TeamID & team) const
 	return false;
 }
 
-std::string CGCreature::getHoverText(PlayerColor player) const
-{
-	if(stacks.empty())
-	{
-		//should not happen...
-		logGlobal->error("Invalid stack at tile %s: subID=%d; id=%d", pos.toString(), subID, id.getNum());
-		return "INVALID_STACK";
-	}
-
-	std::string hoverName;
-	MetaString ms;
-	CCreature::CreatureQuantityId monsterQuantityId = stacks.begin()->second->getQuantityID();
-	int quantityTextIndex = 172 + 3 * (int)monsterQuantityId;
-	if(settings["gameTweaks"]["numericCreaturesQuantities"].Bool())
-		ms << CCreature::getQuantityRangeStringForId(monsterQuantityId);
-	else
-		ms.addTxt(MetaString::ARRAY_TXT, quantityTextIndex);
-	ms << " " ;
-	ms.addTxt(MetaString::CRE_PL_NAMES,subID);
-	ms.toString(hoverName);
-	return hoverName;
-}
-
-std::string CGCreature::getHoverText(const CGHeroInstance * hero) const
-{
-	std::string hoverName;
-	if(hero->hasVisions(this, 0))
-	{
-		MetaString ms;
-		ms << stacks.begin()->second->count;
-		ms << " " ;
-		ms.addTxt(MetaString::CRE_PL_NAMES,subID);
-
-		ms << "\n";
-
-		int decision = takenAction(hero, true);
-
-		switch (decision)
-		{
-		case FIGHT:
-			ms.addTxt(MetaString::GENERAL_TXT,246);
-			break;
-		case FLEE:
-			ms.addTxt(MetaString::GENERAL_TXT,245);
-			break;
-		case JOIN_FOR_FREE:
-			ms.addTxt(MetaString::GENERAL_TXT,243);
-			break;
-		default: //decision = cost in gold
-			ms << boost::to_string(boost::format(VLC->generaltexth->allTexts[244]) % decision);
-			break;
-		}
-
-		ms.toString(hoverName);
-	}
-	else
-	{
-		hoverName = getHoverText(hero->tempOwner);
-	}
-
-	hoverName += VLC->generaltexth->translate("vcmi.adventureMap.monsterThreat.title");
-
-	int choice;
-	double ratio = (static_cast<double>(getArmyStrength()) / hero->getTotalStrength());
-		 if (ratio < 0.1)  choice = 0;
-	else if (ratio < 0.25) choice = 1;
-	else if (ratio < 0.6)  choice = 2;
-	else if (ratio < 0.9)  choice = 3;
-	else if (ratio < 1.1)  choice = 4;
-	else if (ratio < 1.3)  choice = 5;
-	else if (ratio < 1.8)  choice = 6;
-	else if (ratio < 2.5)  choice = 7;
-	else if (ratio < 4)    choice = 8;
-	else if (ratio < 8)    choice = 9;
-	else if (ratio < 20)   choice = 10;
-	else                   choice = 11;
-
-	hoverName += VLC->generaltexth->translate("vcmi.adventureMap.monsterThreat.levels." + std::to_string(choice));
-	return hoverName;
-}
-
-void CGCreature::onHeroVisit( const CGHeroInstance * h ) const
-{
-	int action = takenAction(h);
-	switch( action ) //decide what we do...
-	{
-	case FIGHT:
-		fight(h);
-		break;
-	case FLEE:
-		{
-			flee(h);
-			break;
-		}
-	case JOIN_FOR_FREE: //join for free
-		{
-			BlockingDialog ynd(true,false);
-			ynd.player = h->tempOwner;
-			ynd.text.addTxt(MetaString::ADVOB_TXT, 86);
-			ynd.text.addReplacement(MetaString::CRE_PL_NAMES, subID);
-			cb->showBlockingDialog(&ynd);
-			break;
-		}
-	default: //join for gold
-		{
-			assert(action > 0);
-
-			//ask if player agrees to pay gold
-			BlockingDialog ynd(true,false);
-			ynd.player = h->tempOwner;
-			std::string tmp = VLC->generaltexth->advobtxt[90];
-			boost::algorithm::replace_first(tmp, "%d", std::to_string(getStackCount(SlotID(0))));
-			boost::algorithm::replace_first(tmp, "%d", std::to_string(action));
-			boost::algorithm::replace_first(tmp,"%s",VLC->creh->objects[subID]->getNamePluralTranslated());
-			ynd.text << tmp;
-			cb->showBlockingDialog(&ynd);
-			break;
-		}
-	}
-
-}
-
-void CGCreature::initObj(CRandomGenerator & rand)
-{
-	blockVisit = true;
-	switch(character)
-	{
-	case 0:
-		character = -4;
-		break;
-	case 1:
-		character = rand.nextInt(1, 7);
-		break;
-	case 2:
-		character = rand.nextInt(1, 10);
-		break;
-	case 3:
-		character = rand.nextInt(4, 10);
-		break;
-	case 4:
-		character = 10;
-		break;
-	}
-
-	stacks[SlotID(0)]->setType(CreatureID(subID));
-	TQuantity &amount = stacks[SlotID(0)]->count;
-	CCreature &c = *VLC->creh->objects[subID];
-	if(amount == 0)
-	{
-		amount = rand.nextInt(c.ammMin, c.ammMax);
-
-		if(amount == 0) //armies with 0 creatures are illegal
-		{
-			logGlobal->warn("Stack %s cannot have 0 creatures. Check properties of %s", nodeName(), c.nodeName());
-			amount = 1;
-		}
-	}
-
-	temppower = stacks[SlotID(0)]->count * static_cast<ui64>(1000);
-	refusedJoining = false;
-}
-
-void CGCreature::newTurn(CRandomGenerator & rand) const
-{//Works only for stacks of single type of size up to 2 millions
-	if (!notGrowingTeam)
-	{
-		if (stacks.begin()->second->count < VLC->settings()->getInteger(EGameSettings::CREATURES_WEEKLY_GROWTH_CAP) && cb->getDate(Date::DAY_OF_WEEK) == 1 && cb->getDate(Date::DAY) > 1)
-		{
-			ui32 power = static_cast<ui32>(temppower * (100 + VLC->settings()->getInteger(EGameSettings::CREATURES_WEEKLY_GROWTH_PERCENT)) / 100);
-			cb->setObjProperty(id, ObjProperty::MONSTER_COUNT, std::min<uint32_t>(power / 1000, VLC->settings()->getInteger(EGameSettings::CREATURES_WEEKLY_GROWTH_CAP))); //set new amount
-			cb->setObjProperty(id, ObjProperty::MONSTER_POWER, power); //increase temppower
-		}
-	}
-	if (VLC->settings()->getBoolean(EGameSettings::MODULE_STACK_EXPERIENCE))
-		cb->setObjProperty(id, ObjProperty::MONSTER_EXP, VLC->settings()->getInteger(EGameSettings::CREATURES_DAILY_STACK_EXPERIENCE)); //for testing purpose
-}
-void CGCreature::setPropertyDer(ui8 what, ui32 val)
-{
-	switch (what)
-	{
-		case ObjProperty::MONSTER_COUNT:
-			stacks[SlotID(0)]->count = val;
-			break;
-		case ObjProperty::MONSTER_POWER:
-			temppower = val;
-			break;
-		case ObjProperty::MONSTER_EXP:
-			giveStackExp(val);
-			break;
-		case ObjProperty::MONSTER_RESTORE_TYPE:
-			formation.basicType = val;
-			break;
-		case ObjProperty::MONSTER_REFUSED_JOIN:
-			refusedJoining = val;
-			break;
-	}
-}
-
-int CGCreature::takenAction(const CGHeroInstance *h, bool allowJoin) const
-{
-	//calculate relative strength of hero and creatures armies
-	double relStrength = static_cast<double>(h->getTotalStrength()) / getArmyStrength();
-
-	int powerFactor;
-	if(relStrength >= 7)
-		powerFactor = 11;
-
-	else if(relStrength >= 1)
-		powerFactor = static_cast<int>(2 * (relStrength - 1));
-
-	else if(relStrength >= 0.5)
-		powerFactor = -1;
-
-	else if(relStrength >= 0.333)
-		powerFactor = -2;
-	else
-		powerFactor = -3;
-
-	std::set<CreatureID> myKindCres; //what creatures are the same kind as we
-	const CCreature * myCreature = VLC->creh->objects[subID];
-	myKindCres.insert(myCreature->getId()); //we
-	myKindCres.insert(myCreature->upgrades.begin(), myCreature->upgrades.end()); //our upgrades
-
-	for(ConstTransitivePtr<CCreature> &crea : VLC->creh->objects)
-	{
-		if(vstd::contains(crea->upgrades, myCreature->getId())) //it's our base creatures
-			myKindCres.insert(crea->getId());
-	}
-
-	int count = 0; //how many creatures of similar kind has hero
-	int totalCount = 0;
-
-	for(const auto & elem : h->Slots())
-	{
-		if(vstd::contains(myKindCres,elem.second->type->getId()))
-			count += elem.second->count;
-		totalCount += elem.second->count;
-	}
-
-	int sympathy = 0; // 0 if hero have no similar creatures
-	if(count)
-		sympathy++; // 1 - if hero have at least 1 similar creature
-	if(count*2 > totalCount)
-		sympathy++; // 2 - hero have similar creatures more that 50%
-
-	int diplomacy = h->valOfBonuses(BonusType::WANDERING_CREATURES_JOIN_BONUS);
-	int charisma = powerFactor + diplomacy + sympathy;
-
-	if(charisma < character)
-		return FIGHT;
-
-	if (allowJoin)
-	{
-		if(diplomacy + sympathy + 1 >= character)
-			return JOIN_FOR_FREE;
-
-		else if(diplomacy * 2  +  sympathy  +  1 >= character)
-			return VLC->creatures()->getByIndex(subID)->getRecruitCost(EGameResID::GOLD) * getStackCount(SlotID(0)); //join for gold
-	}
-
-	//we are still here - creatures have not joined hero, flee or fight
-
-	if (charisma > character && !neverFlees)
-		return FLEE;
-	else
-		return FIGHT;
-}
-
-void CGCreature::fleeDecision(const CGHeroInstance *h, ui32 pursue) const
-{
-	if(refusedJoining)
-		cb->setObjProperty(id, ObjProperty::MONSTER_REFUSED_JOIN, false);
-
-	if(pursue)
-	{
-		fight(h);
-	}
-	else
-	{
-		cb->removeObject(this);
-	}
-}
-
-void CGCreature::joinDecision(const CGHeroInstance *h, int cost, ui32 accept) const
-{
-	if(!accept)
-	{
-		if(takenAction(h,false) == FLEE)
-		{
-			cb->setObjProperty(id, ObjProperty::MONSTER_REFUSED_JOIN, true);
-			flee(h);
-		}
-		else //they fight
-		{
-			h->showInfoDialog(87, 0, EInfoWindowMode::MODAL);//Insulted by your refusal of their offer, the monsters attack!
-			fight(h);
-		}
-	}
-	else //accepted
-	{
-		if (cb->getResource(h->tempOwner, EGameResID::GOLD) < cost) //player don't have enough gold!
-		{
-			InfoWindow iw;
-			iw.player = h->tempOwner;
-			iw.text << std::pair<ui8,ui32>(1,29);  //You don't have enough gold
-			cb->showInfoDialog(&iw);
-
-			//act as if player refused
-			joinDecision(h,cost,false);
-			return;
-		}
-
-		//take gold
-		if(cost)
-			cb->giveResource(h->tempOwner,EGameResID::GOLD,-cost);
-
-		giveReward(h);
-		cb->tryJoiningArmy(this, h, true, true);
-	}
-}
-
-void CGCreature::fight( const CGHeroInstance *h ) const
-{
-	//split stacks
-	//TODO: multiple creature types in a stack?
-	int basicType = stacks.begin()->second->type->getId();
-	cb->setObjProperty(id, ObjProperty::MONSTER_RESTORE_TYPE, basicType); //store info about creature stack
-
-	int stacksCount = getNumberOfStacks(h);
-	//source: http://heroescommunity.com/viewthread.php3?TID=27539&PID=1266335#focus
-
-	int amount = getStackCount(SlotID(0));
-	int m = amount / stacksCount;
-	int b = stacksCount * (m + 1) - amount;
-	int a = stacksCount - b;
-
-	SlotID sourceSlot = stacks.begin()->first;
-	for (int slotID = 1; slotID < a; ++slotID)
-	{
-		int stackSize = m + 1;
-		cb->moveStack(StackLocation(this, sourceSlot), StackLocation(this, SlotID(slotID)), stackSize);
-	}
-	for (int slotID = a; slotID < stacksCount; ++slotID)
-	{
-		int stackSize = m;
-		if (slotID) //don't do this when a = 0 -> stack is single
-			cb->moveStack(StackLocation(this, sourceSlot), StackLocation(this, SlotID(slotID)), stackSize);
-	}
-	if (stacksCount > 1)
-	{
-		if (containsUpgradedStack()) //upgrade
-		{
-			SlotID slotID = SlotID(static_cast<si32>(std::floor(static_cast<float>(stacks.size()) / 2.0f)));
-			const auto & upgrades = getStack(slotID).type->upgrades;
-			if(!upgrades.empty())
-			{
-				auto it = RandomGeneratorUtil::nextItem(upgrades, CRandomGenerator::getDefault());
-				cb->changeStackType(StackLocation(this, slotID), VLC->creh->objects[*it]);
-			}
-		}
-	}
-
-	cb->startBattleI(h, this);
-
-}
-
-void CGCreature::flee( const CGHeroInstance * h ) const
-{
-	BlockingDialog ynd(true,false);
-	ynd.player = h->tempOwner;
-	ynd.text.addTxt(MetaString::ADVOB_TXT,91);
-	ynd.text.addReplacement(MetaString::CRE_PL_NAMES, subID);
-	cb->showBlockingDialog(&ynd);
-}
-
-void CGCreature::battleFinished(const CGHeroInstance *hero, const BattleResult &result) const
-{
-	if(result.winner == 0)
-	{
-		giveReward(hero);
-		cb->removeObject(this);
-	}
-	else if(result.winner > 1) // draw
-	{
-		// guarded reward is lost forever on draw
-		cb->removeObject(this);
-	}
-	else
-	{
-		//merge stacks into one
-		TSlots::const_iterator i;
-		CCreature * cre = VLC->creh->objects[formation.basicType];
-		for(i = stacks.begin(); i != stacks.end(); i++)
-		{
-			if(cre->isMyUpgrade(i->second->type))
-			{
-				cb->changeStackType(StackLocation(this, i->first), cre); //un-upgrade creatures
-			}
-		}
-
-		//first stack has to be at slot 0 -> if original one got killed, move there first remaining stack
-		if(!hasStackAtSlot(SlotID(0)))
-			cb->moveStack(StackLocation(this, stacks.begin()->first), StackLocation(this, SlotID(0)), stacks.begin()->second->count);
-
-		while(stacks.size() > 1) //hopefully that's enough
-		{
-			// TODO it's either overcomplicated (if we assume there'll be only one stack) or buggy (if we allow multiple stacks... but that'll also cause troubles elsewhere)
-			i = stacks.end();
-			i--;
-			SlotID slot = getSlotFor(i->second->type);
-			if(slot == i->first) //no reason to move stack to its own slot
-				break;
-			else
-				cb->moveStack(StackLocation(this, i->first), StackLocation(this, slot), i->second->count);
-		}
-
-		cb->setObjProperty(id, ObjProperty::MONSTER_POWER, stacks.begin()->second->count * 1000); //remember casualties
-	}
-}
-
-void CGCreature::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const
-{
-	auto action = takenAction(hero);
-	if(!refusedJoining && action >= JOIN_FOR_FREE) //higher means price
-		joinDecision(hero, action, answer);
-	else if(action != FIGHT)
-		fleeDecision(hero, answer);
-	else
-		assert(0);
-}
-
-bool CGCreature::containsUpgradedStack() const
-{
-	//source http://heroescommunity.com/viewthread.php3?TID=27539&PID=830557#focus
-
-	float a = 2992.911117f;
-	float b = 14174.264968f;
-	float c = 5325.181015f;
-	float d = 32788.727920f;
-
-	int val = static_cast<int>(std::floor(a * pos.x + b * pos.y + c * pos.z + d));
-	return ((val % 32768) % 100) < 50;
-}
-
-int CGCreature::getNumberOfStacks(const CGHeroInstance *hero) const
-{
-	//source http://heroescommunity.com/viewthread.php3?TID=27539&PID=1266094#focus
-
-	double strengthRatio = static_cast<double>(hero->getArmyStrength()) / getArmyStrength();
-	int split = 1;
-
-	if (strengthRatio < 0.5f)
-		split = 7;
-	else if (strengthRatio < 0.67f)
-		split = 6;
-	else if (strengthRatio < 1)
-		split = 5;
-	else if (strengthRatio < 1.5f)
-		split = 4;
-	else if (strengthRatio < 2)
-		split = 3;
-	else
-		split = 2;
-
-	ui32 a = 1550811371u;
-	ui32 b = 3359066809u;
-	ui32 c = 1943276003u;
-	ui32 d = 3174620878u;
-
-	ui32 R1 = a * static_cast<ui32>(pos.x) + b * static_cast<ui32>(pos.y) + c * static_cast<ui32>(pos.z) + d;
-	ui32 R2 = (R1 >> 16) & 0x7fff;
-
-	int R4 = R2 % 100 + 1;
-
-	if (R4 <= 20)
-		split -= 1;
-	else if (R4 >= 80)
-		split += 1;
-
-	vstd::amin(split, getStack(SlotID(0)).count); //can't divide into more stacks than creatures total
-	vstd::amin(split, 7); //can't have more than 7 stacks
-
-	return split;
-}
-
-void CGCreature::giveReward(const CGHeroInstance * h) const
-{
-	InfoWindow iw;
-	iw.player = h->tempOwner;
-
-	if(!resources.empty())
-	{
-		cb->giveResources(h->tempOwner, resources);
-		for(int i = 0; i < resources.size(); i++)
-		{
-			if(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::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);
-	}
-}
-
-static const std::vector<std::string> CHARACTER_JSON  =
-{
-	"compliant", "friendly", "aggressive", "hostile", "savage"
-};
-
-void CGCreature::serializeJsonOptions(JsonSerializeFormat & handler)
-{
-	handler.serializeEnum("character", character, CHARACTER_JSON);
-
-	if(handler.saving)
-	{
-		if(hasStackAtSlot(SlotID(0)))
-		{
-			si32 amount = getStack(SlotID(0)).count;
-			handler.serializeInt("amount", amount, 0);
-		}
-	}
-	else
-	{
-		si32 amount = 0;
-		handler.serializeInt("amount", amount);
-		auto * hlp = new CStackInstance();
-		hlp->count = amount;
-		//type will be set during initialization
-		putStack(SlotID(0), hlp);
-	}
-
-	resources.serializeJson(handler, "rewardResources");
-
-	handler.serializeId("rewardArtifact", gainedArtifact, ArtifactID(ArtifactID::NONE));
-
-	handler.serializeBool("noGrowing", notGrowingTeam);
-	handler.serializeBool("neverFlees", neverFlees);
-	handler.serializeString("rewardMessage", message);
-}
-
 //CGMine
 void CGMine::onHeroVisit( const CGHeroInstance * h ) const
 {
@@ -636,7 +82,7 @@ void CGMine::onHeroVisit( const CGHeroInstance * h ) const
 	{
 		BlockingDialog ynd(true,false);
 		ynd.player = h->tempOwner;
-		ynd.text.addTxt(MetaString::ADVOB_TXT, subID == 7 ? 84 : 187);
+		ynd.text.appendLocalString(EMetaText::ADVOB_TXT, subID == 7 ? 84 : 187);
 		cb->showBlockingDialog(&ynd);
 		return;
 	}
@@ -710,7 +156,7 @@ void CGMine::flagMine(const PlayerColor & player) const
 	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.text.appendLocalString(EMetaText::MINE_EVNTS, producedResource); //not use subID, abandoned mines uses default mine texts
 	iw.player = player;
 	iw.components.emplace_back(Component::EComponentType::RESOURCE, producedResource, producedQuantity, -1);
 	cb->showInfoDialog(&iw);
@@ -822,7 +268,7 @@ void CGResource::onHeroVisit( const CGHeroInstance * h ) const
 		{
 			BlockingDialog ynd(true,false);
 			ynd.player = h->getOwner();
-			ynd.text << message;
+			ynd.text.appendRawString(message);
 			cb->showBlockingDialog(&ynd);
 		}
 		else
@@ -842,13 +288,13 @@ void CGResource::collectRes(const PlayerColor & player) const
 	if(!message.empty())
 	{
 		sii.type = EInfoWindowMode::AUTO;
-		sii.text << message;
+		sii.text.appendRawString(message);
 	}
 	else
 	{
 		sii.type = EInfoWindowMode::INFO;
-		sii.text.addTxt(MetaString::ADVOB_TXT,113);
-		sii.text.addReplacement(MetaString::RES_NAMES, subID);
+		sii.text.appendLocalString(EMetaText::ADVOB_TXT,113);
+		sii.text.replaceLocalString(EMetaText::RES_NAMES, subID);
 	}
 	sii.components.emplace_back(Component::EComponentType::RESOURCE,subID,amount,0);
 	sii.soundID = soundBase::pickup01 + CRandomGenerator::getDefault().nextInt(6);
@@ -1189,7 +635,7 @@ void CGWhirlpool::onHeroVisit( const CGHeroInstance * h ) const
 		InfoWindow iw;
 		iw.type = EInfoWindowMode::AUTO;
 		iw.player = h->tempOwner;
-		iw.text.addTxt(MetaString::ADVOB_TXT, 168);
+		iw.text.appendLocalString(EMetaText::ADVOB_TXT, 168);
 		iw.components.emplace_back(CStackBasicDescriptor(h->getCreature(targetstack), -countToTake));
 		cb->showInfoDialog(&iw);
 		cb->changeStackCount(StackLocation(h, targetstack), -countToTake);
@@ -1281,9 +727,9 @@ void CGArtifact::onHeroVisit(const CGHeroInstance * h) const
 			{
 				iw.components.emplace_back(Component::EComponentType::ARTIFACT, subID, 0, 0);
 				if(message.length())
-					iw.text << message;
+					iw.text.appendRawString(message);
 				else
-					iw.text.addTxt(MetaString::ART_EVNTS, subID);
+					iw.text.appendLocalString(EMetaText::ART_EVNTS, subID);
 			}
 			break;
 			case Obj::SPELL_SCROLL:
@@ -1291,11 +737,11 @@ void CGArtifact::onHeroVisit(const CGHeroInstance * h) const
 				int spellID = storedArtifact->getScrollSpellID();
 				iw.components.emplace_back(Component::EComponentType::SPELL, spellID, 0, 0);
 				if(message.length())
-					iw.text << message;
+					iw.text.appendRawString(message);
 				else
 				{
-					iw.text.addTxt(MetaString::ADVOB_TXT,135);
-					iw.text.addReplacement(MetaString::SPELL_NAME, spellID);
+					iw.text.appendLocalString(EMetaText::ADVOB_TXT,135);
+					iw.text.replaceLocalString(EMetaText::SPELL_NAME, spellID);
 				}
 			}
 			break;
@@ -1303,7 +749,7 @@ void CGArtifact::onHeroVisit(const CGHeroInstance * h) const
 		}
 		else
 		{
-			iw.text.addTxt(MetaString::ADVOB_TXT, 2);
+			iw.text.appendLocalString(EMetaText::ADVOB_TXT, 2);
 		}
 		cb->showInfoDialog(&iw);
 		pick(h);
@@ -1317,14 +763,14 @@ void CGArtifact::onHeroVisit(const CGHeroInstance * h) const
 				BlockingDialog ynd(true,false);
 				ynd.player = h->getOwner();
 				if(message.length())
-					ynd.text << message;
+					ynd.text.appendRawString(message);
 				else
 				{
 					// TODO: Guard text is more complex in H3, see mantis issue 2325 for details
-					ynd.text.addTxt(MetaString::GENERAL_TXT, 420);
-					ynd.text.addReplacement("");
-					ynd.text.addReplacement(getArmyDescription());
-					ynd.text.addReplacement(MetaString::GENERAL_TXT, 43); // creatures
+					ynd.text.appendLocalString(EMetaText::GENERAL_TXT, 420);
+					ynd.text.replaceRawString("");
+					ynd.text.replaceRawString(getArmyDescription());
+					ynd.text.replaceLocalString(EMetaText::GENERAL_TXT, 43); // creatures
 				}
 				cb->showBlockingDialog(&ynd);
 			}
@@ -1335,7 +781,7 @@ void CGArtifact::onHeroVisit(const CGHeroInstance * h) const
 				{
 					BlockingDialog ynd(true,false);
 					ynd.player = h->getOwner();
-					ynd.text << message;
+					ynd.text.appendRawString(message);
 					cb->showBlockingDialog(&ynd);
 				}
 				else
@@ -1432,8 +878,8 @@ void CGWitchHut::onHeroVisit( const CGHeroInstance * h ) const
 		cb->changeSecSkill(h, SecondarySkill(ability), 1, true);
 	}
 
-	iw.text.addTxt(MetaString::ADVOB_TXT,txt_id);
-	iw.text.addReplacement(MetaString::SEC_SKILL_NAME, ability);
+	iw.text.appendLocalString(EMetaText::ADVOB_TXT,txt_id);
+	iw.text.replaceLocalString(EMetaText::SEC_SKILL_NAME, ability);
 	cb->showInfoDialog(&iw);
 }
 
@@ -1494,7 +940,7 @@ void CGObservatory::onHeroVisit( const CGHeroInstance * h ) const
 	case Obj::REDWOOD_OBSERVATORY:
 	case Obj::PILLAR_OF_FIRE:
 	{
-		iw.text.addTxt(MetaString::ADVOB_TXT,98 + (ID==Obj::PILLAR_OF_FIRE));
+		iw.text.appendLocalString(EMetaText::ADVOB_TXT,98 + (ID==Obj::PILLAR_OF_FIRE));
 
 		FoWChange fw;
 		fw.player = h->tempOwner;
@@ -1505,7 +951,7 @@ void CGObservatory::onHeroVisit( const CGHeroInstance * h ) const
 	}
 	case Obj::COVER_OF_DARKNESS:
 	{
-		iw.text.addTxt (MetaString::ADVOB_TXT, 31);
+		iw.text.appendLocalString (EMetaText::ADVOB_TXT, 31);
 		for (auto & player : cb->gameState()->players)
 		{
 			if (cb->getPlayerStatus(player.first) == EPlayerStatus::INGAME &&
@@ -1533,20 +979,20 @@ void CGShrine::onHeroVisit( const CGHeroInstance * h ) const
 	iw.type = EInfoWindowMode::AUTO;
 	iw.player = h->getOwner();
 	iw.text = visitText;
-	iw.text.addTxt(MetaString::SPELL_NAME,spell);
-	iw.text << ".";
+	iw.text.appendLocalString(EMetaText::SPELL_NAME,spell);
+	iw.text.appendRawString(".");
 
 	if(!h->getArt(ArtifactPosition::SPELLBOOK))
 	{
-		iw.text.addTxt(MetaString::ADVOB_TXT,131);
+		iw.text.appendLocalString(EMetaText::ADVOB_TXT,131);
 	}
 	else if(h->spellbookContainsSpell(spell))//hero already knows the spell
 	{
-		iw.text.addTxt(MetaString::ADVOB_TXT,174);
+		iw.text.appendLocalString(EMetaText::ADVOB_TXT,174);
 	}
 	else if(spell.toSpell()->getLevel() > h->maxSpellLevel()) //it's third level spell and hero doesn't have wisdom
 	{
-		iw.text.addTxt(MetaString::ADVOB_TXT,130);
+		iw.text.appendLocalString(EMetaText::ADVOB_TXT,130);
 	}
 	else //give spell
 	{
@@ -1609,7 +1055,7 @@ void CGSignBottle::onHeroVisit( const CGHeroInstance * h ) const
 {
 	InfoWindow iw;
 	iw.player = h->getOwner();
-	iw.text << message;
+	iw.text.appendRawString(message);
 	cb->showInfoDialog(&iw);
 
 	if(ID == Obj::OCEAN_BOTTLE)
@@ -1637,7 +1083,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);
+	iw.text.appendLocalString(EMetaText::ADVOB_TXT,115);
 
 	switch (type)
 	{
@@ -1848,11 +1294,6 @@ CGBoat::CGBoat()
 	layer = EPathfindingLayer::EEPathfindingLayer::SAIL;
 }
 
-void CGBoat::initObj(CRandomGenerator & rand)
-{
-	hero = nullptr;
-}
-
 int3 CGBoat::translatePos(const int3& pos, bool reverse /* = false */)
 {
 	//pos - offset we want to place the boat at the map
@@ -1886,7 +1327,7 @@ void CGSirens::onHeroVisit( const CGHeroInstance * h ) const
 	if(h->hasBonusFrom(BonusSource::OBJECT,ID)) //has already visited Sirens
 	{
 		iw.type = EInfoWindowMode::AUTO;
-		iw.text.addTxt(MetaString::ADVOB_TXT,133);
+		iw.text.appendLocalString(EMetaText::ADVOB_TXT,133);
 	}
 	else
 	{
@@ -1912,13 +1353,13 @@ void CGSirens::onHeroVisit( const CGHeroInstance * h ) const
 		if(xp)
 		{
 			xp = h->calculateXp(static_cast<int>(xp));
-			iw.text.addTxt(MetaString::ADVOB_TXT,132);
-			iw.text.addReplacement(static_cast<int>(xp));
+			iw.text.appendLocalString(EMetaText::ADVOB_TXT,132);
+			iw.text.replaceNumber(static_cast<int>(xp));
 			cb->changePrimSkill(h, PrimarySkill::EXPERIENCE, xp, false);
 		}
 		else
 		{
-			iw.text.addTxt(MetaString::ADVOB_TXT,134);
+			iw.text.appendLocalString(EMetaText::ADVOB_TXT,134);
 		}
 	}
 	cb->showInfoDialog(&iw);
@@ -1998,7 +1439,7 @@ void CCartographer::onHeroVisit( const CGHeroInstance * h ) const
 			assert(text);
 			BlockingDialog bd (true, false);
 			bd.player = h->getOwner();
-			bd.text.addTxt (MetaString::ADVOB_TXT, text);
+			bd.text.appendLocalString (EMetaText::ADVOB_TXT, text);
 			cb->showBlockingDialog (&bd);
 		}
 		else //if he cannot afford
@@ -2061,7 +1502,7 @@ void CGObelisk::onHeroVisit( const CGHeroInstance * h ) const
 
 	if(!wasVisited(team))
 	{
-		iw.text.addTxt(MetaString::ADVOB_TXT, 96);
+		iw.text.appendLocalString(EMetaText::ADVOB_TXT, 96);
 		cb->sendAndApply(&iw);
 
 		// increment general visited obelisks counter
@@ -2077,7 +1518,7 @@ void CGObelisk::onHeroVisit( const CGHeroInstance * h ) const
 	}
 	else
 	{
-		iw.text.addTxt(MetaString::ADVOB_TXT, 97);
+		iw.text.appendLocalString(EMetaText::ADVOB_TXT, 97);
 		cb->sendAndApply(&iw);
 	}
 

+ 1 - 76
lib/mapObjects/MiscObjects.h

@@ -10,7 +10,7 @@
 #pragma once
 
 #include "CArmedInstance.h"
-#include "../ResourceSet.h"
+#include "../MetaString.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 
@@ -40,80 +40,6 @@ public:
 	static constexpr int OBJPROP_VISITED = 10;
 };
 
-class DLL_LINKAGE CGCreature : public CArmedInstance //creatures on map
-{
-public:
-	enum Action {
-		FIGHT = -2, FLEE = -1, JOIN_FOR_FREE = 0 //values > 0 mean gold price
-	};
-
-	enum Character {
-		COMPLIANT = 0, FRIENDLY = 1, AGRESSIVE = 2, HOSTILE = 3, SAVAGE = 4
-	};
-
-	ui32 identifier; //unique code for this monster (used in missions)
-	si8 character; //character of this set of creatures (0 - the most friendly, 4 - the most hostile) => on init changed to -4 (compliant) ... 10 value (savage)
-	std::string message; //message printed for attacking hero
-	TResources resources; // resources given to hero that has won with monsters
-	ArtifactID gainedArtifact; //ID of artifact gained to hero, -1 if none
-	bool neverFlees; //if true, the troops will never flee
-	bool notGrowingTeam; //if true, number of units won't grow
-	ui64 temppower; //used to handle fractional stack growth for tiny stacks
-
-	bool refusedJoining;
-
-	void onHeroVisit(const CGHeroInstance * h) const override;
-	std::string getHoverText(PlayerColor player) const override;
-	std::string getHoverText(const CGHeroInstance * hero) const override;
-	void initObj(CRandomGenerator & rand) override;
-	void newTurn(CRandomGenerator & rand) const override;
-	void battleFinished(const CGHeroInstance *hero, const BattleResult &result) const override;
-	void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override;
-
-	//stack formation depends on position,
-	bool containsUpgradedStack() const;
-	int getNumberOfStacks(const CGHeroInstance *hero) const;
-
-	struct DLL_LINKAGE formationInfo // info about merging stacks after battle back into one
-	{
-		si32 basicType;
-		ui8 upgrade; //random seed used to determine number of stacks and is there's upgraded stack
-		template <typename Handler> void serialize(Handler &h, const int version)
-		{
-			h & basicType;
-			h & upgrade;
-		}
-	} formation;
-
-	template <typename Handler> void serialize(Handler &h, const int version)
-	{
-		h & static_cast<CArmedInstance&>(*this);
-		h & identifier;
-		h & character;
-		h & message;
-		h & resources;
-		h & gainedArtifact;
-		h & neverFlees;
-		h & notGrowingTeam;
-		h & temppower;
-		h & refusedJoining;
-		h & formation;
-	}
-protected:
-	void setPropertyDer(ui8 what, ui32 val) override;
-	void serializeJsonOptions(JsonSerializeFormat & handler) override;
-
-private:
-	void fight(const CGHeroInstance *h) const;
-	void flee( const CGHeroInstance * h ) const;
-	void fleeDecision(const CGHeroInstance *h, ui32 pursue) const;
-	void joinDecision(const CGHeroInstance *h, int cost, ui32 accept) const;
-
-	int takenAction(const CGHeroInstance *h, bool allowJoin=true) const; //action on confrontation: -2 - fight, -1 - flee, >=0 - will join for given value of gold (may be 0)
-	void giveReward(const CGHeroInstance * h) const;
-
-};
-
 class DLL_LINKAGE CGSignBottle : public CGObjectInstance //signs and ocean bottles
 {
 public:
@@ -433,7 +359,6 @@ public:
 	std::array<std::string, PlayerColor::PLAYER_LIMIT_I> flagAnimations;
 
 	CGBoat();
-	void initObj(CRandomGenerator & rand) override;
 	static int3 translatePos(const int3 &pos, bool reverse = false);
 
 	template <typename Handler> void serialize(Handler &h, const int version)

+ 8 - 9
lib/mapping/CMap.cpp

@@ -398,17 +398,17 @@ void CMap::checkForObjectives()
 			switch (cond.condition)
 			{
 				case EventCondition::HAVE_ARTIFACT:
-					boost::algorithm::replace_first(event.onFulfill, "%s", VLC->arth->objects[cond.objectType]->getNameTranslated());
+					event.onFulfill.replaceTextID(VLC->artifacts()->getByIndex(cond.objectType)->getNameTextID());
 					break;
 
 				case EventCondition::HAVE_CREATURES:
-					boost::algorithm::replace_first(event.onFulfill, "%s", VLC->creh->objects[cond.objectType]->getNameSingularTranslated());
-					boost::algorithm::replace_first(event.onFulfill, "%d", std::to_string(cond.value));
+					event.onFulfill.replaceTextID(VLC->creatures()->getByIndex(cond.objectType)->getNameSingularTextID());
+					event.onFulfill.replaceNumber(cond.value);
 					break;
 
 				case EventCondition::HAVE_RESOURCES:
-					boost::algorithm::replace_first(event.onFulfill, "%s", VLC->generaltexth->restypes[cond.objectType]);
-					boost::algorithm::replace_first(event.onFulfill, "%d", std::to_string(cond.value));
+					event.onFulfill.replaceLocalString(EMetaText::RES_NAMES, cond.objectType);
+					event.onFulfill.replaceNumber(cond.value);
 					break;
 
 				case EventCondition::HAVE_BUILDING:
@@ -424,10 +424,10 @@ void CMap::checkForObjectives()
 					{
 						const auto * town = dynamic_cast<const CGTownInstance *>(cond.object);
 						if (town)
-							boost::algorithm::replace_first(event.onFulfill, "%s", town->getNameTranslated());
+							event.onFulfill.replaceRawString(town->getNameTranslated());
 						const auto * hero = dynamic_cast<const CGHeroInstance *>(cond.object);
 						if (hero)
-							boost::algorithm::replace_first(event.onFulfill, "%s", hero->getNameTranslated());
+							event.onFulfill.replaceRawString(hero->getNameTranslated());
 					}
 					break;
 
@@ -439,7 +439,7 @@ void CMap::checkForObjectives()
 					{
 						const auto * hero = dynamic_cast<const CGHeroInstance *>(cond.object);
 						if (hero)
-							boost::algorithm::replace_first(event.onFulfill, "%s", hero->getNameTranslated());
+							event.onFulfill.replaceRawString(hero->getNameTranslated());
 					}
 					break;
 				case EventCondition::TRANSPORT:
@@ -499,7 +499,6 @@ void CMap::removeQuestInstance(CQuest * quest)
 }
 
 void CMap::setUniqueInstanceName(CGObjectInstance * obj)
-
 {
 	//this gives object unique name even if objects are removed later
 

+ 6 - 6
lib/mapping/CMapHeader.cpp

@@ -96,29 +96,29 @@ void CMapHeader::setupEvents()
 	//Victory condition - defeat all
 	TriggeredEvent standardVictory;
 	standardVictory.effect.type = EventEffect::VICTORY;
-	standardVictory.effect.toOtherMessage = VLC->generaltexth->allTexts[5];
+	standardVictory.effect.toOtherMessage.appendTextID("core.genrltxt.5");
 	standardVictory.identifier = "standardVictory";
 	standardVictory.description.clear(); // TODO: display in quest window
-	standardVictory.onFulfill = VLC->generaltexth->allTexts[659];
+	standardVictory.onFulfill.appendTextID("core.genrltxt.659");
 	standardVictory.trigger = EventExpression(victoryCondition);
 
 	//Loss condition - 7 days without town
 	TriggeredEvent standardDefeat;
 	standardDefeat.effect.type = EventEffect::DEFEAT;
-	standardDefeat.effect.toOtherMessage = VLC->generaltexth->allTexts[8];
+	standardDefeat.effect.toOtherMessage.appendTextID("core.genrltxt.8");
 	standardDefeat.identifier = "standardDefeat";
 	standardDefeat.description.clear(); // TODO: display in quest window
-	standardDefeat.onFulfill = VLC->generaltexth->allTexts[7];
+	standardDefeat.onFulfill.appendTextID("core.genrltxt.7");
 	standardDefeat.trigger = EventExpression(defeatCondition);
 
 	triggeredEvents.push_back(standardVictory);
 	triggeredEvents.push_back(standardDefeat);
 
 	victoryIconIndex = 11;
-	victoryMessage = VLC->generaltexth->victoryConditions[0];
+	victoryMessage.appendTextID("core.vcdesc.0");
 
 	defeatIconIndex = 3;
-	defeatMessage = VLC->generaltexth->lossCondtions[0];
+	defeatMessage.appendTextID("core.lcdesc.0");
 }
 
 CMapHeader::CMapHeader() : version(EMapFormat::VCMI), height(72), width(72),

+ 6 - 5
lib/mapping/CMapHeader.h

@@ -13,6 +13,7 @@
 #include "../CModVersion.h"
 #include "../LogicalExpression.h"
 #include "../int3.h"
+#include "../MetaString.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 
@@ -159,7 +160,7 @@ struct DLL_LINKAGE EventEffect
 	si8 type;
 
 	/// message that will be sent to other players
-	std::string toOtherMessage;
+	MetaString toOtherMessage;
 
 	template <typename Handler>
 	void serialize(Handler & h, const int version)
@@ -178,10 +179,10 @@ struct DLL_LINKAGE TriggeredEvent
 	std::string identifier;
 
 	/// string-description, for use in UI (capture town to win)
-	std::string description;
+	MetaString description;
 
 	/// Message that will be displayed when this event is triggered (You captured town. You won!)
-	std::string onFulfill;
+	MetaString onFulfill;
 
 	/// Effect of this event. TODO: refactor into something more flexible
 	EventEffect effect;
@@ -229,8 +230,8 @@ public:
 	///	maximum level for heroes. This is the default value.
 	ui8 levelLimit;
 
-	std::string victoryMessage;
-	std::string defeatMessage;
+	MetaString victoryMessage;
+	MetaString defeatMessage;
 	ui16 victoryIconIndex;
 	ui16 defeatIconIndex;
 

+ 3 - 5
lib/mapping/MapFeaturesH3M.cpp

@@ -135,21 +135,19 @@ MapFormatFeaturesH3M MapFormatFeaturesH3M::getFeaturesHOTA(uint32_t hotaVersion)
 	if(hotaVersion < 3)
 	{
 		result.artifactsCount = 163; // + HotA artifacts
-		result.heroesCount = 177; // + Cove
-		result.heroesPortraitsCount = 187; // + Cove
+		result.heroesCount = 178; // + Cove
+		result.heroesPortraitsCount = 185; // + Cove
 	}
 	if(hotaVersion == 3)
 	{
 		result.artifactsCount = 165; // + HotA artifacts
-		result.heroesCount = 177; // + Cove + Giselle
+		result.heroesCount = 179; // + Cove + Giselle
 		result.heroesPortraitsCount = 188; // + Cove + Giselle
 	}
 
 	assert((result.heroesCount + 7) / 8 == result.heroesBytes);
 	assert((result.artifactsCount + 7) / 8 == result.artifactsBytes);
 
-	result.heroesCount = 179; // + Cove
-
 	return result;
 }
 

+ 98 - 76
lib/mapping/MapFormatH3M.cpp

@@ -32,6 +32,7 @@
 #include "../filesystem/Filesystem.h"
 #include "../mapObjectConstructors/AObjectTypeHandler.h"
 #include "../mapObjectConstructors/CObjectClassesHandler.h"
+#include "../mapObjects/CGCreature.h"
 #include "../mapObjects/MapObjects.h"
 #include "../mapObjects/ObjectTemplate.h"
 #include "../spells/CSpellHandler.h"
@@ -133,13 +134,15 @@ void CMapLoaderH3M::readHeader()
 
 		if(hotaVersion > 0)
 		{
-			reader->skipZero(1);
-			//TODO: HotA
+			bool isMirrorMap = reader->readBool();
 			bool isArenaMap = reader->readBool();
+
+			//TODO: HotA
+			if (isMirrorMap)
+				logGlobal->warn("Map '%s': Mirror maps are not yet supported!", mapName);
+
 			if (isArenaMap)
-			{
 				logGlobal->warn("Map '%s': Arena maps are not supported!", mapName);
-			}
 		}
 
 		if(hotaVersion > 1)
@@ -248,7 +251,7 @@ void CMapLoaderH3M::readPlayerInfo()
 
 		if(playerInfo.mainCustomHeroId != -1)
 		{
-			playerInfo.mainCustomHeroPortrait = reader->readHero().getNum();
+			playerInfo.mainCustomHeroPortrait = reader->readHeroPortrait();
 			playerInfo.mainCustomHeroName = readLocalizedString(TextIdentifier("header", "player", i, "mainHeroName"));
 		}
 
@@ -297,6 +300,8 @@ enum class ELossConditionType : uint8_t
 void CMapLoaderH3M::readVictoryLossConditions()
 {
 	mapHeader->triggeredEvents.clear();
+	mapHeader->victoryMessage.clear();
+	mapHeader->defeatMessage.clear();
 
 	auto vicCondition = static_cast<EVictoryConditionType>(reader->readUInt8());
 
@@ -306,18 +311,18 @@ void CMapLoaderH3M::readVictoryLossConditions()
 
 	TriggeredEvent standardVictory;
 	standardVictory.effect.type = EventEffect::VICTORY;
-	standardVictory.effect.toOtherMessage = VLC->generaltexth->allTexts[5];
+	standardVictory.effect.toOtherMessage.appendTextID("core.genrltxt.5");
 	standardVictory.identifier = "standardVictory";
 	standardVictory.description.clear(); // TODO: display in quest window
-	standardVictory.onFulfill = VLC->generaltexth->allTexts[659];
+	standardVictory.onFulfill.appendTextID("core.genrltxt.659");
 	standardVictory.trigger = EventExpression(victoryCondition);
 
 	TriggeredEvent standardDefeat;
 	standardDefeat.effect.type = EventEffect::DEFEAT;
-	standardDefeat.effect.toOtherMessage = VLC->generaltexth->allTexts[8];
+	standardDefeat.effect.toOtherMessage.appendTextID("core.genrltxt.8");
 	standardDefeat.identifier = "standardDefeat";
 	standardDefeat.description.clear(); // TODO: display in quest window
-	standardDefeat.onFulfill = VLC->generaltexth->allTexts[7];
+	standardDefeat.onFulfill.appendTextID("core.genrltxt.7");
 	standardDefeat.trigger = EventExpression(defeatCondition);
 
 	// Specific victory conditions
@@ -326,7 +331,7 @@ void CMapLoaderH3M::readVictoryLossConditions()
 		// create normal condition
 		mapHeader->triggeredEvents.push_back(standardVictory);
 		mapHeader->victoryIconIndex = 11;
-		mapHeader->victoryMessage = VLC->generaltexth->victoryConditions[0];
+		mapHeader->victoryMessage.appendTextID("core.vcdesc.0");
 	}
 	else
 	{
@@ -336,7 +341,6 @@ void CMapLoaderH3M::readVictoryLossConditions()
 		specialVictory.description.clear(); // TODO: display in quest window
 
 		mapHeader->victoryIconIndex = static_cast<ui16>(vicCondition);
-		mapHeader->victoryMessage = VLC->generaltexth->victoryConditions[static_cast<size_t>(vicCondition) + 1];
 
 		bool allowNormalVictory = reader->readBool();
 		bool appliesToAI = reader->readBool();
@@ -365,9 +369,11 @@ void CMapLoaderH3M::readVictoryLossConditions()
 				EventCondition cond(EventCondition::HAVE_ARTIFACT);
 				cond.objectType = reader->readArtifact();
 
-				specialVictory.effect.toOtherMessage = VLC->generaltexth->allTexts[281];
-				specialVictory.onFulfill = VLC->generaltexth->allTexts[280];
+				specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.281");
+				specialVictory.onFulfill.appendTextID("core.genrltxt.280");
 				specialVictory.trigger = EventExpression(cond);
+
+				mapHeader->victoryMessage.appendTextID("core.vcdesc.1");
 				break;
 			}
 			case EVictoryConditionType::GATHERTROOP:
@@ -376,9 +382,11 @@ void CMapLoaderH3M::readVictoryLossConditions()
 				cond.objectType = reader->readCreature();
 				cond.value = reader->readInt32();
 
-				specialVictory.effect.toOtherMessage = VLC->generaltexth->allTexts[277];
-				specialVictory.onFulfill = VLC->generaltexth->allTexts[276];
+				specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.277");
+				specialVictory.onFulfill.appendTextID("core.genrltxt.6");
 				specialVictory.trigger = EventExpression(cond);
+
+				mapHeader->victoryMessage.appendTextID("core.vcdesc.2");
 				break;
 			}
 			case EVictoryConditionType::GATHERRESOURCE:
@@ -387,9 +395,11 @@ void CMapLoaderH3M::readVictoryLossConditions()
 				cond.objectType = reader->readUInt8();
 				cond.value = reader->readInt32();
 
-				specialVictory.effect.toOtherMessage = VLC->generaltexth->allTexts[279];
-				specialVictory.onFulfill = VLC->generaltexth->allTexts[278];
+				specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.279");
+				specialVictory.onFulfill.appendTextID("core.genrltxt.278");
 				specialVictory.trigger = EventExpression(cond);
+
+				mapHeader->victoryMessage.appendTextID("core.vcdesc.3");
 				break;
 			}
 			case EVictoryConditionType::BUILDCITY:
@@ -402,9 +412,11 @@ void CMapLoaderH3M::readVictoryLossConditions()
 				cond.objectType = BuildingID::FORT + reader->readUInt8();
 				oper.expressions.emplace_back(cond);
 
-				specialVictory.effect.toOtherMessage = VLC->generaltexth->allTexts[283];
-				specialVictory.onFulfill = VLC->generaltexth->allTexts[282];
+				specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.283");
+				specialVictory.onFulfill.appendTextID("core.genrltxt.282");
 				specialVictory.trigger = EventExpression(oper);
+
+				mapHeader->victoryMessage.appendTextID("core.vcdesc.4");
 				break;
 			}
 			case EVictoryConditionType::BUILDGRAIL:
@@ -415,9 +427,11 @@ void CMapLoaderH3M::readVictoryLossConditions()
 				if(cond.position.z > 2)
 					cond.position = int3(-1, -1, -1);
 
-				specialVictory.effect.toOtherMessage = VLC->generaltexth->allTexts[285];
-				specialVictory.onFulfill = VLC->generaltexth->allTexts[284];
+				specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.285");
+				specialVictory.onFulfill.appendTextID("core.genrltxt.284");
 				specialVictory.trigger = EventExpression(cond);
+
+				mapHeader->victoryMessage.appendTextID("core.vcdesc.5");
 				break;
 			}
 			case EVictoryConditionType::BEATHERO:
@@ -426,9 +440,11 @@ void CMapLoaderH3M::readVictoryLossConditions()
 				cond.objectType = Obj::HERO;
 				cond.position = reader->readInt3();
 
-				specialVictory.effect.toOtherMessage = VLC->generaltexth->allTexts[253];
-				specialVictory.onFulfill = VLC->generaltexth->allTexts[252];
+				specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.253");
+				specialVictory.onFulfill.appendTextID("core.genrltxt.252");
 				specialVictory.trigger = EventExpression(cond);
+
+				mapHeader->victoryMessage.appendTextID("core.vcdesc.6");
 				break;
 			}
 			case EVictoryConditionType::CAPTURECITY:
@@ -437,9 +453,11 @@ void CMapLoaderH3M::readVictoryLossConditions()
 				cond.objectType = Obj::TOWN;
 				cond.position = reader->readInt3();
 
-				specialVictory.effect.toOtherMessage = VLC->generaltexth->allTexts[250];
-				specialVictory.onFulfill = VLC->generaltexth->allTexts[249];
+				specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.250");
+				specialVictory.onFulfill.appendTextID("core.genrltxt.249");
 				specialVictory.trigger = EventExpression(cond);
+
+				mapHeader->victoryMessage.appendTextID("core.vcdesc.7");
 				break;
 			}
 			case EVictoryConditionType::BEATMONSTER:
@@ -448,9 +466,11 @@ void CMapLoaderH3M::readVictoryLossConditions()
 				cond.objectType = Obj::MONSTER;
 				cond.position = reader->readInt3();
 
-				specialVictory.effect.toOtherMessage = VLC->generaltexth->allTexts[287];
-				specialVictory.onFulfill = VLC->generaltexth->allTexts[286];
+				specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.287");
+				specialVictory.onFulfill.appendTextID("core.genrltxt.286");
 				specialVictory.trigger = EventExpression(cond);
+
+				mapHeader->victoryMessage.appendTextID("core.vcdesc.8");
 				break;
 			}
 			case EVictoryConditionType::TAKEDWELLINGS:
@@ -459,9 +479,11 @@ void CMapLoaderH3M::readVictoryLossConditions()
 				oper.expressions.emplace_back(EventCondition(EventCondition::CONTROL, 0, Obj::CREATURE_GENERATOR1));
 				oper.expressions.emplace_back(EventCondition(EventCondition::CONTROL, 0, Obj::CREATURE_GENERATOR4));
 
-				specialVictory.effect.toOtherMessage = VLC->generaltexth->allTexts[289];
-				specialVictory.onFulfill = VLC->generaltexth->allTexts[288];
+				specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.289");
+				specialVictory.onFulfill.appendTextID("core.genrltxt.288");
 				specialVictory.trigger = EventExpression(oper);
+
+				mapHeader->victoryMessage.appendTextID("core.vcdesc.9");
 				break;
 			}
 			case EVictoryConditionType::TAKEMINES:
@@ -469,9 +491,11 @@ void CMapLoaderH3M::readVictoryLossConditions()
 				EventCondition cond(EventCondition::CONTROL);
 				cond.objectType = Obj::MINE;
 
-				specialVictory.effect.toOtherMessage = VLC->generaltexth->allTexts[291];
-				specialVictory.onFulfill = VLC->generaltexth->allTexts[290];
+				specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.291");
+				specialVictory.onFulfill.appendTextID("core.genrltxt.290");
 				specialVictory.trigger = EventExpression(cond);
+
+				mapHeader->victoryMessage.appendTextID("core.vcdesc.10");
 				break;
 			}
 			case EVictoryConditionType::TRANSPORTITEM:
@@ -480,20 +504,37 @@ void CMapLoaderH3M::readVictoryLossConditions()
 				cond.objectType = reader->readUInt8();
 				cond.position = reader->readInt3();
 
-				specialVictory.effect.toOtherMessage = VLC->generaltexth->allTexts[293];
-				specialVictory.onFulfill = VLC->generaltexth->allTexts[292];
+				specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.293");
+				specialVictory.onFulfill.appendTextID("core.genrltxt.292");
 				specialVictory.trigger = EventExpression(cond);
+
+				mapHeader->victoryMessage.appendTextID("core.vcdesc.11");
 				break;
 			}
 			case EVictoryConditionType::HOTA_ELIMINATE_ALL_MONSTERS:
-				//TODO: HOTA
-				logGlobal->warn("Map '%s': Victory condition 'Eliminate all monsters' is not implemented!", mapName);
+			{
+				EventCondition cond(EventCondition::DESTROY);
+				cond.objectType = Obj::MONSTER;
+
+				specialVictory.effect.toOtherMessage.appendTextID("vcmi.map.victoryCondition.eliminateMonsters.toOthers");
+				specialVictory.onFulfill.appendTextID("vcmi.map.victoryCondition.eliminateMonsters.toSelf");
+				specialVictory.trigger = EventExpression(cond);
+
+				mapHeader->victoryMessage.appendTextID("core.vcdesc.12");
+				mapHeader->victoryIconIndex = 12;
 				break;
+			}
 			case EVictoryConditionType::HOTA_SURVIVE_FOR_DAYS:
 			{
-				//TODO: HOTA
-				uint32_t daysToSurvive = reader->readUInt32(); // Number of days
-				logGlobal->warn("Map '%s': Victory condition 'Survive for %d days' is not implemented!", mapName, daysToSurvive);
+				EventCondition cond(EventCondition::DAYS_PASSED);
+				cond.value = reader->readUInt32();
+
+				specialVictory.effect.toOtherMessage.appendTextID("vcmi.map.victoryCondition.daysPassed.toOthers");
+				specialVictory.onFulfill.appendTextID("vcmi.map.victoryCondition.daysPassed.toSelf");
+				specialVictory.trigger = EventExpression(cond);
+
+				mapHeader->victoryMessage.appendTextID("core.vcdesc.13");
+				mapHeader->victoryIconIndex = 13;
 				break;
 			}
 			default:
@@ -514,8 +555,8 @@ void CMapLoaderH3M::readVictoryLossConditions()
 		// if normal victory allowed - add one more quest
 		if(allowNormalVictory)
 		{
-			mapHeader->victoryMessage += " / ";
-			mapHeader->victoryMessage += VLC->generaltexth->victoryConditions[0];
+			mapHeader->victoryMessage.appendRawString(" / ");
+			mapHeader->victoryMessage.appendTextID("core.vcdesc.0");
 			mapHeader->triggeredEvents.push_back(standardVictory);
 		}
 		mapHeader->triggeredEvents.push_back(specialVictory);
@@ -526,18 +567,17 @@ void CMapLoaderH3M::readVictoryLossConditions()
 	if(lossCond == ELossConditionType::LOSSSTANDARD)
 	{
 		mapHeader->defeatIconIndex = 3;
-		mapHeader->defeatMessage = VLC->generaltexth->lossCondtions[0];
+		mapHeader->defeatMessage.appendTextID("core.lcdesc.0");
 	}
 	else
 	{
 		TriggeredEvent specialDefeat;
 		specialDefeat.effect.type = EventEffect::DEFEAT;
-		specialDefeat.effect.toOtherMessage = VLC->generaltexth->allTexts[5];
+		specialDefeat.effect.toOtherMessage.appendTextID("core.genrltxt.5");
 		specialDefeat.identifier = "specialDefeat";
 		specialDefeat.description.clear(); // TODO: display in quest window
 
 		mapHeader->defeatIconIndex = static_cast<ui16>(lossCond);
-		mapHeader->defeatMessage = VLC->generaltexth->lossCondtions[static_cast<size_t>(lossCond) + 1];
 
 		switch(lossCond)
 		{
@@ -549,8 +589,10 @@ void CMapLoaderH3M::readVictoryLossConditions()
 				cond.position = reader->readInt3();
 
 				noneOf.expressions.emplace_back(cond);
-				specialDefeat.onFulfill = VLC->generaltexth->allTexts[251];
+				specialDefeat.onFulfill.appendTextID("core.genrltxt.251");
 				specialDefeat.trigger = EventExpression(noneOf);
+
+				mapHeader->defeatMessage.appendTextID("core.lcdesc.1");
 				break;
 			}
 			case ELossConditionType::LOSSHERO:
@@ -561,8 +603,10 @@ void CMapLoaderH3M::readVictoryLossConditions()
 				cond.position = reader->readInt3();
 
 				noneOf.expressions.emplace_back(cond);
-				specialDefeat.onFulfill = VLC->generaltexth->allTexts[253];
+				specialDefeat.onFulfill.appendTextID("core.genrltxt.253");
 				specialDefeat.trigger = EventExpression(noneOf);
+
+				mapHeader->defeatMessage.appendTextID("core.lcdesc.2");
 				break;
 			}
 			case ELossConditionType::TIMEEXPIRES:
@@ -570,8 +614,10 @@ void CMapLoaderH3M::readVictoryLossConditions()
 				EventCondition cond(EventCondition::DAYS_PASSED);
 				cond.value = reader->readUInt16();
 
-				specialDefeat.onFulfill = VLC->generaltexth->allTexts[254];
+				specialDefeat.onFulfill.appendTextID("core.genrltxt.254");
 				specialDefeat.trigger = EventExpression(cond);
+
+				mapHeader->defeatMessage.appendTextID("core.lcdesc.3");
 				break;
 			}
 		}
@@ -640,7 +686,7 @@ void CMapLoaderH3M::readDisposedHeroes()
 		for(int g = 0; g < disp; ++g)
 		{
 			map->disposedHeroes[g].heroId = reader->readHero().getNum();
-			map->disposedHeroes[g].portrait = reader->readHero().getNum();
+			map->disposedHeroes[g].portrait = reader->readHeroPortrait();
 			map->disposedHeroes[g].name = readLocalizedString(TextIdentifier("header", "heroes", map->disposedHeroes[g].heroId));
 			map->disposedHeroes[g].players = reader->readUInt8();
 		}
@@ -1299,24 +1345,6 @@ CGObjectInstance * CMapLoaderH3M::readShipyard(const int3 & mapPosition, std::sh
 	return object;
 }
 
-CGObjectInstance * CMapLoaderH3M::readBorderGuard()
-{
-	return new CGBorderGuard();
-}
-
-CGObjectInstance * CMapLoaderH3M::readBorderGate(const int3 & mapPosition, std::shared_ptr<const ObjectTemplate> objectTemplate)
-{
-	if(objectTemplate->subid < 1000)
-		return new CGBorderGate();
-
-	//TODO: HotA - grave has same ID as border gate? WTF?
-	if(objectTemplate->subid == 1001)
-		return new CGObjectInstance();
-
-	logGlobal->warn("Map '%s: Quest gates at %s are not implemented!", mapName, mapPosition.toString());
-	return readQuestGuard(mapPosition);
-}
-
 CGObjectInstance * CMapLoaderH3M::readLighthouse(const int3 & mapPosition)
 {
 	auto * object = new CGLighthouse();
@@ -1453,12 +1481,6 @@ CGObjectInstance * CMapLoaderH3M::readObject(std::shared_ptr<const ObjectTemplat
 		case Obj::HERO_PLACEHOLDER:
 			return readHeroPlaceholder(mapPosition);
 
-		case Obj::BORDERGUARD:
-			return readBorderGuard();
-
-		case Obj::BORDER_GATE:
-			return readBorderGate(mapPosition, objectTemplate);
-
 		case Obj::PYRAMID:
 			return readPyramid(mapPosition, objectTemplate);
 
@@ -1601,7 +1623,7 @@ CGObjectInstance * CMapLoaderH3M::readHero(const int3 & mapPosition, const Objec
 	}
 
 	PlayerColor owner = reader->readPlayer();
-	object->subID = reader->readUInt8();
+	object->subID = reader->readHero().getNum();
 
 	//If hero of this type has been predefined, use that as a base.
 	//Instance data will overwrite the predefined values where appropriate.
@@ -1617,7 +1639,7 @@ CGObjectInstance * CMapLoaderH3M::readHero(const int3 & mapPosition, const Objec
 	}
 
 	setOwnerAndValidate(mapPosition, object, owner);
-	object->portrait = object->subID;
+	object->portrait = CGHeroInstance::UNINITIALIZED_PORTRAIT;
 
 	for(auto & elem : map->disposedHeroes)
 	{
@@ -1652,7 +1674,7 @@ CGObjectInstance * CMapLoaderH3M::readHero(const int3 & mapPosition, const Objec
 
 	bool hasPortrait = reader->readBool();
 	if(hasPortrait)
-		object->portrait = reader->readHero().getNum();
+		object->portrait = reader->readHeroPortrait();
 
 	bool hasSecSkills = reader->readBool();
 	if(hasSecSkills)
@@ -1955,7 +1977,7 @@ void CMapLoaderH3M::readQuest(IQuestObject * guard, const int3 & position)
 
 			if(missionSubID == 0)
 			{
-				guard->quest->missionType = CQuest::MISSION_HOTA_HERO_CLASS;
+				guard->quest->missionType = CQuest::MISSION_NONE; //TODO: CQuest::MISSION_HOTA_HERO_CLASS;
 				std::set<HeroClassID> heroClasses;
 				reader->readBitmaskHeroClassesSized(heroClasses, false);
 
@@ -1964,7 +1986,7 @@ void CMapLoaderH3M::readQuest(IQuestObject * guard, const int3 & position)
 			}
 			if(missionSubID == 1)
 			{
-				guard->quest->missionType = CQuest::MISSION_HOTA_REACH_DATE;
+				guard->quest->missionType = CQuest::MISSION_NONE; //TODO: CQuest::MISSION_HOTA_REACH_DATE;
 				uint32_t daysPassed = reader->readUInt32();
 
 				logGlobal->warn("Map '%s': Quest at %s 'Wait till %d days passed' is not implemented!", mapName, position.toString(), daysPassed);

+ 0 - 2
lib/mapping/MapFormatH3M.h

@@ -177,8 +177,6 @@ private:
 	CGObjectInstance * readHeroPlaceholder(const int3 & position);
 	CGObjectInstance * readGrail(const int3 & position, std::shared_ptr<const ObjectTemplate> objectTemplate);
 	CGObjectInstance * readPyramid(const int3 & position, std::shared_ptr<const ObjectTemplate> objTempl);
-	CGObjectInstance * readBorderGuard();
-	CGObjectInstance * readBorderGate(const int3 & position, std::shared_ptr<const ObjectTemplate> objTempl);
 	CGObjectInstance * readQuestGuard(const int3 & position);
 	CGObjectInstance * readShipyard(const int3 & mapPosition, std::shared_ptr<const ObjectTemplate> objectTemplate);
 	CGObjectInstance * readLighthouse(const int3 & mapPosition);

+ 8 - 8
lib/mapping/MapFormatJson.cpp

@@ -423,10 +423,10 @@ void CMapFormatJson::serializeHeader(JsonSerializeFormat & handler)
 
 	handler.serializeLIC("allowedHeroes", &HeroTypeID::decode, &HeroTypeID::encode, VLC->heroh->getDefaultAllowed(), mapHeader->allowedHeroes);
 
-	handler.serializeString("victoryString", mapHeader->victoryMessage);
+//	handler.serializeString("victoryString", mapHeader->victoryMessage);
 	handler.serializeInt("victoryIconIndex", mapHeader->victoryIconIndex);
 
-	handler.serializeString("defeatString", mapHeader->defeatMessage);
+//	handler.serializeString("defeatString", mapHeader->defeatMessage);
 	handler.serializeInt("defeatIconIndex", mapHeader->defeatIconIndex);
 }
 
@@ -683,10 +683,10 @@ void CMapFormatJson::readTriggeredEvent(TriggeredEvent & event, const JsonNode &
 {
 	using namespace TriggeredEventsDetail;
 
-	event.onFulfill = source["message"].String();
-	event.description = source["description"].String();
+	event.onFulfill.jsonDeserialize(source["message"]);
+	event.description.jsonDeserialize(source["description"]);
 	event.effect.type = vstd::find_pos(typeNames, source["effect"]["type"].String());
-	event.effect.toOtherMessage = source["effect"]["messageToSend"].String();
+	event.effect.toOtherMessage.jsonDeserialize(source["effect"]["messageToSend"]);
 	event.trigger = EventExpression(source["condition"], JsonToCondition); // logical expression
 }
 
@@ -705,15 +705,15 @@ void CMapFormatJson::writeTriggeredEvent(const TriggeredEvent & event, JsonNode
 	using namespace TriggeredEventsDetail;
 
 	if(!event.onFulfill.empty())
-		dest["message"].String() = event.onFulfill;
+		event.onFulfill.jsonSerialize(dest["message"]);
 
 	if(!event.description.empty())
-		dest["description"].String() = event.description;
+		event.description.jsonSerialize(dest["description"]);
 
 	dest["effect"]["type"].String() = typeNames.at(static_cast<size_t>(event.effect.type));
 
 	if(!event.effect.toOtherMessage.empty())
-		dest["effect"]["messageToSend"].String() = event.effect.toOtherMessage;
+		event.description.jsonSerialize(dest["effect"]["messageToSend"]);
 
 	dest["condition"] = event.trigger.toJson(ConditionToJson);
 }

+ 26 - 0
lib/mapping/MapIdentifiersH3M.cpp

@@ -15,6 +15,7 @@
 #include "../VCMI_Lib.h"
 #include "../CModHandler.h"
 #include "../CTownHandler.h"
+#include "../CHeroHandler.h"
 #include "../filesystem/Filesystem.h"
 #include "../mapObjectConstructors/AObjectTypeHandler.h"
 #include "../mapObjectConstructors/CObjectClassesHandler.h"
@@ -86,6 +87,15 @@ void MapIdentifiersH3M::loadMapping(const JsonNode & mapping)
 		}
 	}
 
+	for (auto entry : mapping["portraits"].Struct())
+	{
+		int32_t sourceID = entry.second.Integer();
+		int32_t targetID = *VLC->modh->identifiers.getIdentifier(VLC->modh->scopeGame(), "hero", entry.first);
+		int32_t iconID = VLC->heroTypes()->getByIndex(targetID)->getIconIndex();
+
+		mappingHeroPortrait[sourceID] = iconID;
+	}
+
 	loadMapping(mappingBuilding, mapping["buildingsCommon"], "building.core:random");
 	loadMapping(mappingFaction, mapping["factions"], "faction");
 	loadMapping(mappingCreature, mapping["creatures"], "creature");
@@ -111,6 +121,15 @@ void MapIdentifiersH3M::remapTemplate(ObjectTemplate & objectTemplate)
 		objectTemplate.id = mappedType.ID;
 		objectTemplate.subid = mappedType.subID;
 	}
+
+	if (objectTemplate.id == Obj::TOWN || objectTemplate.id == Obj::RANDOM_DWELLING_FACTION)
+		objectTemplate.subid = remap(FactionID(objectTemplate.subid));
+
+	if (objectTemplate.id == Obj::MONSTER)
+		objectTemplate.subid = remap(CreatureID(objectTemplate.subid));
+
+	if (objectTemplate.id == Obj::ARTIFACT)
+		objectTemplate.subid = remap(ArtifactID(objectTemplate.subid));
 }
 
 BuildingID MapIdentifiersH3M::remapBuilding(std::optional<FactionID> owner, BuildingID input) const
@@ -149,6 +168,13 @@ HeroTypeID MapIdentifiersH3M::remap(HeroTypeID input) const
 	return input;
 }
 
+int32_t MapIdentifiersH3M::remapPortrrait(int32_t input) const
+{
+	if (mappingHeroPortrait.count(input))
+		return mappingHeroPortrait.at(input);
+	return input;
+}
+
 HeroClassID MapIdentifiersH3M::remap(HeroClassID input) const
 {
 	if (mappingHeroClass.count(input))

+ 3 - 0
lib/mapping/MapIdentifiersH3M.h

@@ -37,6 +37,7 @@ class MapIdentifiersH3M
 	std::map<FactionID, FactionID> mappingFaction;
 	std::map<CreatureID, CreatureID> mappingCreature;
 	std::map<HeroTypeID, HeroTypeID> mappingHeroType;
+	std::map<int32_t, int32_t> mappingHeroPortrait;
 	std::map<HeroClassID, HeroClassID> mappingHeroClass;
 	std::map<TerrainId, TerrainId> mappingTerrain;
 	std::map<ArtifactID, ArtifactID> mappingArtifact;
@@ -53,6 +54,7 @@ public:
 	void remapTemplate(ObjectTemplate & objectTemplate);
 
 	BuildingID remapBuilding(std::optional<FactionID> owner, BuildingID input) const;
+	int32_t remapPortrrait(int32_t input) const;
 	FactionID remap(FactionID input) const;
 	CreatureID remap(CreatureID input) const;
 	HeroTypeID remap(HeroTypeID input) const;
@@ -60,6 +62,7 @@ public:
 	TerrainId remap(TerrainId input) const;
 	ArtifactID remap(ArtifactID input) const;
 	SecondarySkill remap(SecondarySkill input) const;
+
 };
 
 VCMI_LIB_NAMESPACE_END

+ 12 - 1
lib/mapping/MapReaderH3M.cpp

@@ -96,8 +96,19 @@ HeroTypeID MapReaderH3M::readHero()
 	if(result.getNum() == features.heroIdentifierInvalid)
 		return HeroTypeID(-1);
 
+	assert(result.getNum() < features.heroesCount);
+	return remapIdentifier(result);
+}
+
+int32_t MapReaderH3M::readHeroPortrait()
+{
+	HeroTypeID result(reader->readUInt8());
+
+	if(result.getNum() == features.heroIdentifierInvalid)
+		return int32_t(-1);
+
 	assert(result.getNum() < features.heroesPortraitsCount);
-	return remapIdentifier(result);;
+	return remapper.remapPortrrait(result);
 }
 
 CreatureID MapReaderH3M::readCreature()

+ 1 - 0
lib/mapping/MapReaderH3M.h

@@ -35,6 +35,7 @@ public:
 	ArtifactID readArtifact32();
 	CreatureID readCreature();
 	HeroTypeID readHero();
+	int32_t readHeroPortrait();
 	TerrainId readTerrain();
 	RoadId readRoad();
 	RiverId readRiver();

+ 1 - 0
lib/registerTypes/RegisterTypes.h

@@ -26,6 +26,7 @@
 #include "../mapObjectConstructors/ShipyardInstanceConstructor.h"
 #include "../mapObjectConstructors/ShrineInstanceConstructor.h"
 #include "../mapObjects/MapObjects.h"
+#include "../mapObjects/CGCreature.h"
 #include "../mapObjects/CGTownBuilding.h"
 #include "../mapObjects/ObjectTemplate.h"
 #include "../battle/CObstacleInstance.h"

+ 2 - 1
lib/rewardable/Configuration.h

@@ -11,8 +11,9 @@
 #pragma once
 
 #include "Limiter.h"
-#include "Reward.h"
+#include "MetaString.h"
 #include "NetPacksBase.h"
+#include "Reward.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 

+ 4 - 4
lib/rewardable/Info.cpp

@@ -34,9 +34,9 @@ namespace {
 	{
 		MetaString ret;
 		if (value.isNumber())
-			ret.addTxt(MetaString::ADVOB_TXT, static_cast<ui32>(value.Float()));
+			ret.appendLocalString(EMetaText::ADVOB_TXT, static_cast<ui32>(value.Float()));
 		else
-			ret << value.String();
+			ret.appendRawString(value.String());
 		return ret;
 	}
 
@@ -191,10 +191,10 @@ void Rewardable::Info::configureRewards(
 		info.message = loadMessage(reward["message"]);
 
 		for (const auto & artifact : info.reward.artifacts )
-			info.message.addReplacement(MetaString::ART_NAMES, artifact.getNum());
+			info.message.replaceLocalString(EMetaText::ART_NAMES, artifact.getNum());
 
 		for (const auto & artifact : info.reward.spells )
-			info.message.addReplacement(MetaString::SPELL_NAME, artifact.getNum());
+			info.message.replaceLocalString(EMetaText::SPELL_NAME, artifact.getNum());
 
 		object.info.push_back(info);
 	}

+ 1 - 0
lib/rmg/modificators/ConnectionsPlacer.cpp

@@ -15,6 +15,7 @@
 #include "../../TerrainHandler.h"
 #include "../../mapObjectConstructors/AObjectTypeHandler.h"
 #include "../../mapObjectConstructors/CObjectClassesHandler.h"
+#include "../../mapObjects/CGCreature.h"
 #include "../../mapping/CMapEditManager.h"
 #include "../RmgObject.h"
 #include "ObjectManager.h"

+ 1 - 0
lib/rmg/modificators/ObjectManager.cpp

@@ -23,6 +23,7 @@
 #include "../../CCreatureHandler.h"
 #include "../../mapObjectConstructors/AObjectTypeHandler.h"
 #include "../../mapObjectConstructors/CObjectClassesHandler.h"
+#include "../../mapObjects/CGCreature.h"
 #include "../../mapping/CMap.h"
 #include "../../mapping/CMapEditManager.h"
 #include "../Functions.h"

+ 13 - 13
lib/spells/AdventureSpellMechanics.cpp

@@ -147,7 +147,7 @@ ESpellCastResult SummonBoatMechanics::applyAdventureEffects(SpellCastEnvironment
 	{
 		InfoWindow iw;
 		iw.player = parameters.caster->getCasterOwner();
-		iw.text.addTxt(MetaString::GENERAL_TXT, 333);//%s is already in boat
+		iw.text.appendLocalString(EMetaText::GENERAL_TXT, 333);//%s is already in boat
 		parameters.caster->getCasterName(iw.text);
 		env->apply(&iw);
 		return ESpellCastResult::CANCEL;
@@ -159,7 +159,7 @@ ESpellCastResult SummonBoatMechanics::applyAdventureEffects(SpellCastEnvironment
 	{
 		InfoWindow iw;
 		iw.player = parameters.caster->getCasterOwner();
-		iw.text.addTxt(MetaString::GENERAL_TXT, 334);//There is no place to put the boat.
+		iw.text.appendLocalString(EMetaText::GENERAL_TXT, 334);//There is no place to put the boat.
 		env->apply(&iw);
 		return ESpellCastResult::CANCEL;
 	}
@@ -171,7 +171,7 @@ ESpellCastResult SummonBoatMechanics::applyAdventureEffects(SpellCastEnvironment
 	{
 		InfoWindow iw;
 		iw.player = parameters.caster->getCasterOwner();
-		iw.text.addTxt(MetaString::GENERAL_TXT, 336); //%s tried to summon a boat, but failed.
+		iw.text.appendLocalString(EMetaText::GENERAL_TXT, 336); //%s tried to summon a boat, but failed.
 		parameters.caster->getCasterName(iw.text);
 		env->apply(&iw);
 		return ESpellCastResult::OK;
@@ -208,7 +208,7 @@ ESpellCastResult SummonBoatMechanics::applyAdventureEffects(SpellCastEnvironment
 	{
 		InfoWindow iw;
 		iw.player = parameters.caster->getCasterOwner();
-		iw.text.addTxt(MetaString::GENERAL_TXT, 335); //There are no boats to summon.
+		iw.text.appendLocalString(EMetaText::GENERAL_TXT, 335); //There are no boats to summon.
 		env->apply(&iw);
 	}
 	else //create boat
@@ -236,7 +236,7 @@ ESpellCastResult ScuttleBoatMechanics::applyAdventureEffects(SpellCastEnvironmen
 	{
 		InfoWindow iw;
 		iw.player = parameters.caster->getCasterOwner();
-		iw.text.addTxt(MetaString::GENERAL_TXT, 337); //%s tried to scuttle the boat, but failed
+		iw.text.appendLocalString(EMetaText::GENERAL_TXT, 337); //%s tried to scuttle the boat, but failed
 		parameters.caster->getCasterName(iw.text);
 		env->apply(&iw);
 		return ESpellCastResult::OK;
@@ -313,7 +313,7 @@ ESpellCastResult DimensionDoorMechanics::applyAdventureEffects(SpellCastEnvironm
 	{
 		InfoWindow iw;
 		iw.player = parameters.caster->getCasterOwner();
-		iw.text.addTxt(MetaString::GENERAL_TXT, 338); //%s is not skilled enough to cast this spell again today.
+		iw.text.appendLocalString(EMetaText::GENERAL_TXT, 338); //%s is not skilled enough to cast this spell again today.
 		parameters.caster->getCasterName(iw.text);
 		env->apply(&iw);
 		return ESpellCastResult::CANCEL;
@@ -328,7 +328,7 @@ ESpellCastResult DimensionDoorMechanics::applyAdventureEffects(SpellCastEnvironm
 	{
 		InfoWindow iw;
 		iw.player = parameters.caster->getCasterOwner();
-		iw.text.addTxt(MetaString::GENERAL_TXT, 70); //Dimension Door failed!
+		iw.text.appendLocalString(EMetaText::GENERAL_TXT, 70); //Dimension Door failed!
 		env->apply(&iw);
 	}
 	else if(env->moveHero(ObjectInstanceID(parameters.caster->getCasterUnitId()), parameters.caster->getHeroCaster()->convertFromVisitablePos(parameters.pos), true))
@@ -376,7 +376,7 @@ ESpellCastResult TownPortalMechanics::applyAdventureEffects(SpellCastEnvironment
 		{
 			InfoWindow iw;
 			iw.player = parameters.caster->getCasterOwner();
-			iw.text.addTxt(MetaString::GENERAL_TXT, 123);
+			iw.text.appendLocalString(EMetaText::GENERAL_TXT, 123);
 			env->apply(&iw);
 			return ESpellCastResult::CANCEL;
 		}
@@ -461,7 +461,7 @@ ESpellCastResult TownPortalMechanics::beginCast(SpellCastEnvironment * env, cons
 	{
 		InfoWindow iw;
 		iw.player = parameters.caster->getCasterOwner();
-		iw.text.addTxt(MetaString::GENERAL_TXT, 124);
+		iw.text.appendLocalString(EMetaText::GENERAL_TXT, 124);
 		env->apply(&iw);
 		return ESpellCastResult::CANCEL;
 	}
@@ -472,7 +472,7 @@ ESpellCastResult TownPortalMechanics::beginCast(SpellCastEnvironment * env, cons
 	{
 		InfoWindow iw;
 		iw.player = parameters.caster->getCasterOwner();
-		iw.text.addTxt(MetaString::GENERAL_TXT, 125);
+		iw.text.appendLocalString(EMetaText::GENERAL_TXT, 125);
 		env->apply(&iw);
 		return ESpellCastResult::CANCEL;
 	}
@@ -517,14 +517,14 @@ ESpellCastResult TownPortalMechanics::beginCast(SpellCastEnvironment * env, cons
 		{
 			InfoWindow iw;
 			iw.player = parameters.caster->getCasterOwner();
-			iw.text.addTxt(MetaString::GENERAL_TXT, 124);
+			iw.text.appendLocalString(EMetaText::GENERAL_TXT, 124);
 			env->apply(&iw);
 			return ESpellCastResult::CANCEL;
 		}
 
 		request.player = parameters.caster->getCasterOwner();
-		request.title.addTxt(MetaString::JK_TXT, 40);
-		request.description.addTxt(MetaString::JK_TXT, 41);
+		request.title.appendLocalString(EMetaText::JK_TXT, 40);
+		request.description.appendLocalString(EMetaText::JK_TXT, 41);
 		request.icon.id = Component::EComponentType::SPELL;
 		request.icon.subtype = owner->id.toEnum();
 

+ 1 - 1
lib/spells/BattleSpellMechanics.cpp

@@ -309,7 +309,7 @@ void BattleSpellMechanics::cast(ServerCallback * server, const Target & target)
 		{
 			MetaString line;
 			caster->getCastDescription(owner, affectedUnits, line);
-			if(!line.message.empty())
+			if(!line.empty())
 				castDescription.lines.push_back(line);
 		}
 		break;

+ 4 - 4
lib/spells/BonusCaster.cpp

@@ -13,7 +13,7 @@
 
 #include <vcmi/spells/Spell.h>
 
-#include "../NetPacksBase.h"
+#include "../MetaString.h"
 #include "../battle/Unit.h"
 #include "../bonuses/Bonus.h"
 
@@ -33,7 +33,7 @@ BonusCaster::~BonusCaster() = default;
 void BonusCaster::getCasterName(MetaString & text) const
 {
 	if(!bonus->description.empty())
-		text.addReplacement(bonus->description);
+		text.replaceRawString(bonus->description);
 	else
 		actualCaster->getCasterName(text);
 }
@@ -43,9 +43,9 @@ void BonusCaster::getCastDescription(const Spell * spell, const std::vector<cons
 	const bool singleTarget = attacked.size() == 1;
 	const int textIndex = singleTarget ? 195 : 196;
 
-	text.addTxt(MetaString::GENERAL_TXT, textIndex);
+	text.appendLocalString(EMetaText::GENERAL_TXT, textIndex);
 	getCasterName(text);
-	text.addReplacement(MetaString::SPELL_NAME, spell->getIndex());
+	text.replaceLocalString(EMetaText::SPELL_NAME, spell->getIndex());
 	if(singleTarget)
 		attacked.at(0)->addNameReplacement(text, true);
 }

+ 6 - 6
lib/spells/ISpellMechanics.cpp

@@ -460,7 +460,7 @@ bool BaseMechanics::adaptGenericProblem(Problem & target) const
 {
 	MetaString text;
 	// %s recites the incantations but they seem to have no effect.
-	text.addTxt(MetaString::GENERAL_TXT, 541);
+	text.appendLocalString(EMetaText::GENERAL_TXT, 541);
 	assert(caster);
 	caster->getCasterName(text);
 
@@ -489,14 +489,14 @@ bool BaseMechanics::adaptProblem(ESpellCastProblem::ESpellCastProblem source, Pr
 			if(b && b->val == 2 && b->source == BonusSource::ARTIFACT)
 			{
 				//The %s prevents %s from casting 3rd level or higher spells.
-				text.addTxt(MetaString::GENERAL_TXT, 536);
-				text.addReplacement(MetaString::ART_NAMES, b->sid);
+				text.appendLocalString(EMetaText::GENERAL_TXT, 536);
+				text.replaceLocalString(EMetaText::ART_NAMES, b->sid);
 				caster->getCasterName(text);
 				target.add(std::move(text), spells::Problem::NORMAL);
 			}
 			else if(b && b->source == BonusSource::TERRAIN_OVERLAY && VLC->battlefields()->getByIndex(b->sid)->identifier == "cursed_ground")
 			{
-				text.addTxt(MetaString::GENERAL_TXT, 537);
+				text.appendLocalString(EMetaText::GENERAL_TXT, 537);
 				target.add(std::move(text), spells::Problem::NORMAL);
 			}
 			else
@@ -510,14 +510,14 @@ bool BaseMechanics::adaptProblem(ESpellCastProblem::ESpellCastProblem source, Pr
 	case ESpellCastProblem::NO_APPROPRIATE_TARGET:
 		{
 			MetaString text;
-			text.addTxt(MetaString::GENERAL_TXT, 185);
+			text.appendLocalString(EMetaText::GENERAL_TXT, 185);
 			target.add(std::move(text), spells::Problem::NORMAL);
 		}
 		break;
 	case ESpellCastProblem::INVALID:
 		{
 			MetaString text;
-			text.addReplacement("Internal error during check of spell cast.");
+			text.appendRawString("Internal error during check of spell cast.");
 			target.add(std::move(text), spells::Problem::CRITICAL);
 		}
 		break;

+ 0 - 1
lib/spells/Problem.cpp

@@ -8,7 +8,6 @@
  *
  */
 #include "StdInc.h"
-
 #include "Problem.h"
 
 VCMI_LIB_NAMESPACE_BEGIN

+ 1 - 1
lib/spells/Problem.h

@@ -12,7 +12,7 @@
 
 #include <vcmi/spells/Magic.h>
 
-#include "../NetPacksBase.h"
+#include "../MetaString.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 

+ 14 - 14
lib/spells/effects/Damage.cpp

@@ -135,13 +135,13 @@ void Damage::describeEffect(std::vector<MetaString> & log, const Mechanics * m,
 		MetaString line;
 		if(kills > 1)
 		{
-			line.addTxt(MetaString::GENERAL_TXT, 119); //%d %s die under the terrible gaze of the %s.
-			line.addReplacement(kills);
+			line.appendLocalString(EMetaText::GENERAL_TXT, 119); //%d %s die under the terrible gaze of the %s.
+			line.replaceNumber(kills);
 			firstTarget->addNameReplacement(line, true);
 		}
 		else
 		{
-			line.addTxt(MetaString::GENERAL_TXT, 118); //One %s dies under the terrible gaze of the %s.
+			line.appendLocalString(EMetaText::GENERAL_TXT, 118); //One %s dies under the terrible gaze of the %s.
 			firstTarget->addNameReplacement(line, false);
 		}
 		m->caster->getCasterName(line);
@@ -151,7 +151,7 @@ void Damage::describeEffect(std::vector<MetaString> & log, const Mechanics * m,
 	{
 		{
 			MetaString line;
-			firstTarget->addText(line, MetaString::GENERAL_TXT, -367, true);
+			firstTarget->addText(line, EMetaText::GENERAL_TXT, -367, true);
 			firstTarget->addNameReplacement(line, true);
 			log.push_back(line);
 		}
@@ -161,8 +161,8 @@ void Damage::describeEffect(std::vector<MetaString> & log, const Mechanics * m,
 			//todo: handle newlines in metastring
 			std::string text = VLC->generaltexth->allTexts[343]; //Does %d points of damage.
 			boost::algorithm::trim(text);
-			line << text;
-			line.addReplacement(static_cast<int>(damage)); //no more text afterwards
+			line.appendRawString(text);
+			line.replaceNumber(static_cast<int>(damage)); //no more text afterwards
 			log.push_back(line);
 		}
 	}
@@ -170,9 +170,9 @@ void Damage::describeEffect(std::vector<MetaString> & log, const Mechanics * m,
 	{
 		{
 			MetaString line;
-			line.addTxt(MetaString::GENERAL_TXT, 376); // Spell %s does %d damage
-			line.addReplacement(MetaString::SPELL_NAME, m->getSpellIndex());
-			line.addReplacement(static_cast<int>(damage));
+			line.appendLocalString(EMetaText::GENERAL_TXT, 376); // Spell %s does %d damage
+			line.replaceLocalString(EMetaText::SPELL_NAME, m->getSpellIndex());
+			line.replaceNumber(static_cast<int>(damage));
 
 			log.push_back(line);
 		}
@@ -183,19 +183,19 @@ void Damage::describeEffect(std::vector<MetaString> & log, const Mechanics * m,
 
 			if(kills > 1)
 			{
-				line.addTxt(MetaString::GENERAL_TXT, 379); // %d %s perishes
-				line.addReplacement(kills);
+				line.appendLocalString(EMetaText::GENERAL_TXT, 379); // %d %s perishes
+				line.replaceNumber(kills);
 
 				if(multiple)
-					line.addReplacement(MetaString::GENERAL_TXT, 43); // creatures
+					line.replaceLocalString(EMetaText::GENERAL_TXT, 43); // creatures
 				else
 					firstTarget->addNameReplacement(line, true);
 			}
 			else // single creature killed
 			{
-				line.addTxt(MetaString::GENERAL_TXT, 378); // one %s perishes
+				line.appendLocalString(EMetaText::GENERAL_TXT, 378); // one %s perishes
 				if(multiple)
-					line.addReplacement(MetaString::GENERAL_TXT, 42); // creature
+					line.replaceLocalString(EMetaText::GENERAL_TXT, 42); // creature
 				else
 					firstTarget->addNameReplacement(line, false);
 			}

+ 1 - 1
lib/spells/effects/Dispel.cpp

@@ -43,7 +43,7 @@ void Dispel::apply(ServerCallback * server, const Mechanics * m, const EffectTar
 			if(describe && positive && !negative && !neutral)
 			{
 				MetaString line;
-				unit->addText(line, MetaString::GENERAL_TXT, -555, true);
+				unit->addText(line, EMetaText::GENERAL_TXT, -555, true);
 				unit->addNameReplacement(line, true);
 				blm.lines.push_back(std::move(line));
 			}

+ 4 - 4
lib/spells/effects/Heal.cpp

@@ -119,19 +119,19 @@ void Heal::prepareHealEffect(int64_t value, BattleUnitsChanged & pack, BattleLog
 				// %d %s rise from the dead!
 				// in the table first comes plural string, then the singular one
 				MetaString resurrectText;
-				state->addText(resurrectText, MetaString::GENERAL_TXT, 116, resurrectedCount == 1);
+				state->addText(resurrectText, EMetaText::GENERAL_TXT, 116, resurrectedCount == 1);
 				state->addNameReplacement(resurrectText);
-				resurrectText.addReplacement(resurrectedCount);
+				resurrectText.replaceNumber(resurrectedCount);
 				logMessage.lines.push_back(std::move(resurrectText));
 			}
 			else if (unitHPgained > 0 && m->caster->getHeroCaster() == nullptr) //Show text about healed HP if healed by unit
 			{
 				MetaString healText;
 				auto casterUnit = dynamic_cast<const battle::Unit*>(m->caster);
-				healText.addTxt(MetaString::GENERAL_TXT, 414);
+				healText.appendLocalString(EMetaText::GENERAL_TXT, 414);
 				casterUnit->addNameReplacement(healText, false);
 				state->addNameReplacement(healText, false);
-				healText.addReplacement((int)unitHPgained);
+				healText.replaceNumber((int)unitHPgained);
 				logMessage.lines.push_back(std::move(healText));
 			}
 

+ 2 - 2
lib/spells/effects/Obstacle.cpp

@@ -259,8 +259,8 @@ bool Obstacle::isHexAvailable(const CBattleInfoCallback * cb, const BattleHex &
 bool Obstacle::noRoomToPlace(Problem & problem, const Mechanics * m)
 {
 	MetaString text;
-	text.addTxt(MetaString::GENERAL_TXT, 181);//No room to place %s here
-	text.addReplacement(m->getSpellName());
+	text.appendLocalString(EMetaText::GENERAL_TXT, 181);//No room to place %s here
+	text.replaceRawString(m->getSpellName());
 	problem.add(std::move(text));
 	return false;
 }

+ 5 - 5
lib/spells/effects/Summon.cpp

@@ -58,19 +58,19 @@ bool Summon::applicable(Problem & problem, const Mechanics * m) const
 			const auto *elemental = otherSummoned.front();
 
 			MetaString text;
-			text.addTxt(MetaString::GENERAL_TXT, 538);
+			text.appendLocalString(EMetaText::GENERAL_TXT, 538);
 
 			const auto *caster = dynamic_cast<const CGHeroInstance *>(m->caster);
 			if(caster)
 			{
-				text.addReplacement(caster->getNameTranslated());
+				text.replaceRawString(caster->getNameTranslated());
 
-				text.addReplacement(MetaString::CRE_PL_NAMES, elemental->creatureIndex());
+				text.replaceLocalString(EMetaText::CRE_PL_NAMES, elemental->creatureIndex());
 
 				if(caster->type->gender == EHeroGender::FEMALE)
-					text.addReplacement(MetaString::GENERAL_TXT, 540);
+					text.replaceLocalString(EMetaText::GENERAL_TXT, 540);
 				else
-					text.addReplacement(MetaString::GENERAL_TXT, 539);
+					text.replaceLocalString(EMetaText::GENERAL_TXT, 539);
 
 			}
 			problem.add(std::move(text), Problem::NORMAL);

+ 3 - 3
lib/spells/effects/Timed.cpp

@@ -30,7 +30,7 @@ static void describeEffect(std::vector<MetaString> & log, const Mechanics * m, c
 	auto addLogLine = [&](const int32_t baseTextID, const boost::logic::tribool & plural)
 	{
 		MetaString line;
-		target->addText(line, MetaString::GENERAL_TXT, baseTextID, plural);
+		target->addText(line, EMetaText::GENERAL_TXT, baseTextID, plural);
 		target->addNameReplacement(line, plural);
 		log.push_back(std::move(line));
 	};
@@ -78,10 +78,10 @@ static void describeEffect(std::vector<MetaString> & log, const Mechanics * m, c
 
 					//"The %s shrivel with age, and lose %d hit points."
 					MetaString line;
-					target->addText(line, MetaString::GENERAL_TXT, 551);
+					target->addText(line, EMetaText::GENERAL_TXT, 551);
 					target->addNameReplacement(line);
 
-					line.addReplacement(oldHealth - newHealth);
+					line.replaceNumber(oldHealth - newHealth);
 					log.push_back(std::move(line));
 					return;
 				}

+ 1 - 1
lib/spells/effects/Timed.h

@@ -16,7 +16,7 @@ VCMI_LIB_NAMESPACE_BEGIN
 
 struct Bonus;
 struct SetStackEffect;
-struct MetaString;
+class MetaString;
 
 namespace spells
 {

+ 49 - 6
mapeditor/Animation.cpp

@@ -539,7 +539,6 @@ bool Animation::loadFrame(size_t frame, size_t group)
 				return true;
 			}
 		}
-		return false;
 		// still here? image is missing
 
 		printError(frame, group, "LoadFrame");
@@ -547,7 +546,14 @@ bool Animation::loadFrame(size_t frame, size_t group)
 	}
 	else //load from separate file
 	{
-		images[group][frame] = getFromExtraDef(source[group][frame]["file"].String());;
+		auto img = getFromExtraDef(source[group][frame]["file"].String());
+		if(!img)
+		{
+			auto bitmap = BitmapHandler::loadBitmap(source[group][frame]["file"].String());
+			img.reset(new QImage(bitmap));
+		}
+
+		images[group][frame] = img;
 		return true;
 	}
 	return false;
@@ -577,7 +583,6 @@ void Animation::init()
 			source[defEntry.first].resize(defEntry.second);
 	}
 
-#if 0 //this code is not used but maybe requred if there will be configurable sprites
 	ResourceID resID(std::string("SPRITES/") + name, EResType::TEXT);
 
 	//if(vstd::contains(graphics->imageLists, resID.getName()))
@@ -593,9 +598,47 @@ void Animation::init()
 
 		const JsonNode config((char*)textData.get(), stream->getSize());
 
-		//initFromJson(config);
+		initFromJson(config);
+	}
+}
+
+void Animation::initFromJson(const JsonNode & config)
+{
+	std::string basepath;
+	basepath = config["basepath"].String();
+
+	JsonNode base(JsonNode::JsonType::DATA_STRUCT);
+	base["margins"] = config["margins"];
+	base["width"] = config["width"];
+	base["height"] = config["height"];
+
+	for(const JsonNode & group : config["sequences"].Vector())
+	{
+		size_t groupID = group["group"].Integer();//TODO: string-to-value conversion("moving" -> MOVING)
+		source[groupID].clear();
+
+		for(const JsonNode & frame : group["frames"].Vector())
+		{
+			JsonNode toAdd(JsonNode::JsonType::DATA_STRUCT);
+			JsonUtils::inherit(toAdd, base);
+			toAdd["file"].String() = basepath + frame.String();
+			source[groupID].push_back(toAdd);
+		}
+	}
+
+	for(const JsonNode & node : config["images"].Vector())
+	{
+		size_t group = node["group"].Integer();
+		size_t frame = node["frame"].Integer();
+
+		if (source[group].size() <= frame)
+			source[group].resize(frame+1);
+
+		JsonNode toAdd(JsonNode::JsonType::DATA_STRUCT);
+		JsonUtils::inherit(toAdd, base);
+		toAdd["file"].String() = basepath + node["file"].String();
+		source[group][frame] = toAdd;
 	}
-#endif
 }
 
 void Animation::printError(size_t frame, size_t group, std::string type) const
@@ -808,4 +851,4 @@ void Animation::createFlippedGroup(const size_t sourceGroup, const size_t target
 		auto image = getImage(frame, targetGroup);
 		*image = image->transformed(QTransform::fromScale(1, -1));
 	}
-}
+}

+ 1 - 1
mapeditor/Animation.h

@@ -44,7 +44,7 @@ private:
 	bool unloadFrame(size_t frame, size_t group);
 
 	//initialize animation from file
-	//void initFromJson(const JsonNode & input);
+	void initFromJson(const JsonNode & input);
 	void init();
 
 	//to get rid of copy-pasting error message :]

+ 1 - 0
mapeditor/BitmapHandler.cpp

@@ -131,6 +131,7 @@ namespace BitmapHandler
 						c = qRgb(qRed(c), qGreen(c), qBlue(c));
 					image.setColorTable(colorTable);
 				}
+				return image;
 			}
 			else
 			{

+ 1 - 0
mapeditor/inspector/inspector.h

@@ -15,6 +15,7 @@
 #include <QStyledItemDelegate>
 #include "../lib/int3.h"
 #include "../lib/GameConstants.h"
+#include "../lib/mapObjects/CGCreature.h"
 #include "../lib/mapObjects/MapObjects.h"
 #include "../lib/ResourceSet.h"
 

+ 35 - 33
mapeditor/mapsettings.cpp

@@ -19,7 +19,7 @@
 #include "../lib/CGeneralTextHandler.h"
 #include "../lib/CModHandler.h"
 #include "../lib/mapObjects/CGHeroInstance.h"
-#include "../lib/mapObjects/MiscObjects.h"
+#include "../lib/mapObjects/CGCreature.h"
 #include "../lib/mapping/CMapService.h"
 #include "../lib/StringConstants.h"
 #include "inspector/townbulidingswidget.h" //to convert BuildingID to string
@@ -166,8 +166,8 @@ MapSettings::MapSettings(MapController & ctrl, QWidget *parent) :
 	};
 	
 	//victory & loss messages
-	ui->victoryMessageEdit->setText(QString::fromStdString(controller.map()->victoryMessage));
-	ui->defeatMessageEdit->setText(QString::fromStdString(controller.map()->defeatMessage));
+	ui->victoryMessageEdit->setText(QString::fromStdString(controller.map()->victoryMessage.toString()));
+	ui->defeatMessageEdit->setText(QString::fromStdString(controller.map()->defeatMessage.toString()));
 	
 	//victory & loss conditions
 	const std::array<std::string, 8> conditionStringsWin = {
@@ -550,8 +550,8 @@ void MapSettings::on_pushButton_clicked()
 	
 	//victory & loss messages
 	
-	controller.map()->victoryMessage = ui->victoryMessageEdit->text().toStdString();
-	controller.map()->defeatMessage = ui->defeatMessageEdit->text().toStdString();
+	controller.map()->victoryMessage = MetaString::createFromRawString(ui->victoryMessageEdit->text().toStdString());
+	controller.map()->defeatMessage = MetaString::createFromRawString(ui->defeatMessageEdit->text().toStdString());
 	
 	//victory & loss conditions
 	EventCondition victoryCondition(EventCondition::STANDARD_WIN);
@@ -561,19 +561,19 @@ void MapSettings::on_pushButton_clicked()
 	//Victory condition - defeat all
 	TriggeredEvent standardVictory;
 	standardVictory.effect.type = EventEffect::VICTORY;
-	standardVictory.effect.toOtherMessage = VLC->generaltexth->allTexts[5];
+	standardVictory.effect.toOtherMessage.appendTextID("core.genrltxt.5");
 	standardVictory.identifier = "standardVictory";
 	standardVictory.description.clear(); // TODO: display in quest window
-	standardVictory.onFulfill = VLC->generaltexth->allTexts[659];
+	standardVictory.onFulfill.appendTextID("core.genrltxt.659");
 	standardVictory.trigger = EventExpression(victoryCondition);
 
 	//Loss condition - 7 days without town
 	TriggeredEvent standardDefeat;
 	standardDefeat.effect.type = EventEffect::DEFEAT;
-	standardDefeat.effect.toOtherMessage = VLC->generaltexth->allTexts[8];
+	standardDefeat.effect.toOtherMessage.appendTextID("core.genrltxt.8");
 	standardDefeat.identifier = "standardDefeat";
 	standardDefeat.description.clear(); // TODO: display in quest window
-	standardDefeat.onFulfill = VLC->generaltexth->allTexts[7];
+	standardDefeat.onFulfill.appendTextID("core.genrltxt.7");
 	standardDefeat.trigger = EventExpression(defeatCondition);
 	
 	controller.map()->triggeredEvents.clear();
@@ -583,7 +583,7 @@ void MapSettings::on_pushButton_clicked()
 	{
 		controller.map()->triggeredEvents.push_back(standardVictory);
 		controller.map()->victoryIconIndex = 11;
-		controller.map()->victoryMessage = VLC->generaltexth->victoryConditions[0];
+		controller.map()->victoryMessage.appendTextID(VLC->generaltexth->victoryConditions[0]);
 	}
 	else
 	{
@@ -595,7 +595,7 @@ void MapSettings::on_pushButton_clicked()
 		specialVictory.description.clear(); // TODO: display in quest window
 		
 		controller.map()->victoryIconIndex = vicCondition;
-		controller.map()->victoryMessage = VLC->generaltexth->victoryConditions[size_t(vicCondition) + 1];
+		controller.map()->victoryMessage.appendTextID(VLC->generaltexth->victoryConditions[size_t(vicCondition) + 1]);
 		
 		switch(vicCondition)
 		{
@@ -603,8 +603,8 @@ void MapSettings::on_pushButton_clicked()
 				EventCondition cond(EventCondition::HAVE_ARTIFACT);
 				assert(victoryTypeWidget);
 				cond.objectType = victoryTypeWidget->currentData().toInt();
-				specialVictory.effect.toOtherMessage = VLC->generaltexth->allTexts[281];
-				specialVictory.onFulfill = VLC->generaltexth->allTexts[280];
+				specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.281");
+				specialVictory.onFulfill.appendTextID("core.genrltxt.280");
 				specialVictory.trigger = EventExpression(cond);
 				break;
 			}
@@ -614,8 +614,8 @@ void MapSettings::on_pushButton_clicked()
 				assert(victoryTypeWidget);
 				cond.objectType = victoryTypeWidget->currentData().toInt();
 				cond.value = victoryValueWidget->text().toInt();
-				specialVictory.effect.toOtherMessage = VLC->generaltexth->allTexts[277];
-				specialVictory.onFulfill = VLC->generaltexth->allTexts[276];
+				specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.277");
+				specialVictory.onFulfill.appendTextID("core.genrltxt.276");
 				specialVictory.trigger = EventExpression(cond);
 				break;
 			}
@@ -625,8 +625,8 @@ void MapSettings::on_pushButton_clicked()
 				assert(victoryTypeWidget);
 				cond.objectType = victoryTypeWidget->currentData().toInt();
 				cond.value = victoryValueWidget->text().toInt();
-				specialVictory.effect.toOtherMessage = VLC->generaltexth->allTexts[279];
-				specialVictory.onFulfill = VLC->generaltexth->allTexts[278];
+				specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.279");
+				specialVictory.onFulfill.appendTextID("core.genrltxt.278");
 				specialVictory.trigger = EventExpression(cond);
 				break;
 			}
@@ -638,8 +638,8 @@ void MapSettings::on_pushButton_clicked()
 				int townIdx = victorySelectWidget->currentData().toInt();
 				if(townIdx > -1)
 					cond.position = controller.map()->objects[townIdx]->pos;
-				specialVictory.effect.toOtherMessage = VLC->generaltexth->allTexts[283];
-				specialVictory.onFulfill = VLC->generaltexth->allTexts[282];
+				specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.283");
+				specialVictory.onFulfill.appendTextID("core.genrltxt.282");
 				specialVictory.trigger = EventExpression(cond);
 				break;
 			}
@@ -650,8 +650,8 @@ void MapSettings::on_pushButton_clicked()
 				cond.objectType = Obj::TOWN;
 				int townIdx = victoryTypeWidget->currentData().toInt();
 				cond.position = controller.map()->objects[townIdx]->pos;
-				specialVictory.effect.toOtherMessage = VLC->generaltexth->allTexts[250];
-				specialVictory.onFulfill = VLC->generaltexth->allTexts[249];
+				specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.250");
+				specialVictory.onFulfill.appendTextID("core.genrltxt.249");
 				specialVictory.trigger = EventExpression(cond);
 				break;
 			}
@@ -662,8 +662,8 @@ void MapSettings::on_pushButton_clicked()
 				cond.objectType = Obj::HERO;
 				int heroIdx = victoryTypeWidget->currentData().toInt();
 				cond.position = controller.map()->objects[heroIdx]->pos;
-				specialVictory.effect.toOtherMessage = VLC->generaltexth->allTexts[253];
-				specialVictory.onFulfill = VLC->generaltexth->allTexts[252];
+				specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.253");
+				specialVictory.onFulfill.appendTextID("core.genrltxt.252");
 				specialVictory.trigger = EventExpression(cond);
 				break;
 			}
@@ -675,8 +675,8 @@ void MapSettings::on_pushButton_clicked()
 				int townIdx = victorySelectWidget->currentData().toInt();
 				if(townIdx > -1)
 					cond.position = controller.map()->objects[townIdx]->pos;
-				specialVictory.effect.toOtherMessage = VLC->generaltexth->allTexts[293];
-				specialVictory.onFulfill = VLC->generaltexth->allTexts[292];
+				specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.293");
+				specialVictory.onFulfill.appendTextID("core.genrltxt.292");
 				specialVictory.trigger = EventExpression(cond);
 				break;
 			}
@@ -697,8 +697,8 @@ void MapSettings::on_pushButton_clicked()
 		// if normal victory allowed - add one more quest
 		if(ui->standardVictoryCheck->isChecked())
 		{
-			controller.map()->victoryMessage += " / ";
-			controller.map()->victoryMessage += VLC->generaltexth->victoryConditions[0];
+			controller.map()->victoryMessage.appendRawString(" / ");
+			controller.map()->victoryMessage.appendTextID(VLC->generaltexth->victoryConditions[0]);
 			controller.map()->triggeredEvents.push_back(standardVictory);
 		}
 		controller.map()->triggeredEvents.push_back(specialVictory);
@@ -709,7 +709,7 @@ void MapSettings::on_pushButton_clicked()
 	{
 		controller.map()->triggeredEvents.push_back(standardDefeat);
 		controller.map()->defeatIconIndex = 3;
-		controller.map()->defeatMessage = VLC->generaltexth->lossCondtions[0];
+		controller.map()->defeatMessage.appendTextID("core.lcdesc.0");
 	}
 	else
 	{
@@ -721,7 +721,6 @@ void MapSettings::on_pushButton_clicked()
 		specialDefeat.description.clear(); // TODO: display in quest window
 		
 		controller.map()->defeatIconIndex = lossCondition;
-		controller.map()->defeatMessage = VLC->generaltexth->lossCondtions[size_t(lossCondition) + 1];
 		
 		switch(lossCondition)
 		{
@@ -733,8 +732,9 @@ void MapSettings::on_pushButton_clicked()
 				int townIdx = loseTypeWidget->currentData().toInt();
 				cond.position = controller.map()->objects[townIdx]->pos;
 				noneOf.expressions.push_back(cond);
-				specialDefeat.onFulfill = VLC->generaltexth->allTexts[251];
+				specialDefeat.onFulfill.appendTextID("core.genrltxt.251");
 				specialDefeat.trigger = EventExpression(noneOf);
+				controller.map()->defeatMessage.appendTextID("core.lcdesc.1");
 				break;
 			}
 				
@@ -746,8 +746,9 @@ void MapSettings::on_pushButton_clicked()
 				int townIdx = loseTypeWidget->currentData().toInt();
 				cond.position = controller.map()->objects[townIdx]->pos;
 				noneOf.expressions.push_back(cond);
-				specialDefeat.onFulfill = VLC->generaltexth->allTexts[253];
+				specialDefeat.onFulfill.appendTextID("core.genrltxt.253");
 				specialDefeat.trigger = EventExpression(noneOf);
+				controller.map()->defeatMessage.appendTextID("core.lcdesc.2");
 				break;
 			}
 				
@@ -755,8 +756,9 @@ void MapSettings::on_pushButton_clicked()
 				EventCondition cond(EventCondition::DAYS_PASSED);
 				assert(loseValueWidget);
 				cond.value = expiredDate(loseValueWidget->text());
-				specialDefeat.onFulfill = VLC->generaltexth->allTexts[254];
+				specialDefeat.onFulfill.appendTextID("core.genrltxt.254");
 				specialDefeat.trigger = EventExpression(cond);
+				controller.map()->defeatMessage.appendTextID("core.lcdesc.3");
 				break;
 			}
 				
@@ -764,7 +766,7 @@ void MapSettings::on_pushButton_clicked()
 				EventCondition cond(EventCondition::DAYS_WITHOUT_TOWN);
 				assert(loseValueWidget);
 				cond.value = loseValueWidget->text().toInt();
-				specialDefeat.onFulfill = VLC->generaltexth->allTexts[7];
+				specialDefeat.onFulfill.appendTextID("core.genrltxt.7");
 				specialDefeat.trigger = EventExpression(cond);
 				break;
 			}

Alguns ficheiros não foram mostrados porque muitos ficheiros mudaram neste diff