浏览代码

Merge pull request #6136 from Laserlicht/resource

Configurable resources
Ivan Savenko 2 月之前
父节点
当前提交
c469c6c31e
共有 96 个文件被更改,包括 673 次插入313 次删除
  1. 2 1
      AI/Nullkiller/AIUtility.cpp
  2. 4 2
      AI/Nullkiller/Engine/PriorityEvaluator.cpp
  3. 3 2
      AI/Nullkiller/Goals/AbstractGoal.cpp
  4. 3 2
      AI/VCAI/Goals/AbstractGoal.cpp
  5. 2 1
      AI/VCAI/Goals/CollectRes.cpp
  6. 5 3
      client/adventureMap/AdventureMapWidget.cpp
  7. 15 0
      client/adventureMap/CResDataBar.cpp
  8. 1 0
      client/adventureMap/CResDataBar.h
  9. 2 3
      client/lobby/OptionsTab.cpp
  10. 14 13
      client/mainmenu/CStatisticScreen.cpp
  11. 0 1
      client/mapView/MapViewCache.cpp
  12. 2 0
      client/renderSDL/RenderHandler.cpp
  13. 1 1
      client/widgets/MiscWidgets.cpp
  14. 2 1
      client/widgets/markets/TradePanels.cpp
  15. 6 4
      client/windows/CCastleInterface.cpp
  16. 2 2
      client/windows/GUIClasses.cpp
  17. 1 1
      client/windows/GUIClasses.h
  18. 10 10
      config/difficulty.json
  19. 4 0
      config/gameConfig.json
  20. 34 2
      config/resources.json
  21. 4 11
      config/schemas/creature.json
  22. 3 10
      config/schemas/flaggable.json
  23. 5 1
      config/schemas/mod.json
  24. 44 0
      config/schemas/resources.json
  25. 2 9
      config/schemas/template.json
  26. 8 22
      config/schemas/townBuilding.json
  27. 1 1
      docs/modders/Difficulty.md
  28. 20 0
      docs/modders/Entities_Format/Resource_Format.md
  29. 0 1
      docs/modders/Game_Identifiers.md
  30. 1 1
      docs/modders/Mod_File_Format.md
  31. 0 4
      docs/players/Game_Mechanics.md
  32. 25 0
      include/vcmi/ResourceType.h
  33. 6 8
      include/vcmi/ResourceTypeService.h
  34. 2 0
      include/vcmi/Services.h
  35. 1 0
      launcher/modManager/modstateitemmodel_moc.cpp
  36. 2 2
      lib/CCreatureHandler.cpp
  37. 4 2
      lib/CMakeLists.txt
  38. 1 1
      lib/CPlayerState.cpp
  39. 7 2
      lib/GameLibrary.cpp
  40. 3 3
      lib/GameLibrary.h
  41. 30 15
      lib/ResourceSet.cpp
  42. 26 3
      lib/ResourceSet.h
  43. 14 16
      lib/constants/EntityIdentifiers.cpp
  44. 5 2
      lib/constants/EntityIdentifiers.h
  45. 1 1
      lib/constants/NumericConstants.h
  46. 1 1
      lib/constants/StringConstants.h
  47. 83 0
      lib/entities/ResourceTypeHandler.cpp
  48. 66 0
      lib/entities/ResourceTypeHandler.h
  49. 3 6
      lib/entities/faction/CTownHandler.cpp
  50. 1 1
      lib/gameState/CGameState.cpp
  51. 10 9
      lib/gameState/GameStatistics.cpp
  52. 4 3
      lib/json/JsonRandom.cpp
  53. 2 1
      lib/mapObjectConstructors/CommonConstructors.cpp
  54. 1 1
      lib/mapObjectConstructors/FlaggableInstanceConstructor.cpp
  55. 2 1
      lib/mapObjects/CGCreature.cpp
  56. 2 1
      lib/mapObjects/CGHeroInstance.cpp
  57. 1 1
      lib/mapObjects/CGObjectInstance.cpp
  58. 3 2
      lib/mapObjects/CGResource.cpp
  59. 2 1
      lib/mapObjects/CGTownInstance.cpp
  60. 0 31
      lib/mapObjects/CObjectHandler.cpp
  61. 5 6
      lib/mapObjects/CQuest.cpp
  62. 7 7
      lib/mapObjects/IMarket.cpp
  63. 0 2
      lib/mapObjects/MapObjects.h
  64. 9 5
      lib/mapObjects/MiscObjects.cpp
  65. 2 0
      lib/modding/ContentTypeHandler.cpp
  66. 2 1
      lib/rewardable/Info.cpp
  67. 4 8
      lib/rmg/CRmgTemplate.cpp
  68. 4 4
      lib/rmg/CRmgTemplate.h
  69. 10 0
      lib/serializer/JsonDeserializer.cpp
  70. 1 0
      lib/serializer/JsonDeserializer.h
  71. 30 0
      lib/serializer/JsonSerializeFormat.h
  72. 11 0
      lib/serializer/JsonSerializer.cpp
  73. 1 0
      lib/serializer/JsonSerializer.h
  74. 5 0
      lib/serializer/JsonUpdater.cpp
  75. 1 0
      lib/serializer/JsonUpdater.h
  76. 3 2
      lib/texts/MetaString.cpp
  77. 2 0
      mapeditor/graphics.cpp
  78. 4 5
      mapeditor/inspector/questwidget.cpp
  79. 5 6
      mapeditor/inspector/rewardswidget.cpp
  80. 7 5
      mapeditor/inspector/towneventdialog.cpp
  81. 7 3
      mapeditor/mapsettings/eventsettings.cpp
  82. 7 5
      mapeditor/mapsettings/timedevent.cpp
  83. 4 2
      mapeditor/mapsettings/victoryconditions.cpp
  84. 4 2
      mapeditor/templateeditor/graphicelements/CardItem.cpp
  85. 11 9
      mapeditor/templateeditor/mineselector.cpp
  86. 3 3
      mapeditor/templateeditor/mineselector.h
  87. 0 2
      scripting/lua/api/ObjectInstance.h
  88. 1 1
      scripting/lua/api/netpacks/SetResources.cpp
  89. 1 1
      scripts/lib/erm/MA.lua
  90. 2 1
      server/CGameHandler.cpp
  91. 3 2
      server/CVCMIServer.cpp
  92. 5 4
      server/processors/NewTurnProcessor.cpp
  93. 2 1
      server/processors/PlayerMessageProcessor.cpp
  94. 0 2
      test/erm/ERM_OB_T.cpp
  95. 1 0
      test/mock/mock_Services.h
  96. 0 1
      test/vcai/ResourceManagerTest.cpp

+ 2 - 1
AI/Nullkiller/AIUtility.cpp

@@ -15,6 +15,7 @@
 #include "../../lib/UnlockGuard.h"
 #include "../../lib/CConfigHandler.h"
 #include "../../lib/entities/artifact/CArtifact.h"
+#include "../../lib/entities/ResourceTypeHandler.h"
 #include "../../lib/mapObjects/MapObjects.h"
 #include "../../lib/mapObjects/CQuest.h"
 #include "../../lib/mapping/TerrainTile.h"
@@ -470,7 +471,7 @@ int32_t getArtifactBonusScoreImpl(const std::shared_ptr<Bonus> & bonus)
 		case BonusType::UNDEAD_RAISE_PERCENTAGE:
 			return bonus->val * 400;
 		case BonusType::GENERATE_RESOURCE:
-			return bonus->val * LIBRARY->objh->resVals.at(bonus->subtype.as<GameResID>().getNum()) * 10;
+			return bonus->val * bonus->subtype.as<GameResID>().toResource()->getPrice() * 10;
 		case BonusType::SPELL_DURATION:
 			return bonus->val * 200;
 		case BonusType::MAGIC_RESISTANCE:

+ 4 - 2
AI/Nullkiller/Engine/PriorityEvaluator.cpp

@@ -12,6 +12,7 @@
 
 #include "Nullkiller.h"
 #include "../../../lib/entities/artifact/CArtifact.h"
+#include "../../../lib/entities/ResourceTypeHandler.h"
 #include "../../../lib/mapObjectConstructors/AObjectTypeHandler.h"
 #include "../../../lib/mapObjectConstructors/CObjectClassesHandler.h"
 #include "../../../lib/mapObjects/CGResource.h"
@@ -22,6 +23,7 @@
 #include "../../../lib/StartInfo.h"
 #include "../../../lib/GameSettings.h"
 #include "../../../lib/filesystem/Filesystem.h"
+#include "../../../lib/entities/ResourceTypeHandler.h"
 #include "../Goals/ExecuteHeroChain.h"
 #include "../Goals/BuildThis.h"
 #include "../Goals/StayAtTown.h"
