Explorar o código

Merge pull request #5148 from Laserlicht/h3c_vcmp_converter

[1.6.x] h3c to vcmp converter
Ivan Savenko hai 9 meses
pai
achega
af83a3043e

+ 9 - 0
Global.h

@@ -369,6 +369,15 @@ namespace vstd
 		return it->second;
 	}
 
+	// given a map from keys to values, creates a new map from values to keys 
+	template<typename K, typename V>
+	static std::map<V, K> reverseMap(const std::map<K, V>& m) {
+		std::map<V, K> r;
+ 		for (const auto& kv : m)
+			r[kv.second] = kv.first;
+		return r;
+	}
+
 	//returns first key that maps to given value if present, returns success via found if provided
 	template <typename Key, typename T>
 	Key findKey(const std::map<Key, T> & map, const T & value, bool * found = nullptr)

+ 205 - 52
lib/campaign/CampaignHandler.cpp

@@ -171,6 +171,26 @@ void CampaignHandler::readHeaderFromJson(CampaignHeader & ret, JsonNode & reader
 	ret.outroVideo = VideoPath::fromJson(reader["outroVideo"]);
 }
 
+JsonNode CampaignHandler::writeHeaderToJson(CampaignHeader & header)
+{
+	JsonNode node;
+	node["version"].Integer() = static_cast<ui64>(CampaignVersion::VCMI);
+	node["regions"] = CampaignRegions::toJson(header.campaignRegions);
+	node["name"].String() = header.name.toString();
+	node["description"].String() = header.description.toString();
+	node["author"].String() = header.author.toString();
+	node["authorContact"].String() = header.authorContact.toString();
+	node["campaignVersion"].String() = header.campaignVersion.toString();
+	node["creationDateTime"].Integer() = header.creationDateTime;
+	node["allowDifficultySelection"].Bool() = header.difficultyChosenByPlayer;
+	node["music"].String() = header.music.getName();
+	node["loadingBackground"].String() = header.loadingBackground.getName();
+	node["videoRim"].String() = header.videoRim.getName();
+	node["introVideo"].String() = header.introVideo.getName();
+	node["outroVideo"].String() = header.outroVideo.getName();
+	return node;
+}
+
 CampaignScenario CampaignHandler::readScenarioFromJson(JsonNode & reader)
 {
 	auto prologEpilogReader = [](JsonNode & identifier) -> CampaignScenarioPrologEpilog
@@ -203,56 +223,86 @@ CampaignScenario CampaignHandler::readScenarioFromJson(JsonNode & reader)
 	return ret;
 }
 
