Selaa lähdekoodia

Merge pull request #4428 from Laserlicht/vcmp_rework

VCMP format -> change to zip
Ivan Savenko 1 vuosi sitten
vanhempi
sitoutus
39d3217d20

+ 17 - 3
client/lobby/SelectionTab.cpp

@@ -390,14 +390,28 @@ void SelectionTab::showPopupWindow(const Point & cursorPosition)
 
 	if(!curItems[py]->isFolder)
 	{
-		auto creationDateTime = tabType == ESelectionScreen::newGame && curItems[py]->mapHeader->creationDateTime ? TextOperations::getFormattedDateTimeLocal(curItems[py]->mapHeader->creationDateTime) : curItems[py]->date;
-		auto author = curItems[py]->mapHeader->author.toString() + (!curItems[py]->mapHeader->authorContact.toString().empty() ? (" <" + curItems[py]->mapHeader->authorContact.toString() + ">") : "");
+		std::string creationDateTime;
+		std::string author;
+		std::string mapVersion;
+		if(tabType != ESelectionScreen::campaignList)
+		{
+			author = curItems[py]->mapHeader->author.toString() + (!curItems[py]->mapHeader->authorContact.toString().empty() ? (" <" + curItems[py]->mapHeader->authorContact.toString() + ">") : "");
+			mapVersion = curItems[py]->mapHeader->mapVersion.toString();
+			creationDateTime = tabType == ESelectionScreen::newGame && curItems[py]->mapHeader->creationDateTime ? TextOperations::getFormattedDateTimeLocal(curItems[py]->mapHeader->creationDateTime) : curItems[py]->date;
+		}
+		else
+		{
+			author = curItems[py]->campaign->getAuthor() + (!curItems[py]->campaign->getAuthorContact().empty() ? (" <" + curItems[py]->campaign->getAuthorContact() + ">") : "");
+			mapVersion = curItems[py]->campaign->getCampaignVersion();
+			creationDateTime = curItems[py]->campaign->getCreationDateTime() ? TextOperations::getFormattedDateTimeLocal(curItems[py]->campaign->getCreationDateTime()) : curItems[py]->date;
+		}
+
 		GH.windows().createAndPushWindow<CMapOverview>(
 			curItems[py]->getNameTranslated(),
 			curItems[py]->fullFileURI,
 			creationDateTime,
 			author,
-			curItems[py]->mapHeader->mapVersion.toString(),
+			mapVersion,
 			ResourcePath(curItems[py]->fileURI),
 			tabType
 		);

+ 3 - 18
docs/modders/Campaign_Format.md

@@ -5,7 +5,7 @@
 Starting from version 1.3, VCMI supports its own campaign format.
 Campaigns have *.vcmp file format and it consists from campaign json and set of scenarios (can be both *.vmap and *.h3m)
 
-To start making campaign, create file named `00.json`. See also [Packing campaign](#packing-campaign)
+To start making campaign, create file named `header.json`. See also [Packing campaign](#packing-campaign)
 
 Basic structure of this file is here, each section is described in details below
 ```js
@@ -199,24 +199,9 @@ Predefined campaign regions are located in file `campaign_regions.json`
 ## Packing campaign
 
 After campaign scenarios and campaign description are ready, you should pack them into *.vcmp file.
-This file is basically headless gz archive.
+This file is a zip archive.
 
-Your campaign should be stored in some folder with json describing campaign information.
-Place all your scenarios inside same folder and enumerate their filenames, e.g `01.vmap`, '02.vmap', etc.
-```
-my-campaign/
-|-- 00.json
-|-- 01.vmap
-|-- 02.vmap
-|-- 03.vmap
-```
-
-If you use unix system, execute this command to pack your campaign:
-```
-gzip -c -n ./* >> my-campaign.vcmp
-```
-
-If you are using Windows system, try this https://gnuwin32.sourceforge.net/packages/gzip.htm
+The scenarios should be named as in `"map"` field from header. Subfolders are allowed.
 
 ## Compatibility table
 | Version | Min VCMI | Max VCMI | Description |

+ 66 - 23
lib/campaign/CampaignHandler.cpp

@@ -16,6 +16,7 @@
 #include "../filesystem/CCompressedStream.h"
 #include "../filesystem/CMemoryStream.h"
 #include "../filesystem/CBinaryReader.h"
+#include "../filesystem/CZipLoader.h"
 #include "../VCMI_Lib.h"
 #include "../constants/StringConstants.h"
 #include "../mapping/CMapHeader.h"
@@ -126,14 +127,19 @@ static std::string convertMapName(std::string input)
 
 std::string CampaignHandler::readLocalizedString(CampaignHeader & target, CBinaryReader & reader, std::string filename, std::string modName, std::string encoding, std::string identifier)
 {
-	TextIdentifier stringID( "campaign", convertMapName(filename), identifier);
-
 	std::string input = TextOperations::toUnicode(reader.readBaseString(), encoding);
 
-	if (input.empty())
+	return readLocalizedString(target, input, filename, modName, identifier);
+}
+
+std::string CampaignHandler::readLocalizedString(CampaignHeader & target, std::string text, std::string filename, std::string modName, std::string identifier)
+{
+	TextIdentifier stringID( "campaign", convertMapName(filename), identifier);
+
+	if (text.empty())
 		return "";
 
-	target.getTexts().registerString(modName, stringID, input);
+	target.getTexts().registerString(modName, stringID, text);
 	return stringID.get();
 }
 
@@ -149,8 +155,8 @@ void CampaignHandler::readHeaderFromJson(CampaignHeader & ret, JsonNode & reader
 	ret.version = CampaignVersion::VCMI;
 	ret.campaignRegions = CampaignRegions::fromJson(reader["regions"]);
 	ret.numberOfScenarios = reader["scenarios"].Vector().size();
-	ret.name.appendTextID(reader["name"].String());
-	ret.description.appendTextID(reader["description"].String());
+	ret.name.appendTextID(readLocalizedString(ret, reader["name"].String(), filename, modName, "name"));
+	ret.description.appendTextID(readLocalizedString(ret, reader["description"].String(), filename, modName, "description"));
 	ret.author.appendRawString(reader["author"].String());
 	ret.authorContact.appendRawString(reader["authorContact"].String());
 	ret.campaignVersion.appendRawString(reader["campaignVersion"].String());
@@ -588,32 +594,69 @@ CampaignTravel CampaignHandler::readScenarioTravelFromMemory(CBinaryReader & rea
 
 std::vector< std::vector<ui8> > CampaignHandler::getFile(std::unique_ptr<CInputStream> file, const std::string & filename, bool headerOnly)
 {
-	CCompressedStream stream(std::move(file), true);
+	std::array<ui8, 2> magic;
+	file->read(magic.data(), magic.size());
+	file->seek(0);
 
 	std::vector< std::vector<ui8> > ret;
 
-	try
+	static const std::array<ui8, 2> zipHeaderMagic{0x50, 0x4B};
+	if (magic == zipHeaderMagic) // ZIP archive - assume VCMP format
 	{
-		do
+		CInputStream * buffer(file.get());
+		std::shared_ptr<CIOApi> ioApi(new CProxyROIOApi(buffer));
+		CZipLoader loader("", "_", ioApi);
+
+		// load header
+		JsonPath resource = JsonPath::builtin(VCMP_HEADER_FILE_NAME);
+		if(!loader.existsResource(resource))
+			throw std::runtime_error(resource.getName() + " not found in " + filename);
+		auto data = loader.load(resource)->readAll();
+		ret.push_back(std::vector<ui8>(data.first.get(), data.first.get() + data.second));
+
+		if(headerOnly)
+			return ret;
+
+		// load scenarios
+		JsonNode header(reinterpret_cast<const std::byte*>(data.first.get()), data.second, VCMP_HEADER_FILE_NAME);
+		for(auto scenario : header["scenarios"].Vector())
 		{
-			std::vector<ui8> block(stream.getSize());
-			stream.read(block.data(), block.size());
-			ret.push_back(block);
-			ret.back().shrink_to_fit();
+			ResourcePath resource = ResourcePath(scenario["map"].String(), EResType::MAP);
+			if(!loader.existsResource(resource))
+				throw std::runtime_error(resource.getName() + " not found in " + filename);
+			auto data = loader.load(resource)->readAll();
+			ret.push_back(std::vector<ui8>(data.first.get(), data.first.get() + data.second));
 		}
-		while (!headerOnly && stream.getNextBlock());
+
+		return ret;
 	}
-	catch (const DecompressionException & e)
+	else // H3C
 	{
-		// Some campaigns in French version from gog.com have trailing garbage bytes
-		// For example, slayer.h3c consist from 5 parts: header + 4 maps
-		// However file also contains ~100 "extra" bytes after those 5 parts are decompressed that do not represent gzip stream
-		// leading to exception "Incorrect header check"
-		// Since H3 handles these files correctly, simply log this as warning and proceed
-		logGlobal->warn("Failed to read file %s. Encountered error during decompression: %s", filename, e.what());
-	}
+		CCompressedStream stream(std::move(file), true);
 
-	return ret;
+		try
+		{
+			do
+			{
+				std::vector<ui8> block(stream.getSize());
+				stream.read(block.data(), block.size());
+				ret.push_back(block);
+				ret.back().shrink_to_fit();
+			}
+			while (!headerOnly && stream.getNextBlock());
+		}
+		catch (const DecompressionException & e)
+		{
+			// Some campaigns in French version from gog.com have trailing garbage bytes
+			// For example, slayer.h3c consist from 5 parts: header + 4 maps
+			// However file also contains ~100 "extra" bytes after those 5 parts are decompressed that do not represent gzip stream
+			// leading to exception "Incorrect header check"
+			// Since H3 handles these files correctly, simply log this as warning and proceed
+			logGlobal->warn("Failed to read file %s. Encountered error during decompression: %s", filename, e.what());
+		}
+
+		return ret;
+	}
 }
 
 VideoPath CampaignHandler::prologVideoName(ui8 index)

+ 2 - 0
lib/campaign/CampaignHandler.h

@@ -17,6 +17,7 @@ VCMI_LIB_NAMESPACE_BEGIN
 class DLL_LINKAGE CampaignHandler
 {
 	static std::string readLocalizedString(CampaignHeader & target, CBinaryReader & reader, std::string filename, std::string modName, std::string encoding, std::string identifier);
+	static std::string readLocalizedString(CampaignHeader & target, std::string text, std::string filename, std::string modName, std::string identifier);
 
 	static void readCampaign(Campaign * target, const std::vector<ui8> & stream, std::string filename, std::string modName, std::string encoding);
 
@@ -37,6 +38,7 @@ class DLL_LINKAGE CampaignHandler
 	static AudioPath prologMusicName(ui8 index);
 	static AudioPath prologVoiceName(ui8 index);
 
+	static constexpr auto VCMP_HEADER_FILE_NAME = "header.json";
 public:
 	static std::unique_ptr<Campaign> getHeader( const std::string & name); //name - name of appropriate file
 

+ 20 - 0
lib/campaign/CampaignState.cpp

@@ -144,6 +144,26 @@ std::string CampaignHeader::getNameTranslated() const
 	return name.toString();
 }
 
+std::string CampaignHeader::getAuthor() const
+{
+	return authorContact.toString();
+}
+
+std::string CampaignHeader::getAuthorContact() const
+{
+	return authorContact.toString();
+}
+
+std::string CampaignHeader::getCampaignVersion() const
+{
+	return campaignVersion.toString();
+}
+
+time_t CampaignHeader::getCreationDateTime() const
+{
+	return creationDateTime;
+}
+
 std::string CampaignHeader::getFilename() const
 {
 	return filename;

+ 4 - 0
lib/campaign/CampaignState.h

@@ -103,6 +103,10 @@ public:
 
 	std::string getDescriptionTranslated() const;
 	std::string getNameTranslated() const;
+	std::string getAuthor() const;
+	std::string getAuthorContact() const;
+	std::string getCampaignVersion() const;
+	time_t getCreationDateTime() const;
 	std::string getFilename() const;
 	std::string getModName() const;
 	std::string getEncoding() const;