From d97fdfdefac51953c3212cf74c6a4f95bbc97c60 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 10 Aug 2024 13:30:03 +0200 Subject: [PATCH 1/5] zip format VCMP --- lib/campaign/CampaignHandler.cpp | 76 +++++++++++++++++++++++--------- lib/campaign/CampaignHandler.h | 1 + 2 files changed, 56 insertions(+), 21 deletions(-) diff --git a/lib/campaign/CampaignHandler.cpp b/lib/campaign/CampaignHandler.cpp index c52555f23..b7c592e45 100644 --- a/lib/campaign/CampaignHandler.cpp +++ b/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" @@ -588,32 +589,65 @@ CampaignTravel CampaignHandler::readScenarioTravelFromMemory(CBinaryReader & rea std::vector< std::vector > CampaignHandler::getFile(std::unique_ptr file, const std::string & filename, bool headerOnly) { - CCompressedStream stream(std::move(file), true); + std::vector magic(2); + file->read(magic.data(), magic.size()); + file->seek(0); std::vector< std::vector > ret; - try + if (magic == std::vector{0x50, 0x4B}) // VCMP (ZIP) { - do - { - std::vector 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()); - } + CInputStream * buffer(file.get()); + std::shared_ptr ioApi(new CProxyROIOApi(buffer)); + CZipLoader loader("", "_", ioApi); - return ret; + // 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(data.first.get(), data.first.get() + data.second)); + + // load scenarios + JsonNode header(reinterpret_cast(data.first.get()), data.second, VCMP_HEADER_FILE_NAME); + for(auto scenario : header["scenarios"].Vector()) + { + 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(data.first.get(), data.first.get() + data.second)); + } + + return ret; + } + else // H3M + { + CCompressedStream stream(std::move(file), true); + + try + { + do + { + std::vector 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) diff --git a/lib/campaign/CampaignHandler.h b/lib/campaign/CampaignHandler.h index cd7332627..9d6558bb2 100644 --- a/lib/campaign/CampaignHandler.h +++ b/lib/campaign/CampaignHandler.h @@ -37,6 +37,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 getHeader( const std::string & name); //name - name of appropriate file From be61daa95a785bda632568b93494d1539ebff6b9 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 10 Aug 2024 13:36:31 +0200 Subject: [PATCH 2/5] campaigns docs --- docs/modders/Campaign_Format.md | 21 +++------------------ 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/docs/modders/Campaign_Format.md b/docs/modders/Campaign_Format.md index 554e98566..6779a1e7c 100644 --- a/docs/modders/Campaign_Format.md +++ b/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 | From b16f721b3949e12584c0486e20501ca680cad409 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 10 Aug 2024 13:57:13 +0200 Subject: [PATCH 3/5] respect headeronly --- lib/campaign/CampaignHandler.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/campaign/CampaignHandler.cpp b/lib/campaign/CampaignHandler.cpp index b7c592e45..b01accb3c 100644 --- a/lib/campaign/CampaignHandler.cpp +++ b/lib/campaign/CampaignHandler.cpp @@ -608,6 +608,9 @@ std::vector< std::vector > CampaignHandler::getFile(std::unique_ptrreadAll(); ret.push_back(std::vector(data.first.get(), data.first.get() + data.second)); + if(headerOnly) + return ret; + // load scenarios JsonNode header(reinterpret_cast(data.first.get()), data.second, VCMP_HEADER_FILE_NAME); for(auto scenario : header["scenarios"].Vector()) From 772224e46ca02ad2de3db227d83c1d6411fd4c69 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 10 Aug 2024 14:28:50 +0200 Subject: [PATCH 4/5] enable translation for vcmp --- lib/campaign/CampaignHandler.cpp | 17 +++++++++++------ lib/campaign/CampaignHandler.h | 1 + 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/lib/campaign/CampaignHandler.cpp b/lib/campaign/CampaignHandler.cpp index b01accb3c..55b65381e 100644 --- a/lib/campaign/CampaignHandler.cpp +++ b/lib/campaign/CampaignHandler.cpp @@ -127,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(); } @@ -150,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, "name")); ret.author.appendRawString(reader["author"].String()); ret.authorContact.appendRawString(reader["authorContact"].String()); ret.campaignVersion.appendRawString(reader["campaignVersion"].String()); diff --git a/lib/campaign/CampaignHandler.h b/lib/campaign/CampaignHandler.h index 9d6558bb2..cda84fb05 100644 --- a/lib/campaign/CampaignHandler.h +++ b/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 & stream, std::string filename, std::string modName, std::string encoding); From 661c374bf2b6f5d184f96b8387905e696118db70 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 10 Aug 2024 14:59:22 +0200 Subject: [PATCH 5/5] fixes & code review --- client/lobby/SelectionTab.cpp | 20 +++++++++++++++++--- lib/campaign/CampaignHandler.cpp | 9 +++++---- lib/campaign/CampaignState.cpp | 20 ++++++++++++++++++++ lib/campaign/CampaignState.h | 4 ++++ 4 files changed, 46 insertions(+), 7 deletions(-) diff --git a/client/lobby/SelectionTab.cpp b/client/lobby/SelectionTab.cpp index 33276ea1b..0c190e5e7 100644 --- a/client/lobby/SelectionTab.cpp +++ b/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( curItems[py]->getNameTranslated(), curItems[py]->fullFileURI, creationDateTime, author, - curItems[py]->mapHeader->mapVersion.toString(), + mapVersion, ResourcePath(curItems[py]->fileURI), tabType ); diff --git a/lib/campaign/CampaignHandler.cpp b/lib/campaign/CampaignHandler.cpp index 55b65381e..44dcc763b 100644 --- a/lib/campaign/CampaignHandler.cpp +++ b/lib/campaign/CampaignHandler.cpp @@ -156,7 +156,7 @@ void CampaignHandler::readHeaderFromJson(CampaignHeader & ret, JsonNode & reader ret.campaignRegions = CampaignRegions::fromJson(reader["regions"]); ret.numberOfScenarios = reader["scenarios"].Vector().size(); ret.name.appendTextID(readLocalizedString(ret, reader["name"].String(), filename, modName, "name")); - ret.description.appendTextID(readLocalizedString(ret, reader["description"].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()); @@ -594,13 +594,14 @@ CampaignTravel CampaignHandler::readScenarioTravelFromMemory(CBinaryReader & rea std::vector< std::vector > CampaignHandler::getFile(std::unique_ptr file, const std::string & filename, bool headerOnly) { - std::vector magic(2); + std::array magic; file->read(magic.data(), magic.size()); file->seek(0); std::vector< std::vector > ret; - if (magic == std::vector{0x50, 0x4B}) // VCMP (ZIP) + static const std::array zipHeaderMagic{0x50, 0x4B}; + if (magic == zipHeaderMagic) // ZIP archive - assume VCMP format { CInputStream * buffer(file.get()); std::shared_ptr ioApi(new CProxyROIOApi(buffer)); @@ -629,7 +630,7 @@ std::vector< std::vector > CampaignHandler::getFile(std::unique_ptr