+JsonNode CampaignHandler::writeScenarioToJson(const CampaignScenario & scenario)
+{
+	auto prologEpilogWriter = [](const CampaignScenarioPrologEpilog & elem) -> JsonNode
+	{
+		JsonNode node;
+		if(elem.hasPrologEpilog)
+		{
+			node["video"].String() = elem.prologVideo.getName();
+			node["music"].String() = elem.prologMusic.getName();
+			node["voice"].String() = elem.prologVoice.getName();
+			node["text"].String() = elem.prologText.toString();
+		}
+		return node;
+	};
+
+	JsonNode node;
+	node["map"].String() = scenario.mapName;
+	for(auto & g : scenario.preconditionRegions)
+		node["preconditions"].Vector().push_back(JsonNode(static_cast<ui32>(g)));
+	node["color"].Integer() = scenario.regionColor;
+	node["difficulty"].Integer() = scenario.difficulty;
+	node["regionText"].String() = scenario.regionText.toString();
+	node["prolog"] = prologEpilogWriter(scenario.prolog);
+	node["epilog"] = prologEpilogWriter(scenario.epilog);
+
+	writeScenarioTravelToJson(node, scenario.travelOptions);
+
+	return node;
+}
+
+static const std::map<std::string, CampaignStartOptions> startOptionsMap = {
+	{"none", CampaignStartOptions::NONE},
+	{"bonus", CampaignStartOptions::START_BONUS},
+	{"crossover", CampaignStartOptions::HERO_CROSSOVER},
+	{"hero", CampaignStartOptions::HERO_OPTIONS}
+};
+
+static const std::map<std::string, CampaignBonusType> bonusTypeMap = {
+	{"spell", CampaignBonusType::SPELL},
+	{"creature", CampaignBonusType::MONSTER},
+	{"building", CampaignBonusType::BUILDING},
+	{"artifact", CampaignBonusType::ARTIFACT},
+	{"scroll", CampaignBonusType::SPELL_SCROLL},
+	{"primarySkill", CampaignBonusType::PRIMARY_SKILL},
+	{"secondarySkill", CampaignBonusType::SECONDARY_SKILL},
+	{"resource", CampaignBonusType::RESOURCE},
+	//{"prevHero", CScenarioTravel::STravelBonus::EBonusType::HEROES_FROM_PREVIOUS_SCENARIO},
+	//{"hero", CScenarioTravel::STravelBonus::EBonusType::HERO},
+};
+
+static const std::map<std::string, ui32> primarySkillsMap = {
+	{"attack", 0},
+	{"defence", 8},
+	{"spellpower", 16},
+	{"knowledge", 24},
+};
+
+static const std::map<std::string, ui16> heroSpecialMap = {
+	{"strongest", 0xFFFD},
+	{"generated", 0xFFFE},
+	{"random", 0xFFFF}
+};
+
+static const std::map<std::string, ui8> resourceTypeMap = {
+	//FD - wood+ore
+	//FE - mercury+sulfur+crystal+gem
+	{"wood", 0},
+	{"mercury", 1},
+	{"ore", 2},
+	{"sulfur", 3},
+	{"crystal", 4},
+	{"gems", 5},
+	{"gold", 6},
+	{"common", 0xFD},
+	{"rare", 0xFE}
+};
+
 CampaignTravel CampaignHandler::readScenarioTravelFromJson(JsonNode & reader)
 {
 	CampaignTravel ret;
-
-	std::map<std::string, CampaignStartOptions> startOptionsMap = {
-		{"none", CampaignStartOptions::NONE},
-		{"bonus", CampaignStartOptions::START_BONUS},
-		{"crossover", CampaignStartOptions::HERO_CROSSOVER},
-		{"hero", CampaignStartOptions::HERO_OPTIONS}
-	};
-	
-	std::map<std::string, CampaignBonusType> bonusTypeMap = {
-		{"spell", CampaignBonusType::SPELL},
-		{"creature", CampaignBonusType::MONSTER},
-		{"building", CampaignBonusType::BUILDING},
-		{"artifact", CampaignBonusType::ARTIFACT},
-		{"scroll", CampaignBonusType::SPELL_SCROLL},
-		{"primarySkill", CampaignBonusType::PRIMARY_SKILL},
-		{"secondarySkill", CampaignBonusType::SECONDARY_SKILL},
-		{"resource", CampaignBonusType::RESOURCE},
-		//{"prevHero", CScenarioTravel::STravelBonus::EBonusType::HEROES_FROM_PREVIOUS_SCENARIO},
-		//{"hero", CScenarioTravel::STravelBonus::EBonusType::HERO},
-	};
-	
-	std::map<std::string, ui32> primarySkillsMap = {
-		{"attack", 0},
-		{"defence", 8},
-		{"spellpower", 16},
-		{"knowledge", 24},
-	};
-	
-	std::map<std::string, ui16> heroSpecialMap = {
-		{"strongest", 0xFFFD},
-		{"generated", 0xFFFE},
-		{"random", 0xFFFF}
-	};
-	
-	std::map<std::string, ui8> resourceTypeMap = {
-		//FD - wood+ore
-		//FE - mercury+sulfur+crystal+gem
-		{"wood", 0},
-		{"mercury", 1},
-		{"ore", 2},
-		{"sulfur", 3},
-		{"crystal", 4},
-		{"gems", 5},
-		{"gold", 6},
-		{"common", 0xFD},
-		{"rare", 0xFE}
-	};
 	
 	for(auto & k : reader["heroKeeps"].Vector())
 	{
@@ -278,7 +328,7 @@ CampaignTravel CampaignHandler::readScenarioTravelFromJson(JsonNode & reader)
 			logGlobal->warn("VCMP Loading: keepArtifacts contains unresolved identifier %s", k.String());
 	}
 
-	ret.startOptions = startOptionsMap[reader["startOptions"].String()];
+	ret.startOptions = startOptionsMap.at(reader["startOptions"].String());
 	switch(ret.startOptions)
 	{
 	case CampaignStartOptions::NONE:
@@ -290,11 +340,11 @@ CampaignTravel CampaignHandler::readScenarioTravelFromJson(JsonNode & reader)
 			for(auto & bjson : reader["bonuses"].Vector())
 			{
 				CampaignBonus bonus;
-				bonus.type = bonusTypeMap[bjson["what"].String()];
+				bonus.type = bonusTypeMap.at(bjson["what"].String());
 				switch (bonus.type)
 				{
 					case CampaignBonusType::RESOURCE:
-						bonus.info1 = resourceTypeMap[bjson["type"].String()];
+						bonus.info1 = resourceTypeMap.at(bjson["type"].String());
 						bonus.info2 = bjson["amount"].Integer();
 						break;
 						
@@ -305,7 +355,7 @@ CampaignTravel CampaignHandler::readScenarioTravelFromJson(JsonNode & reader)
 						break;
 						
 					default:
-						if(int heroId = heroSpecialMap[bjson["hero"].String()])
+						if(int heroId = heroSpecialMap.at(bjson["hero"].String()))
 							bonus.info1 = heroId;
 						else
 							if(auto identifier = VLC->identifiers()->getIdentifier(ModScope::scopeMap(), "hero", bjson["hero"].String()))
@@ -368,7 +418,7 @@ CampaignTravel CampaignHandler::readScenarioTravelFromJson(JsonNode & reader)
 				bonus.type = CampaignBonusType::HERO;
 				bonus.info1 = bjson["playerColor"].Integer(); //player color
 				
-				if(int heroId = heroSpecialMap[bjson["hero"].String()])
+				if(int heroId = heroSpecialMap.at(bjson["hero"].String()))
 					bonus.info2 = heroId;
 				else
 					if (auto identifier = VLC->identifiers()->getIdentifier(ModScope::scopeMap(), "hero", bjson["hero"].String()))
@@ -390,6 +440,109 @@ CampaignTravel CampaignHandler::readScenarioTravelFromJson(JsonNode & reader)
 	return ret;
 }
 
+void CampaignHandler::writeScenarioTravelToJson(JsonNode & node, const CampaignTravel & travel)
+{
+	if(travel.whatHeroKeeps.experience)
+		node["heroKeeps"].Vector().push_back(JsonNode("experience"));
+	if(travel.whatHeroKeeps.primarySkills)
+		node["heroKeeps"].Vector().push_back(JsonNode("primarySkills"));
+	if(travel.whatHeroKeeps.secondarySkills)
+		node["heroKeeps"].Vector().push_back(JsonNode("secondarySkills"));
+	if(travel.whatHeroKeeps.spells)
+		node["heroKeeps"].Vector().push_back(JsonNode("spells"));
+	if(travel.whatHeroKeeps.artifacts)
+		node["heroKeeps"].Vector().push_back(JsonNode("artifacts"));
+	for(auto & c : travel.monstersKeptByHero)
+		node["keepCreatures"].Vector().push_back(JsonNode(CreatureID::encode(c)));
+	for(auto & a : travel.artifactsKeptByHero)
+		node["keepArtifacts"].Vector().push_back(JsonNode(ArtifactID::encode(a)));
+	node["startOptions"].String() = vstd::reverseMap(startOptionsMap)[travel.startOptions];
+
+	switch(travel.startOptions)
+	{
+	case CampaignStartOptions::NONE:
+		break;
+	case CampaignStartOptions::START_BONUS:
+		{
+			node["playerColor"].String() = PlayerColor::encode(travel.playerColor);
+			for(auto & bonus : travel.bonusesToChoose)
+			{
+				JsonNode bnode;
+				bnode["what"].String() = vstd::reverseMap(bonusTypeMap)[bonus.type];
+				switch (bonus.type)
+				{
+					case CampaignBonusType::RESOURCE:
+						bnode["type"].String() = vstd::reverseMap(resourceTypeMap)[bonus.info1];
+						bnode["amount"].Integer() = bonus.info2;
+						break;
+					case CampaignBonusType::BUILDING:
+						bnode["type"].String() = EBuildingType::names[bonus.info1];
+						break;
+					default:
+						if(vstd::contains(vstd::reverseMap(heroSpecialMap), bonus.info1))
+							bnode["hero"].String() = vstd::reverseMap(heroSpecialMap)[bonus.info1];
+						else
+							bnode["hero"].String() = HeroTypeID::encode(bonus.info1);
+						bnode["amount"].Integer() = bonus.info3;
+						switch(bonus.type)
+						{
+							case CampaignBonusType::SPELL:
+								bnode["type"].String() = SpellID::encode(bonus.info2);
+								break;
+							case CampaignBonusType::MONSTER:
+								bnode["type"].String() = CreatureID::encode(bonus.info2);
+								break;
+							case CampaignBonusType::SECONDARY_SKILL:
+								bnode["type"].String() = SecondarySkill::encode(bonus.info2);
+								break;
+							case CampaignBonusType::ARTIFACT:
+								bnode["type"].String() = ArtifactID::encode(bonus.info2);
+								break;
+							case CampaignBonusType::SPELL_SCROLL:
+								bnode["type"].String() = SpellID::encode(bonus.info2);
+								break;
+							case CampaignBonusType::PRIMARY_SKILL:
+								for(auto & ps : primarySkillsMap)
+									bnode[ps.first].Integer() = (bonus.info2 >> ps.second) & 0xff;
+								break;
+							default:
+								bnode["type"].Integer() = bonus.info2;
+						}
+						break;
+				}
+				node["bonuses"].Vector().push_back(bnode);
+			}
+			break;
+		}
+	case CampaignStartOptions::HERO_CROSSOVER:
+		{
+			for(auto & bonus : travel.bonusesToChoose)
+			{
+				JsonNode bnode;
+				bnode["playerColor"].Integer() = bonus.info1;
+				bnode["scenario"].Integer() = bonus.info2;
+				node["bonuses"].Vector().push_back(bnode);
+			}
+			break;
+		}
+	case CampaignStartOptions::HERO_OPTIONS:
+		{
+			for(auto & bonus : travel.bonusesToChoose)
+			{
+				JsonNode bnode;
+				bnode["playerColor"].Integer() = bonus.info1;
+
+				if(vstd::contains(vstd::reverseMap(heroSpecialMap), bonus.info2))
+					bnode["hero"].String() = vstd::reverseMap(heroSpecialMap)[bonus.info2];
+				else
+					bnode["hero"].String() = HeroTypeID::encode(bonus.info2);
+				
+				node["bonuses"].Vector().push_back(bnode);
+			}
+			break;
+		}
+	}
+}
 
 void CampaignHandler::readHeaderFromMemory( CampaignHeader & ret, CBinaryReader & reader, std::string filename, std::string modName, std::string encoding )
 {

+ 7 - 0
lib/campaign/CampaignHandler.h

@@ -26,6 +26,9 @@ class DLL_LINKAGE CampaignHandler
 	static CampaignScenario readScenarioFromJson(JsonNode & reader);
 	static CampaignTravel readScenarioTravelFromJson(JsonNode & reader);
 
+	//writer for VCMI campaigns (*.vcmp)
+	static void writeScenarioTravelToJson(JsonNode & node, const CampaignTravel & travel);
+
 	//parsers for original H3C campaigns
 	static void readHeaderFromMemory(CampaignHeader & target, CBinaryReader & reader, std::string filename, std::string modName, std::string encoding);
 	static CampaignScenario readScenarioFromMemory(CBinaryReader & reader, CampaignHeader & header);
@@ -43,6 +46,10 @@ public:
 	static std::unique_ptr<Campaign> getHeader( const std::string & name); //name - name of appropriate file
 
 	static std::shared_ptr<CampaignState> getCampaign(const std::string & name); //name - name of appropriate file
+
+	//writer for VCMI campaigns (*.vcmp)
+	static JsonNode writeHeaderToJson(CampaignHeader & header);
+	static JsonNode writeScenarioToJson(const CampaignScenario & scenario);
 };
 
 VCMI_LIB_NAMESPACE_END

+ 35 - 0
lib/campaign/CampaignState.cpp

@@ -45,6 +45,22 @@ CampaignRegions::RegionDescription CampaignRegions::RegionDescription::fromJson(
 	return rd;
 }
 
+JsonNode CampaignRegions::RegionDescription::toJson(CampaignRegions::RegionDescription & rd)
+{
+	JsonNode node;
+	node["infix"].String() = rd.infix;
+	node["x"].Float() = rd.pos.x;
+	node["y"].Float() = rd.pos.y;
+	if(rd.labelPos != std::nullopt)
+	{
+		node["labelPos"]["x"].Float() = (*rd.labelPos).x;
+		node["labelPos"]["y"].Float() = (*rd.labelPos).y;
+	}
+	else
+		node["labelPos"].clear();
+	return node;
+}
+
 CampaignRegions CampaignRegions::fromJson(const JsonNode & node)
 {
 	CampaignRegions cr;
@@ -59,6 +75,25 @@ CampaignRegions CampaignRegions::fromJson(const JsonNode & node)
 	return cr;
 }
 
+JsonNode CampaignRegions::toJson(CampaignRegions cr)
+{
+	JsonNode node;
+	node["prefix"].String() = cr.campPrefix;
+	node["colorSuffixLength"].Float() = cr.colorSuffixLength;
+	if(!cr.campSuffix.size())
+		node["suffix"].clear();
+	else
+		node["suffix"].Vector() = JsonVector{ JsonNode(cr.campSuffix[0]), JsonNode(cr.campSuffix[1]), JsonNode(cr.campSuffix[2]) };
+	if(cr.campBackground.empty())
+		node["background"].clear();
+	else
+		node["background"].String() = cr.campBackground;
+	node["desc"].Vector() = JsonVector();
+	for(auto & region : cr.regions)
+		node["desc"].Vector().push_back(CampaignRegions::RegionDescription::toJson(region));
+	return node;
+}
+
 CampaignRegions CampaignRegions::getLegacy(int campId)
 {
 	static std::vector<CampaignRegions> campDescriptions;

+ 2 - 0
lib/campaign/CampaignState.h

@@ -59,6 +59,7 @@ class DLL_LINKAGE CampaignRegions
 		}
 
 		static CampaignRegions::RegionDescription fromJson(const JsonNode & node);
+		static JsonNode toJson(CampaignRegions::RegionDescription & rd);
 	};
 
 	std::vector<RegionDescription> regions;
@@ -86,6 +87,7 @@ public:
 	}
 
 	static CampaignRegions fromJson(const JsonNode & node);
+	static JsonNode toJson(CampaignRegions cr);
 	static CampaignRegions getLegacy(int campId);
 };
 

