Browse Source

Merge pull request #4824 from IvanSavenko/translate_fix

Fixes for issues with translations
Ivan Savenko 11 months ago
parent
commit
a97deea563

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

@@ -42,6 +42,12 @@
 	"vcmi.heroOverview.secondarySkills" : "Secondary Skills",
 	"vcmi.heroOverview.spells" : "Spells",
 	
+	"vcmi.quickExchange.moveUnit" : "Move Unit",
+	"vcmi.quickExchange.moveAllUnits" : "Move All Units",
+	"vcmi.quickExchange.swapAllUnits" : "Swap Armies",
+	"vcmi.quickExchange.moveAllArtifacts" : "Move All Artifacts",
+	"vcmi.quickExchange.swapAllArtifacts" : "Swap Artifact",
+	
 	"vcmi.radialWheel.mergeSameUnit" : "Merge same creatures",
 	"vcmi.radialWheel.fillSingleUnit" : "Fill with single creatures",
 	"vcmi.radialWheel.splitSingleUnit" : "Split off single creature",
@@ -60,6 +66,16 @@
 	"vcmi.radialWheel.moveUp" : "Move up",
 	"vcmi.radialWheel.moveDown" : "Move down",
 	"vcmi.radialWheel.moveBottom" : "Move to bottom",
+	
+	"vcmi.randomMap.description" : "Map created by the Random Map Generator.\nTemplate was %s, size %dx%d, levels %d, players %d, computers %d, water %s, monster %s, VCMI map",
+	"vcmi.randomMap.description.isHuman" : ", %s is human",
+	"vcmi.randomMap.description.townChoice" : ", %s town choice is %s",
+	"vcmi.randomMap.description.water.none" : "none",
+	"vcmi.randomMap.description.water.normal" : "normal",
+	"vcmi.randomMap.description.water.islands" : "islands",
+	"vcmi.randomMap.description.monster.weak" : "weak",
+	"vcmi.randomMap.description.monster.normal" : "normal",
+	"vcmi.randomMap.description.monster.strong" : "strong",
 
 	"vcmi.spellBook.search" : "search...",
 

+ 66 - 0
Mods/vcmi/config/vcmi/spells.json

@@ -0,0 +1,66 @@
+{
+	"core:summonDemons" : {
+		"name": "Summon Demons"
+	},
+	"core:firstAid" : {
+		"name": "First Aid"
+	},
+	"core:catapultShot" : {
+		"name": "Catapult shot"
+	},
+	"core:cyclopsShot" : {
+		"name": "Siege shot"
+	},
+	
+	"core:fireWallTrigger" : {
+		"name" : "Fire Wall"
+	},
+	"core:landMineTrigger" : {
+		"name" : "Land Mine",
+	},	
+	"core:castleMoatTrigger" : {
+		"name": "Moat"
+	},
+	"core:castleMoat": {
+		"name": "Moat"
+	},
+	"core:rampartMoatTrigger" : {
+		"name": "Brambles"
+	},
+	"core:rampartMoat": {
+		"name": "Brambles"
+	},
+	"core:towerMoat": {
+		"name": "Land Mine"
+	},
+	"core:infernoMoatTrigger" : {
+		"name": "Lava"
+	},
+	"core:infernoMoat": {
+		"name": "Lava"
+	},
+	"core:necropolisMoatTrigger" : {
+		"name": "Boneyard"
+	},
+	"core:necropolisMoat": {
+		"name": "Boneyard"
+	},
+	"core:dungeonMoatTrigger" : {
+		"name": "Boiling Oil"
+	},
+	"core:dungeonMoat": {
+		"name": "Boiling Oil"
+	},
+	"core:strongholdMoatTrigger" : {
+		"name": "Wooden Spikes"
+	},
+	"core:strongholdMoat": {
+		"name": "Wooden Spikes"
+	},
+	"core:fortressMoatTrigger" : {
+		"name": "Boiling Tar"
+	},
+	"core:fortressMoat": {
+		"name": "Boiling Tar"
+	}
+}

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

@@ -612,18 +612,5 @@
 	"core.bonus.WIDE_BREATH.name" : "Широкий подих",
 	"core.bonus.WIDE_BREATH.description" : "Атака широким подихом",
 	"core.bonus.LIMITED_SHOOTING_RANGE.name" : "Обмежена дальність стрільби",
-	"core.bonus.LIMITED_SHOOTING_RANGE.description" : "Не може стріляти по цілях на відстані більше ${val} гексів",
-	
-	"vcmi.stackExperience.description" : "» S t a c k   E x p e r i e n c e   D e t a i l s «\n\nCreature Type ................... : %s\nExperience Rank ................. : %s (%i)\nExperience Points ............... : %i\nExperience Points to Next Rank .. : %i\nMaximum Experience per Battle ... : %i%% (%i)\nNumber of Creatures in stack .... : %i\nMaximum New Recruits\n without losing current Rank .... : %i\nExperience Multiplier ........... : %.2f\nUpgrade Multiplier .............. : %.2f\nExperience after Rank 10 ........ : %i\nMaximum New Recruits to remain at\n Rank 10 if at Maximum Experience : %i",
-	"vcmi.stackExperience.rank.0" :  "Початковий",
-	"vcmi.stackExperience.rank.1" :  "Новачок",
-	"vcmi.stackExperience.rank.2" :  "Підготовлений",
-	"vcmi.stackExperience.rank.3" :  "Досвідчений",
-	"vcmi.stackExperience.rank.4" :  "Випробуваний",
-	"vcmi.stackExperience.rank.5" :  "Ветеран",
-	"vcmi.stackExperience.rank.6" :  "Адепт",
-	"vcmi.stackExperience.rank.7" :  "Експерт",
-	"vcmi.stackExperience.rank.8" :  "Еліта",
-	"vcmi.stackExperience.rank.9" : "Майстер",
-	"vcmi.stackExperience.rank.10" : "Профі"
+	"core.bonus.LIMITED_SHOOTING_RANGE.description" : "Не може стріляти по цілях на відстані більше ${val} гексів"
 }

+ 1 - 0
Mods/vcmi/mod.json

@@ -127,6 +127,7 @@
 
 	"factions" : [ "config/vcmi/towerFactions" ],
 	"creatures" : [ "config/vcmi/towerCreature" ],
+	"spells" : [ "config/vcmi/spells" ],
 
 	"translations" : [
 		"config/vcmi/english.json"

+ 18 - 8
client/ClientCommandManager.cpp

@@ -185,12 +185,12 @@ void ClientCommandManager::handleRedrawCommand()
 	GH.windows().totalRedraw();
 }
 