@@ -136,7 +138,7 @@ int32_t getResourcesGoldReward(const TResources & res)
 {
 	int32_t result = 0;
 
-	for(auto r : GameResID::ALL_RESOURCES())
+	for(auto r : LIBRARY->resourceTypeHandler->getAllObjects())
 	{
 		if(res[r] > 0)
 			result += r == EGameResID::GOLD ? res[r] : res[r] * 100;
@@ -1589,7 +1591,7 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
 						needed.positive();
 						int turnsTo = needed.maxPurchasableCount(income);
 						bool haveEverythingButGold = true;
-						for (int i = 0; i < GameConstants::RESOURCE_QUANTITY; i++)
+						for (auto & i : LIBRARY->resourceTypeHandler->getAllObjects())
 						{
 							if (i != GameResID::GOLD && resourcesAvailable[i] < evaluationContext.buildingCost[i])
 								haveEverythingButGold = false;

+ 3 - 2
AI/Nullkiller/Goals/AbstractGoal.cpp

@@ -12,6 +12,7 @@
 #include "../AIGateway.h"
 #include "../../../lib/constants/StringConstants.h"
 #include "../../../lib/entities/artifact/CArtifact.h"
+#include "../../../lib/entities/ResourceTypeHandler.h"
 
 namespace NKAI
 {
@@ -43,13 +44,13 @@ std::string AbstractGoal::toString() const
 	switch(goalType)
 	{
 	case COLLECT_RES:
-		desc = "COLLECT RESOURCE " + GameConstants::RESOURCE_NAMES[resID] + " (" + std::to_string(value) + ")";
+		desc = "COLLECT RESOURCE " + GameResID(resID).toResource()->getJsonKey() + " (" + std::to_string(value) + ")";
 		break;
 	case TRADE:
 	{
 		auto obj = cb->getObjInstance(ObjectInstanceID(objid));
 		if (obj)
-			desc = (boost::format("TRADE %d of %s at %s") % value % GameConstants::RESOURCE_NAMES[resID] % obj->getObjectName()).str();
+			desc = (boost::format("TRADE %d of %s at %s") % value % GameResID(resID).toResource()->getJsonKey() % obj->getObjectName()).str();
 	}
 	break;
 	case GATHER_TROOPS:

+ 3 - 2
AI/VCAI/Goals/AbstractGoal.cpp

@@ -16,6 +16,7 @@
 #include "../BuildingManager.h"
 #include "../../../lib/constants/StringConstants.h"
 #include "../../../lib/entities/artifact/CArtifact.h"
+#include "../../../lib/entities/ResourceTypeHandler.h"
 
 using namespace Goals;
 
@@ -56,13 +57,13 @@ std::string AbstractGoal::name() const //TODO: virtualize
 	case BUILD_STRUCTURE:
 		return "BUILD STRUCTURE";
 	case COLLECT_RES:
-		desc = "COLLECT RESOURCE " + GameConstants::RESOURCE_NAMES[resID] + " (" + std::to_string(value) + ")";
+		desc = "COLLECT RESOURCE " + GameResID(resID).toResource()->getJsonKey() + " (" + std::to_string(value) + ")";
 		break;
 	case TRADE:
 	{
 		auto obj = cb->getObjInstance(ObjectInstanceID(objid));
 		if (obj)
-			desc = (boost::format("TRADE %d of %s at %s") % value % GameConstants::RESOURCE_NAMES[resID] % obj->getObjectName()).str();
+			desc = (boost::format("TRADE %d of %s at %s") % value % GameResID(resID).toResource()->getJsonKey() % obj->getObjectName()).str();
 	}
 	break;
 	case GATHER_TROOPS:

+ 2 - 1
AI/VCAI/Goals/CollectRes.cpp

@@ -18,6 +18,7 @@
 #include "../../../lib/mapObjects/CGMarket.h"
 #include "../../../lib/mapObjects/CGResource.h"
 #include "../../../lib/constants/StringConstants.h"
+#include "../../../lib/entities/ResourceTypeHandler.h"
 
 using namespace Goals;
 
@@ -171,7 +172,7 @@ TSubgoal CollectRes::whatToDoToTrade()
 		const IMarket * m = markets.back();
 		//attempt trade at back (best prices)
 		int howManyCanWeBuy = 0;
-		for (GameResID i = EGameResID::WOOD; i <= EGameResID::GOLD; ++i)
+		for (auto & i : LIBRARY->resourceTypeHandler->getAllObjects())
 		{
 			if (i.getNum() == resID)
 				continue;

+ 5 - 3
client/adventureMap/AdventureMapWidget.cpp

@@ -31,10 +31,12 @@
 #include "../CPlayerInterface.h"
 #include "../PlayerLocalState.h"
 
+#include "../../lib/GameLibrary.h"
 #include "../../lib/callback/CCallback.h"
 #include "../../lib/constants/StringConstants.h"
 #include "../../lib/mapping/CMapHeader.h"
 #include "../../lib/filesystem/ResourcePath.h"
+#include "../../lib/entities/ResourceTypeHandler.h"
 
 AdventureMapWidget::AdventureMapWidget( std::shared_ptr<AdventureMapShortcuts> shortcuts )
 	: shortcuts(shortcuts)
@@ -294,14 +296,14 @@ std::shared_ptr<CIntObject> AdventureMapWidget::buildResourceDateBar(const JsonN
 
 	auto result = std::make_shared<CResDataBar>(image, area.topLeft());
 
-	for(auto i = 0; i < GameConstants::RESOURCE_QUANTITY; i++)
+	for (auto & i : LIBRARY->resourceTypeHandler->getAllObjects())
 	{
-		const auto & node = input[GameConstants::RESOURCE_NAMES[i]];
+		const auto & node = input[i.toResource()->getJsonKey()];
 
 		if(node.isNull())
 			continue;
 
-		result->setResourcePosition(GameResID(i), Point(node["x"].Integer(), node["y"].Integer()));
+		result->setResourcePosition(i, Point(node["x"].Integer(), node["y"].Integer()));
 	}
 
 	result->setDatePosition(Point(input["date"]["x"].Integer(), input["date"]["y"].Integer()));

+ 15 - 0
client/adventureMap/CResDataBar.cpp

@@ -18,15 +18,21 @@
 #include "../GameInstance.h"
 #include "../gui/TextAlignment.h"
 #include "../widgets/Images.h"
+#include "../widgets/CComponent.h"
+#include "../windows/InfoWindows.h"
 
 #include "../../lib/CConfigHandler.h"
 #include "../../lib/callback/CCallback.h"
 #include "../../lib/texts/CGeneralTextHandler.h"
 #include "../../lib/ResourceSet.h"
 #include "../../lib/GameLibrary.h"
+#include "../../lib/entities/ResourceTypeHandler.h"
+#include "../../lib/networkPacks/Component.h"
 
 CResDataBar::CResDataBar(const ImagePath & imageName, const Point & position)
 {
+	addUsedEvents(SHOW_POPUP);
+
 	pos.x += position.x;
 	pos.y += position.y;
 
@@ -89,3 +95,12 @@ void CResDataBar::setPlayerColor(PlayerColor player)
 {
 	background->setPlayerColor(player);
 }
+
+void CResDataBar::showPopupWindow(const Point & cursorPosition)
+{
+	std::vector<std::shared_ptr<CComponent>> comp;
+	for (auto & i : LIBRARY->resourceTypeHandler->getAllObjects())
+		comp.push_back(std::make_shared<CComponent>(ComponentType::RESOURCE, i, GAME->interface()->cb->getResourceAmount(i)));
+	
+	CRClickPopup::createAndPush(LIBRARY->generaltexth->translate("core.genrltxt.270"), comp);
+}

+ 1 - 0
client/adventureMap/CResDataBar.h

@@ -35,6 +35,7 @@ public:
 	void setResourcePosition(const GameResID & resource, const Point & position);
 
 	void setPlayerColor(PlayerColor player);
+	void showPopupWindow(const Point & cursorPosition) override;
 	void showAll(Canvas & to) override;
 };
 

+ 2 - 3
client/lobby/OptionsTab.cpp

@@ -41,6 +41,7 @@
 #include "../../lib/entities/faction/CTownHandler.h"
 #include "../../lib/entities/hero/CHeroHandler.h"
 #include "../../lib/entities/hero/CHeroClass.h"
+#include "../../lib/entities/ResourceTypeHandler.h"
 #include "../../lib/filesystem/Filesystem.h"
 #include "../../lib/networkPacks/PacksForLobby.h"
 #include "../../lib/texts/CGeneralTextHandler.h"
@@ -161,8 +162,6 @@ size_t OptionsTab::CPlayerSettingsHelper::getImageIndex(bool big)
 				return GEM;
 			case EGameResID::GOLD:
 				return GOLD;
-			case EGameResID::MITHRIL:
-				return MITHRIL;
 			}
 		}
 		}
@@ -1057,7 +1056,7 @@ OptionsTab::PlayerOptionsEntry::PlayerOptionsEntry(const PlayerSettings & S, con
 		{
 			auto str = MetaString::createFromTextID("vcmi.lobby.handicap");
 			str.appendRawString(":\n");
-			for(auto & res : EGameResID::ALL_RESOURCES())
+			for(auto & res : LIBRARY->resourceTypeHandler->getAllObjects())
 				if(s->handicap.startBonus[res] != 0)
 				{
 					str.appendRawString("\n");

+ 14 - 13
client/mainmenu/CStatisticScreen.cpp

@@ -28,6 +28,7 @@
 #include "../windows/InfoWindows.h"
 #include "../widgets/Slider.h"
 
+#include "../../lib/entities/ResourceTypeHandler.h"
 #include "../../lib/gameState/GameStatistics.h"
 #include "../../lib/gameState/CGameState.h"
 #include "../../lib/texts/CGeneralTextHandler.h"
@@ -76,10 +77,10 @@ void CStatisticScreen::onSelectButton()
 		else
 		{
 			auto content = static_cast<Content>(selectedIndex);
-			auto possibleRes = std::vector<EGameResID>{EGameResID::GOLD, EGameResID::WOOD, EGameResID::MERCURY, EGameResID::ORE, EGameResID::SULFUR, EGameResID::CRYSTAL, EGameResID::GEMS};
+			auto possibleRes = LIBRARY->resourceTypeHandler->getAllObjects();
 			std::vector<std::string> resourceText;
 			for(const auto & res : possibleRes)
-				resourceText.emplace_back(LIBRARY->generaltexth->translate(TextIdentifier("core.restypes", res.getNum()).get()));
+				resourceText.emplace_back(res.toResource()->getNameTranslated());
 			
 			ENGINE->windows().createAndPushWindow<StatisticSelector>(resourceText, [this, content, possibleRes](int index)
 			{
@@ -169,7 +170,7 @@ std::shared_ptr<CIntObject> CStatisticScreen::getContent(Content c, EGameResID r
 	
 	case CHART_RESOURCES:
 		plotData = extractData(statistic, [res](const StatisticDataSetEntry & val) -> float { return val.resources[res]; });
-		return std::make_shared<LineChart>(contentArea.resize(-5), LIBRARY->generaltexth->translate(std::get<0>(contentInfo[c])) + " - " + LIBRARY->generaltexth->translate(TextIdentifier("core.restypes", res.getNum()).get()), plotData, icons, 0);
+		return std::make_shared<LineChart>(contentArea.resize(-5), LIBRARY->generaltexth->translate(std::get<0>(contentInfo[c])) + " - " + res.toResource()->getNameTranslated(), plotData, icons, 0);
 	
 	case CHART_INCOME:
 		plotData = extractData(statistic, [](const StatisticDataSetEntry & val) -> float { return val.income; });
@@ -193,7 +194,7 @@ std::shared_ptr<CIntObject> CStatisticScreen::getContent(Content c, EGameResID r
 	
 	case CHART_NUMBER_OF_MINES:
 		plotData = extractData(statistic, [res](StatisticDataSetEntry val) -> float { return val.numMines[res]; });
-		return std::make_shared<LineChart>(contentArea.resize(-5), LIBRARY->generaltexth->translate(std::get<0>(contentInfo[c])) + " - " + LIBRARY->generaltexth->translate(TextIdentifier("core.restypes", res.getNum()).get()), plotData, icons, 0);
+		return std::make_shared<LineChart>(contentArea.resize(-5), LIBRARY->generaltexth->translate(std::get<0>(contentInfo[c])) + " - " + res.toResource()->getNameTranslated(), plotData, icons, 0);
 	
 	case CHART_ARMY_STRENGTH:
 		plotData = extractData(statistic, [](const StatisticDataSetEntry & val) -> float { return val.armyStrength; });
@@ -205,11 +206,11 @@ std::shared_ptr<CIntObject> CStatisticScreen::getContent(Content c, EGameResID r
 	
 	case CHART_RESOURCES_SPENT_ARMY:
 		plotData = extractData(statistic, [res](const StatisticDataSetEntry & val) -> float { return val.spentResourcesForArmy[res]; });
-		return std::make_shared<LineChart>(contentArea.resize(-5), LIBRARY->generaltexth->translate(std::get<0>(contentInfo[c])) + " - " + LIBRARY->generaltexth->translate(TextIdentifier("core.restypes", res.getNum()).get()), plotData, icons, 0);
+		return std::make_shared<LineChart>(contentArea.resize(-5), LIBRARY->generaltexth->translate(std::get<0>(contentInfo[c])) + " - " + res.toResource()->getNameTranslated(), plotData, icons, 0);
 	
 	case CHART_RESOURCES_SPENT_BUILDINGS:
 		plotData = extractData(statistic, [res](const StatisticDataSetEntry & val) -> float { return val.spentResourcesForBuildings[res]; });
-		return std::make_shared<LineChart>(contentArea.resize(-5), LIBRARY->generaltexth->translate(std::get<0>(contentInfo[c])) + " - " + LIBRARY->generaltexth->translate(TextIdentifier("core.restypes", res.getNum()).get()), plotData, icons, 0);
+		return std::make_shared<LineChart>(contentArea.resize(-5), LIBRARY->generaltexth->translate(std::get<0>(contentInfo[c])) + " - " + res.toResource()->getNameTranslated(), plotData, icons, 0);
 	
 	case CHART_MAP_EXPLORED:
 		plotData = extractData(statistic, [](const StatisticDataSetEntry & val) -> float { return val.mapExploredRatio; });
@@ -330,43 +331,43 @@ OverviewPanel::OverviewPanel(Rect position, std::string title, const StatisticDa
 			}
 		},
 		{
-			LIBRARY->generaltexth->translate("vcmi.statisticWindow.param.tradeVolume") + " - " + LIBRARY->generaltexth->translate(TextIdentifier("core.restypes", EGameResID::GOLD).get()), [this](PlayerColor color){
+			LIBRARY->generaltexth->translate("vcmi.statisticWindow.param.tradeVolume") + " - " + GameResID(EGameResID::GOLD).toResource()->getNameTranslated(), [this](PlayerColor color){
 				auto val = playerDataFilter(color).back();
 				return std::to_string(val.tradeVolume[EGameResID::GOLD]);
 			}
 		},
 		{
-			LIBRARY->generaltexth->translate("vcmi.statisticWindow.param.tradeVolume") + " - " + LIBRARY->generaltexth->translate(TextIdentifier("core.restypes", EGameResID::WOOD).get()), [this](PlayerColor color){
+			LIBRARY->generaltexth->translate("vcmi.statisticWindow.param.tradeVolume") + " - " + GameResID(EGameResID::WOOD).toResource()->getNameTranslated(), [this](PlayerColor color){
 				auto val = playerDataFilter(color).back();
 				return std::to_string(val.tradeVolume[EGameResID::WOOD]);
 			}
 		},
 		{
-			LIBRARY->generaltexth->translate("vcmi.statisticWindow.param.tradeVolume") + " - " + LIBRARY->generaltexth->translate(TextIdentifier("core.restypes", EGameResID::MERCURY).get()), [this](PlayerColor color){
+			LIBRARY->generaltexth->translate("vcmi.statisticWindow.param.tradeVolume") + " - " + GameResID(EGameResID::MERCURY).toResource()->getNameTranslated(), [this](PlayerColor color){
 				auto val = playerDataFilter(color).back();
 				return std::to_string(val.tradeVolume[EGameResID::MERCURY]);
 			}
 		},
 		{
-			LIBRARY->generaltexth->translate("vcmi.statisticWindow.param.tradeVolume") + " - " + LIBRARY->generaltexth->translate(TextIdentifier("core.restypes", EGameResID::ORE).get()), [this](PlayerColor color){
+			LIBRARY->generaltexth->translate("vcmi.statisticWindow.param.tradeVolume") + " - " + GameResID(EGameResID::ORE).toResource()->getNameTranslated(), [this](PlayerColor color){
 				auto val = playerDataFilter(color).back();
 				return std::to_string(val.tradeVolume[EGameResID::ORE]);
 			}
 		},
 		{
-			LIBRARY->generaltexth->translate("vcmi.statisticWindow.param.tradeVolume") + " - " + LIBRARY->generaltexth->translate(TextIdentifier("core.restypes", EGameResID::SULFUR).get()), [this](PlayerColor color){
+			LIBRARY->generaltexth->translate("vcmi.statisticWindow.param.tradeVolume") + " - " + GameResID(EGameResID::SULFUR).toResource()->getNameTranslated(), [this](PlayerColor color){
 				auto val = playerDataFilter(color).back();
 				return std::to_string(val.tradeVolume[EGameResID::SULFUR]);
 			}
 		},
 		{
-			LIBRARY->generaltexth->translate("vcmi.statisticWindow.param.tradeVolume") + " - " + LIBRARY->generaltexth->translate(TextIdentifier("core.restypes", EGameResID::CRYSTAL).get()), [this](PlayerColor color){
+			LIBRARY->generaltexth->translate("vcmi.statisticWindow.param.tradeVolume") + " - " + GameResID(EGameResID::CRYSTAL).toResource()->getNameTranslated(), [this](PlayerColor color){
 				auto val = playerDataFilter(color).back();
 				return std::to_string(val.tradeVolume[EGameResID::CRYSTAL]);
 			}
 		},
 		{
-			LIBRARY->generaltexth->translate("vcmi.statisticWindow.param.tradeVolume") + " - " + LIBRARY->generaltexth->translate(TextIdentifier("core.restypes", EGameResID::GEMS).get()), [this](PlayerColor color){
+			LIBRARY->generaltexth->translate("vcmi.statisticWindow.param.tradeVolume") + " - " + GameResID(EGameResID::GEMS).toResource()->getNameTranslated(), [this](PlayerColor color){
 				auto val = playerDataFilter(color).back();
 				return std::to_string(val.tradeVolume[EGameResID::GEMS]);
 			}

+ 0 - 1
client/mapView/MapViewCache.cpp

@@ -25,7 +25,6 @@
 #include "../GameEngine.h"
 #include "../widgets/TextControls.h"
 
-#include "../../lib/mapObjects/CObjectHandler.h"
 #include "../../lib/int3.h"
 
 MapViewCache::~MapViewCache() = default;

+ 2 - 0
client/renderSDL/RenderHandler.cpp

@@ -43,6 +43,7 @@
 #include <vcmi/Services.h>
 #include <vcmi/SkillService.h>
 #include <vcmi/spells/Service.h>
+#include <vcmi/ResourceTypeService.h>
 
 RenderHandler::RenderHandler()
 	:assetGenerator(std::make_unique<AssetGenerator>())
@@ -494,6 +495,7 @@ void RenderHandler::onLibraryLoadingFinished(const Services * services)
 	addImageListEntries(services->factions());
 	addImageListEntries(services->spells());
 	addImageListEntries(services->skills());
+	addImageListEntries(services->resources());
 
 	if (settings["mods"]["validation"].String() == "full")
 	{

+ 1 - 1
client/widgets/MiscWidgets.cpp

@@ -227,7 +227,7 @@ void CMinorResDataBar::showAll(Canvas & to)
 {
 	CIntObject::showAll(to);
 
-	for (GameResID i=EGameResID::WOOD; i<=EGameResID::GOLD; ++i)
+	for (GameResID i=EGameResID::WOOD; i<=EGameResID::GOLD; ++i) //todo: configurable resource support
 	{
 		std::string text = std::to_string(GAME->interface()->cb->getResourceAmount(i));
 

+ 2 - 1
client/widgets/markets/TradePanels.cpp

@@ -23,6 +23,7 @@
 #include "../../../lib/entities/artifact/CArtHandler.h"
 #include "../../../lib/texts/CGeneralTextHandler.h"
 #include "../../../lib/mapObjects/CGHeroInstance.h"
+#include "../../../lib/entities/ResourceTypeHandler.h"
 
 CTradeableItem::CTradeableItem(const Rect & area, EType Type, int32_t ID, int32_t serial)
 	: SelectableSlot(area, Point(1, 1))
@@ -175,7 +176,7 @@ void CTradeableItem::hover(bool on)
 			ENGINE->statusbar()->write(LIBRARY->artifacts()->getByIndex(id)->getNameTranslated());
 		break;
 	case EType::RESOURCE:
-		ENGINE->statusbar()->write(LIBRARY->generaltexth->restypes[id]);
+		ENGINE->statusbar()->write(GameResID(id).toResource()->getNameTranslated());
 		break;
 	case EType::PLAYER:
 		ENGINE->statusbar()->write(LIBRARY->generaltexth->capColors[id]);

+ 6 - 4
client/windows/CCastleInterface.cpp

@@ -58,6 +58,7 @@
 #include "../../lib/campaign/CampaignState.h"
 #include "../../lib/entities/artifact/CArtifact.h"
 #include "../../lib/entities/building/CBuilding.h"
+#include "../../lib/entities/ResourceTypeHandler.h"
 #include "../../lib/mapObjects/CGHeroInstance.h"
 #include "../../lib/mapObjects/CGTownInstance.h"
 #include "../../lib/mapObjects/TownBuildingInstance.h"
@@ -292,7 +293,7 @@ CDwellingInfoBox::CDwellingInfoBox(int centerX, int centerY, const CGTownInstanc
 	available = std::make_shared<CLabel>(80,190, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, LIBRARY->generaltexth->allTexts[217] + text);
 	costPerTroop = std::make_shared<CLabel>(80, 227, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, LIBRARY->generaltexth->allTexts[346]);
 
-	for(int i = 0; i<GameConstants::RESOURCE_QUANTITY; i++)
+	for (auto & i : LIBRARY->resourceTypeHandler->getAllObjects())
 	{
 		auto res = static_cast<EGameResID>(i);
 		if(creature->getRecruitCost(res))
@@ -1126,7 +1127,7 @@ void CCastleBuildings::enterFountain(const BuildingID & building, BuildingSubID:
 		else //Mystic Pond produced something;
 		{
 			descr += "\n\n" + hasProduced;
-			boost::algorithm::replace_first(descr,"%s",LIBRARY->generaltexth->restypes[town->bonusValue.first]);
+			boost::algorithm::replace_first(descr,"%s",GameResID(town->bonusValue.first).toResource()->getNameTranslated());
 			boost::algorithm::replace_first(descr,"%d",std::to_string(town->bonusValue.second));
 		}
 	}
@@ -1795,7 +1796,7 @@ CBuildWindow::CBuildWindow(const CGTownInstance *Town, const CBuilding * Buildin
 	//Create components for all required resources
 	std::vector<std::shared_ptr<CComponent>> components;
 
-	for(GameResID i : GameResID::ALL_RESOURCES())
+	for(GameResID i : LIBRARY->resourceTypeHandler->getAllObjects())
 	{
 		if(building->resources[i])
 		{
@@ -2211,7 +2212,8 @@ void CMageGuildScreen::Scroll::clickPressed(const Point & cursorPosition)
 			return;
 		}
 
-		auto costBase = TResources(GAME->interface()->cb->getSettings().getValue(EGameSettings::TOWNS_SPELL_RESEARCH_COST).Vector()[level]);
+		ResourceSet costBase;
+		costBase.resolveFromJson(GAME->interface()->cb->getSettings().getValue(EGameSettings::TOWNS_SPELL_RESEARCH_COST).Vector()[level]);
 		auto costExponent = GAME->interface()->cb->getSettings().getValue(EGameSettings::TOWNS_SPELL_RESEARCH_COST_EXPONENT_PER_RESEARCH).Vector()[level].Float();
 		auto cost = costBase * std::pow(town->spellResearchAcceptedCounter + 1, costExponent);
 

+ 2 - 2
client/windows/GUIClasses.cpp

@@ -46,6 +46,7 @@
 #include "../lib/entities/building/CBuilding.h"
 #include "../lib/entities/faction/CTownHandler.h"
 #include "../lib/entities/hero/CHeroHandler.h"
+#include "../lib/entities/ResourceTypeHandler.h"
 #include "../lib/mapObjectConstructors/CObjectClassesHandler.h"
 #include "../lib/mapObjectConstructors/CommonConstructors.h"
 #include "../lib/mapObjects/CGHeroInstance.h"
@@ -830,7 +831,7 @@ CShipyardWindow::CShipyardWindow(const TResources & cost, int state, BoatId boat
 	build = std::make_shared<CButton>(Point(42, 312), AnimationPath::builtin("IBUY30"), CButton::tooltip(LIBRARY->generaltexth->allTexts[598]), std::bind(&CShipyardWindow::close, this), EShortcut::GLOBAL_ACCEPT);
 	build->addCallback(onBuy);
 
-	for(GameResID i = EGameResID::WOOD; i <= EGameResID::GOLD; ++i)
+	for(auto & i : LIBRARY->resourceTypeHandler->getAllObjects())
 	{
 		if(cost[i] > GAME->interface()->cb->getResourceAmount(i))
 		{
@@ -1215,7 +1216,6 @@ void CHillFortWindow::updateGarrisons()
 
 	for(int i=0; i<slotsCount; i++)
 	{
-		std::fill(costs[i].begin(), costs[i].end(), 0);
 		State newState = getState(SlotID(i));
 		if(newState != State::EMPTY)
 		{

+ 1 - 1
client/windows/GUIClasses.h

@@ -464,7 +464,7 @@ private:
 
 	enum class State { UNAFFORDABLE, ALREADY_UPGRADED, MAKE_UPGRADE, EMPTY, UNAVAILABLE };
 	static constexpr std::size_t slotsCount = 7;
-	//todo: mithril support
+	//todo: configurable resource support
 	static constexpr std::size_t resCount = 7;
 
 	const CGObjectInstance * fort;

+ 10 - 10
config/difficulty.json

@@ -4,31 +4,31 @@
 	{
 		"pawn":
 		{
-			"resources": { "wood" : 30, "mercury": 15, "ore": 30, "sulfur": 15, "crystal": 15, "gems": 15, "gold": 30000, "mithril": 0 },
+			"resources": { "wood" : 30, "mercury": 15, "ore": 30, "sulfur": 15, "crystal": 15, "gems": 15, "gold": 30000 },
 			"globalBonuses": [],
 			"battleBonuses": []
 		},
 		"knight":
 		{
-			"resources": { "wood" : 20, "mercury": 10, "ore": 20, "sulfur": 10, "crystal": 10, "gems": 10, "gold": 20000, "mithril": 0 },
+			"resources": { "wood" : 20, "mercury": 10, "ore": 20, "sulfur": 10, "crystal": 10, "gems": 10, "gold": 20000 },
 			"globalBonuses": [],
 			"battleBonuses": []
 		},
 		"rook":
 		{
-			"resources": { "wood" : 15, "mercury": 7, "ore": 15, "sulfur": 7, "crystal": 7, "gems": 7, "gold": 15000, "mithril": 0 },
+			"resources": { "wood" : 15, "mercury": 7, "ore": 15, "sulfur": 7, "crystal": 7, "gems": 7, "gold": 15000 },
 			"globalBonuses": [],
 			"battleBonuses": []
 		},
 		"queen":
 		{
-			"resources": { "wood" : 10, "mercury": 4, "ore": 10, "sulfur": 4, "crystal": 4, "gems": 4, "gold": 10000, "mithril": 0 },
+			"resources": { "wood" : 10, "mercury": 4, "ore": 10, "sulfur": 4, "crystal": 4, "gems": 4, "gold": 10000 },
 			"globalBonuses": [],
 			"battleBonuses": []
 		},
 		"king":
 		{
-			"resources": { "wood" : 0, "mercury": 0, "ore": 0, "sulfur": 0, "crystal": 0, "gems": 0, "gold": 0, "mithril": 0 },
+			"resources": { "wood" : 0, "mercury": 0, "ore": 0, "sulfur": 0, "crystal": 0, "gems": 0, "gold": 0 },
 			"globalBonuses": [],
 			"battleBonuses": []
 		}
@@ -37,31 +37,31 @@
 	{
 		"pawn":
 		{
-			"resources": { "wood" : 5, "mercury": 2, "ore": 5, "sulfur": 2, "crystal": 2, "gems": 2, "gold": 5000, "mithril": 0 },
+			"resources": { "wood" : 5, "mercury": 2, "ore": 5, "sulfur": 2, "crystal": 2, "gems": 2, "gold": 5000 },
 			"globalBonuses": [],
 			"battleBonuses": []
 		},
 		"knight":
 		{
-			"resources": { "wood" : 10, "mercury": 4, "ore": 10, "sulfur": 4, "crystal": 4, "gems": 4, "gold": 7500, "mithril": 0 },
+			"resources": { "wood" : 10, "mercury": 4, "ore": 10, "sulfur": 4, "crystal": 4, "gems": 4, "gold": 7500 },
 			"globalBonuses": [],
 			"battleBonuses": []
 		},
 		"rook":
 		{
-			"resources": { "wood" : 15, "mercury": 7, "ore": 15, "sulfur": 7, "crystal": 7, "gems": 7, "gold": 10000, "mithril": 0 },
+			"resources": { "wood" : 15, "mercury": 7, "ore": 15, "sulfur": 7, "crystal": 7, "gems": 7, "gold": 10000 },
 			"globalBonuses": [],
 			"battleBonuses": []
 		},
 		"queen":
 		{
-			"resources": { "wood" : 15, "mercury": 7, "ore": 15, "sulfur": 7, "crystal": 7, "gems": 7, "gold": 10000, "mithril": 0 },
+			"resources": { "wood" : 15, "mercury": 7, "ore": 15, "sulfur": 7, "crystal": 7, "gems": 7, "gold": 10000 },
 			"globalBonuses": [],
 			"battleBonuses": []
 		},
 		"king":
 		{
-			"resources": { "wood" : 15, "mercury": 7, "ore": 15, "sulfur": 7, "crystal": 7, "gems": 7, "gold": 10000, "mithril": 0 },
+			"resources": { "wood" : 15, "mercury": 7, "ore": 15, "sulfur": 7, "crystal": 7, "gems": 7, "gold": 10000 },
 			"globalBonuses": [],
 			"battleBonuses": []
 		}

+ 4 - 0
config/gameConfig.json

@@ -111,6 +111,10 @@
 	[
 		"config/terrains.json"
 	],
+	"resources" :
+	[
+		"config/resources.json"
+	],
 	"roads":
 	[
 		"config/roads.json"

+ 34 - 2
config/resources.json

@@ -1,4 +1,36 @@
 {
-	// Price of each resource in gold, in usual resource order
-	"resources_prices": [ 250, 500, 250, 500, 500, 500, 1, 0 ]
+	"wood": {
+		"index" : 0,
+		"price": 250
+	},
+	
+	"mercury": {
+		"index" : 1,
+		"price": 500
+	},
+	
+	"ore": {
+		"index" : 2,
+		"price": 250
+	},
+	
+	"sulfur": {
+		"index" : 3,
+		"price": 500
+	},
+	
+	"crystal": {
+		"index" : 4,
+		"price": 500
+	},
+	
+	"gems": {
+		"index" : 5,
+		"price": 500
+	},
+	
+	"gold": {
+		"index" : 6,
+		"price": 1
+	}
 }

+ 4 - 11
config/schemas/creature.json

@@ -93,17 +93,10 @@
 		},
 		"cost" : {
 			"type" : "object",
-			"additionalProperties" : false,
-			"description" : "Cost to recruit this creature",
-			"properties" : {
-				"gold" :    { "type" : "number"},
-				"wood" :    { "type" : "number"},
-				"ore" :     { "type" : "number"},
-				"mercury" : { "type" : "number"},
-				"sulfur" :  { "type" : "number"},
-				"crystal" : { "type" : "number"},
-				"gems" :    { "type" : "number"}
-			}
+			"additionalProperties" : {
+				"type" : "number"
+			},
+			"description" : "Cost to recruit this creature"
 		},
 		"speed" :     { "type" : "number" },
 		"hitPoints" : { "type" : "number" },

+ 3 - 10
config/schemas/flaggable.json

@@ -35,17 +35,10 @@
 
 		"dailyIncome" : {
 			"type" : "object",
-			"additionalProperties" : false,
+			"additionalProperties" : {
+				"type" : "number"
+			},
 			"description" : "Daily income that this building provides to owner, if any",
-			"properties" : {
-				"gold" :    { "type" : "number"},
-				"wood" :    { "type" : "number"},
-				"ore" :     { "type" : "number"},
-				"mercury" : { "type" : "number"},
-				"sulfur" :  { "type" : "number"},
-				"crystal" : { "type" : "number"},
-				"gems" :    { "type" : "number"}
-			}
 		},
 
 		// Properties that might appear since this node is shared with object config

+ 5 - 1
config/schemas/mod.json

@@ -65,7 +65,7 @@
 		},
 		"modType" : {
 			"type" : "string",
-			"enum" : [ "Translation", "Town", "Test", "Templates", "Spells", "Music", "Maps", "Sounds", "Skills", "Other", "Objects", "Mechanics", "Interface", "Heroes", "Graphical", "Expansion", "Creatures", "Compatibility", "Campaigns", "Artifacts", "AI" ],
+			"enum" : [ "Translation", "Town", "Test", "Templates", "Spells", "Music", "Maps", "Sounds", "Skills", "Other", "Objects", "Mechanics", "Interface", "Heroes", "Graphical", "Expansion", "Creatures", "Compatibility", "Campaigns", "Artifacts", "AI", "Resources" ],
 			"description" : "Type of mod, e.g. Town, Artifacts, Graphical."
 		},
 		"author" : {
@@ -303,6 +303,10 @@
 			"description" : "List of configuration files for terrains",
 			"$ref" : "#/definitions/fileListOrObject"
 		},
+		"resources" : {
+			"description" : "List of configuration files for resources",
+			"$ref" : "#/definitions/fileListOrObject"
+		},
 		"roads" : {
 			"description" : "List of configuration files for roads",
 			"$ref" : "#/definitions/fileListOrObject"

+ 44 - 0
config/schemas/resources.json

@@ -0,0 +1,44 @@
+{
+	"type" : "object",
+	"$schema" : "http://json-schema.org/draft-04/schema",
+	"title" : "VCMI resources format",
+	"description" : "Format used to define new resources in VCMI",
+	"required" : [ "name", "price" ],
+	"additionalProperties" : false,
+	"properties" : {
+		"index" : {
+			"type" : "number",
+			"description" : "numeric id of h3 resource, prohibited for new resources"
+		},
+		"name" : {
+			"type" : "string",
+			"description" : "Localizable name of this resource"
+		},
+		"images" : {
+			"type" : "object",
+			"description" : "Resource icons of varying size",
+			"required" : [ "small", "medium", "large"],
+			"properties" : {
+				"small" : {
+					"type" : "string",
+					"description" : "20x18 resource icon",
+					"format" : "imageFile"
+				},
+				"medium" : {
+					"type" : "string",
+					"description" : "32x32 resource icon",
+					"format" : "imageFile"
+				},
+				"large" : {
+					"type" : "string",
+					"description" : "82x93 resource icon",
+					"format" : "imageFile"
+				}
+			}
+		},
+		"price" : {
+			"type" : "number",
+			"description" : "Price of resource in gold"
+		}
+	}
+}

+ 2 - 9
config/schemas/template.json

@@ -139,15 +139,8 @@
 		},
 		"mines" : {
 			"type" : "object",
-			"additionalProperties" : false,
-			"properties" : {
-				"gold" :    { "type" : "number"},
-				"wood" :    { "type" : "number"},
-				"ore" :     { "type" : "number"},
-				"mercury" : { "type" : "number"},
-				"sulfur" :  { "type" : "number"},
-				"crystal" : { "type" : "number"},
-				"gems" :    { "type" : "number"}
+			"additionalProperties" : {
+				"type" : "number"
 			}
 		},
 		"connection" :

+ 8 - 22
config/schemas/townBuilding.json

@@ -86,31 +86,17 @@
 		},
 		"cost" : {
 			"type" : "object",
-			"additionalProperties" : false,
-			"description" : "Resources needed to build building",
-			"properties" : {
-				"gold" :    { "type" : "number"},
-				"wood" :    { "type" : "number"},
-				"ore" :     { "type" : "number"},
-				"mercury" : { "type" : "number"},
-				"sulfur" :  { "type" : "number"},
-				"crystal" : { "type" : "number"},
-				"gems" :    { "type" : "number"}
-			}
+			"additionalProperties" : {
+				"type" : "number"
+			},
+			"description" : "Resources needed to build building"
 		},
 		"produce" : {
 			"type" : "object",
-			"additionalProperties" : false,
-			"description" : "Resources produced each day by this building",
-			"properties" : {
-				"gold" :    { "type" : "number"},
-				"wood" :    { "type" : "number"},
-				"ore" :     { "type" : "number"},
-				"mercury" : { "type" : "number"},
-				"sulfur" :  { "type" : "number"},
-				"crystal" : { "type" : "number"},
-				"gems" :    { "type" : "number"}
-			}
+			"additionalProperties" : {
+				"type" : "number"
+			},
+			"description" : "Resources produced each day by this building"
 		},
 		"warMachine" : {
 			"type" : "string",

+ 1 - 1
docs/modders/Difficulty.md

@@ -14,7 +14,7 @@ Difficulty configuration is located in [config/difficulty.json](../../config/dif
 		"pawn": //parameters for specific difficulty
 		{
 			//starting resources
-			"resources": { "wood" : 30, "mercury": 15, "ore": 30, "sulfur": 15, "crystal": 15, "gems": 15, "gold": 30000, "mithril": 0 },
+			"resources": { "wood" : 30, "mercury": 15, "ore": 30, "sulfur": 15, "crystal": 15, "gems": 15, "gold": 30000 },
 			//bonuses will be given to player globally
 			"globalBonuses": [],
 			//bonuses will be given to player every battle

+ 20 - 0
docs/modders/Entities_Format/Resource_Format.md

@@ -0,0 +1,20 @@
+# Resource Format
+
+```json
+	// Internal field for H3 resources. Do not use for mods
+	"index" : "",
+
+	// displayed name of the resource
+	"name" : "",
+	
+	// Resource icons of varying size
+	"images" : {
+		// 20x18 resource icon
+		"small" : "",
+		// 32x32 resource icon
+		"medium" : "",
+		// 82x93 resource icon
+		"large" : ""
+	}
+
+```

+ 0 - 1
docs/modders/Game_Identifiers.md

@@ -568,7 +568,6 @@ Deprecated, please use primarySkill instead
 - resource.gems
 - resource.gold
 - resource.mercury
-- resource.mithril
 - resource.ore
 - resource.sulfur
 - resource.wood

+ 1 - 1
docs/modders/Mod_File_Format.md

@@ -31,7 +31,7 @@
 
 	// Type of mod, list of all possible values:
 	// "Translation", "Town", "Test", "Templates", "Spells", "Music", "Maps", "Sounds", "Skills", "Other", "Objects", 
-	// "Mechanics", "Interface", "Heroes", "Graphical", "Expansion", "Creatures", "Compatibility", "Campaigns", "Artifacts", "AI"
+	// "Mechanics", "Interface", "Heroes", "Graphical", "Expansion", "Creatures", "Compatibility", "Campaigns", "Artifacts", "AI", "Resources"
 	//
 	// Some mod types have additional effects on your mod:
 	// Translation: mod of this type is only active if player uses base language of this mod. See "language" property. 

+ 0 - 4
docs/players/Game_Mechanics.md

@@ -41,10 +41,6 @@ Stack experience interface has been merged with regular creature window. Among o
 
 VCMI offers native support for Commanders. Commanders are part of WoG mod for VCMI and require it to be enabled. However, once this is done, any new faction can use its own Commander, too.
 
-### Mithril module
-
-VCMI natively supports Mithril resource known from WoG. However, it is not currently used by any mod.
-
 ### Stack Artifact module
 
 In original WoG, there is one available Stack Artifact - Warlord's Banner, which is related directly to stack experience. VCMI natively supports any number of Stack Artifacts regardless if of Stack Experience module is enabled or not. However, currently no mods make use of this feature and it hasn't been tested for many years.

+ 25 - 0
include/vcmi/ResourceType.h

@@ -0,0 +1,25 @@
+/*
+ * ResourceType.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 "Entity.h"
+
+VCMI_LIB_NAMESPACE_BEGIN
+
+class GameResID;
+
+class DLL_LINKAGE ResourceType : public EntityT<GameResID>
+{
+	virtual int getPrice() const = 0;
+};
+
+
+VCMI_LIB_NAMESPACE_END

+ 6 - 8
lib/mapObjects/CObjectHandler.h → include/vcmi/ResourceTypeService.h

@@ -1,5 +1,5 @@
 /*
- * CObjectHandler.h, part of VCMI engine
+ * ResourceTypeService.h, part of VCMI engine
  *
  * Authors: listed in file AUTHORS in main folder
  *
@@ -7,21 +7,19 @@
  * Full text of license available in license.txt file, in main folder
  *
  */
+
 #pragma once
 
-#include "../GameConstants.h"
+#include "EntityService.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 
-class CGObjectInstance;
-class int3;
+class GameResID;
+class ResourceType;
 
-class DLL_LINKAGE CObjectHandler
+class DLL_LINKAGE ResourceTypeService : public EntityServiceT<GameResID, ResourceType>
 {
 public:
-	std::vector<ui32> resVals; //default values of resources in gold
-
-	CObjectHandler();
 };
 
 VCMI_LIB_NAMESPACE_END

+ 2 - 0
include/vcmi/Services.h

@@ -19,6 +19,7 @@ class CreatureService;
 class FactionService;
 class HeroClassService;
 class HeroTypeService;
+class ResourceTypeService;
 class SkillService;
 class JsonNode;
 class BattleFieldService;
@@ -52,6 +53,7 @@ public:
 	virtual const FactionService * factions() const = 0;
 	virtual const HeroClassService * heroClasses() const = 0;
 	virtual const HeroTypeService * heroTypes() const = 0;
+	virtual const ResourceTypeService * resources() const = 0;
 #if SCRIPTING_ENABLED
 	virtual const scripting::Service * scripts() const = 0;
 #endif

+ 1 - 0
launcher/modManager/modstateitemmodel_moc.cpp

@@ -54,6 +54,7 @@ QString ModStateItemModel::modTypeName(QString modTypeID) const
 		QT_TR_NOOP("Campaigns"),
 		QT_TR_NOOP("Artifacts"),
 		QT_TR_NOOP("AI"),
+		QT_TR_NOOP("Resources"),
 	};
 
 	if (modTypes.contains(modTypeID))

+ 2 - 2
lib/CCreatureHandler.cpp

@@ -568,7 +568,7 @@ std::vector<JsonNode> CCreatureHandler::loadLegacyData()
 
 		data["name"]["plural"].String() =  parser.readString();
 
-		for(int v=0; v<7; ++v)
+		for(int v=0; v<GameConstants::RESOURCE_QUANTITY; ++v)
 			data["cost"][GameConstants::RESOURCE_NAMES[v]].Float() = parser.readNumber();
 
 		data["fightValue"].Float() = parser.readNumber();
@@ -623,7 +623,7 @@ std::shared_ptr<CCreature> CCreatureHandler::loadFromJson(const std::string & sc
 	JsonDeserializer handler(nullptr, node);
 	cre->serializeJson(handler);
 
-	cre->cost = ResourceSet(node["cost"]);
+	cre->cost.resolveFromJson(node["cost"]);
 
 	LIBRARY->generaltexth->registerString(scope, cre->getNameSingularTextID(), node["name"]["singular"]);
 	LIBRARY->generaltexth->registerString(scope, cre->getNamePluralTextID(), node["name"]["plural"]);

+ 4 - 2
lib/CMakeLists.txt

@@ -111,6 +111,7 @@ set(lib_MAIN_SRCS
 	entities/hero/CHeroClass.cpp
 	entities/hero/CHeroClassHandler.cpp
 	entities/hero/CHeroHandler.cpp
+	entities/ResourceTypeHandler.cpp
 
 	events/ApplyDamage.cpp
 	events/GameResumed.cpp
@@ -152,7 +153,6 @@ set(lib_MAIN_SRCS
 	mapObjects/CGResource.cpp
 	mapObjects/TownBuildingInstance.cpp
 	mapObjects/CGTownInstance.cpp
-	mapObjects/CObjectHandler.cpp
 	mapObjects/CQuest.cpp
 	mapObjects/CRewardableObject.cpp
 	mapObjects/FlaggableMapObject.cpp
@@ -428,6 +428,8 @@ set(lib_MAIN_HEADERS
 	../include/vcmi/HeroClassService.h
 	../include/vcmi/HeroType.h
 	../include/vcmi/HeroTypeService.h
+	../include/vcmi/ResourceType.h
+	../include/vcmi/ResourceTypeService.h
 	../include/vcmi/Metatype.h
 	../include/vcmi/Player.h
 	../include/vcmi/ServerCallback.h
@@ -534,6 +536,7 @@ set(lib_MAIN_HEADERS
 	entities/hero/CHeroClassHandler.h
 	entities/hero/CHeroHandler.h
 	entities/hero/EHeroGender.h
+	entities/ResourceTypeHandler.h
 
 	events/ApplyDamage.h
 	events/GameResumed.h
@@ -581,7 +584,6 @@ set(lib_MAIN_HEADERS
 	mapObjects/TownBuildingInstance.h
 	mapObjects/CGResource.h
 	mapObjects/CGTownInstance.h
-	mapObjects/CObjectHandler.h
 	mapObjects/CQuest.h
 	mapObjects/CRewardableObject.h
 	mapObjects/FlaggableMapObject.h

+ 1 - 1
lib/CPlayerState.cpp

@@ -98,7 +98,7 @@ const IBonusBearer * PlayerState::getBonusBearer() const
 
 int PlayerState::getResourceAmount(int type) const
 {
-	return vstd::atOrDefault(resources, static_cast<size_t>(type), 0);
+	return resources[type];
 }
 
 template<typename T>

+ 7 - 2
lib/GameLibrary.cpp

@@ -25,6 +25,7 @@
 #include "entities/faction/CTownHandler.h"
 #include "entities/hero/CHeroClassHandler.h"
 #include "entities/hero/CHeroHandler.h"
+#include "entities/ResourceTypeHandler.h"
 #include "texts/CGeneralTextHandler.h"
 #include "campaign/CampaignRegionsHandler.h"
 #include "mapping/MapFormatSettings.h"
@@ -36,7 +37,6 @@
 #include "filesystem/Filesystem.h"
 #include "rmg/CRmgTemplateStorage.h"
 #include "mapObjectConstructors/CObjectClassesHandler.h"
-#include "mapObjects/CObjectHandler.h"
 #include "mapObjects/ObstacleSetHandler.h"
 #include "mapping/CMapEditManager.h"
 #include "ScriptHandler.h"
@@ -75,6 +75,11 @@ const HeroTypeService * GameLibrary::heroTypes() const
 	return heroh.get();
 }
 
+const ResourceTypeService * GameLibrary::resources() const
+{
+	return resourceTypeHandler.get();
+}
+
 #if SCRIPTING_ENABLED
 const scripting::Service * GameLibrary::scripts() const
 {
@@ -171,6 +176,7 @@ void GameLibrary::initializeLibrary()
 
 	createHandler(generaltexth);
 	createHandler(bth);
+	createHandler(resourceTypeHandler);
 	createHandler(roadTypeHandler);
 	createHandler(riverTypeHandler);
 	createHandler(terrainTypeHandler);
@@ -180,7 +186,6 @@ void GameLibrary::initializeLibrary()
 	createHandler(creh);
 	createHandler(townh);
 	createHandler(biomeHandler);
-	createHandler(objh);
 	createHandler(objtypeh);
 	createHandler(spellSchoolHandler);
 	createHandler(spellh);

+ 3 - 3
lib/GameLibrary.h

@@ -20,7 +20,6 @@ class CHeroClassHandler;
 class CCreatureHandler;
 class CSpellHandler;
 class CSkillHandler;
-class CObjectHandler;
 class CObjectClassesHandler;
 class ObstacleSetHandler;
 class CTownHandler;
@@ -31,6 +30,7 @@ class BattleFieldHandler;
 class IBonusTypeHandler;
 class CBonusTypeHandler;
 class TerrainTypeHandler;
+class ResourceTypeHandler;
 class RoadTypeHandler;
 class RiverTypeHandler;
 class ObstacleHandler;
@@ -60,6 +60,7 @@ public:
 	const FactionService * factions() const override;
 	const HeroClassService * heroClasses() const override;
 	const HeroTypeService * heroTypes() const override;
+	const ResourceTypeService * resources() const override;
 #if SCRIPTING_ENABLED
 	const scripting::Service * scripts() const override;
 #endif
@@ -83,13 +84,12 @@ public:
 	std::unique_ptr<CSpellHandler> spellh;
 	std::unique_ptr<SpellSchoolHandler> spellSchoolHandler;
 	std::unique_ptr<CSkillHandler> skillh;
-	// TODO: Remove ObjectHandler altogether?
-	std::unique_ptr<CObjectHandler> objh;
 	std::unique_ptr<CObjectClassesHandler> objtypeh;
 	std::unique_ptr<CTownHandler> townh;
 	std::unique_ptr<CGeneralTextHandler> generaltexth;
 	std::unique_ptr<CModHandler> modh;
 	std::unique_ptr<TerrainTypeHandler> terrainTypeHandler;
+	std::unique_ptr<ResourceTypeHandler> resourceTypeHandler;
 	std::unique_ptr<RoadTypeHandler> roadTypeHandler;
 	std::unique_ptr<RiverTypeHandler> riverTypeHandler;
 	std::unique_ptr<CIdentifierStorage> identifiersHandler;

+ 30 - 15
lib/ResourceSet.cpp

@@ -13,17 +13,34 @@
 #include "ResourceSet.h"
 #include "constants/StringConstants.h"
 #include "serializer/JsonSerializeFormat.h"
-#include "mapObjects/CObjectHandler.h"
+#include "entities/ResourceTypeHandler.h"
 #include "GameLibrary.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 
-ResourceSet::ResourceSet() = default;
+ResourceSet::ResourceSet()
+{
+	resizeContainer();
+};
+
+ResourceSet::ResourceSet(const ResourceSet& rhs)
+	: container(rhs.container) // vector copy constructor
+{
+	resizeContainer();
+}
+
+void ResourceSet::resizeContainer()
+{
+	container.resize(std::max(static_cast<int>(LIBRARY->resourceTypeHandler->getAllObjects().size()), GameConstants::RESOURCE_QUANTITY));
+}
 
-ResourceSet::ResourceSet(const JsonNode & node)
+void ResourceSet::resolveFromJson(const JsonNode & node)
 {
-	for(auto i = 0; i < GameConstants::RESOURCE_QUANTITY; i++)
-		container[i] = static_cast<int>(node[GameConstants::RESOURCE_NAMES[i]].Float());
+	for(auto & n : node.Struct())
+		LIBRARY->identifiers()->requestIdentifier(n.second.getModScope(), "resource", n.first, [n, this](int32_t identifier)
+		{
+			(*this)[identifier] = static_cast<int>(n.second.Float());
+		});
 }
 
 void ResourceSet::serializeJson(JsonSerializeFormat & handler, const std::string & fieldName)
@@ -32,9 +49,8 @@ void ResourceSet::serializeJson(JsonSerializeFormat & handler, const std::string
 		return;
 	auto s = handler.enterStruct(fieldName);
 
-	//TODO: add proper support for mithril to map format
-	for(int idx = 0; idx < GameConstants::RESOURCE_QUANTITY - 1; idx ++)
-		handler.serializeInt(GameConstants::RESOURCE_NAMES[idx], this->operator[](idx), 0);
+	for(auto & idx : LIBRARY->resourceTypeHandler->getAllObjects())
+		handler.serializeInt(idx.toResource()->getJsonKey(), this->operator[](idx), 0);
 }
 
 bool ResourceSet::nonZero() const
@@ -76,8 +92,7 @@ void ResourceSet::applyHandicap(int percentage)
 
 static bool canAfford(const ResourceSet &res, const ResourceSet &price)
 {
-	assert(res.size() == price.size() && price.size() == GameConstants::RESOURCE_QUANTITY);
-	for(int i = 0; i < GameConstants::RESOURCE_QUANTITY; i++)
+	for(auto & i : LIBRARY->resourceTypeHandler->getAllObjects())
 		if(price[i] > res[i])
 			return false;
 
@@ -97,8 +112,8 @@ bool ResourceSet::canAfford(const ResourceSet &price) const
 TResourceCap ResourceSet::marketValue() const
 {
 	TResourceCap total = 0;
-	for(int i = 0; i < GameConstants::RESOURCE_QUANTITY; i++)
-		total += static_cast<TResourceCap>(LIBRARY->objh->resVals[i]) * static_cast<TResourceCap>(operator[](i));
+	for(auto & i : LIBRARY->resourceTypeHandler->getAllObjects())
+		total += static_cast<TResourceCap>(i.toResource()->getPrice()) * static_cast<TResourceCap>(operator[](i));
 	return total;
 }
 
@@ -117,7 +132,7 @@ std::string ResourceSet::toString() const
 
 bool ResourceSet::nziterator::valid() const
 {
-	return cur.resType < GameResID::COUNT && cur.resVal;
+	return static_cast<int>(cur.resType) < LIBRARY->resourceTypeHandler->getAllObjects().size() && cur.resVal;
 }
 
 ResourceSet::nziterator ResourceSet::nziterator::operator++()
@@ -148,9 +163,9 @@ void ResourceSet::nziterator::advance()
 	do
 	{
 		++cur.resType;
-	} while(cur.resType < GameResID::COUNT && !(cur.resVal=rs[cur.resType]));
+	} while(static_cast<int>(cur.resType) < LIBRARY->resourceTypeHandler->getAllObjects().size() && !(cur.resVal=rs[cur.resType]));
 
-	if(cur.resType >= GameResID::COUNT)
+	if(static_cast<int>(cur.resType) >= LIBRARY->resourceTypeHandler->getAllObjects().size())
 		cur.resVal = -1;
 }
 

+ 26 - 3
lib/ResourceSet.h

@@ -26,16 +26,19 @@ class ResourceSet;
 class ResourceSet
 {
 private:
-	std::array<TResource, GameConstants::RESOURCE_QUANTITY> container = {};
+	std::vector<TResource> container = {};
+	DLL_LINKAGE void resizeContainer();
 public:
-	// read resources set from json. Format example: { "gold": 500, "wood":5 }
-	DLL_LINKAGE ResourceSet(const JsonNode & node);
 	DLL_LINKAGE ResourceSet();
+	DLL_LINKAGE ResourceSet(const ResourceSet& rhs);
+
+	DLL_LINKAGE void resolveFromJson(const JsonNode & node);
 
 
 #define scalarOperator(OPSIGN)									\
 	ResourceSet& operator OPSIGN ## =(const TResource &rhs) \
 	{														\
+		resizeContainer(); \
 		for(auto i = 0; i < container.size(); i++)						\
 			container.at(i) OPSIGN ## = rhs;						\
 															\
@@ -45,6 +48,7 @@ public:
 #define vectorOperator(OPSIGN)										\
 	ResourceSet& operator OPSIGN ## =(const ResourceSet &rhs)	\
 	{															\
+		resizeContainer(); \
 		for(auto i = 0; i < container.size(); i++)							\
 			container.at(i) OPSIGN ## = rhs[i];						\
 																\
@@ -84,21 +88,31 @@ public:
 	// Array-like interface
 	TResource & operator[](GameResID index)
 	{
+		resizeContainer();
 		return operator[](index.getNum());
 	}
 
 	const TResource & operator[](GameResID index) const 
 	{
+		if (index.getNum() >= container.size()) {
+			static const TResource defaultValue{};
+			return defaultValue;
+		}
 		return operator[](index.getNum());
 	}
 
 	TResource & operator[](size_t index)
 	{
+		resizeContainer();
 		return container.at(index);
 	}
 
 	const TResource & operator[](size_t index) const 
 	{
+		if (index >= container.size()) {
+			static const TResource defaultValue{};
+			return defaultValue;
+		}
 		return container.at(index);
 	}
 
@@ -176,6 +190,15 @@ public:
 		return *this;
 	}
 
+	ResourceSet& operator=(const ResourceSet& rhs)
+	{
+		if (this != &rhs)
+		{
+			container = rhs.container;
+		}
+		return *this;
+	}
+
 	ResourceSet operator-() const
 	{
 		ResourceSet ret;

+ 14 - 16
lib/constants/EntityIdentifiers.cpp

@@ -21,6 +21,8 @@
 #include <vcmi/HeroTypeService.h>
 #include <vcmi/HeroClass.h>
 #include <vcmi/HeroClassService.h>
+#include <vcmi/ResourceType.h>
+#include <vcmi/ResourceTypeService.h>
 #include <vcmi/Services.h>
 
 #include <vcmi/spells/Spell.h>
@@ -37,6 +39,7 @@
 #include "entities/faction/CFaction.h"
 #include "entities/hero/CHero.h"
 #include "entities/hero/CHeroClass.h"
+#include "entities/ResourceTypeHandler.h"
 #include "mapObjectConstructors/AObjectTypeHandler.h"
 #include "constants/StringConstants.h"
 #include "texts/CGeneralTextHandler.h"
@@ -398,6 +401,16 @@ const HeroType * HeroTypeID::toEntity(const Services * services) const
 	return services->heroTypes()->getByIndex(num);
 }
 
+const Resource * GameResID::toResource() const
+{
+	return dynamic_cast<const Resource*>(toEntity(LIBRARY));
+}
+
+const ResourceType * GameResID::toEntity(const Services * services) const
+{
+	return services->resources()->getByIndex(num);
+}
+
 si32 SpellID::decode(const std::string & identifier)
 {
 	if (identifier == "preset")
@@ -628,7 +641,7 @@ si32 GameResID::decode(const std::string & identifier)
 
 std::string GameResID::encode(const si32 index)
 {
-	return GameConstants::RESOURCE_NAMES[index];
+	return GameResID(index).toResource()->getJsonKey();
 }
 
 si32 BuildingTypeUniqueID::decode(const std::string & identifier)
@@ -676,21 +689,6 @@ const std::array<PrimarySkill, 4> & PrimarySkill::ALL_SKILLS()
 	return allSkills;
 }
 
-const std::array<GameResID, 7> & GameResID::ALL_RESOURCES()
-{
-	static const std::array allResources = {
-		GameResID(WOOD),
-		GameResID(MERCURY),
-		GameResID(ORE),
-		GameResID(SULFUR),
-		GameResID(CRYSTAL),
-		GameResID(GEMS),
-		GameResID(GOLD)
-	};
-
-	return allResources;
-}
-
 std::string SecondarySkill::entityType()
 {
 	return "secondarySkill";

+ 5 - 2
lib/constants/EntityIdentifiers.h

@@ -25,6 +25,9 @@ class CHero;
 class CHeroClass;
 class HeroClass;
 class HeroTypeService;
+class Resource;
+class ResourceType;
+class ResourceTypeService;
 class CFaction;
 class Faction;
 class Skill;
@@ -1061,7 +1064,6 @@ public:
 		CRYSTAL,
 		GEMS,
 		GOLD,
-		MITHRIL,
 		COUNT,
 
 		WOOD_AND_ORE = -4,  // special case for town bonus resource
@@ -1080,7 +1082,8 @@ public:
 	static std::string encode(const si32 index);
 	static std::string entityType();
 
-	static const std::array<GameResID, 7> & ALL_RESOURCES();
+	const Resource * toResource() const;
+	const ResourceType * toEntity(const Services * services) const;
 };
 
 class DLL_LINKAGE BuildingTypeUniqueID : public Identifier<BuildingTypeUniqueID>

+ 1 - 1
lib/constants/NumericConstants.h

@@ -37,7 +37,7 @@ namespace GameConstants
 
 	constexpr int SKILL_QUANTITY=28;
 	constexpr int PRIMARY_SKILLS=4;
-	constexpr int RESOURCE_QUANTITY=8;
+	constexpr int RESOURCE_QUANTITY=7;
 	constexpr int HEROES_PER_TYPE=8; //amount of heroes of each type
 
 	// amounts of OH3 objects. Can be changed by mods, should be used only during H3 loading phase

+ 1 - 1
lib/constants/StringConstants.h

@@ -19,7 +19,7 @@ VCMI_LIB_NAMESPACE_BEGIN
 namespace GameConstants
 {
 	const std::string RESOURCE_NAMES [RESOURCE_QUANTITY] = {
-		"wood", "mercury", "ore", "sulfur", "crystal", "gems", "gold", "mithril"
+		"wood", "mercury", "ore", "sulfur", "crystal", "gems", "gold"
 	};
 
 	const std::string PLAYER_COLOR_NAMES [PlayerColor::PLAYER_LIMIT_I] = {

+ 83 - 0
lib/entities/ResourceTypeHandler.cpp

@@ -0,0 +1,83 @@
+/*
+ * ResourceTypeHandler.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 "ResourceTypeHandler.h"
+
+#include "../GameLibrary.h"
+#include "../json/JsonNode.h"
+#include "../texts/CGeneralTextHandler.h"
+#include "../texts/TextIdentifier.h"
+
+VCMI_LIB_NAMESPACE_BEGIN
+
+std::string Resource::getNameTextID() const
+{
+	if(id.getNum() < GameConstants::RESOURCE_QUANTITY) // OH3 resources
+		return TextIdentifier("core.restypes", id).get();
+	return TextIdentifier( "resources", modScope, identifier, "name" ).get();
+}
+
+std::string Resource::getNameTranslated() const
+{
+	return LIBRARY->generaltexth->translate(getNameTextID());
+}
+
+void Resource::registerIcons(const IconRegistar & cb) const
+{
+	cb(getIconIndex(), 0, "SMALRES", iconSmall);
+	cb(getIconIndex(), 0, "RESOURCE", iconMedium);
+	cb(getIconIndex(), 0, "RESOUR82", iconLarge);
+}
+
+std::vector<JsonNode> ResourceTypeHandler::loadLegacyData()
+{
+	objects.resize(GameConstants::RESOURCE_QUANTITY);
+
+	return std::vector<JsonNode>(GameConstants::RESOURCE_QUANTITY, JsonNode(JsonMap()));
+}
+
+std::shared_ptr<Resource> ResourceTypeHandler::loadFromJson(const std::string & scope, const JsonNode & json, const std::string & identifier, size_t index)
+{
+	auto ret = std::make_shared<Resource>();
+
+	ret->id = GameResID(index);
+	ret->modScope = scope;
+	ret->identifier = identifier;
+
+	ret->price = json["price"].Integer();
+	ret->iconSmall = json["images"]["small"].String();
+	ret->iconMedium = json["images"]["medium"].String();
+	ret->iconLarge = json["images"]["large"].String();
+
+	if(ret->id.getNum() >= GameConstants::RESOURCE_QUANTITY) // not OH3 resources
+		LIBRARY->generaltexth->registerString(scope, ret->getNameTextID(), json["name"]);
+
+	return ret;
+}
+
+const std::vector<std::string> & ResourceTypeHandler::getTypeNames() const
+{
+	static const std::vector<std::string> types = { "resource" };
+	return types;
+}
+
+std::vector<GameResID> ResourceTypeHandler::getAllObjects() const
+{
+	std::vector<GameResID> result;
+
+	for (const auto & resource : objects)
+		if(resource)
+			result.push_back(resource->getId());
+
+	return result;
+}
+
+VCMI_LIB_NAMESPACE_END

+ 66 - 0
lib/entities/ResourceTypeHandler.h

@@ -0,0 +1,66 @@
+/*
+ * ResourceTypeHandler.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 <vcmi/EntityService.h>
+#include <vcmi/Entity.h>
+#include <vcmi/ResourceType.h>
+#include <vcmi/ResourceTypeService.h>
+#include "../constants/EntityIdentifiers.h"
+#include "../IHandlerBase.h"
+#include "../filesystem/ResourcePath.h"
+
+VCMI_LIB_NAMESPACE_BEGIN
+
+class ResourceTypeHandler;
+
+class DLL_LINKAGE Resource : public ResourceType
+{
+	friend class ResourceTypeHandler;
+
+	GameResID id; //backlink
+
+	int price;
+	std::string iconSmall;
+	std::string iconMedium;
+	std::string iconLarge;
+
+	std::string identifier;
+	std::string modScope;
+
+public:
+	int getPrice() const override { return price; }
+
+	std::string getJsonKey() const override { return identifier; }
+	int32_t getIndex() const override { return id.getNum(); }
+	GameResID getId() const override { return id;}
+	int32_t getIconIndex() const override { return id.getNum(); }
+	std::string getModScope() const override { return modScope; };
+	void registerIcons(const IconRegistar & cb) const override;
+	std::string getNameTextID() const override;
+	std::string getNameTranslated() const override;
+};
+
+class DLL_LINKAGE ResourceTypeHandler : public CHandlerBase<GameResID, ResourceType, Resource, ResourceTypeService>
+{
+public:
+	std::shared_ptr<Resource> loadFromJson(const std::string & scope,
+										const JsonNode & json,
+										const std::string & identifier,
+										size_t index) override;
+	
+	const std::vector<std::string> & getTypeNames() const override;
+	std::vector<JsonNode> loadLegacyData() override;
+
+	std::vector<GameResID> getAllObjects() const;
+};
+
+VCMI_LIB_NAMESPACE_END

+ 3 - 6
lib/entities/faction/CTownHandler.cpp

@@ -53,12 +53,9 @@ JsonNode readBuilding(CLegacyConfigParser & parser)
 	JsonNode ret;
 	JsonNode & cost = ret["cost"];
 
-	//note: this code will try to parse mithril as well but wil always return 0 for it
 	for(const std::string & resID : GameConstants::RESOURCE_NAMES)
 		cost[resID].Float() = parser.readNumber();
-
-	cost.Struct().erase("mithril"); // erase mithril to avoid confusing validator
-
+	
 	parser.endLine();
 
 	return ret;
@@ -284,8 +281,8 @@ void CTownHandler::loadBuilding(CTown * town, const std::string & stringID, cons
 	LIBRARY->generaltexth->registerString(source.getModScope(), ret->getDescriptionTextID(), source["description"]);
 
 	ret->subId = vstd::find_or(MappedKeys::SPECIAL_BUILDINGS, source["type"].String(), BuildingSubID::NONE);
-	ret->resources = TResources(source["cost"]);
-	ret->produce =   TResources(source["produce"]);
+	ret->resources.resolveFromJson(source["cost"]);
+	ret->produce.resolveFromJson(source["produce"]);
 
 	ret->manualHeroVisit = source["manualHeroVisit"].Bool();
 	ret->upgradeReplacesBonuses = source["upgradeReplacesBonuses"].Bool();

+ 1 - 1
lib/gameState/CGameState.cpp

@@ -397,7 +397,7 @@ void CGameState::initDifficulty()
 	auto setDifficulty = [this](PlayerState & state, const JsonNode & json)
 	{
 		//set starting resources
-		state.resources = TResources(json["resources"]);
+		state.resources.resolveFromJson(json["resources"]);
 
 		//handicap
 		const PlayerSettings &ps = scenarioOps->getIthPlayersSettings(state.color);

+ 10 - 9
lib/gameState/GameStatistics.cpp

@@ -24,6 +24,7 @@
 #include "../entities/building/CBuilding.h"
 #include "../serializer/JsonDeserializer.h"
 #include "../serializer/JsonUpdater.h"
+#include "../entities/ResourceTypeHandler.h"
 
 
 VCMI_LIB_NAMESPACE_BEGIN
@@ -105,8 +106,8 @@ void StatisticDataSetEntry::serializeJson(JsonSerializeFormat & handler)
 	handler.serializeBool("hasGrail", hasGrail);
 	{
 		auto zonesData = handler.enterStruct("numMines");
-		for(TResource idx = 0; idx < (GameConstants::RESOURCE_QUANTITY - 1); idx++)
-			handler.serializeInt(GameConstants::RESOURCE_NAMES[idx], numMines[idx], 0);
+		for(auto & idx : LIBRARY->resourceTypeHandler->getAllObjects())
+			handler.serializeInt(idx.toResource()->getJsonKey(), numMines[idx], 0);
 	}
 	handler.serializeInt("score", score);
 	handler.serializeInt("maxHeroLevel", maxHeroLevel);
@@ -158,7 +159,7 @@ std::string StatisticDataSet::toCsv(std::string sep) const
 {
 	std::stringstream ss;
 
-	auto resources = std::vector<EGameResID>{EGameResID::GOLD, EGameResID::WOOD, EGameResID::MERCURY, EGameResID::ORE, EGameResID::SULFUR, EGameResID::CRYSTAL, EGameResID::GEMS};
+	auto resources = std::vector<EGameResID>{EGameResID::GOLD, EGameResID::WOOD, EGameResID::MERCURY, EGameResID::ORE, EGameResID::SULFUR, EGameResID::CRYSTAL, EGameResID::GEMS}; //todo: configurable resource support
 
 	ss << "Map" << sep;
 	ss << "Timestamp" << sep;
@@ -191,15 +192,15 @@ std::string StatisticDataSet::toCsv(std::string sep) const
 	ss << "EventDefeatedStrongestHero" << sep;
 	ss << "MovementPointsUsed";
 	for(auto & resource : resources)
-		ss << sep << GameConstants::RESOURCE_NAMES[resource];
+		ss << sep << resource.toResource()->getJsonKey();
 	for(auto & resource : resources)
-		ss << sep << GameConstants::RESOURCE_NAMES[resource] + "Mines";
+		ss << sep << resource.toResource()->getJsonKey() + "Mines";
 	for(auto & resource : resources)
-		ss << sep << GameConstants::RESOURCE_NAMES[resource] + "SpentResourcesForArmy";
+		ss << sep << resource.toResource()->getJsonKey() + "SpentResourcesForArmy";
 	for(auto & resource : resources)
-		ss << sep << GameConstants::RESOURCE_NAMES[resource] + "SpentResourcesForBuildings";
+		ss << sep << resource.toResource()->getJsonKey() + "SpentResourcesForBuildings";
 	for(auto & resource : resources)
-		ss << sep << GameConstants::RESOURCE_NAMES[resource] + "TradeVolume";
+		ss << sep << resource.toResource()->getJsonKey() + "TradeVolume";
 	ss << "\r\n";
 
 	for(auto & entry : data)
@@ -403,7 +404,7 @@ std::map<EGameResID, int> Statistic::getNumMines(const CGameState * gs, const Pl
 {
 	std::map<EGameResID, int> tmp;
 
-	for(auto & res : EGameResID::ALL_RESOURCES())
+	for(auto & res : LIBRARY->resourceTypeHandler->getAllObjects())
 		tmp[res] = 0;
 
 	for(const auto * object : ps->getOwnedObjects())

+ 4 - 3
lib/json/JsonRandom.cpp

@@ -28,6 +28,7 @@
 #include "../entities/artifact/CArtHandler.h"
 #include "../entities/hero/CHero.h"
 #include "../entities/hero/CHeroClass.h"
+#include "../entities/ResourceTypeHandler.h"
 #include "../gameState/CGameState.h"
 #include "../mapObjects/army/CStackBasicDescriptor.h"
 #include "../mapObjects/IObjectInterface.h"
@@ -298,9 +299,9 @@ JsonRandom::JsonRandom(IGameInfoCallback * cb, IGameRandomizer & gameRandomizer)
 			return ret;
 		}
 
-		for (size_t i=0; i<GameConstants::RESOURCE_QUANTITY; i++)
+		for(auto & i : LIBRARY->resourceTypeHandler->getAllObjects())
 		{
-			ret[i] = loadValue(value[GameConstants::RESOURCE_NAMES[i]], variables);
+			ret[i] = loadValue(value[i.toResource()->getJsonKey()], variables);
 		}
 		return ret;
 	}
@@ -315,7 +316,7 @@ JsonRandom::JsonRandom(IGameInfoCallback * cb, IGameRandomizer & gameRandomizer)
 			GameResID::CRYSTAL,
 			GameResID::GEMS,
 			GameResID::GOLD
-		};
+		}; //todo: configurable resource support
 
 		std::set<GameResID> potentialPicks = filterKeys(value, defaultResources, variables);
 		GameResID resourceID = *RandomGeneratorUtil::nextItem(potentialPicks, rng);

+ 2 - 1
lib/mapObjectConstructors/CommonConstructors.cpp

@@ -17,6 +17,7 @@
 #include "../callback/IGameInfoCallback.h"
 #include "../entities/faction/CTownHandler.h"
 #include "../entities/hero/CHeroClass.h"
+#include "../entities/ResourceTypeHandler.h"
 #include "../mapObjects/CGHeroInstance.h"
 #include "../mapObjects/CGTownInstance.h"
 #include "../mapObjects/MiscObjects.h"
@@ -60,7 +61,7 @@ bool ResourceInstanceConstructor::hasNameTextID() const
 
 std::string ResourceInstanceConstructor::getNameTextID() const
 {
-	return TextIdentifier("core", "restypes", resourceType.getNum()).get();
+	return resourceType.toResource()->getNameTextID();
 }
 
 GameResID ResourceInstanceConstructor::getResourceType() const

+ 1 - 1
lib/mapObjectConstructors/FlaggableInstanceConstructor.cpp

@@ -40,7 +40,7 @@ void FlaggableInstanceConstructor::initTypeData(const JsonNode & config)
 		}
 	}
 
-	dailyIncome = ResourceSet(config["dailyIncome"]);
+	dailyIncome.resolveFromJson(config["dailyIncome"]);
 }
 
 void FlaggableInstanceConstructor::initializeObject(FlaggableMapObject * flaggable) const

+ 2 - 1
lib/mapObjects/CGCreature.cpp

@@ -25,6 +25,7 @@
 #include "../networkPacks/StackLocation.h"
 #include "../serializer/JsonSerializeFormat.h"
 #include "../entities/faction/CTownHandler.h"
+#include "../entities/ResourceTypeHandler.h"
 
 #include <vstd/RNG.h>
 
@@ -630,7 +631,7 @@ void CGCreature::giveReward(IGameEventCallback & gameEvents, const CGHeroInstanc
 	if(!resources.empty())
 	{
 		gameEvents.giveResources(h->tempOwner, resources);
-		for(const auto & res : GameResID::ALL_RESOURCES())
+		for(const auto & res : LIBRARY->resourceTypeHandler->getAllObjects())
 		{
 			if(resources[res] > 0)
 				iw.components.emplace_back(ComponentType::RESOURCE, res, resources[res]);

+ 2 - 1
lib/mapObjects/CGHeroInstance.cpp

@@ -38,6 +38,7 @@
 #include "../entities/faction/CTownHandler.h"
 #include "../entities/hero/CHeroHandler.h"
 #include "../entities/hero/CHeroClass.h"
+#include "../entities/ResourceTypeHandler.h"
 #include "../battle/CBattleInfoEssentials.h"
 #include "../campaign/CampaignState.h"
 #include "../json/JsonBonus.h"
@@ -1816,7 +1817,7 @@ ResourceSet CGHeroInstance::dailyIncome() const
 {
 	ResourceSet income;
 
-	for (GameResID k : GameResID::ALL_RESOURCES())
+	for (GameResID k : LIBRARY->resourceTypeHandler->getAllObjects())
 		income[k] += valOfBonuses(BonusType::GENERATE_RESOURCE, BonusSubtypeID(k));
 
 	const auto & playerSettings = cb->getPlayerSettings(getOwner());

+ 1 - 1
lib/mapObjects/CGObjectInstance.cpp

@@ -1,5 +1,5 @@
 /*
- * CObjectHandler.cpp, part of VCMI engine
+ * CGObjectInstance.cpp, part of VCMI engine
  *
  * Authors: listed in file AUTHORS in main folder
  *

+ 3 - 2
lib/mapObjects/CGResource.cpp

@@ -21,6 +21,7 @@
 #include "../gameState/CGameState.h"
 #include "../serializer/JsonSerializeFormat.h"
 #include "../CSoundBase.h"
+#include "../entities/ResourceTypeHandler.h"
 
 #include <vstd/RNG.h>
 
@@ -50,7 +51,7 @@ GameResID CGResource::resourceID() const
 
 std::string CGResource::getHoverText(PlayerColor player) const
 {
-	return LIBRARY->generaltexth->restypes[resourceID().getNum()];
+	return resourceID().toResource()->getNameTranslated();
 }
 
 void CGResource::pickRandomObject(IGameRandomizer & gameRandomizer)
@@ -60,7 +61,7 @@ void CGResource::pickRandomObject(IGameRandomizer & gameRandomizer)
 	if (ID == Obj::RANDOM_RESOURCE)
 	{
 		ID = Obj::RESOURCE;
-		subID = gameRandomizer.getDefault().nextInt(EGameResID::WOOD, EGameResID::GOLD);
+		subID = gameRandomizer.getDefault().nextInt(EGameResID::WOOD, EGameResID::GOLD); //todo: configurable resource support
 		setType(ID, subID);
 
 		amount *= getAmountMultiplier();

+ 2 - 1
lib/mapObjects/CGTownInstance.cpp

@@ -32,6 +32,7 @@
 #include "../callback/IGameRandomizer.h"
 #include "../entities/building/CBuilding.h"
 #include "../entities/faction/CTownHandler.h"
+#include "../entities/ResourceTypeHandler.h"
 #include "../mapObjectConstructors/AObjectTypeHandler.h"
 #include "../mapObjectConstructors/CObjectClassesHandler.h"
 #include "../mapObjects/CGHeroInstance.h"
@@ -209,7 +210,7 @@ TResources CGTownInstance::dailyIncome() const
 {
 	ResourceSet ret;
 
-	for (GameResID k : GameResID::ALL_RESOURCES())
+	for (GameResID k : LIBRARY->resourceTypeHandler->getAllObjects())
 		ret[k] += valOfBonuses(BonusType::GENERATE_RESOURCE, BonusSubtypeID(k));
 
 	for(const auto & p : getTown()->buildings)

+ 0 - 31
lib/mapObjects/CObjectHandler.cpp

@@ -1,31 +0,0 @@
-/*
- * CObjectHandler.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 "CObjectHandler.h"
-
-#include "CGObjectInstance.h"
-#include "../filesystem/ResourcePath.h"
-#include "../json/JsonNode.h"
-
-VCMI_LIB_NAMESPACE_BEGIN
-
-CObjectHandler::CObjectHandler()
-{
-	logGlobal->trace("\t\tReading resources prices ");
-	const JsonNode config2(JsonPath::builtin("config/resources.json"));
-	for(const JsonNode &price : config2["resources_prices"].Vector())
-	{
-		resVals.push_back(static_cast<ui32>(price.Float()));
-	}
-	logGlobal->trace("\t\tDone loading resource prices!");
-}
-
-VCMI_LIB_NAMESPACE_END

+ 5 - 6
lib/mapObjects/CQuest.cpp

@@ -22,6 +22,7 @@
 #include "../callback/IGameRandomizer.h"
 #include "../entities/artifact/CArtifact.h"
 #include "../entities/hero/CHeroHandler.h"
+#include "../entities/ResourceTypeHandler.h"
 #include "../mapObjectConstructors/CObjectClassesHandler.h"
 #include "../serializer/JsonSerializeFormat.h"
 #include "../GameConstants.h"
@@ -233,7 +234,7 @@ void CQuest::addTextReplacements(const IGameInfoCallback * cb, MetaString & text
 	if(mission.resources.nonZero())
 	{
 		MetaString loot;
-		for(auto i : GameResID::ALL_RESOURCES())
+		for(auto i : LIBRARY->resourceTypeHandler->getAllObjects())
 		{
 			if(mission.resources[i])
 			{
@@ -372,11 +373,9 @@ void CQuest::serializeJson(JsonSerializeFormat & handler, const std::string & fi
 		if(missionType == "Resources")
 		{
 			auto r = handler.enterStruct("resources");
-
-			for(size_t idx = 0; idx < (GameConstants::RESOURCE_QUANTITY - 1); idx++)
-			{
-				handler.serializeInt(GameConstants::RESOURCE_NAMES[idx], mission.resources[idx], 0);
-			}
+			
+			for(auto & idx : LIBRARY->resourceTypeHandler->getAllObjects())
+				handler.serializeInt(idx.toResource()->getJsonKey(), mission.resources[idx], 0);
 		}
 		
 		if(missionType == "Hero")

+ 7 - 7
lib/mapObjects/IMarket.cpp

@@ -13,10 +13,10 @@
 
 #include "CCreatureHandler.h"
 #include "CGObjectInstance.h"
-#include "CObjectHandler.h"
 
 #include "../GameLibrary.h"
 #include "../entities/artifact/CArtHandler.h"
+#include "../entities/ResourceTypeHandler.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 
@@ -33,8 +33,8 @@ bool IMarket::getOffer(int id1, int id2, int &val1, int &val2, EMarketMode mode)
 		{
 			double effectiveness = std::min((getMarketEfficiency() + 1.0) / 20.0, 0.5);
 
-			double r = LIBRARY->objh->resVals[id1]; //value of given resource
-			double g = LIBRARY->objh->resVals[id2] / effectiveness; //value of wanted resource
+			double r = GameResID(id1).toResource()->getPrice(); //value of given resource
+			double g = GameResID(id2).toResource()->getPrice() / effectiveness; //value of wanted resource
 
 			if(r>g) //if given resource is more expensive than wanted
 			{
@@ -54,7 +54,7 @@ bool IMarket::getOffer(int id1, int id2, int &val1, int &val2, EMarketMode mode)
 			double effectiveness = effectivenessArray[std::min(getMarketEfficiency(), 8)];
 
 			double r = LIBRARY->creatures()->getByIndex(id1)->getRecruitCost(EGameResID::GOLD); //value of given creature in gold
-			double g = LIBRARY->objh->resVals[id2] / effectiveness; //value of wanted resource
+			double g = GameResID(id2).toResource()->getPrice() / effectiveness; //value of wanted resource
 
 			if(r>g) //if given resource is more expensive than wanted
 			{
@@ -75,7 +75,7 @@ bool IMarket::getOffer(int id1, int id2, int &val1, int &val2, EMarketMode mode)
 	case EMarketMode::RESOURCE_ARTIFACT:
 		{
 			double effectiveness = std::min((getMarketEfficiency() + 3.0) / 20.0, 0.6);
-			double r = LIBRARY->objh->resVals[id1]; //value of offered resource
+			double r = GameResID(id1).toResource()->getPrice(); //value of offered resource
 			double g = LIBRARY->artifacts()->getByIndex(id2)->getPrice() / effectiveness; //value of bought artifact in gold
 
 			if(id1 != 6) //non-gold prices are doubled
@@ -89,7 +89,7 @@ bool IMarket::getOffer(int id1, int id2, int &val1, int &val2, EMarketMode mode)
 		{
 			double effectiveness = std::min((getMarketEfficiency() + 3.0) / 20.0, 0.6);
 			double r = LIBRARY->artifacts()->getByIndex(id1)->getPrice() * effectiveness;
-			double g = LIBRARY->objh->resVals[id2];
+			double g = GameResID(id2).toResource()->getPrice();
 
 // 			if(id2 != 6) //non-gold prices are doubled
 // 				r /= 2;
@@ -163,7 +163,7 @@ std::vector<TradeItemBuy> IMarket::availableItemsIds(const EMarketMode mode) con
 	case EMarketMode::RESOURCE_RESOURCE:
 	case EMarketMode::ARTIFACT_RESOURCE:
 	case EMarketMode::CREATURE_RESOURCE:
-		for(const auto & res : GameResID::ALL_RESOURCES())
+		for(const auto & res : LIBRARY->resourceTypeHandler->getAllObjects())
 			ret.push_back(res);
 	}
 	return ret;

+ 0 - 2
lib/mapObjects/MapObjects.h

@@ -10,8 +10,6 @@
 #pragma once
 
 // Helper header that includes all map objects, similar to old CObjectHandler.h
-// Possible TODO - remove this header after CObjectHandler.cpp will be fully split into smaller files
-#include "CObjectHandler.h"
 
 #include "CGDwelling.h"
 #include "CGHeroInstance.h"

+ 9 - 5
lib/mapObjects/MiscObjects.cpp

@@ -18,6 +18,7 @@
 #include "../constants/StringConstants.h"
 #include "../entities/artifact/ArtifactUtils.h"
 #include "../entities/artifact/CArtifact.h"
+#include "../entities/ResourceTypeHandler.h"
 #include "../CConfigHandler.h"
 #include "../texts/CGeneralTextHandler.h"
 #include "../CSkillHandler.h"
@@ -143,7 +144,7 @@ ResourceSet CGMine::dailyIncome() const
 {
 	ResourceSet result;
 
-	for (GameResID k : GameResID::ALL_RESOURCES())
+	for (GameResID k : LIBRARY->resourceTypeHandler->getAllObjects())
 		result[k] += valOfBonuses(BonusType::GENERATE_RESOURCE, BonusSubtypeID(k));
 
 	result[producedResource] += defaultResProduction();
@@ -164,7 +165,7 @@ std::string CGMine::getHoverText(PlayerColor player) const
 	std::string hoverName = CArmedInstance::getHoverText(player);
 
 	if (tempOwner != PlayerColor::NEUTRAL)
-		hoverName += "\n(" + LIBRARY->generaltexth->restypes[producedResource.getNum()] + ")";
+		hoverName += "\n(" + producedResource.toResource()->getNameTranslated() + ")";
 
 	if(stacksCount())
 	{
@@ -238,7 +239,7 @@ void CGMine::serializeJsonOptions(JsonSerializeFormat & handler)
 		{
 			JsonNode node;
 			for(const auto & resID : abandonedMineResources)
-				node.Vector().emplace_back(GameConstants::RESOURCE_NAMES[resID.getNum()]);
+				node.Vector().emplace_back(resID.toResource()->getJsonKey());
 
 			handler.serializeRaw("possibleResources", node, std::nullopt);
 		}
@@ -251,7 +252,10 @@ void CGMine::serializeJsonOptions(JsonSerializeFormat & handler)
 
 			for(const std::string & s : names)
 			{
-				int raw_res = vstd::find_pos(GameConstants::RESOURCE_NAMES, s);
+				std::vector<std::string> resNames;
+				for(auto & res : LIBRARY->resourceTypeHandler->getAllObjects())
+					resNames.push_back(res.toResource()->getJsonKey());
+				int raw_res = vstd::find_pos(resNames, s);
 				if(raw_res < 0)
 					logGlobal->error("Invalid resource name: %s", s);
 				else
@@ -872,7 +876,7 @@ const IOwnableObject * CGGarrison::asOwnable() const
 ResourceSet CGGarrison::dailyIncome() const
 {
 	ResourceSet result;
-	for (GameResID k : GameResID::ALL_RESOURCES())
+	for (GameResID k : LIBRARY->resourceTypeHandler->getAllObjects())
 		result[k] += valOfBonuses(BonusType::GENERATE_RESOURCE, BonusSubtypeID(k));
 
 	return result;

+ 2 - 0
lib/modding/ContentTypeHandler.cpp

@@ -23,6 +23,7 @@
 #include "../entities/faction/CTownHandler.h"
 #include "../entities/hero/CHeroClassHandler.h"
 #include "../entities/hero/CHeroHandler.h"
+#include "../entities/ResourceTypeHandler.h"
 #include "../texts/CGeneralTextHandler.h"
 #include "../CBonusTypeHandler.h"
 #include "../CSkillHandler.h"
@@ -263,6 +264,7 @@ void CContentHandler::init()
 	handlers.insert(std::make_pair("roads", ContentTypeHandler(LIBRARY->roadTypeHandler.get(), "road")));
 	handlers.insert(std::make_pair("obstacles", ContentTypeHandler(LIBRARY->obstacleHandler.get(), "obstacle")));
 	handlers.insert(std::make_pair("biomes", ContentTypeHandler(LIBRARY->biomeHandler.get(), "biome")));
+	handlers.insert(std::make_pair("resources", ContentTypeHandler(LIBRARY->resourceTypeHandler.get(), "resources")));
 }
 
 bool CContentHandler::preloadData(const ModDescription & mod, bool validate)

+ 2 - 1
lib/rewardable/Info.cpp

@@ -22,6 +22,7 @@
 #include "../mapObjects/IObjectInterface.h"
 #include "../modding/IdentifierStorage.h"
 #include "../texts/CGeneralTextHandler.h"
+#include "../entities/ResourceTypeHandler.h"
 
 #include <vstd/RNG.h>
 
@@ -296,7 +297,7 @@ void Rewardable::Info::replaceTextPlaceholders(MetaString & target, const Variab
 
 		MetaString loot;
 
-		for (GameResID it : GameResID::ALL_RESOURCES())
+		for (GameResID it : LIBRARY->resourceTypeHandler->getAllObjects())
 		{
 			if (info.reward.resources[it] != 0)
 			{

+ 4 - 8
lib/rmg/CRmgTemplate.cpp

@@ -18,6 +18,7 @@
 #include "../GameLibrary.h"
 #include "../constants/StringConstants.h"
 #include "../entities/faction/CTownHandler.h"
+#include "../entities/ResourceTypeHandler.h"
 #include "../modding/ModScope.h"
 #include "../serializer/JsonSerializeFormat.h"
 
@@ -258,12 +259,12 @@ std::set<FactionID> ZoneOptions::getMonsterTypes() const
 	return vstd::difference(monsterTypes, bannedMonsters);
 }
 
-void ZoneOptions::setMinesInfo(const std::map<TResource, ui16> & value)
+void ZoneOptions::setMinesInfo(const std::map<GameResID, ui16> & value)
 {
 	mines = value;
 }
 
-std::map<TResource, ui16> ZoneOptions::getMinesInfo() const
+std::map<GameResID, ui16> ZoneOptions::getMinesInfo() const
 {
 	return mines;
 }
@@ -532,12 +533,7 @@ void ZoneOptions::serializeJson(JsonSerializeFormat & handler)
 
 	if((minesLikeZone == NO_ZONE) && (!handler.saving || !mines.empty()))
 	{
-		auto minesData = handler.enterStruct("mines");
-
-		for(TResource idx = 0; idx < (GameConstants::RESOURCE_QUANTITY - 1); idx++)
-		{
-			handler.serializeInt(GameConstants::RESOURCE_NAMES[idx], mines[idx], 0);
-		}
+		handler.serializeIdMap<GameResID, ui16>("mines", mines);
 	}
 
 	handler.serializeStruct("customObjects", objectConfig);

+ 4 - 4
lib/rmg/CRmgTemplate.h

@@ -209,8 +209,8 @@ public:
 
 	void setMonsterTypes(const std::set<FactionID> & value);
 
-	void setMinesInfo(const std::map<TResource, ui16> & value);
-	std::map<TResource, ui16> getMinesInfo() const;
+	void setMinesInfo(const std::map<GameResID, ui16> & value);
+	std::map<GameResID, ui16> getMinesInfo() const;
 
 	void setTreasureInfo(const std::vector<CTreasureInfo> & value);
 	void addTreasureInfo(const CTreasureInfo & value);
@@ -277,7 +277,7 @@ protected:
 	std::set<FactionID> monsterTypes;
 	std::set<FactionID> bannedMonsters;
 
-	std::map<TResource, ui16> mines; //obligatory mines to spawn in this zone
+	std::map<GameResID, ui16> mines; //obligatory mines to spawn in this zone
 
 	std::vector<CTreasureInfo> treasureInfo;
 
@@ -373,7 +373,7 @@ private:
 	std::set<HeroTypeID> bannedHeroes;
 
 	std::set<TerrainId> inheritTerrainType(std::shared_ptr<rmg::ZoneOptions> zone, uint32_t iteration = 0);
-	std::map<TResource, ui16> inheritMineTypes(std::shared_ptr<rmg::ZoneOptions> zone, uint32_t iteration = 0);
+	std::map<GameResID, ui16> inheritMineTypes(std::shared_ptr<rmg::ZoneOptions> zone, uint32_t iteration = 0);
 	std::vector<CTreasureInfo> inheritTreasureInfo(std::shared_ptr<rmg::ZoneOptions> zone, uint32_t iteration = 0);
 
 	void inheritTownProperties(std::shared_ptr<rmg::ZoneOptions> zone, uint32_t iteration = 0);

+ 10 - 0
lib/serializer/JsonDeserializer.cpp

@@ -119,6 +119,16 @@ void JsonDeserializer::serializeInternal(const std::string & fieldName, std::vec
 	}
 }
 
+void JsonDeserializer::serializeInternal(const std::string & fieldName, std::map<std::string, uint16_t> & value)
+{
+	const JsonMap & data = currentObject->operator[](fieldName).Struct();
+
+	value.clear();
+
+	for(const auto & [id, elem] : data)
+		value[id] = elem.Integer();
+}
+
 void JsonDeserializer::serializeInternal(std::string & value)
 {
 	value = currentObject->String();

+ 1 - 0
lib/serializer/JsonDeserializer.h

@@ -32,6 +32,7 @@ protected:
 	void serializeInternal(const std::string & fieldName, si64 & value, const std::optional<si64> & defaultValue) override;
 	void serializeInternal(const std::string & fieldName, si32 & value, const std::optional<si32> & defaultValue, const std::vector<std::string> & enumMap) override;
 	void serializeInternal(const std::string & fieldName, std::vector<std::string> & value) override;
+	void serializeInternal(const std::string & fieldName, std::map<std::string, uint16_t> & value) override;
 
 	void serializeInternal(std::string & value) override;
 	void serializeInternal(int64_t & value) override;

+ 30 - 0
lib/serializer/JsonSerializeFormat.h

@@ -331,6 +331,33 @@ public:
 		}
 	}
 
+	/// si32-convertible identifier map <-> Json object of {key: string}
+	template <typename Key, typename T, typename E = T>
+	void serializeIdMap(const std::string & fieldName, std::map<Key, T> & value)
+	{
+		if (saving)
+		{
+			std::map<std::string, T> fieldValue;
+
+			for (const auto & [key, val] : value)
+				fieldValue[Key::encode(key.getNum())] = val;
+
+			serializeInternal(fieldName, fieldValue);
+		}
+		else
+		{
+			const JsonNode & node = getCurrent()[fieldName];
+			for (const auto & [keyStr, jsonVal] : node.Struct())
+			{
+				Key key = Key::decode(keyStr);
+
+				LIBRARY->identifiers()->requestIdentifier(node.getModScope(), Key::entityType(), keyStr, [&value, key](int32_t index) {
+					value[key] = T(index);
+				});
+			}
+		}
+	}
+
 	///si32-convertible identifier vector <-> Json array of string
 	template <typename T, typename E = T>
 	void serializeIdArray(const std::string & fieldName, std::vector<T> & value)
@@ -443,6 +470,9 @@ protected:
 	///String vector <-> Json string vector
 	virtual void serializeInternal(const std::string & fieldName, std::vector<std::string> & value) = 0;
 
+	///String map <-> Json map of int
+	virtual void serializeInternal(const std::string & fieldName, std::map<std::string, uint16_t> & value) = 0;
+
 	virtual void pop() = 0;
 	virtual void pushStruct(const std::string & fieldName) = 0;
 	virtual void pushArray(const std::string & fieldName) = 0;

+ 11 - 0
lib/serializer/JsonSerializer.cpp

@@ -75,6 +75,17 @@ void JsonSerializer::serializeInternal(const std::string & fieldName, std::vecto
 		data.emplace_back(rawId);
 }
 
+void JsonSerializer::serializeInternal(const std::string & fieldName, std::map<std::string, uint16_t> & value)
+{
+	if(value.empty())
+		return;
+
+	JsonMap & data = currentObject->operator[](fieldName).Struct();
+
+	for(const auto & [rawId, val] : value)
+		data[rawId].Integer() = val;
+}
+
 void JsonSerializer::serializeInternal(std::string & value)
 {
 	currentObject->String() = value;

+ 1 - 0
lib/serializer/JsonSerializer.h

@@ -32,6 +32,7 @@ protected:
 	void serializeInternal(const std::string & fieldName, si64 & value, const std::optional<si64> & defaultValue) override;
 	void serializeInternal(const std::string & fieldName, si32 & value, const std::optional<si32> & defaultValue, const std::vector<std::string> & enumMap) override;
 	void serializeInternal(const std::string & fieldName, std::vector<std::string> & value) override;
+	void serializeInternal(const std::string & fieldName, std::map<std::string, uint16_t> & value) override;
 
 	void serializeInternal(std::string & value) override;
 	void serializeInternal(int64_t & value) override;

+ 5 - 0
lib/serializer/JsonUpdater.cpp

@@ -65,6 +65,11 @@ void JsonUpdater::serializeInternal(const std::string & fieldName, std::vector<s
 	// TODO
 }
 
+void JsonUpdater::serializeInternal(const std::string & fieldName, std::map<std::string, uint16_t> & value)
+{
+	// TODO
+}
+
 void JsonUpdater::serializeInternal(const std::string & fieldName, double & value, const std::optional<double> & defaultValue)
 {
 	const JsonNode & data = currentObject->operator[](fieldName);

+ 1 - 0
lib/serializer/JsonUpdater.h

@@ -36,6 +36,7 @@ protected:
 	void serializeInternal(const std::string & fieldName, si64 & value, const std::optional<si64> & defaultValue) override;
 	void serializeInternal(const std::string & fieldName, si32 & value, const std::optional<si32> & defaultValue, const std::vector<std::string> & enumMap) override;
 	void serializeInternal(const std::string & fieldName, std::vector<std::string> & value) override;
+	void serializeInternal(const std::string & fieldName, std::map<std::string, uint16_t> & value) override;
 
 	void serializeInternal(std::string & value) override;
 	void serializeInternal(int64_t & value) override;

+ 3 - 2
lib/texts/MetaString.cpp

@@ -14,6 +14,7 @@
 #include "entities/artifact/CArtifact.h"
 #include "entities/faction/CFaction.h"
 #include "entities/hero/CHero.h"
+#include "entities/ResourceTypeHandler.h"
 #include "texts/CGeneralTextHandler.h"
 #include "CSkillHandler.h"
 #include "GameConstants.h"
@@ -378,7 +379,7 @@ void MetaString::appendName(const CreatureID & id, TQuantity count)
 
 void MetaString::appendName(const GameResID& id)
 {
-	appendTextID(TextIdentifier("core.restypes", id.getNum()).get());
+	appendTextID(id.toResource()->getNameTextID());
 }
 
 void MetaString::appendNameSingular(const CreatureID & id)
@@ -423,7 +424,7 @@ void MetaString::replaceName(const SpellID & id)
 
 void MetaString::replaceName(const GameResID& id)
 {
-	replaceTextID(TextIdentifier("core.restypes", id.getNum()).get());
+	replaceTextID(id.toResource()->getNameTextID());
 }
 
 void MetaString::replaceNameSingular(const CreatureID & id)

+ 2 - 0
mapeditor/graphics.cpp

@@ -19,6 +19,7 @@
 #include <vcmi/HeroTypeService.h>
 #include <vcmi/SkillService.h>
 #include <vcmi/spells/Service.h>
+#include <vcmi/ResourceTypeService.h>
 
 #include "../lib/filesystem/Filesystem.h"
 #include "../lib/filesystem/CBinaryReader.h"
@@ -342,4 +343,5 @@ void Graphics::initializeImageLists()
 	addImageListEntries(LIBRARY->factions());
 	addImageListEntries(LIBRARY->spells());
 	addImageListEntries(LIBRARY->skills());
+	addImageListEntries(LIBRARY->resources());
 }

+ 4 - 5
mapeditor/inspector/questwidget.cpp

@@ -17,6 +17,7 @@
 #include "../lib/CCreatureHandler.h"
 #include "../lib/constants/StringConstants.h"
 #include "../lib/entities/artifact/CArtHandler.h"
+#include "../lib/entities/ResourceTypeHandler.h"
 #include "../lib/mapping/CMap.h"
 #include "../lib/mapObjects/CGHeroInstance.h"
 #include "../lib/mapObjects/CGCreature.h"
@@ -40,13 +41,13 @@ QuestWidget::QuestWidget(MapController & _controller, CQuest & _sh, QWidget *par
 		ui->lDayOfWeek->addItem(tr("Day %1").arg(i));
 	
 	//fill resources
-	ui->lResources->setRowCount(GameConstants::RESOURCE_QUANTITY - 1);
-	for(int i = 0; i < GameConstants::RESOURCE_QUANTITY - 1; ++i)
+	ui->lResources->setRowCount(LIBRARY->resourceTypeHandler->getAllObjects().size() - 1);
+	for(auto & i : LIBRARY->resourceTypeHandler->getAllObjects())
 	{
 		MetaString str;
 		str.appendName(GameResID(i));
 		auto * item = new QTableWidgetItem(QString::fromStdString(str.toString()));
-		item->setData(Qt::UserRole, QVariant::fromValue(i));
+		item->setData(Qt::UserRole, QVariant::fromValue(i.getNum()));
 		ui->lResources->setItem(i, 0, item);
 		auto * spinBox = new QSpinBox;
 		spinBox->setMaximum(i == GameResID::GOLD ? 999999 : 999);
@@ -455,8 +456,6 @@ void QuestDelegate::updateModelData(QAbstractItemModel * model, const QModelInde
 	QStringList resourcesList;
 	for(GameResID resource = GameResID::WOOD; resource < GameResID::COUNT ; resource++)
 	{
-		if(resource == GameResID::MITHRIL)
-			continue;
 		if(quest.mission.resources[resource] == 0)
 			continue;
 		MetaString str;

+ 5 - 6
mapeditor/inspector/rewardswidget.cpp

@@ -17,6 +17,7 @@
 #include "../lib/CCreatureHandler.h"
 #include "../lib/constants/StringConstants.h"
 #include "../lib/entities/artifact/CArtifact.h"
+#include "../lib/entities/ResourceTypeHandler.h"
 #include "../lib/mapping/CMap.h"
 #include "../lib/modding/IdentifierStorage.h"
 #include "../lib/modding/ModScope.h"
@@ -55,16 +56,16 @@ RewardsWidget::RewardsWidget(CMap & m, CRewardableObject & p, QWidget *parent) :
 		ui->lDayOfWeek->addItem(tr("Day %1").arg(i));
 	
 	//fill resources
-	ui->rResources->setRowCount(GameConstants::RESOURCE_QUANTITY - 1);
-	ui->lResources->setRowCount(GameConstants::RESOURCE_QUANTITY - 1);
-	for(int i = 0; i < GameConstants::RESOURCE_QUANTITY - 1; ++i)
+	ui->rResources->setRowCount(LIBRARY->resourceTypeHandler->getAllObjects().size() - 1);
+	ui->lResources->setRowCount(LIBRARY->resourceTypeHandler->getAllObjects().size() - 1);
+	for(auto & i : LIBRARY->resourceTypeHandler->getAllObjects())
 	{
 		MetaString str;
 		str.appendName(GameResID(i));
 		for(auto * w : {ui->rResources, ui->lResources})
 		{
 			auto * item = new QTableWidgetItem(QString::fromStdString(str.toString()));
-			item->setData(Qt::UserRole, QVariant::fromValue(i));
+			item->setData(Qt::UserRole, QVariant::fromValue(i.getNum()));
 			w->setItem(i, 0, item);
 			auto * spinBox = new QSpinBox;
 			spinBox->setMaximum(i == GameResID::GOLD ? 999999 : 999);
@@ -779,8 +780,6 @@ void RewardsDelegate::updateModelData(QAbstractItemModel * model, const QModelIn
 		QStringList resourcesList;
 		for(GameResID resource = GameResID::WOOD; resource < GameResID::COUNT ; resource++)
 		{
-			if(resource == GameResID::MITHRIL)
-				continue; // translated as "Abandoned"?
 			if(vinfo.reward.resources[resource] == 0)
 				continue;
 			MetaString str;

+ 7 - 5
mapeditor/inspector/towneventdialog.cpp

@@ -18,6 +18,8 @@
 #include "../../lib/entities/faction/CTownHandler.h"
 #include "../../lib/constants/NumericConstants.h"
 #include "../../lib/constants/StringConstants.h"
+#include "../../lib/GameLibrary.h"
+#include "../../lib/entities/ResourceTypeHandler.h"
 
 static const int FIRST_DAY_FOR_EVENT = 1;
 static const int LAST_DAY_FOR_EVENT = 999;
@@ -79,9 +81,9 @@ void TownEventDialog::initPlayers()
 
 void TownEventDialog::initResources()
 {
-	ui->resourcesTable->setRowCount(GameConstants::RESOURCE_QUANTITY);
+	ui->resourcesTable->setRowCount(LIBRARY->resourceTypeHandler->getAllObjects().size());
 	auto resourcesMap = params.value("resources").toMap();
-	for (int i = 0; i < GameConstants::RESOURCE_QUANTITY; ++i)
+	for(auto & i : LIBRARY->resourceTypeHandler->getAllObjects())
 	{
 		MetaString str;
 		str.appendName(GameResID(i));
@@ -91,7 +93,7 @@ void TownEventDialog::initResources()
 		item->setText(name);
 		ui->resourcesTable->setItem(i, 0, item);
 
-		int val = resourcesMap.value(QString::fromStdString(GameConstants::RESOURCE_NAMES[i])).toInt();
+		int val = resourcesMap.value(QString::fromStdString(i.toResource()->getJsonKey())).toInt();
 		auto * edit = new QSpinBox(ui->resourcesTable);
 		edit->setMaximum(i == GameResID::GOLD ? MAXIMUM_GOLD_CHANGE : MAXIMUM_RESOURCE_CHANGE);
 		edit->setMinimum(i == GameResID::GOLD ? -MAXIMUM_GOLD_CHANGE : -MAXIMUM_RESOURCE_CHANGE);
@@ -228,9 +230,9 @@ QVariant TownEventDialog::playersToVariant()
 QVariantMap TownEventDialog::resourcesToVariant()
 {
 	auto res = params.value("resources").toMap();
-	for (int i = 0; i < GameConstants::RESOURCE_QUANTITY; ++i)
+	for(auto & i : LIBRARY->resourceTypeHandler->getAllObjects())
 	{
-		auto itemType = QString::fromStdString(GameConstants::RESOURCE_NAMES[i]);
+		auto itemType = QString::fromStdString(i.toResource()->getJsonKey());
 		auto * itemQty = static_cast<QSpinBox *> (ui->resourcesTable->cellWidget(i, 1));
 
 		res[itemType] = QVariant::fromValue(itemQty->value());

+ 7 - 3
mapeditor/mapsettings/eventsettings.cpp

@@ -14,6 +14,8 @@
 #include "../mapcontroller.h"
 #include "../../lib/constants/NumericConstants.h"
 #include "../../lib/constants/StringConstants.h"
+#include "../../lib/GameLibrary.h"
+#include "../../lib/entities/ResourceTypeHandler.h"
 
 QString toQString(const PlayerColor & player)
 {
@@ -41,8 +43,8 @@ std::set<PlayerColor> playersFromVariant(const QVariant & v)
 QVariant toVariant(const TResources & resources)
 {
 	QVariantMap result;
-	for(int i = 0; i < GameConstants::RESOURCE_QUANTITY; ++i)
-		result[QString::fromStdString(GameConstants::RESOURCE_NAMES[i])] = QVariant::fromValue(resources[i]);
+	for(auto & i : LIBRARY->resourceTypeHandler->getAllObjects())
+		result[QString::fromStdString(i.toResource()->getJsonKey())] = QVariant::fromValue(resources[i]);
 	return result;
 }
 
@@ -51,7 +53,9 @@ TResources resourcesFromVariant(const QVariant & v)
 	JsonNode vJson;
 	for(auto r : v.toMap().keys())
 		vJson[r.toStdString()].Integer() = v.toMap().value(r).toInt();
-	return TResources(vJson);
+	ResourceSet res;
+	res.resolveFromJson(vJson);
+	return res;
 }
 
 QVariant toVariant(std::vector<ObjectInstanceID> objects)

+ 7 - 5
mapeditor/mapsettings/timedevent.cpp

@@ -14,6 +14,8 @@
 #include "../mapeditorroles.h"
 #include "../../lib/constants/EntityIdentifiers.h"
 #include "../../lib/constants/StringConstants.h"
+#include "../../lib/GameLibrary.h"
+#include "../../lib/entities/ResourceTypeHandler.h"
 
 TimedEvent::TimedEvent(MapController & c, QListWidgetItem * t, QWidget *parent) : 
 	controller(c),
@@ -45,13 +47,13 @@ TimedEvent::TimedEvent(MapController & c, QListWidgetItem * t, QWidget *parent)
 		ui->playersAffected->addItem(item);
 	}
 
-	ui->resources->setRowCount(GameConstants::RESOURCE_QUANTITY);
-	for(int i = 0; i < GameConstants::RESOURCE_QUANTITY; ++i)
+	ui->resources->setRowCount(LIBRARY->resourceTypeHandler->getAllObjects().size());
+	for(auto & i : LIBRARY->resourceTypeHandler->getAllObjects())
 	{
 		MetaString str;
 		str.appendName(GameResID(i));
 		auto name = QString::fromStdString(str.toString());
-		int val = params.value("resources").toMap().value(QString::fromStdString(GameConstants::RESOURCE_NAMES[i])).toInt();
+		int val = params.value("resources").toMap().value(QString::fromStdString(i.toResource()->getJsonKey())).toInt();
 		ui->resources->setItem(i, 0, new QTableWidgetItem(name));
 		auto nval = new QTableWidgetItem(QString::number(val));
 		nval->setFlags(nval->flags() | Qt::ItemIsEditable);
@@ -94,9 +96,9 @@ void TimedEvent::on_TimedEvent_finished(int result)
 	descriptor["players"] = QVariant::fromValue(players);
 
 	auto res = target->data(Qt::UserRole).toMap().value("resources").toMap();
-	for(int i = 0; i < GameConstants::RESOURCE_QUANTITY; ++i)
+	for(auto & i : LIBRARY->resourceTypeHandler->getAllObjects())
 	{
-		auto itemType = QString::fromStdString(GameConstants::RESOURCE_NAMES[i]);
+		auto itemType = QString::fromStdString(i.toResource()->getJsonKey());
 		auto * itemQty = ui->resources->item(i, 1);
 		res[itemType] = QVariant::fromValue(itemQty->text().toInt());
 	}

+ 4 - 2
mapeditor/mapsettings/victoryconditions.cpp

@@ -12,9 +12,11 @@
 #include "ui_victoryconditions.h"
 #include "../mapcontroller.h"
 
+#include "../../lib/GameLibrary.h"
 #include "../../lib/constants/StringConstants.h"
 #include "../../lib/entities/artifact/CArtHandler.h"
 #include "../../lib/entities/faction/CTownHandler.h"
+#include "../../lib/entities/ResourceTypeHandler.h"
 #include "../../lib/mapObjects/CGCreature.h"
 #include "../../lib/texts/CGeneralTextHandler.h"
 
@@ -406,12 +408,12 @@ void VictoryConditions::on_victoryComboBox_currentIndexChanged(int index)
 			victoryTypeWidget = new QComboBox;
 			ui->victoryParamsLayout->addWidget(victoryTypeWidget);
 			{
-				for(int resType = 0; resType < GameConstants::RESOURCE_QUANTITY; ++resType)
+				for(auto & resType : LIBRARY->resourceTypeHandler->getAllObjects())
 				{
 					MetaString str;
 					str.appendName(GameResID(resType));
 					auto resName = QString::fromStdString(str.toString());
-					victoryTypeWidget->addItem(resName, QVariant::fromValue(resType));
+					victoryTypeWidget->addItem(resName, QVariant::fromValue(resType.getNum()));
 				}
 			}
 

+ 4 - 2
mapeditor/templateeditor/graphicelements/CardItem.cpp

@@ -16,6 +16,8 @@
 #include "../../../lib/constants/EntityIdentifiers.h"
 #include "../../../lib/constants/StringConstants.h"
 #include "../../../lib/rmg/CRmgTemplate.h"
+#include "../../../lib/GameLibrary.h"
+#include "../../../lib/entities/ResourceTypeHandler.h"
 
 QDomElement CardItem::getElementById(const QDomDocument& doc, const QString& id)
 {
@@ -149,11 +151,11 @@ int CardItem::getId()
 
 void CardItem::setResAmount(GameResID res, int val)
 {
-	auto textElem = getElementById(doc, "text" + QString::fromStdString(GameConstants::RESOURCE_NAMES[res]));
+	auto textElem = getElementById(doc, "text" + QString::fromStdString(res.toResource()->getJsonKey()));
 	textElem.setAttribute("style", textElem.attribute("style").replace(QRegularExpression("fill:.*?;"), "fill:" + QColor(useBlackText ? Qt::black : Qt::white).name() + ";"));
 	textElem.firstChild().setNodeValue(val ? QString::number(val) : "");
 
-	auto iconElem = getElementById(doc, "icon" + QString::fromStdString(GameConstants::RESOURCE_NAMES[res]));
+	auto iconElem = getElementById(doc, "icon" + QString::fromStdString(res.toResource()->getJsonKey()));
 	iconElem.setAttribute("opacity", val ? "1.0" : "0.1");
 }
 

+ 11 - 9
mapeditor/templateeditor/mineselector.cpp

@@ -15,10 +15,12 @@
 
 #include "../../lib/GameLibrary.h"
 #include "../../lib/texts/CGeneralTextHandler.h"
+#include "../../lib/texts/MetaString.h"
+#include "../../lib/entities/ResourceTypeHandler.h"
 
-auto resources = std::vector<EGameResID>{EGameResID::GOLD, EGameResID::WOOD, EGameResID::MERCURY, EGameResID::ORE, EGameResID::SULFUR, EGameResID::CRYSTAL, EGameResID::GEMS};
+auto resourcesToShow = std::vector<EGameResID>{EGameResID::GOLD, EGameResID::WOOD, EGameResID::MERCURY, EGameResID::ORE, EGameResID::SULFUR, EGameResID::CRYSTAL, EGameResID::GEMS}; //todo: configurable resource support
 
-MineSelector::MineSelector(std::map<TResource, ui16> & mines) :
+MineSelector::MineSelector(std::map<GameResID, ui16> & mines) :
 	ui(new Ui::MineSelector),
 	minesSelected(mines)
 {
@@ -29,18 +31,18 @@ MineSelector::MineSelector(std::map<TResource, ui16> & mines) :
 	setWindowModality(Qt::ApplicationModal);
 
 	ui->tableWidgetMines->setColumnCount(2);
-	ui->tableWidgetMines->setRowCount(resources.size());
+	ui->tableWidgetMines->setRowCount(resourcesToShow.size());
 	ui->tableWidgetMines->setHorizontalHeaderLabels({tr("Resource"), tr("Mines")});
-	for (int row = 0; row < resources.size(); ++row)
+	for (int row = 0; row < resourcesToShow.size(); ++row)
 	{
-		auto name = LIBRARY->generaltexth->translate(TextIdentifier("core.restypes", resources[row].getNum()).get());
+		auto name = resourcesToShow[row].toResource()->getNameTranslated();
 		auto label = new QLabel(QString::fromStdString(name));
 		label->setAlignment(Qt::AlignCenter);
 		ui->tableWidgetMines->setCellWidget(row, 0, label);
 
 		auto spinBox = new QSpinBox();
 		spinBox->setRange(0, 100);
-		spinBox->setValue(mines[resources[row]]);
+		spinBox->setValue(mines[resourcesToShow[row]]);
 		ui->tableWidgetMines->setCellWidget(row, 1, spinBox);
 	}
 	ui->tableWidgetMines->resizeColumnsToContents();
@@ -48,7 +50,7 @@ MineSelector::MineSelector(std::map<TResource, ui16> & mines) :
 	show();
 }
 
-void MineSelector::showMineSelector(std::map<TResource, ui16> & mines)
+void MineSelector::showMineSelector(std::map<GameResID, ui16> & mines)
 {
 	auto * dialog = new MineSelector(mines);
 	dialog->setAttribute(Qt::WA_DeleteOnClose);
@@ -57,8 +59,8 @@ void MineSelector::showMineSelector(std::map<TResource, ui16> & mines)
 
 void MineSelector::on_buttonBoxResult_accepted()
 {
-	for (int row = 0; row < resources.size(); ++row)
-		minesSelected[resources[row]] = static_cast<QSpinBox *>(ui->tableWidgetMines->cellWidget(row, 1))->value();
+	for (int row = 0; row < resourcesToShow.size(); ++row)
+		minesSelected[resourcesToShow[row]] = static_cast<QSpinBox *>(ui->tableWidgetMines->cellWidget(row, 1))->value();
 
 	close();
 }

+ 3 - 3
mapeditor/templateeditor/mineselector.h

@@ -22,9 +22,9 @@ class MineSelector : public QDialog
 	Q_OBJECT
 
 public:
-	explicit MineSelector(std::map<TResource, ui16> & mines);
+	explicit MineSelector(std::map<GameResID, ui16> & mines);
 
-	static void showMineSelector(std::map<TResource, ui16> & mines);
+	static void showMineSelector(std::map<GameResID, ui16> & mines);
 
 private slots:
 	void on_buttonBoxResult_accepted();
@@ -33,5 +33,5 @@ private slots:
 private:
 	Ui::MineSelector *ui;
 
-	std::map<TResource, ui16> & minesSelected;
+	std::map<GameResID, ui16> & minesSelected;
 };

+ 0 - 2
scripting/lua/api/ObjectInstance.h

@@ -14,8 +14,6 @@
 
 #include "../LuaWrapper.h"
 
-#include "../../../lib/mapObjects/CObjectHandler.h"
-
 namespace scripting
 {
 namespace api

+ 1 - 1
scripting/lua/api/netpacks/SetResources.cpp

@@ -82,7 +82,7 @@ int SetResourcesProxy::getAmount(lua_State * L)
 
 	S.clear();
 
-	const TQuantity amount = vstd::atOrDefault(object->res, static_cast<size_t>(type), 0);
+	const TQuantity amount = object->res[type];
 	S.push(amount);
 	return 1;
 }

+ 1 - 1
scripts/lib/erm/MA.lua

@@ -8,7 +8,7 @@ local Bonus = require("Bonus")
 local BonusBearer = require("BonusBearer")
 local BonusList = require("BonusList")
 
-local RES = {[0] = "wood", [1] = "mercury", [2] = "ore", [3] = "sulfur", [4] = "crystal", [5] = "gems", [6] = "gold", [7] = "mithril"}
+local RES = {[0] = "wood", [1] = "mercury", [2] = "ore", [3] = "sulfur", [4] = "crystal", [5] = "gems", [6] = "gold"}
 
 local SERVICES = SERVICES
 local creatures = SERVICES:creatures()

+ 2 - 1
server/CGameHandler.cpp

@@ -2293,7 +2293,8 @@ bool CGameHandler::spellResearch(ObjectInstanceID tid, SpellID spellAtSlot, bool
 		return true;
 	}
 
-	auto costBase = TResources(gameInfo().getSettings().getValue(EGameSettings::TOWNS_SPELL_RESEARCH_COST).Vector()[level]);
+	ResourceSet costBase;
+	costBase.resolveFromJson(gameInfo().getSettings().getValue(EGameSettings::TOWNS_SPELL_RESEARCH_COST).Vector()[level]);
 	auto costExponent = gameInfo().getSettings().getValue(EGameSettings::TOWNS_SPELL_RESEARCH_COST_EXPONENT_PER_RESEARCH).Vector()[level].Float();
 	auto cost = costBase * std::pow(t->spellResearchAcceptedCounter + 1, costExponent);
 

+ 3 - 2
server/CVCMIServer.cpp

@@ -20,6 +20,7 @@
 #include "../lib/campaign/CampaignState.h"
 #include "../lib/entities/hero/CHeroHandler.h"
 #include "../lib/entities/hero/CHeroClass.h"
+#include "../lib/entities/ResourceTypeHandler.h"
 #include "../lib/gameState/CGameState.h"
 #include "../lib/mapping/CMapInfo.h"
 #include "../lib/mapping/CMapHeader.h"
@@ -709,7 +710,7 @@ void CVCMIServer::setPlayerHandicap(PlayerColor color, Handicap handicap)
 		return;
 	}
 
-	for(auto & res : EGameResID::ALL_RESOURCES())
+	for(auto & res : LIBRARY->resourceTypeHandler->getAllObjects())
 		if(handicap.startBonus[res] != 0)
 		{
 			str.appendRawString(" ");
@@ -1008,7 +1009,7 @@ void CVCMIServer::multiplayerWelcomeMessage()
 			str.appendRawString(" ");
 			str.appendName(pi.first);
 			str.appendRawString(":");
-			for(auto & res : EGameResID::ALL_RESOURCES())
+			for(auto & res : LIBRARY->resourceTypeHandler->getAllObjects())
 				if(pi.second.handicap.startBonus[res] != 0)
 				{
 					str.appendRawString(" ");

+ 5 - 4
server/processors/NewTurnProcessor.cpp

@@ -21,6 +21,7 @@
 #include "../../lib/constants/StringConstants.h"
 #include "../../lib/entities/building/CBuilding.h"
 #include "../../lib/entities/faction/CTownHandler.h"
+#include "../../lib/entities/ResourceTypeHandler.h"
 #include "../../lib/gameState/CGameState.h"
 #include "../../lib/gameState/SThievesGuildInfo.h"
 #include "../../lib/mapObjects/CGHeroInstance.h"
@@ -58,7 +59,7 @@ void NewTurnProcessor::handleTimeEvents(PlayerColor color)
 		if (!event.resources.empty())
 		{
 			gameHandler->giveResources(color, event.resources);
-			for (GameResID i : GameResID::ALL_RESOURCES())
+			for (GameResID i : LIBRARY->resourceTypeHandler->getAllObjects())
 				if (event.resources[i])
 					iw.components.emplace_back(ComponentType::RESOURCE, i, event.resources[i]);
 		}
@@ -94,7 +95,7 @@ void NewTurnProcessor::handleTownEvents(const CGTownInstance * town)
 		{
 			gameHandler->giveResources(player, event.resources);
 
-			for (GameResID i : GameResID::ALL_RESOURCES())
+			for (GameResID i : LIBRARY->resourceTypeHandler->getAllObjects())
 				if (event.resources[i])
 					iw.components.emplace_back(ComponentType::RESOURCE, i, event.resources[i]);
 		}
@@ -256,9 +257,9 @@ ResourceSet NewTurnProcessor::generatePlayerIncome(PlayerColor playerID, bool ne
 		const JsonNode & difficultyConfig = weeklyBonusesConfig[difficultyName];
 
 		// Distribute weekly bonuses over 7 days, depending on the current day of the week
-		for (GameResID i : GameResID::ALL_RESOURCES())
+		for (GameResID i : LIBRARY->resourceTypeHandler->getAllObjects())
 		{
-			const std::string & name = GameConstants::RESOURCE_NAMES[i.getNum()];
+			const std::string & name = i.toResource()->getJsonKey();
 			int64_t weeklyBonus = difficultyConfig[name].Integer();
 			int64_t dayOfWeek = gameHandler->gameState().getDate(Date::DAY_OF_WEEK);
 			int64_t dailyIncome = incomeHandicapped[i];

+ 2 - 1
server/processors/PlayerMessageProcessor.cpp

@@ -23,6 +23,7 @@
 #include "../../lib/entities/artifact/CArtHandler.h"
 #include "../../lib/entities/building/CBuilding.h"
 #include "../../lib/entities/hero/CHeroHandler.h"
+#include "../../lib/entities/ResourceTypeHandler.h"
 #include "../../lib/gameState/CGameState.h"
 #include "../../lib/mapObjects/CGTownInstance.h"
 #include "../../lib/mapObjects/CGHeroInstance.h"
@@ -593,7 +594,7 @@ void PlayerMessageProcessor::cheatResources(PlayerColor player, std::vector<std:
 
 	TResources resources;
 	resources[EGameResID::GOLD] = baseResourceAmount * 1000;
-	for (GameResID i = EGameResID::WOOD; i < EGameResID::GOLD; ++i)
+	for (auto & i : LIBRARY->resourceTypeHandler->getAllObjects())
 		resources[i] = baseResourceAmount;
 
 	gameHandler->giveResources(player, resources);

+ 0 - 2
test/erm/ERM_OB_T.cpp

@@ -13,8 +13,6 @@
 
 #include "../scripting/ScriptFixture.h"
 
-#include "../../lib/mapObjects/CObjectHandler.h"
-
 namespace test
 {
 namespace scripting

+ 1 - 0
test/mock/mock_Services.h

@@ -21,6 +21,7 @@ public:
 	MOCK_CONST_METHOD0(factions, const FactionService *());
 	MOCK_CONST_METHOD0(heroClasses, const HeroClassService *());
 	MOCK_CONST_METHOD0(heroTypes, const HeroTypeService *());
+	MOCK_CONST_METHOD0(resources, const ResourceTypeService *());
 #if SCRIPTING_ENABLED
 	MOCK_CONST_METHOD0(scripts, const scripting::Service *());
 #endif

+ 0 - 1
test/vcai/ResourceManagerTest.cpp

@@ -214,7 +214,6 @@ TEST_F(ResourceManagerTest, freeResources)
 	ASSERT_GE(res[EGameResID::CRYSTAL], 0);
 	ASSERT_GE(res[EGameResID::GEMS], 0);
 	ASSERT_GE(res[EGameResID::GOLD], 0);
-	ASSERT_GE(res[EGameResID::MITHRIL], 0);
 }
 
 TEST_F(ResourceManagerTest, freeResourcesWithManyGoals)