+ 3 - 1
lib/modding/CModHandler.cpp

@@ -144,7 +144,7 @@ TModID CModHandler::findResourceOrigin(const ResourcePath & name) const
 			return "core";
 
 		if(CResourceHandler::get("mapEditor")->existsResource(name))
-			return "core"; // Workaround for loading maps via map editor
+			return "mapEditor"; // Workaround for loading maps via map editor
 	}
 	catch( const std::out_of_range & e)
 	{
@@ -189,6 +189,8 @@ std::string CModHandler::getModLanguage(const TModID& modId) const
 		return VLC->generaltexth->getInstalledLanguage();
 	if(modId == "map")
 		return VLC->generaltexth->getPreferredLanguage();
+	if(modId == "mapEditor")
+		return VLC->generaltexth->getPreferredLanguage();
 	return getModInfo(modId).getBaseLanguage();
 }
 

+ 71 - 0
mapeditor/mainwindow.cpp

@@ -24,7 +24,9 @@
 #include "../lib/logging/CBasicLogConfigurator.h"
 #include "../lib/CConfigHandler.h"
 #include "../lib/filesystem/Filesystem.h"
+#include "../lib/filesystem/CMemoryBuffer.h"
 #include "../lib/GameConstants.h"
+#include "../lib/campaign/CampaignHandler.h"
 #include "../lib/mapObjectConstructors/AObjectTypeHandler.h"
 #include "../lib/mapObjectConstructors/CObjectClassesHandler.h"
 #include "../lib/mapObjects/ObjectTemplate.h"