-void ClientCommandManager::handleTranslateGameCommand()
+void ClientCommandManager::handleTranslateGameCommand(bool onlyMissing)
 {
 	std::map<std::string, std::map<std::string, std::string>> textsByMod;
-	VLC->generaltexth->exportAllTexts(textsByMod);
+	VLC->generaltexth->exportAllTexts(textsByMod, onlyMissing);
 
-	const boost::filesystem::path outPath = VCMIDirs::get().userExtractedPath() / "translation";
+	const boost::filesystem::path outPath = VCMIDirs::get().userExtractedPath() / ( onlyMissing ? "translationMissing" : "translation");
 	boost::filesystem::create_directories(outPath);
 
 	for(const auto & modEntry : textsByMod)
@@ -254,13 +254,20 @@ void ClientCommandManager::handleTranslateMapsCommand()
 	logGlobal->info("Loading campaigns for export");
 	for (auto const & campaignName : campaignList)
 	{
-		loadedCampaigns.push_back(CampaignHandler::getCampaign(campaignName.getName()));
-		for (auto const & part : loadedCampaigns.back()->allScenarios())
-			loadedCampaigns.back()->getMap(part, nullptr);
+		try
+		{
+			loadedCampaigns.push_back(CampaignHandler::getCampaign(campaignName.getName()));
+			for (auto const & part : loadedCampaigns.back()->allScenarios())
+				loadedCampaigns.back()->getMap(part, nullptr);
+		}
+		catch(std::exception & e)
+		{
+			logGlobal->warn("Campaign %s is invalid. Message: %s", campaignName.getName(), e.what());
+		}
 	}
 
 	std::map<std::string, std::map<std::string, std::string>> textsByMod;
-	VLC->generaltexth->exportAllTexts(textsByMod);
+	VLC->generaltexth->exportAllTexts(textsByMod, false);
 
 	const boost::filesystem::path outPath = VCMIDirs::get().userExtractedPath() / "translation";
 	boost::filesystem::create_directories(outPath);
@@ -591,7 +598,10 @@ void ClientCommandManager::processCommand(const std::string & message, bool call
 		handleRedrawCommand();
 
 	else if(message=="translate" || message=="translate game")
-		handleTranslateGameCommand();
+		handleTranslateGameCommand(false);
+
+	else if(message=="translate missing")
+		handleTranslateGameCommand(true);
 
 	else if(message=="translate maps")
 		handleTranslateMapsCommand();

+ 1 - 1
client/ClientCommandManager.h

@@ -46,7 +46,7 @@ class ClientCommandManager //take mantis #2292 issue about account if thinking a
 	void handleRedrawCommand();
 
 	// Extracts all translateable game texts into Translation directory, separating files on per-mod basis
-	void handleTranslateGameCommand();
+	void handleTranslateGameCommand(bool onlyMissing);
 
 	// Extracts all translateable texts from maps and campaigns into Translation directory, separating files on per-mod basis
 	void handleTranslateMapsCommand();

+ 19 - 3
client/windows/CCreatureWindow.cpp

@@ -393,7 +393,7 @@ CStackWindow::CommanderMainSection::CommanderMainSection(CStackWindow * owner, i
 
 	auto getSkillDescription = [this](int skillIndex) -> std::string
 	{
-		return CGI->generaltexth->znpc00[152 + (12 * skillIndex) + (parent->info->commander->secondarySkills[skillIndex] * 2)];
+		return parent->getCommanderSkillDescription(skillIndex, parent->info->commander->secondarySkills[skillIndex]);
 	};
 
 	for(int index = ECommander::ATTACK; index <= ECommander::SPELL_POWER; ++index)
@@ -905,14 +905,30 @@ std::string CStackWindow::generateStackExpDescription()
 	return expText;
 }
 
+std::string CStackWindow::getCommanderSkillDescription(int skillIndex, int skillLevel)
+{
+	constexpr std::array skillNames = {
+		"attack",
+		"defence",
+		"health",
+		"damage",
+		"speed",
+		"magic"
+	};
+
+	std::string textID = TextIdentifier("vcmi", "commander", "skill", skillNames.at(skillIndex), skillLevel).get();
+
+	return CGI->generaltexth->translate(textID);
+}
+
 void CStackWindow::setSelection(si32 newSkill, std::shared_ptr<CCommanderSkillIcon> newIcon)
 {
 	auto getSkillDescription = [this](int skillIndex, bool selected) -> std::string
 	{
 		if(selected)
-			return CGI->generaltexth->znpc00[152 + (12 * skillIndex) + ((info->commander->secondarySkills[skillIndex] + 1) * 2)]; //upgrade description
+			return getCommanderSkillDescription(skillIndex, info->commander->secondarySkills[skillIndex] + 1); //upgrade description
 		else
-			return CGI->generaltexth->znpc00[152 + (12 * skillIndex) + (info->commander->secondarySkills[skillIndex] * 2)];
+			return getCommanderSkillDescription(skillIndex, info->commander->secondarySkills[skillIndex]);
 	};
 
 	auto getSkillImage = [this](int skillIndex)

+ 1 - 0
client/windows/CCreatureWindow.h

@@ -189,6 +189,7 @@ class CStackWindow : public CWindowObject
 	void init();
 
 	std::string generateStackExpDescription();
+	std::string getCommanderSkillDescription(int skillIndex, int skillLevel);
 
 public:
 	// for battles

+ 46 - 12
client/windows/CExchangeWindow.cpp

@@ -192,18 +192,52 @@ CExchangeWindow::CExchangeWindow(ObjectInstanceID hero1, ObjectInstanceID hero2,
 
 	if(qeLayout)
 	{
-		buttonMoveUnitsFromLeftToRight = std::make_shared<CButton>(Point(325, 118), AnimationPath::builtin("quick-exchange/armRight.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[1]), [this](){ this->moveUnitsShortcut(true); });
-		buttonMoveUnitsFromRightToLeft = std::make_shared<CButton>(Point(425, 118), AnimationPath::builtin("quick-exchange/armLeft.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[1]), [this](){ this->moveUnitsShortcut(false); });
-		buttonMoveArtifactsFromLeftToRight = std::make_shared<CButton>(Point(325, 154), AnimationPath::builtin("quick-exchange/artRight.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[3]), [this](){ this->moveArtifactsCallback(true);});
-		buttonMoveArtifactsFromRightToLeft = std::make_shared<CButton>(Point(425, 154), AnimationPath::builtin("quick-exchange/artLeft.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[3]), [this](){ this->moveArtifactsCallback(false);});
-
-		exchangeUnitsButton = std::make_shared<CButton>(Point(377, 118), AnimationPath::builtin("quick-exchange/swapAll.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[2]), [this](){ controller.swapArmy(); });
-		exchangeArtifactsButton  = std::make_shared<CButton>(Point(377, 154), AnimationPath::builtin("quick-exchange/swapAll.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[4]), [this](){ this->swapArtifactsCallback(); });
-
-		backpackButtonLeft = std::make_shared<CButton>(Point(325, 518), AnimationPath::builtin("heroBackpack"), CButton::tooltipLocalized("vcmi.heroWindow.openBackpack"),
+		buttonMoveUnitsFromLeftToRight = std::make_shared<CButton>(
+			Point(325, 118),
+			AnimationPath::builtin("quick-exchange/armRight.DEF"),
+			CButton::tooltip(CGI->generaltexth->translate("vcmi.quickExchange.moveAllUnits")),
+			[this](){ this->moveUnitsShortcut(true); });
+
+		buttonMoveUnitsFromRightToLeft = std::make_shared<CButton>(
+			Point(425, 118),
+			AnimationPath::builtin("quick-exchange/armLeft.DEF"),
+			CButton::tooltip(CGI->generaltexth->translate("vcmi.quickExchange.moveAllUnits")),
+			[this](){ this->moveUnitsShortcut(false); });
+
+		buttonMoveArtifactsFromLeftToRight = std::make_shared<CButton>(
+			Point(325, 154), AnimationPath::builtin("quick-exchange/artRight.DEF"),
+			CButton::tooltip(CGI->generaltexth->translate("vcmi.quickExchange.moveAllArtifacts")),
+			[this](){ this->moveArtifactsCallback(true);});
+
+		buttonMoveArtifactsFromRightToLeft = std::make_shared<CButton>(
+			Point(425, 154), AnimationPath::builtin("quick-exchange/artLeft.DEF"),
+			CButton::tooltip(CGI->generaltexth->translate("vcmi.quickExchange.moveAllArtifacts")),
+			[this](){ this->moveArtifactsCallback(false);});
+
+		exchangeUnitsButton = std::make_shared<CButton>(
+			Point(377, 118),
+			AnimationPath::builtin("quick-exchange/swapAll.DEF"),
+			CButton::tooltip(CGI->generaltexth->translate("vcmi.quickExchange.swapAllUnits")),
+			[this](){ controller.swapArmy(); });
+
+		exchangeArtifactsButton  = std::make_shared<CButton>(
+			Point(377, 154),
+			AnimationPath::builtin("quick-exchange/swapAll.DEF"),
+			CButton::tooltip(CGI->generaltexth->translate("vcmi.quickExchange.swapAllArtifacts")),
+			[this](){ this->swapArtifactsCallback(); });
+
+		backpackButtonLeft = std::make_shared<CButton>(
+			Point(325, 518),
+			AnimationPath::builtin("heroBackpack"),
+			CButton::tooltipLocalized("vcmi.heroWindow.openBackpack"),
 			[this](){ this->backpackShortcut(true); });
-		backpackButtonRight = std::make_shared<CButton>(Point(419, 518), AnimationPath::builtin("heroBackpack"), CButton::tooltipLocalized("vcmi.heroWindow.openBackpack"),
+
+		backpackButtonRight = std::make_shared<CButton>(
+			Point(419, 518),
+			AnimationPath::builtin("heroBackpack"),
+			CButton::tooltipLocalized("vcmi.heroWindow.openBackpack"),
 			[this](){ this->backpackShortcut(false); });
+
 		backpackButtonLeft->setOverlay(std::make_shared<CPicture>(ImagePath::builtin("heroWindow/backpackButtonIcon")));
 		backpackButtonRight->setOverlay(std::make_shared<CPicture>(ImagePath::builtin("heroWindow/backpackButtonIcon")));
 
@@ -227,7 +261,7 @@ CExchangeWindow::CExchangeWindow(ObjectInstanceID hero1, ObjectInstanceID hero2,
 				std::make_shared<CButton>(
 					Point(484 + 35 * i, 154),
 					AnimationPath::builtin("quick-exchange/unitLeft.DEF"),
-					CButton::tooltip(CGI->generaltexth->qeModCommands[1]),
+					CButton::tooltip(CGI->generaltexth->translate("vcmi.quickExchange.moveUnit")),
 					std::bind(&CExchangeController::moveStack, &controller, false, SlotID(i))));
 			moveUnitFromRightToLeftButtons.back()->block(leftHeroBlock);
 
@@ -235,7 +269,7 @@ CExchangeWindow::CExchangeWindow(ObjectInstanceID hero1, ObjectInstanceID hero2,
 				std::make_shared<CButton>(
 					Point(66 + 35 * i, 154),
 					AnimationPath::builtin("quick-exchange/unitRight.DEF"),
-					CButton::tooltip(CGI->generaltexth->qeModCommands[1]),
+					CButton::tooltip(CGI->generaltexth->translate("vcmi.quickExchange.moveUnit")),
 					std::bind(&CExchangeController::moveStack, &controller, true, SlotID(i))));
 			moveUnitFromLeftToRightButtons.back()->block(rightHeroBlock);
 		}

+ 730 - 730
config/spells/moats.json

@@ -1,732 +1,732 @@
 {
-    "castleMoatTrigger" :
-    {
-        "targetType" : "CREATURE",
-        "type": "ability",
-        "name": "Moat",
-        "school": {},
-        "level": 0,
-        "power": 0,
-        "gainChance": {},
-        "levels" : {
-            "base": {
-                "power" : 0,
-                "range" : "0",
-                "description" : "", //For validation
-                "cost" : 0, //For validation
-                "aiValue" : 0, //For validation
-                "battleEffects" : {
-                    "directDamage" : {
-                        "type":"core:damage"
-                    }
-                },
-                "targetModifier":{"smart":false}
-            },
-            "none" : {
-            },
-            "basic" : {
-            },
-            "advanced" : {
-            },
-            "expert" : {
-            }
-        },
-        "flags" : {
-            "damage": true,
-            "negative": true,
-            "nonMagical" : true,
-            "special": true
-        },
-        "targetCondition" : {
-        }
-    },
-    "castleMoat": {
-        "targetType" : "NO_TARGET",
-        "type": "ability",
-        "name": "Moat",
-        "school" : {},
-        "level": 0,
-        "power": 0,
-        "defaultGainChance": 0,
-        "gainChance": {},
-        "levels" : {
-            "base":{
-                "description" : "",
-                "aiValue" : 0,
-                "power" : 0,
-                "cost" : 0,
-                "targetModifier":{"smart":false},
-                "battleEffects":{
-                    "moat":{
-                        "type":"core:moat",
-                        "hidden" : false,
-                        "trap" : true,
-                        "triggerAbility" : "core:castleMoatTrigger",
-                        "dispellable" : false,
-                        "removeOnTrigger" : false,
-                        "moatDamage" : 70,
-                        "moatHexes" : [[11, 28, 44, 61, 77, 111, 129, 146, 164, 181]],
-                        "defender" :{
-                        },
-                        "bonus" :{
-                            "primarySkill" : {
-                                "val" : -3,
-                                "type" : "PRIMARY_SKILL",
-                                "subtype" : "primarySkill.defence",
-                                "valueType" : "ADDITIVE_VALUE"
-                            }
-                        }
-                    }
-                },
-                "range" : "X"
-            },
-            "none" :{
-            },
-            "basic" :{
-            },
-            "advanced" :{
-            },
-            "expert" :{
-            }
-        },
-        "flags" : {
-            "nonMagical" : true,
-            "indifferent": true
-        },
-        "targetCondition" : {
-        }
-    },
-    "rampartMoatTrigger" :
-    {
-        "targetType" : "CREATURE",
-        "type": "ability",
-        "name": "Brambles",
-        "school": {},
-        "level": 0,
-        "power": 0,
-        "gainChance": {},
-        "levels" : {
-            "base": {
-                "power" : 0,
-                "range" : "0",
-                "description" : "", //For validation
-                "cost" : 0, //For validation
-                "aiValue" : 0, //For validation
-                "battleEffects" : {
-                    "directDamage" : {
-                        "type":"core:damage"
-                    }
-                },
-                "targetModifier":{"smart":false}
-            },
-            "none" : {
-            },
-            "basic" : {
-            },
-            "advanced" : {
-            },
-            "expert" : {
-            }
-        },
-        "flags" : {
-            "damage": true,
-            "negative": true,
-            "nonMagical" : true,
-            "special": true
-        },
-        "targetCondition" : {
-        }
-    },
-    "rampartMoat": {
-        "targetType" : "NO_TARGET",
-        "type": "ability",
-        "name": "Brambles",
-        "school" : {},
-        "level": 0,
-        "power": 0,
-        "defaultGainChance": 0,
-        "gainChance": {},
-        "levels" : {
-            "base":{
-                "description" : "",
-                "aiValue" : 0,
-                "power" : 0,
-                "cost" : 0,
-                "targetModifier":{"smart":false},
-                "battleEffects":{
-                    "moat":{
-                        "type":"core:moat",
-                        "hidden" : false,
-                        "trap" : true,
-                        "triggerAbility" : "core:rampartMoatTrigger",
-                        "dispellable" : false,
-                        "removeOnTrigger" : false,
-                        "moatDamage" : 70,
-                        "moatHexes" : [[11, 28, 44, 61, 77, 111, 129, 146, 164, 181]],
-                        "defender" :{
-                        },
-                        "bonus" :{
-                            "primarySkill" : {
-                                "val" : -3,
-                                "type" : "PRIMARY_SKILL",
-                                "subtype" : "primarySkill.defence",
-                                "valueType" : "ADDITIVE_VALUE"
-                            }
-                        }
-                    }
-                },
-                "range" : "X"
-            },
-            "none" :{
-            },
-            "basic" :{
-            },
-            "advanced" :{
-            },
-            "expert" :{
-            }
-        },
-        "flags" : {
-            "nonMagical" : true,
-            "indifferent": true
-        },
-        "targetCondition" : {
-        }
-    },
-    "towerMoat": {
-        "targetType" : "NO_TARGET",
-        "type": "ability",
-        "name": "Land Mine",
-        "school" : {},
-        "level": 3,
-        "power": 0,
-        "defaultGainChance": 0,
-        "gainChance": {},
-        "levels" : {
-            "base":{
-                "description" : "",
-                "aiValue" : 0,
-                "power" : 0,
-                "cost" : 0,
-                "targetModifier":{"smart":false},
-                "battleEffects":{
-                    "moat":{
-                        "type":"core:moat",
-                        "hidden" : true,
-                        "trap" : false,
-                        "triggerAbility" : "core:landMineTrigger",
-                        "dispellable" : true,
-                        "removeOnTrigger" : true,
-                        "moatDamage" : 150,
-                        "moatHexes" : [[11], [28], [44], [61], [77], [111], [129], [146], [164], [181]],
-                        "defender" :{
-                            "animation" : "C09SPF1",
-                            "appearAnimation" : "C09SPF0",
-                            "appearSound" : "LANDMINE"
-                        }
-                    }
-                },
-                "range" : "X"
-            },
-            "none" :{
-            },
-            "basic" :{
-            },
-            "advanced" :{
-            },
-            "expert" :{
-            }
-        },
-        "flags" : {
-            "nonMagical" : true,
-            "indifferent": true
-        },
-        "targetCondition" : {
-        }
-    },
-    "infernoMoatTrigger" :
-    {
-        "targetType" : "CREATURE",
-        "type": "ability",
-        "name": "Lava",
-        "school": {},
-        "level": 0,
-        "power": 0,
-        "gainChance": {},
-        "levels" : {
-            "base": {
-                "power" : 0,
-                "range" : "0",
-                "description" : "", //For validation
-                "cost" : 0, //For validation
-                "aiValue" : 0, //For validation
-                "battleEffects" : {
-                    "directDamage" : {
-                        "type":"core:damage"
-                    }
-                },
-                "targetModifier":{"smart":false}
-            },
-            "none" : {
-            },
-            "basic" : {
-            },
-            "advanced" : {
-            },
-            "expert" : {
-            }
-        },
-        "flags" : {
-            "damage": true,
-            "negative": true,
-            "nonMagical" : true,
-            "special": true
-        },
-        "targetCondition" : {
-        }
-    },
-    "infernoMoat": {
-        "targetType" : "NO_TARGET",
-        "type": "ability",
-        "name": "Lava",
-        "school" : {},
-        "level": 0,
-        "power": 0,
-        "defaultGainChance": 0,
-        "gainChance": {},
-        "levels" : {
-            "base":{
-                "description" : "",
-                "aiValue" : 0,
-                "power" : 0,
-                "cost" : 0,
-                "targetModifier":{"smart":false},
-                "battleEffects":{
-                    "moat":{
-                        "type":"core:moat",
-                        "hidden" : false,
-                        "trap" : true,
-                        "triggerAbility" : "core:infernoMoatTrigger",
-                        "dispellable" : false,
-                        "removeOnTrigger" : false,
-                        "moatDamage" : 90,
-                        "moatHexes" : [[11, 28, 44, 61, 77, 111, 129, 146, 164, 181]],
-                        "defender" :{
-                        },
-                        "bonus" :{
-                            "primarySkill" : {
-                                "val" : -3,
-                                "type" : "PRIMARY_SKILL",
-                                "subtype" : "primarySkill.defence",
-                                "valueType" : "ADDITIVE_VALUE"
-                            }
-                        }
-                    }
-                },
-                "range" : "X"
-            },
-            "none" :{
-            },
-            "basic" :{
-            },
-            "advanced" :{
-            },
-            "expert" :{
-            }
-        },
-        "flags" : {
-            "nonMagical" : true,
-            "indifferent": true
-        },
-        "targetCondition" : {
-        }
-    },
-    "necropolisMoatTrigger" :
-    {
-        "targetType" : "CREATURE",
-        "type": "ability",
-        "name": "Boneyard",
-        "school": {},
-        "level": 0,
-        "power": 0,
-        "gainChance": {},
-        "levels" : {
-            "base": {
-                "power" : 0,
-                "range" : "0",
-                "description" : "", //For validation
-                "cost" : 0, //For validation
-                "aiValue" : 0, //For validation
-                "battleEffects" : {
-                    "directDamage" : {
-                        "type":"core:damage"
-                    }
-                },
-                "targetModifier":{"smart":false}
-            },
-            "none" : {
-            },
-            "basic" : {
-            },
-            "advanced" : {
-            },
-            "expert" : {
-            }
-        },
-        "flags" : {
-            "damage": true,
-            "negative": true,
-            "nonMagical" : true,
-            "special": true
-        },
-        "targetCondition" : {
-        }
-    },
-    "necropolisMoat": {
-        "targetType" : "NO_TARGET",
-        "type": "ability",
-        "name": "Boneyard",
-        "school" : {},
-        "level": 0,
-        "power": 0,
-        "defaultGainChance": 0,
-        "gainChance": {},
-        "levels" : {
-            "base":{
-                "description" : "",
-                "aiValue" : 0,
-                "power" : 0,
-                "cost" : 0,
-                "targetModifier":{"smart":false},
-                "battleEffects":{
-                    "moat":{
-                        "type":"core:moat",
-                        "hidden" : false,
-                        "trap" : true,
-                        "triggerAbility" : "core:necropolisMoatTrigger",
-                        "dispellable" : false,
-                        "removeOnTrigger" : false,
-                        "moatDamage" : 70,
-                        "moatHexes" : [[11, 28, 44, 61, 77, 111, 129, 146, 164, 181]],
-                        "defender" :{
-                        },
-                        "bonus" :{
-                            "primarySkill" : {
-                                "val" : -3,
-                                "type" : "PRIMARY_SKILL",
-                                "subtype" : "primarySkill.defence",
-                                "valueType" : "ADDITIVE_VALUE"
-                            }
-                        }
-                    }
-                },
-                "range" : "X"
-            },
-            "none" :{
-            },
-            "basic" :{
-            },
-            "advanced" :{
-            },
-            "expert" :{
-            }
-        },
-        "flags" : {
-            "nonMagical" : true,
-            "indifferent": true
-        },
-        "targetCondition" : {
-        }
-    },
-    "dungeonMoatTrigger" :
-    {
-        "targetType" : "CREATURE",
-        "type": "ability",
-        "name": "Boiling Oil",
-        "school": {},
-        "level": 0,
-        "power": 0,
-        "gainChance": {},
-        "levels" : {
-            "base": {
-                "power" : 0,
-                "range" : "0",
-                "description" : "", //For validation
-                "cost" : 0, //For validation
-                "aiValue" : 0, //For validation
-                "battleEffects" : {
-                    "directDamage" : {
-                        "type":"core:damage"
-                    }
-                },
-                "targetModifier":{"smart":false}
-            },
-            "none" : {
-            },
-            "basic" : {
-            },
-            "advanced" : {
-            },
-            "expert" : {
-            }
-        },
-        "flags" : {
-            "damage": true,
-            "negative": true,
-            "nonMagical" : true,
-            "special": true
-        },
-        "targetCondition" : {
-        }
-    },
-    "dungeonMoat": {
-        "targetType" : "NO_TARGET",
-        "type": "ability",
-        "name": "Boiling Oil",
-        "school" : {},
-        "level": 0,
-        "power": 0,
-        "defaultGainChance": 0,
-        "gainChance": {},
-        "levels" : {
-            "base":{
-                "description" : "",
-                "aiValue" : 0,
-                "power" : 0,
-                "cost" : 0,
-                "targetModifier":{"smart":false},
-                "battleEffects":{
-                    "moat":{
-                        "type":"core:moat",
-                        "hidden" : false,
-                        "trap" : true,
-                        "triggerAbility" : "core:dungeonMoatTrigger",
-                        "dispellable" : false,
-                        "removeOnTrigger" : false,
-                        "moatDamage" : 90,
-                        "moatHexes" : [[11, 28, 44, 61, 77, 111, 129, 146, 164, 181]],
-                        "defender" :{
-                        },
-                        "bonus" :{
-                            "primarySkill" : {
-                                "val" : -3,
-                                "type" : "PRIMARY_SKILL",
-                                "subtype" : "primarySkill.defence",
-                                "valueType" : "ADDITIVE_VALUE"
-                            }
-                        }
-                    }
-                },
-                "range" : "X"
-            },
-            "none" :{
-            },
-            "basic" :{
-            },
-            "advanced" :{
-            },
-            "expert" :{
-            }
-        },
-        "flags" : {
-            "nonMagical" : true,
-            "indifferent": true
-        },
-        "targetCondition" : {
-        }
-    },
-    "strongholdMoatTrigger" :
-    {
-        "targetType" : "CREATURE",
-        "type": "ability",
-        "name": "Wooden Spikes",
-        "school": {},
-        "level": 0,
-        "power": 0,
-        "gainChance": {},
-        "levels" : {
-            "base": {
-                "power" : 0,
-                "range" : "0",
-                "description" : "", //For validation
-                "cost" : 0, //For validation
-                "aiValue" : 0, //For validation
-                "battleEffects" : {
-                    "directDamage" : {
-                        "type":"core:damage"
-                    }
-                },
-                "targetModifier":{"smart":false}
-            },
-            "none" : {
-            },
-            "basic" : {
-            },
-            "advanced" : {
-            },
-            "expert" : {
-            }
-        },
-        "flags" : {
-            "damage": true,
-            "negative": true,
-            "nonMagical" : true,
-            "special": true
-        },
-        "targetCondition" : {
-        }
-    },
-    "strongholdMoat": {
-        "targetType" : "NO_TARGET",
-        "type": "ability",
-        "name": "Wooden Spikes",
-        "school" : {},
-        "level": 0,
-        "power": 0,
-        "defaultGainChance": 0,
-        "gainChance": {},
-        "levels" : {
-            "base":{
-                "description" : "",
-                "aiValue" : 0,
-                "power" : 0,
-                "cost" : 0,
-                "targetModifier":{"smart":false},
-                "battleEffects":{
-                    "moat":{
-                        "type":"core:moat",
-                        "hidden" : false,
-                        "trap" : true,
-                        "triggerAbility" : "core:strongholdMoatTrigger",
-                        "dispellable" : false,
-                        "removeOnTrigger" : false,
-                        "moatDamage" : 70,
-                        "moatHexes" : [[11, 28, 44, 61, 77, 111, 129, 146, 164, 181]],
-                        "defender" :{
-                        },
-                        "bonus" :{
-                            "primarySkill" : {
-                                "val" : -3,
-                                "type" : "PRIMARY_SKILL",
-                                "subtype" : "primarySkill.defence",
-                                "valueType" : "ADDITIVE_VALUE"
-                            }
-                        }
-                    }
-                },
-                "range" : "X"
-            },
-            "none" :{
-            },
-            "basic" :{
-            },
-            "advanced" :{
-            },
-            "expert" :{
-            }
-        },
-        "flags" : {
-            "nonMagical" : true,
-            "indifferent": true
-        },
-        "targetCondition" : {
-        }
-    },
-    "fortressMoatTrigger" :
-    {
-        "targetType" : "CREATURE",
-        "type": "ability",
-        "name": "Boiling Tar",
-        "school": {},
-        "level": 0,
-        "power": 0,
-        "gainChance": {},
-        "levels" : {
-            "base": {
-                "power" : 0,
-                "range" : "0",
-                "description" : "", //For validation
-                "cost" : 0, //For validation
-                "aiValue" : 0, //For validation
-                "battleEffects" : {
-                    "directDamage" : {
-                        "type":"core:damage"
-                    }
-                },
-                "targetModifier":{"smart":false}
-            },
-            "none" : {
-            },
-            "basic" : {
-            },
-            "advanced" : {
-            },
-            "expert" : {
-            }
-        },
-        "flags" : {
-            "damage": true,
-            "negative": true,
-            "nonMagical" : true,
-            "special": true
-        },
-        "targetCondition" : {
-        }
-    },
-    "fortressMoat": {
-        "targetType" : "NO_TARGET",
-        "type": "ability",
-        "name": "Boiling Tar",
-        "school" : {},
-        "level": 0,
-        "power": 0,
-        "defaultGainChance": 0,
-        "gainChance": {},
-        "levels" : {
-            "base":{
-                "description" : "",
-                "aiValue" : 0,
-                "power" : 0,
-                "cost" : 0,
-                "targetModifier":{"smart":false},
-                "battleEffects":{
-                    "moat":{
-                        "type":"core:moat",
-                        "hidden" : false,
-                        "trap" : true,
-                        "triggerAbility" : "core:fortressMoatTrigger",
-                        "dispellable" : false,
-                        "removeOnTrigger" : false,
-                        "moatDamage" : 90,
-                        "moatHexes" : [[10, 11, 27, 28, 43, 44, 60, 61, 76, 77, 94, 110, 111, 128, 129, 145, 146, 163, 164, 180, 181]],
-                        "defender" :{
-                        },
-                        "bonus" :{
-                            "primarySkill" : {
-                                "val" : -3,
-                                "type" : "PRIMARY_SKILL",
-                                "subtype" : "primarySkill.defence",
-                                "valueType" : "ADDITIVE_VALUE"
-                            }
-                        }
-                    }
-                },
-                "range" : "X"
-            },
-            "none" :{
-            },
-            "basic" :{
-            },
-            "advanced" :{
-            },
-            "expert" :{
-            }
-        },
-        "flags" : {
-            "nonMagical" : true,
-            "indifferent": true
-        },
-        "targetCondition" : {
-        }
-    }
+	"castleMoatTrigger" :
+	{
+		"targetType" : "CREATURE",
+		"type": "ability",
+		"name": "",
+		"school": {},
+		"level": 0,
+		"power": 0,
+		"gainChance": {},
+		"levels" : {
+			"base": {
+				"power" : 0,
+				"range" : "0",
+				"description" : "", //For validation
+				"cost" : 0, //For validation
+				"aiValue" : 0, //For validation
+				"battleEffects" : {
+					"directDamage" : {
+						"type":"core:damage"
+					}
+				},
+				"targetModifier":{"smart":false}
+			},
+			"none" : {
+			},
+			"basic" : {
+			},
+			"advanced" : {
+			},
+			"expert" : {
+			}
+		},
+		"flags" : {
+			"damage": true,
+			"negative": true,
+			"nonMagical" : true,
+			"special": true
+		},
+		"targetCondition" : {
+		}
+	},
+	"castleMoat": {
+		"targetType" : "NO_TARGET",
+		"type": "ability",
+		"name": "",
+		"school" : {},
+		"level": 0,
+		"power": 0,
+		"defaultGainChance": 0,
+		"gainChance": {},
+		"levels" : {
+			"base":{
+				"description" : "",
+				"aiValue" : 0,
+				"power" : 0,
+				"cost" : 0,
+				"targetModifier":{"smart":false},
+				"battleEffects":{
+					"moat":{
+						"type":"core:moat",
+						"hidden" : false,
+						"trap" : true,
+						"triggerAbility" : "core:castleMoatTrigger",
+						"dispellable" : false,
+						"removeOnTrigger" : false,
+						"moatDamage" : 70,
+						"moatHexes" : [[11, 28, 44, 61, 77, 111, 129, 146, 164, 181]],
+						"defender" :{
+						},
+						"bonus" :{
+							"primarySkill" : {
+								"val" : -3,
+								"type" : "PRIMARY_SKILL",
+								"subtype" : "primarySkill.defence",
+								"valueType" : "ADDITIVE_VALUE"
+							}
+						}
+					}
+				},
+				"range" : "X"
+			},
+			"none" :{
+			},
+			"basic" :{
+			},
+			"advanced" :{
+			},
+			"expert" :{
+			}
+		},
+		"flags" : {
+			"nonMagical" : true,
+			"indifferent": true
+		},
+		"targetCondition" : {
+		}
+	},
+	"rampartMoatTrigger" :
+	{
+		"targetType" : "CREATURE",
+		"type": "ability",
+		"name": "",
+		"school": {},
+		"level": 0,
+		"power": 0,
+		"gainChance": {},
+		"levels" : {
+			"base": {
+				"power" : 0,
+				"range" : "0",
+				"description" : "", //For validation
+				"cost" : 0, //For validation
+				"aiValue" : 0, //For validation
+				"battleEffects" : {
+					"directDamage" : {
+						"type":"core:damage"
+					}
+				},
+				"targetModifier":{"smart":false}
+			},
+			"none" : {
+			},
+			"basic" : {
+			},
+			"advanced" : {
+			},
+			"expert" : {
+			}
+		},
+		"flags" : {
+			"damage": true,
+			"negative": true,
+			"nonMagical" : true,
+			"special": true
+		},
+		"targetCondition" : {
+		}
+	},
+	"rampartMoat": {
+		"targetType" : "NO_TARGET",
+		"type": "ability",
+		"name": "",
+		"school" : {},
+		"level": 0,
+		"power": 0,
+		"defaultGainChance": 0,
+		"gainChance": {},
+		"levels" : {
+			"base":{
+				"description" : "",
+				"aiValue" : 0,
+				"power" : 0,
+				"cost" : 0,
+				"targetModifier":{"smart":false},
+				"battleEffects":{
+					"moat":{
+						"type":"core:moat",
+						"hidden" : false,
+						"trap" : true,
+						"triggerAbility" : "core:rampartMoatTrigger",
+						"dispellable" : false,
+						"removeOnTrigger" : false,
+						"moatDamage" : 70,
+						"moatHexes" : [[11, 28, 44, 61, 77, 111, 129, 146, 164, 181]],
+						"defender" :{
+						},
+						"bonus" :{
+							"primarySkill" : {
+								"val" : -3,
+								"type" : "PRIMARY_SKILL",
+								"subtype" : "primarySkill.defence",
+								"valueType" : "ADDITIVE_VALUE"
+							}
+						}
+					}
+				},
+				"range" : "X"
+			},
+			"none" :{
+			},
+			"basic" :{
+			},
+			"advanced" :{
+			},
+			"expert" :{
+			}
+		},
+		"flags" : {
+			"nonMagical" : true,
+			"indifferent": true
+		},
+		"targetCondition" : {
+		}
+	},
+	"towerMoat": {
+		"targetType" : "NO_TARGET",
+		"type": "ability",
+		"name": "",
+		"school" : {},
+		"level": 3,
+		"power": 0,
+		"defaultGainChance": 0,
+		"gainChance": {},
+		"levels" : {
+			"base":{
+				"description" : "",
+				"aiValue" : 0,
+				"power" : 0,
+				"cost" : 0,
+				"targetModifier":{"smart":false},
+				"battleEffects":{
+					"moat":{
+						"type":"core:moat",
+						"hidden" : true,
+						"trap" : false,
+						"triggerAbility" : "core:landMineTrigger",
+						"dispellable" : true,
+						"removeOnTrigger" : true,
+						"moatDamage" : 150,
+						"moatHexes" : [[11], [28], [44], [61], [77], [111], [129], [146], [164], [181]],
+						"defender" :{
+							"animation" : "C09SPF1",
+							"appearAnimation" : "C09SPF0",
+							"appearSound" : "LANDMINE"
+						}
+					}
+				},
+				"range" : "X"
+			},
+			"none" :{
+			},
+			"basic" :{
+			},
+			"advanced" :{
+			},
+			"expert" :{
+			}
+		},
+		"flags" : {
+			"nonMagical" : true,
+			"indifferent": true
+		},
+		"targetCondition" : {
+		}
+	},
+	"infernoMoatTrigger" :
+	{
+		"targetType" : "CREATURE",
+		"type": "ability",
+		"name": "",
+		"school": {},
+		"level": 0,
+		"power": 0,
+		"gainChance": {},
+		"levels" : {
+			"base": {
+				"power" : 0,
+				"range" : "0",
+				"description" : "", //For validation
+				"cost" : 0, //For validation
+				"aiValue" : 0, //For validation
+				"battleEffects" : {
+					"directDamage" : {
+						"type":"core:damage"
+					}
+				},
+				"targetModifier":{"smart":false}
+			},
+			"none" : {
+			},
+			"basic" : {
+			},
+			"advanced" : {
+			},
+			"expert" : {
+			}
+		},
+		"flags" : {
+			"damage": true,
+			"negative": true,
+			"nonMagical" : true,
+			"special": true
+		},
+		"targetCondition" : {
+		}
+	},
+	"infernoMoat": {
+		"targetType" : "NO_TARGET",
+		"type": "ability",
+		"name": "",
+		"school" : {},
+		"level": 0,
+		"power": 0,
+		"defaultGainChance": 0,
+		"gainChance": {},
+		"levels" : {
+			"base":{
+				"description" : "",
+				"aiValue" : 0,
+				"power" : 0,
+				"cost" : 0,
+				"targetModifier":{"smart":false},
+				"battleEffects":{
+					"moat":{
+						"type":"core:moat",
+						"hidden" : false,
+						"trap" : true,
+						"triggerAbility" : "core:infernoMoatTrigger",
+						"dispellable" : false,
+						"removeOnTrigger" : false,
+						"moatDamage" : 90,
+						"moatHexes" : [[11, 28, 44, 61, 77, 111, 129, 146, 164, 181]],
+						"defender" :{
+						},
+						"bonus" :{
+							"primarySkill" : {
+								"val" : -3,
+								"type" : "PRIMARY_SKILL",
+								"subtype" : "primarySkill.defence",
+								"valueType" : "ADDITIVE_VALUE"
+							}
+						}
+					}
+				},
+				"range" : "X"
+			},
+			"none" :{
+			},
+			"basic" :{
+			},
+			"advanced" :{
+			},
+			"expert" :{
+			}
+		},
+		"flags" : {
+			"nonMagical" : true,
+			"indifferent": true
+		},
+		"targetCondition" : {
+		}
+	},
+	"necropolisMoatTrigger" :
+	{
+		"targetType" : "CREATURE",
+		"type": "ability",
+		"name": "",
+		"school": {},
+		"level": 0,
+		"power": 0,
+		"gainChance": {},
+		"levels" : {
+			"base": {
+				"power" : 0,
+				"range" : "0",
+				"description" : "", //For validation
+				"cost" : 0, //For validation
+				"aiValue" : 0, //For validation
+				"battleEffects" : {
+					"directDamage" : {
+						"type":"core:damage"
+					}
+				},
+				"targetModifier":{"smart":false}
+			},
+			"none" : {
+			},
+			"basic" : {
+			},
+			"advanced" : {
+			},
+			"expert" : {
+			}
+		},
+		"flags" : {
+			"damage": true,
+			"negative": true,
+			"nonMagical" : true,
+			"special": true
+		},
+		"targetCondition" : {
+		}
+	},
+	"necropolisMoat": {
+		"targetType" : "NO_TARGET",
+		"type": "ability",
+		"name": "",
+		"school" : {},
+		"level": 0,
+		"power": 0,
+		"defaultGainChance": 0,
+		"gainChance": {},
+		"levels" : {
+			"base":{
+				"description" : "",
+				"aiValue" : 0,
+				"power" : 0,
+				"cost" : 0,
+				"targetModifier":{"smart":false},
+				"battleEffects":{
+					"moat":{
+						"type":"core:moat",
+						"hidden" : false,
+						"trap" : true,
+						"triggerAbility" : "core:necropolisMoatTrigger",
+						"dispellable" : false,
+						"removeOnTrigger" : false,
+						"moatDamage" : 70,
+						"moatHexes" : [[11, 28, 44, 61, 77, 111, 129, 146, 164, 181]],
+						"defender" :{
+						},
+						"bonus" :{
+							"primarySkill" : {
+								"val" : -3,
+								"type" : "PRIMARY_SKILL",
+								"subtype" : "primarySkill.defence",
+								"valueType" : "ADDITIVE_VALUE"
+							}
+						}
+					}
+				},
+				"range" : "X"
+			},
+			"none" :{
+			},
+			"basic" :{
+			},
+			"advanced" :{
+			},
+			"expert" :{
+			}
+		},
+		"flags" : {
+			"nonMagical" : true,
+			"indifferent": true
+		},
+		"targetCondition" : {
+		}
+	},
+	"dungeonMoatTrigger" :
+	{
+		"targetType" : "CREATURE",
+		"type": "ability",
+		"name": "",
+		"school": {},
+		"level": 0,
+		"power": 0,
+		"gainChance": {},
+		"levels" : {
+			"base": {
+				"power" : 0,
+				"range" : "0",
+				"description" : "", //For validation
+				"cost" : 0, //For validation
+				"aiValue" : 0, //For validation
+				"battleEffects" : {
+					"directDamage" : {
+						"type":"core:damage"
+					}
+				},
+				"targetModifier":{"smart":false}
+			},
+			"none" : {
+			},
+			"basic" : {
+			},
+			"advanced" : {
+			},
+			"expert" : {
+			}
+		},
+		"flags" : {
+			"damage": true,
+			"negative": true,
+			"nonMagical" : true,
+			"special": true
+		},
+		"targetCondition" : {
+		}
+	},
+	"dungeonMoat": {
+		"targetType" : "NO_TARGET",
+		"type": "ability",
+		"name": "",
+		"school" : {},
+		"level": 0,
+		"power": 0,
+		"defaultGainChance": 0,
+		"gainChance": {},
+		"levels" : {
+			"base":{
+				"description" : "",
+				"aiValue" : 0,
+				"power" : 0,
+				"cost" : 0,
+				"targetModifier":{"smart":false},
+				"battleEffects":{
+					"moat":{
+						"type":"core:moat",
+						"hidden" : false,
+						"trap" : true,
+						"triggerAbility" : "core:dungeonMoatTrigger",
+						"dispellable" : false,
+						"removeOnTrigger" : false,
+						"moatDamage" : 90,
+						"moatHexes" : [[11, 28, 44, 61, 77, 111, 129, 146, 164, 181]],
+						"defender" :{
+						},
+						"bonus" :{
+							"primarySkill" : {
+								"val" : -3,
+								"type" : "PRIMARY_SKILL",
+								"subtype" : "primarySkill.defence",
+								"valueType" : "ADDITIVE_VALUE"
+							}
+						}
+					}
+				},
+				"range" : "X"
+			},
+			"none" :{
+			},
+			"basic" :{
+			},
+			"advanced" :{
+			},
+			"expert" :{
+			}
+		},
+		"flags" : {
+			"nonMagical" : true,
+			"indifferent": true
+		},
+		"targetCondition" : {
+		}
+	},
+	"strongholdMoatTrigger" :
+	{
+		"targetType" : "CREATURE",
+		"type": "ability",
+		"name": "",
+		"school": {},
+		"level": 0,
+		"power": 0,
+		"gainChance": {},
+		"levels" : {
+			"base": {
+				"power" : 0,
+				"range" : "0",
+				"description" : "", //For validation
+				"cost" : 0, //For validation
+				"aiValue" : 0, //For validation
+				"battleEffects" : {
+					"directDamage" : {
+						"type":"core:damage"
+					}
+				},
+				"targetModifier":{"smart":false}
+			},
+			"none" : {
+			},
+			"basic" : {
+			},
+			"advanced" : {
+			},
+			"expert" : {
+			}
+		},
+		"flags" : {
+			"damage": true,
+			"negative": true,
+			"nonMagical" : true,
+			"special": true
+		},
+		"targetCondition" : {
+		}
+	},
+	"strongholdMoat": {
+		"targetType" : "NO_TARGET",
+		"type": "ability",
+		"name": "",
+		"school" : {},
+		"level": 0,
+		"power": 0,
+		"defaultGainChance": 0,
+		"gainChance": {},
+		"levels" : {
+			"base":{
+				"description" : "",
+				"aiValue" : 0,
+				"power" : 0,
+				"cost" : 0,
+				"targetModifier":{"smart":false},
+				"battleEffects":{
+					"moat":{
+						"type":"core:moat",
+						"hidden" : false,
+						"trap" : true,
+						"triggerAbility" : "core:strongholdMoatTrigger",
+						"dispellable" : false,
+						"removeOnTrigger" : false,
+						"moatDamage" : 70,
+						"moatHexes" : [[11, 28, 44, 61, 77, 111, 129, 146, 164, 181]],
+						"defender" :{
+						},
+						"bonus" :{
+							"primarySkill" : {
+								"val" : -3,
+								"type" : "PRIMARY_SKILL",
+								"subtype" : "primarySkill.defence",
+								"valueType" : "ADDITIVE_VALUE"
+							}
+						}
+					}
+				},
+				"range" : "X"
+			},
+			"none" :{
+			},
+			"basic" :{
+			},
+			"advanced" :{
+			},
+			"expert" :{
+			}
+		},
+		"flags" : {
+			"nonMagical" : true,
+			"indifferent": true
+		},
+		"targetCondition" : {
+		}
+	},
+	"fortressMoatTrigger" :
+	{
+		"targetType" : "CREATURE",
+		"type": "ability",
+		"name": "",
+		"school": {},
+		"level": 0,
+		"power": 0,
+		"gainChance": {},
+		"levels" : {
+			"base": {
+				"power" : 0,
+				"range" : "0",
+				"description" : "", //For validation
+				"cost" : 0, //For validation
+				"aiValue" : 0, //For validation
+				"battleEffects" : {
+					"directDamage" : {
+						"type":"core:damage"
+					}
+				},
+				"targetModifier":{"smart":false}
+			},
+			"none" : {
+			},
+			"basic" : {
+			},
+			"advanced" : {
+			},
+			"expert" : {
+			}
+		},
+		"flags" : {
+			"damage": true,
+			"negative": true,
+			"nonMagical" : true,
+			"special": true
+		},
+		"targetCondition" : {
+		}
+	},
+	"fortressMoat": {
+		"targetType" : "NO_TARGET",
+		"type": "ability",
+		"name": "",
+		"school" : {},
+		"level": 0,
+		"power": 0,
+		"defaultGainChance": 0,
+		"gainChance": {},
+		"levels" : {
+			"base":{
+				"description" : "",
+				"aiValue" : 0,
+				"power" : 0,
+				"cost" : 0,
+				"targetModifier":{"smart":false},
+				"battleEffects":{
+					"moat":{
+						"type":"core:moat",
+						"hidden" : false,
+						"trap" : true,
+						"triggerAbility" : "core:fortressMoatTrigger",
+						"dispellable" : false,
+						"removeOnTrigger" : false,
+						"moatDamage" : 90,
+						"moatHexes" : [[10, 11, 27, 28, 43, 44, 60, 61, 76, 77, 94, 110, 111, 128, 129, 145, 146, 163, 164, 180, 181]],
+						"defender" :{
+						},
+						"bonus" :{
+							"primarySkill" : {
+								"val" : -3,
+								"type" : "PRIMARY_SKILL",
+								"subtype" : "primarySkill.defence",
+								"valueType" : "ADDITIVE_VALUE"
+							}
+						}
+					}
+				},
+				"range" : "X"
+			},
+			"none" :{
+			},
+			"basic" :{
+			},
+			"advanced" :{
+			},
+			"expert" :{
+			}
+		},
+		"flags" : {
+			"nonMagical" : true,
+			"indifferent": true
+		},
+		"targetCondition" : {
+		}
+	}
 }

+ 2 - 2
config/spells/other.json

@@ -55,7 +55,7 @@
 	{
 		"targetType" : "CREATURE",
 		"type": "combat",
-		"name": "Land Mine",
+		"name": "",
 		"school":
 		{
 			"air": false,
@@ -237,7 +237,7 @@
 	"fireWallTrigger" : {
 		"targetType" : "CREATURE",
 		"type": "combat",
-		"name": "Fire Wall",
+		"name": "",
 		"school":
 		{
 			"air": false,

+ 7 - 7
config/spells/vcmiAbility.json

@@ -1,8 +1,8 @@
 {
-    "summonDemons" : {
+	"summonDemons" : {
 		"type": "ability",
 		"targetType" : "CREATURE",
-		"name": "Summon Demons",
+		"name": "",
 		"school" : {},
 		"level": 2,
 		"power": 50,
@@ -46,11 +46,11 @@
 				"bonus.GARGOYLE" : "absolute"
 			}
 		}
-    },
-    "firstAid" : {
+	},
+	"firstAid" : {
 		"targetType" : "CREATURE",
 		"type": "ability",
-		"name": "First Aid",
+		"name": "",
 		"school" : {},
 		"level": 1,
 		"power": 10,
@@ -106,7 +106,7 @@
 	"catapultShot" : {
 		"targetType" : "LOCATION",
 		"type": "ability",
-		"name": "Catapult shot",
+		"name": "",
 		"school" : {},
 		"level": 1,
 		"power": 1,
@@ -187,7 +187,7 @@
 	"cyclopsShot" : {
 		"targetType" : "LOCATION",
 		"type": "ability",
-		"name": "Siege shot",
+		"name": "",
 		"school" : {},
 		"level": 1,
 		"power": 1,

+ 1 - 0
docs/players/Cheat_Codes.md

@@ -121,6 +121,7 @@ Below a list of supported commands, with their arguments wrapped in `<>`
 
 #### Extract commands
 `translate` - save game texts into json files
+`translate missing` - save untranslated game texts into json files
 `translate maps` - save map and campaign texts into json files
 `get config` - save game objects data into json files
 `get scripts` - dumps lua script stuff into files (currently inactive due to scripting disabled for default builds)

+ 2 - 0
docs/translators/Translations.md

@@ -136,6 +136,8 @@ After that, start Launcher, switch to Help tab and open "log files directory". Y
 
 If your mod also contains maps or campaigns that you want to translate, then use '/translate maps' command instead.
 
+If you want to update existing translation, you can use '/translate missing' command that will export only strings that were not translated
+
 ### Translating mod information
 In order to display information in Launcher in language selected by user add following block into your `mod.json`:
 ```

+ 3 - 0
lib/filesystem/CCompressedStream.cpp

@@ -136,6 +136,9 @@ si64 CCompressedStream::readMore(ui8 *data, si64 size)
 	{
 		if (inflateState->avail_in == 0)
 		{
+			if (gzipStream == nullptr)
+				throw std::runtime_error("Potentially truncated gzip file");
+
 			//inflate ran out of available data or was not initialized yet
 			// get new input data and update state accordingly
 			si64 availSize = gzipStream->read(compressedBuffer.data(), compressedBuffer.size());

+ 40 - 19
lib/json/JsonParser.cpp

@@ -158,40 +158,58 @@ bool JsonParser::extractEscaping(std::string & str)
 
 	switch(input[pos])
 	{
+		case '\r':
+			if(settings.mode == JsonParsingSettings::JsonFormatMode::JSON5 && input.size() > pos && input[pos+1] == '\n')
+			{
+				pos += 2;
+				return true;
+			}
+			break;
+		case '\n':
+			if(settings.mode == JsonParsingSettings::JsonFormatMode::JSON5)
+			{
+				pos += 1;
+				return true;
+			}
+			break;
 		case '\"':
 			str += '\"';
-			break;
+			pos++;
+			return true;
 		case '\\':
 			str += '\\';
-			break;
+			pos++;
+			return true;
 		case 'b':
 			str += '\b';
-			break;
+			pos++;
+			return true;
 		case 'f':
 			str += '\f';
-			break;
+			pos++;
+			return true;
 		case 'n':
 			str += '\n';
-			break;
+			pos++;
+			return true;
 		case 'r':
 			str += '\r';
-			break;
+			pos++;
+			return true;
 		case 't':
 			str += '\t';
-			break;
+			pos++;
+			return true;
 		case '/':
 			str += '/';
-			break;
-		default:
-			return error("Unknown escape sequence!", true);
+			pos++;
+			return true;
 	}
-	return true;
+	return error("Unknown escape sequence!", true);
 }
 
 bool JsonParser::extractString(std::string & str)
 {
-	//TODO: JSON5 - line breaks escaping
-
 	if(settings.mode < JsonParsingSettings::JsonFormatMode::JSON5)
 	{
 		if(input[pos] != '\"')
@@ -216,27 +234,30 @@ bool JsonParser::extractString(std::string & str)
 			pos++;
 			return true;
 		}
-		if(input[pos] == '\\') // Escaping
+		else if(input[pos] == '\\') // Escaping
 		{
 			str.append(&input[first], pos - first);
 			pos++;
 			if(pos == input.size())
 				break;
+
 			extractEscaping(str);
-			first = pos + 1;
+			first = pos;
 		}
-		if(input[pos] == '\n') // end-of-line
+		else if(input[pos] == '\n') // end-of-line
 		{
 			str.append(&input[first], pos - first);
 			return error("Closing quote not found!", true);
 		}
-		if(static_cast<unsigned char>(input[pos]) < ' ') // control character
+		else if(static_cast<unsigned char>(input[pos]) < ' ') // control character
 		{
 			str.append(&input[first], pos - first);
-			first = pos + 1;
+			pos++;
+			first = pos;
 			error("Illegal character in the string!", true);
 		}
-		pos++;
+		else
+			pos++;
 	}
 	return error("Unterminated string!");
 }

+ 2 - 2
lib/mapObjects/CBank.cpp

@@ -144,7 +144,7 @@ void CBank::onHeroVisit(const CGHeroInstance * h) const
 	bd.player = h->getOwner();
 	bd.text.appendLocalString(EMetaText::ADVOB_TXT, 32);
 	bd.components = getPopupComponents(h->getOwner());
-	bd.text.replaceRawString(getObjectName());
+	bd.text.replaceTextID(getObjectHandler()->getNameTextID());
 	cb->showBlockingDialog(this, &bd);
 }
 
@@ -158,7 +158,7 @@ void CBank::doVisit(const CGHeroInstance * hero) const
 	if (!bankConfig)
 	{
 		iw.text.appendRawString(VLC->generaltexth->advobtxt[33]);// This was X, now is completely empty
-		iw.text.replaceRawString(getObjectName());
+		iw.text.replaceTextID(getObjectHandler()->getNameTextID());
 		cb->showInfoDialog(&iw);
 	}
 

+ 3 - 0
lib/mapping/MapFormatH3M.cpp

@@ -208,6 +208,9 @@ void CMapLoaderH3M::readHeader()
 
 	// optimization - load mappings only once to avoid slow parsing of map headers for map list
 	static const std::map<EMapFormat, MapIdentifiersH3M> identifierMappers = generateMappings();
+	if (!identifierMappers.count(mapHeader->version))
+		throw std::runtime_error("Unsupported map format! Format ID " + std::to_string(static_cast<int>(mapHeader->version)));
+
 	const MapIdentifiersH3M & identifierMapper = identifierMappers.at(mapHeader->version);
 
 	reader->setIdentifierRemapper(identifierMapper);

+ 0 - 2
lib/mapping/MapFormatJson.cpp

@@ -1014,8 +1014,6 @@ void CMapLoaderJson::readTerrain()
 		const JsonNode underground = getFromArchive(TERRAIN_FILE_NAMES[1]);
 		readTerrainLevel(underground, 1);
 	}
-
-	map->calculateWaterContent();
 }
 
 CMapLoaderJson::MapObjectLoader::MapObjectLoader(CMapLoaderJson * _owner, JsonMap::value_type & json):

+ 5 - 3
lib/mapping/MapReaderH3M.cpp

@@ -410,9 +410,11 @@ bool MapReaderH3M::readBool()
 int8_t MapReaderH3M::readInt8Checked(int8_t lowerLimit, int8_t upperLimit)
 {
 	int8_t result = readInt8();
-	assert(result >= lowerLimit);
-	assert(result <= upperLimit);
-	return std::clamp(result, lowerLimit, upperLimit);
+	int8_t resultClamped = std::clamp(result, lowerLimit, upperLimit);
+	if (result != resultClamped)
+		logGlobal->warn("Map contains out of range value %d! Expected %d-%d", static_cast<int>(result), static_cast<int>(lowerLimit), static_cast<int>(upperLimit));
+
+	return resultClamped;
 }
 
 uint8_t MapReaderH3M::readUInt8()

+ 2 - 2
lib/modding/CModHandler.cpp

@@ -446,8 +446,8 @@ void CModHandler::loadTranslation(const TModID & modName)
 	JsonNode baseTranslation = JsonUtils::assembleFromFiles(mod.config["translations"]);
 	JsonNode extraTranslation = JsonUtils::assembleFromFiles(mod.config[preferredLanguage]["translations"]);
 
-	VLC->generaltexth->loadTranslationOverrides(modName, baseTranslation);
-	VLC->generaltexth->loadTranslationOverrides(modName, extraTranslation);
+	VLC->generaltexth->loadTranslationOverrides(modName, modBaseLanguage, baseTranslation);
+	VLC->generaltexth->loadTranslationOverrides(modName, preferredLanguage, extraTranslation);
 }
 
 void CModHandler::load()

+ 33 - 19
lib/rmg/CMapGenerator.cpp

@@ -152,41 +152,55 @@ std::unique_ptr<CMap> CMapGenerator::generate()
 	return std::move(map->mapInstance);
 }
 
-std::string CMapGenerator::getMapDescription() const
+MetaString CMapGenerator::getMapDescription() const
 {
-	assert(map);
+	const TextIdentifier mainPattern("vcmi", "randomMap", "description");
+	const TextIdentifier isHuman("vcmi", "randomMap", "description", "isHuman");
+	const TextIdentifier townChoiceIs("vcmi", "randomMap", "description", "townChoice");
+	const std::array waterContent = {
+		TextIdentifier("vcmi", "randomMap", "description", "water", "none"),
+		TextIdentifier("vcmi", "randomMap", "description", "water", "normal"),
+		TextIdentifier("vcmi", "randomMap", "description", "water", "islands")
+	};
+	const std::array monsterStrength = {
+		TextIdentifier("vcmi", "randomMap", "description", "monster", "weak"),
+		TextIdentifier("vcmi", "randomMap", "description", "monster", "normal"),
+		TextIdentifier("vcmi", "randomMap", "description", "monster", "strong")
+	};
 
-	const std::string waterContentStr[3] = { "none", "normal", "islands" };
-	const std::string monsterStrengthStr[3] = { "weak", "normal", "strong" };
-
-	int monsterStrengthIndex = mapGenOptions.getMonsterStrength() - EMonsterStrength::GLOBAL_WEAK; //does not start from 0
 	const auto * mapTemplate = mapGenOptions.getMapTemplate();
+	int monsterStrengthIndex = mapGenOptions.getMonsterStrength() - EMonsterStrength::GLOBAL_WEAK; //does not start from 0
 
-	if(!mapTemplate)
-		throw rmgException("Map template for Random Map Generator is not found. Could not start the game.");
+	MetaString result = MetaString::createFromTextID(mainPattern.get());
 
-    std::stringstream ss;
-    ss << boost::str(boost::format(std::string("Map created by the Random Map Generator.\nTemplate was %s, size %dx%d") +
-        ", levels %d, players %d, computers %d, water %s, monster %s, VCMI map") % mapTemplate->getName() %
-		map->width() % map->height() % static_cast<int>(map->levels()) % static_cast<int>(mapGenOptions.getHumanOrCpuPlayerCount()) %
-		static_cast<int>(mapGenOptions.getCompOnlyPlayerCount()) % waterContentStr[mapGenOptions.getWaterContent()] %
-		monsterStrengthStr[monsterStrengthIndex]);
+	result.replaceRawString(mapTemplate->getName());
+	result.replaceNumber(map->width());
+	result.replaceNumber(map->height());
+	result.replaceNumber(map->levels());
+	result.replaceNumber(mapGenOptions.getHumanOrCpuPlayerCount());
+	result.replaceNumber(mapGenOptions.getCompOnlyPlayerCount());
+	result.replaceTextID(waterContent.at(mapGenOptions.getWaterContent()).get());
+	result.replaceTextID(monsterStrength.at(monsterStrengthIndex).get());
 
 	for(const auto & pair : mapGenOptions.getPlayersSettings())
 	{
 		const auto & pSettings = pair.second;
+
 		if(pSettings.getPlayerType() == EPlayerType::HUMAN)
 		{
-			ss << ", " << GameConstants::PLAYER_COLOR_NAMES[pSettings.getColor().getNum()] << " is human";
+			result.appendTextID(isHuman.get());
+			result.replaceName(pSettings.getColor());
 		}
+
 		if(pSettings.getStartingTown() != FactionID::RANDOM)
 		{
-			ss << ", " << GameConstants::PLAYER_COLOR_NAMES[pSettings.getColor().getNum()]
-			   << " town choice is " << (*VLC->townh)[pSettings.getStartingTown()]->getNameTranslated();
+			result.appendTextID(townChoiceIs.get());
+			result.replaceName(pSettings.getColor());
+			result.replaceName(pSettings.getStartingTown());
 		}
 	}
 
-	return ss.str();
+	return result;
 }
 
 void CMapGenerator::addPlayerInfo()
@@ -451,7 +465,7 @@ void CMapGenerator::addHeaderInfo()
 	m.height = mapGenOptions.getHeight();
 	m.twoLevel = mapGenOptions.getHasTwoLevels();
 	m.name.appendLocalString(EMetaText::GENERAL_TXT, 740);
-	m.description.appendRawString(getMapDescription());
+	m.description = getMapDescription();
 	m.difficulty = EMapDifficulty::NORMAL;
 	addPlayerInfo();
 	m.waterMap = (mapGenOptions.getWaterContent() != EWaterContent::EWaterContent::NONE);

+ 2 - 4
lib/rmg/CMapGenerator.h

@@ -10,14 +10,12 @@
 
 #pragma once
 
-#include "../GameConstants.h"
 #include "CMapGenOptions.h"
-#include "../int3.h"
-#include "CRmgTemplate.h"
 #include "../LoadProgress.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 
+class MetaString;
 class CRmgTemplate;
 class CMapGenOptions;
 class JsonNode;
@@ -93,7 +91,7 @@ private:
 	/// Generation methods
 	void loadConfig();
 	
-	std::string getMapDescription() const;
+	MetaString getMapDescription() const;
 
 	void initPrisonsRemaining();
 	void initQuestArtsRemaining();

+ 1 - 12
lib/texts/CGeneralTextHandler.cpp

@@ -139,9 +139,7 @@ CGeneralTextHandler::CGeneralTextHandler():
 	// pseudo-array, that don't have H3 file with same name
 	seerEmpty        (*this, "core.seerhut.empty"  ),
 	seerNames        (*this, "core.seerhut.names"  ),
-	capColors        (*this, "vcmi.capitalColors"  ),
-	znpc00           (*this, "vcmi.znpc00"  ), // technically - wog
-	qeModCommands    (*this, "vcmi.quickExchange" )
+	capColors        (*this, "vcmi.capitalColors"  )
 {
 	readToVector("core.vcdesc",   "DATA/VCDESC.TXT"   );
 	readToVector("core.lcdesc",   "DATA/LCDESC.TXT"   );
@@ -166,10 +164,6 @@ CGeneralTextHandler::CGeneralTextHandler():
 	readToVector("core.mineevnt", "DATA/MINEEVNT.TXT" );
 	readToVector("core.xtrainfo", "DATA/XTRAINFO.TXT" );
 
-	static const std::string QE_MOD_COMMANDS = "DATA/QECOMMANDS.TXT";
-	if (CResourceHandler::get()->existsResource(TextPath::builtin(QE_MOD_COMMANDS)))
-		readToVector("vcmi.quickExchange", QE_MOD_COMMANDS);
-
 	{
 		CLegacyConfigParser parser(TextPath::builtin("DATA/RANDTVRN.TXT"));
 		parser.endLine();
@@ -298,11 +292,6 @@ CGeneralTextHandler::CGeneralTextHandler():
 			scenariosCountPerCampaign.push_back(region);
 		}
 	}
-	if (VLC->engineSettings()->getBoolean(EGameSettings::MODULE_COMMANDERS))
-	{
-		if(CResourceHandler::get()->existsResource(TextPath::builtin("DATA/ZNPC00.TXT")))
-			readToVector("vcmi.znpc00", "DATA/ZNPC00.TXT" );
-	}
 }
 
 int32_t CGeneralTextHandler::pluralText(const int32_t textIndex, const int32_t count) const

+ 0 - 4
lib/texts/CGeneralTextHandler.h

@@ -62,8 +62,6 @@ public:
 	LegacyTextContainer fcommands; // fort screen
 	LegacyTextContainer tavernInfo;
 
-	LegacyTextContainer qeModCommands;
-
 	LegacyHelpContainer zelp;
 
 	//objects
@@ -75,8 +73,6 @@ public:
 
 	//sec skills
 	LegacyTextContainer levels;
-	//commanders
-	LegacyTextContainer znpc00; //more or less useful content of that file
 
 	std::vector<std::string> findStringsWithPrefix(const std::string & prefix);
 

+ 6 - 0
lib/texts/MetaString.cpp

@@ -13,6 +13,7 @@
 #include "CArtHandler.h"
 #include "CCreatureHandler.h"
 #include "CCreatureSet.h"
+#include "entities/faction/CFaction.h"
 #include "texts/CGeneralTextHandler.h"
 #include "CSkillHandler.h"
 #include "GameConstants.h"
@@ -387,6 +388,11 @@ void MetaString::replaceName(const ArtifactID & id)
 	replaceTextID(id.toEntity(VLC)->getNameTextID());
 }
 
+void MetaString::replaceName(const FactionID & id)
+{
+	replaceTextID(id.toEntity(VLC)->getNameTextID());
+}
+
 void MetaString::replaceName(const MapObjectID& id)
 {
 	replaceTextID(VLC->objtypeh->getObjectName(id, 0));

+ 2 - 0
lib/texts/MetaString.h

@@ -21,6 +21,7 @@ class MapObjectSubID;
 class PlayerColor;
 class SecondarySkill;
 class SpellID;
+class FactionID;
 class GameResID;
 using TQuantity = si32;
 
@@ -97,6 +98,7 @@ public:
 	void replacePositiveNumber(int64_t txt);
 
 	void replaceName(const ArtifactID & id);
+	void replaceName(const FactionID& id);
 	void replaceName(const MapObjectID& id);
 	void replaceName(const PlayerColor& id);
 	void replaceName(const SecondarySkill& id);

+ 13 - 5
lib/texts/TextLocalizationContainer.cpp

@@ -22,7 +22,7 @@ VCMI_LIB_NAMESPACE_BEGIN
 
 std::recursive_mutex TextLocalizationContainer::globalTextMutex;
 
-void TextLocalizationContainer::registerStringOverride(const std::string & modContext, const TextIdentifier & UID, const std::string & localized)
+void TextLocalizationContainer::registerStringOverride(const std::string & modContext, const TextIdentifier & UID, const std::string & localized, const std::string & language)
 {
 	std::lock_guard globalLock(globalTextMutex);
 
@@ -42,6 +42,11 @@ void TextLocalizationContainer::registerStringOverride(const std::string & modCo
 			entry.identifierModContext = modContext;
 			entry.baseStringModContext = modContext;
 		}
+		else
+		{
+			if (language == VLC->generaltexth->getPreferredLanguage())
+				entry.overriden = true;
+		}
 	}
 	else
 	{
@@ -127,10 +132,10 @@ void TextLocalizationContainer::registerString(const std::string & identifierMod
 	}
 }
 
-void TextLocalizationContainer::loadTranslationOverrides(const std::string & modContext, const JsonNode & config)
+void TextLocalizationContainer::loadTranslationOverrides(const std::string & modContext, const std::string & language, const JsonNode & config)
 {
 	for(const auto & node : config.Struct())
-		registerStringOverride(modContext, node.first, node.second.String());
+		registerStringOverride(modContext, node.first, node.second.String(), language);
 }
 
 bool TextLocalizationContainer::identifierExists(const TextIdentifier & UID) const
@@ -140,15 +145,18 @@ bool TextLocalizationContainer::identifierExists(const TextIdentifier & UID) con
 	return stringsLocalizations.count(UID.get());
 }
 
-void TextLocalizationContainer::exportAllTexts(std::map<std::string, std::map<std::string, std::string>> & storage) const
+void TextLocalizationContainer::exportAllTexts(std::map<std::string, std::map<std::string, std::string>> & storage, bool onlyMissing) const
 {
 	std::lock_guard globalLock(globalTextMutex);
 
 	for (auto const & subContainer : subContainers)
-		subContainer->exportAllTexts(storage);
+		subContainer->exportAllTexts(storage, onlyMissing);
 
 	for (auto const & entry : stringsLocalizations)
 	{
+		if (onlyMissing && entry.second.overriden)
+			continue;
+
 		std::string textToWrite;
 		std::string modName = entry.second.baseStringModContext;
 

+ 5 - 3
lib/texts/TextLocalizationContainer.h

@@ -32,6 +32,8 @@ protected:
 		/// Different from identifierModContext if mod has modified object from another mod (e.g. rebalance mods)
 		std::string baseStringModContext;
 
+		bool overriden = false;
+
 		template <typename Handler>
 		void serialize(Handler & h)
 		{
@@ -47,7 +49,7 @@ protected:
 	std::vector<const TextLocalizationContainer *> subContainers;
 
 	/// add selected string to internal storage as high-priority strings
-	void registerStringOverride(const std::string & modContext, const TextIdentifier & UID, const std::string & localized);
+	void registerStringOverride(const std::string & modContext, const TextIdentifier & UID, const std::string & localized, const std::string & language);
 
 	std::string getModLanguage(const std::string & modContext);
 
@@ -57,7 +59,7 @@ protected:
 public:
 	/// Loads translation from provided json
 	/// Any entries loaded by this will have priority over texts registered normally
-	void loadTranslationOverrides(const std::string & modContext, JsonNode const & file);
+	void loadTranslationOverrides(const std::string & modContext, const std::string & language, JsonNode const & file);
 
 	/// add selected string to internal storage
 	void registerString(const std::string & modContext, const TextIdentifier & UID, const JsonNode & localized);
@@ -77,7 +79,7 @@ public:
 
 	/// Debug method, returns all currently stored texts
 	/// Format: [mod ID][string ID] -> human-readable text
-	void exportAllTexts(std::map<std::string, std::map<std::string, std::string>> & storage) const;
+	void exportAllTexts(std::map<std::string, std::map<std::string, std::string>> & storage, bool onlyMissing) const;
 
 	/// Add or override subcontainer which can store identifiers
 	void addSubContainer(const TextLocalizationContainer & container);