@@ -32,6 +34,7 @@
 #include "../lib/mapping/CMap.h"
 #include "../lib/mapping/CMapEditManager.h"
 #include "../lib/mapping/MapFormat.h"
+#include "../lib/mapping/MapFormatJson.h"
 #include "../lib/modding/ModIncompatibility.h"
 #include "../lib/RoadHandler.h"
 #include "../lib/RiverHandler.h"
@@ -398,6 +401,27 @@ std::unique_ptr<CMap> MainWindow::openMapInternal(const QString & filenameSelect
 		throw std::runtime_error("Corrupted map");
 }
 
+std::shared_ptr<CampaignState> MainWindow::openCampaignInternal(const QString & filenameSelect)
+{
+	QFileInfo fi(filenameSelect);
+	std::string fname = fi.fileName().toStdString();
+	std::string fdir = fi.dir().path().toStdString();
+	
+	ResourcePath resId("MAPEDITOR/" + fname, EResType::CAMPAIGN);
+	
+	//addFilesystem takes care about memory deallocation if case of failure, no memory leak here
+	auto * mapEditorFilesystem = new CFilesystemLoader("MAPEDITOR/", fdir, 0);
+	CResourceHandler::removeFilesystem("local", "mapEditor");
+	CResourceHandler::addFilesystem("local", "mapEditor", mapEditorFilesystem);
+	
+	if(!CResourceHandler::get("mapEditor")->existsResource(resId))
+		throw std::runtime_error("Cannot open campaign from this folder");
+	if(auto campaign = CampaignHandler::getCampaign(resId.getName()))
+		return campaign;
+	else
+		throw std::runtime_error("Corrupted campaign");
+}
+
 bool MainWindow::openMap(const QString & filenameSelect)
 {
 	try
@@ -1373,6 +1397,53 @@ void MainWindow::on_actionh3m_converter_triggered()
 	}
 }
 
+void MainWindow::on_actionh3c_converter_triggered()
+{
+	auto campaignFile = QFileDialog::getOpenFileName(this, tr("Select campaign to convert"),
+		QString::fromStdString(VCMIDirs::get().userDataPath().make_preferred().string()),
+		tr("HoMM3 campaigns (*.h3c)"));
+	if(campaignFile.isEmpty())
+		return;
+	
+	auto campaignFileDest = QFileDialog::getSaveFileName(this, tr("Select destination file"),
+		QString::fromStdString(VCMIDirs::get().userDataPath().make_preferred().string()),
+		tr("VCMI campaigns (*.vcmp)"));
+	if(campaignFileDest.isEmpty())
+		return;
+	
+	QFileInfo fileInfo(campaignFileDest);
+	if(fileInfo.suffix().toLower() != "vcmp")
+		campaignFileDest += ".vcmp";
+	auto campaign = openCampaignInternal(campaignFile);
+
+	auto jsonCampaign = CampaignHandler::writeHeaderToJson(*campaign);
+	
+	std::shared_ptr<CIOApi> io(new CDefaultIOApi());
+	auto saver = std::make_shared<CZipSaver>(io, campaignFileDest.toStdString());
+	for(auto & scenario : campaign->allScenarios())
+	{
+		CMapService mapService;
+		auto map = campaign->getMap(scenario, nullptr);
+		controller.repairMap(map.get());
+		CMemoryBuffer serializeBuffer;
+		{
+			CMapSaverJson jsonSaver(&serializeBuffer);
+			jsonSaver.saveMap(map);
+		}
+
+		auto mapName = boost::algorithm::to_lower_copy(campaign->scenario(scenario).mapName);
+		mapName = boost::replace_all_copy(mapName, ".h3m", std::string("")) + ".vmap";
+
+		auto stream = saver->addFile(mapName);
+		stream->write(reinterpret_cast<const ui8 *>(serializeBuffer.getBuffer().data()), serializeBuffer.getSize());
+
+		jsonCampaign["scenarios"].Vector().push_back(CampaignHandler::writeScenarioToJson(campaign->scenario(scenario)));
+		jsonCampaign["scenarios"].Vector().back()["map"].String() = mapName;
+	}
+
+	auto jsonCampaignStr = jsonCampaign.toString();
+	saver->addFile("header.json")->write(reinterpret_cast<const ui8 *>(jsonCampaignStr.data()), jsonCampaignStr.length());
+}
 
 void MainWindow::on_actionLock_triggered()
 {

+ 4 - 0
mapeditor/mainwindow.h

@@ -11,6 +11,7 @@ class ObjectBrowserProxyModel;
 
 VCMI_LIB_NAMESPACE_BEGIN
 class CMap;
+class CampaignState;
 class CGObjectInstance;
 VCMI_LIB_NAMESPACE_END
 
@@ -35,6 +36,7 @@ class MainWindow : public QMainWindow
 #endif
 	
 	std::unique_ptr<CMap> openMapInternal(const QString &);
+	std::shared_ptr<CampaignState> openCampaignInternal(const QString &);
 
 public:
     explicit MainWindow(QWidget *parent = nullptr);
@@ -118,6 +120,8 @@ private slots:
 	
 	void on_actionh3m_converter_triggered();
 
+	void on_actionh3c_converter_triggered();
+
 	void on_actionLock_triggered();
 
 	void on_actionUnlock_triggered();

+ 9 - 0
mapeditor/mainwindow.ui

@@ -71,6 +71,7 @@
     <addaction name="actionSave_as"/>
     <addaction name="actionExport"/>
     <addaction name="actionh3m_converter"/>
+    <addaction name="actionh3c_converter"/>
    </widget>
    <widget class="QMenu" name="menuMap">
     <property name="title">
@@ -1352,6 +1353,14 @@
     <string>h3m converter</string>
    </property>
   </action>
+  <action name="actionh3c_converter">
+   <property name="text">
+    <string>h3c converter</string>
+   </property>
+   <property name="toolTip">
+    <string>h3c converter</string>
+   </property>
+  </action>
   <action name="actionLock">
    <property name="text">
     <string>Lock</string>