diff --git a/client/lobby/CBonusSelection.cpp b/client/lobby/CBonusSelection.cpp index 104184418..595cb50b2 100644 --- a/client/lobby/CBonusSelection.cpp +++ b/client/lobby/CBonusSelection.cpp @@ -51,6 +51,7 @@ #include "../../lib/mapObjects/CGHeroInstance.h" + std::shared_ptr CBonusSelection::getCampaign() { return CSH->si->campState; @@ -60,14 +61,9 @@ CBonusSelection::CBonusSelection() : CWindowObject(BORDERED) { OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; - static const std::string bgNames[] = - { - "E1_BG.BMP", "G2_BG.BMP", "E2_BG.BMP", "G1_BG.BMP", "G3_BG.BMP", "N1_BG.BMP", - "S1_BG.BMP", "BR_BG.BMP", "IS_BG.BMP", "KR_BG.BMP", "NI_BG.BMP", "TA_BG.BMP", "AR_BG.BMP", "HS_BG.BMP", - "BB_BG.BMP", "NB_BG.BMP", "EL_BG.BMP", "RN_BG.BMP", "UA_BG.BMP", "SP_BG.BMP" - }; - loadPositionsOfGraphics(); - setBackground(bgNames[getCampaign()->camp->header.mapVersion]); + + std::string bgName = getCampaign()->camp->header.campaignRegions.campPrefix + "_BG.BMP"; + setBackground(bgName); panelBackground = std::make_shared("CAMPBRF.BMP", 456, 6); @@ -110,35 +106,9 @@ CBonusSelection::CBonusSelection() for(int g = 0; g < getCampaign()->camp->scenarios.size(); ++g) { if(getCampaign()->camp->conquerable(g)) - regions.push_back(std::make_shared(g, true, true, campDescriptions[getCampaign()->camp->header.mapVersion])); + regions.push_back(std::make_shared(g, true, true, getCampaign()->camp->header.campaignRegions)); else if(getCampaign()->camp->scenarios[g].conquered) //display as striped - regions.push_back(std::make_shared(g, false, false, campDescriptions[getCampaign()->camp->header.mapVersion])); - } -} - -void CBonusSelection::loadPositionsOfGraphics() -{ - const JsonNode config(ResourceID("config/campaign_regions.json")); - - for(const JsonNode & campaign : config["campaign_regions"].Vector()) - { - SCampPositions sc; - - sc.campPrefix = campaign["prefix"].String(); - sc.colorSuffixLength = static_cast(campaign["color_suffix_length"].Float()); - - for(const JsonNode & desc : campaign["desc"].Vector()) - { - SCampPositions::SRegionDesc rd; - - rd.infix = desc["infix"].String(); - rd.xpos = static_cast(desc["x"].Float()); - rd.ypos = static_cast(desc["y"].Float()); - sc.regions.push_back(rd); - } - - campDescriptions.push_back(sc); - + regions.push_back(std::make_shared(g, false, false, getCampaign()->camp->header.campaignRegions)); } } @@ -196,7 +166,11 @@ void CBonusSelection::createBonusesIcons() } assert(faction != -1); - BuildingID buildID = CBuildingHandler::campToERMU(bonDescs[i].info1, faction, std::set()); + BuildingID buildID; + if(getCampaign()->camp->header.version == CampaignVersion::VCMI) + buildID = BuildingID(bonDescs[i].info1); + else + buildID = CBuildingHandler::campToERMU(bonDescs[i].info1, faction, std::set()); picName = graphics->ERMUtoPicture[faction][buildID]; picNumber = -1; @@ -470,7 +444,7 @@ void CBonusSelection::decreaseDifficulty() CSH->setDifficulty(CSH->si->difficulty - 1); } -CBonusSelection::CRegion::CRegion(int id, bool accessible, bool selectable, const SCampPositions & campDsc) +CBonusSelection::CRegion::CRegion(int id, bool accessible, bool selectable, const CampaignRegions & campDsc) : CIntObject(LCLICK | RCLICK), idOfMapAndRegion(id), accessible(accessible), selectable(selectable) { OBJ_CONSTRUCTION; @@ -480,7 +454,7 @@ CBonusSelection::CRegion::CRegion(int id, bool accessible, bool selectable, cons {"Re", "Bl", "Br", "Gr", "Or", "Vi", "Te", "Pi"} }; - const SCampPositions::SRegionDesc & desc = campDsc.regions[idOfMapAndRegion]; + const CampaignRegions::RegionDescription & desc = campDsc.regions[idOfMapAndRegion]; pos.x += desc.xpos; pos.y += desc.ypos; diff --git a/client/lobby/CBonusSelection.h b/client/lobby/CBonusSelection.h index ecf322758..55b77cd9e 100644 --- a/client/lobby/CBonusSelection.h +++ b/client/lobby/CBonusSelection.h @@ -8,6 +8,7 @@ * */ #pragma once +#include "../../lib/mapping/CCampaignHandler.h" #include "../windows/CWindowObject.h" VCMI_LIB_NAMESPACE_BEGIN @@ -32,21 +33,6 @@ public: std::shared_ptr getCampaign(); CBonusSelection(); - struct SCampPositions - { - std::string campPrefix; - int colorSuffixLength; - - struct SRegionDesc - { - std::string infix; - int xpos, ypos; - }; - - std::vector regions; - - }; - class CRegion : public CIntObject { @@ -57,13 +43,12 @@ public: bool accessible; // false if region should be striped bool selectable; // true if region should be selectable public: - CRegion(int id, bool accessible, bool selectable, const SCampPositions & campDsc); + CRegion(int id, bool accessible, bool selectable, const CampaignRegions & campDsc); void updateState(); void clickLeft(tribool down, bool previousState) override; void clickRight(tribool down, bool previousState) override; }; - void loadPositionsOfGraphics(); void createBonusesIcons(); void updateAfterStateChange(); @@ -84,7 +69,6 @@ public: std::shared_ptr mapName; std::shared_ptr labelMapDescription; std::shared_ptr mapDescription; - std::vector campDescriptions; std::vector> regions; std::shared_ptr flagbox; diff --git a/client/lobby/CLobbyScreen.cpp b/client/lobby/CLobbyScreen.cpp index f4ab04e74..a591dbd65 100644 --- a/client/lobby/CLobbyScreen.cpp +++ b/client/lobby/CLobbyScreen.cpp @@ -131,6 +131,15 @@ void CLobbyScreen::startScenario(bool allowOnlyAI) CSH->sendStartGame(allowOnlyAI); buttonStart->block(true); } + catch(CModHandler::Incompatibility & e) + { + logGlobal->warn("Incompatibility exception during start scenario: %s", e.what()); + + auto errorMsg = VLC->generaltexth->translate("vcmi.server.errors.modsIncompatibility") + '\n'; + errorMsg += e.what(); + + CInfoWindow::showInfoDialog(errorMsg, CInfoWindow::TCompsInfo(), PlayerColor(1)); + } catch(std::exception & e) { logGlobal->error("Exception during startScenario: %s", e.what()); diff --git a/client/lobby/SelectionTab.cpp b/client/lobby/SelectionTab.cpp index 9e59a1260..7a300d315 100644 --- a/client/lobby/SelectionTab.cpp +++ b/client/lobby/SelectionTab.cpp @@ -100,8 +100,8 @@ bool mapSorter::operator()(const std::shared_ptr aaa, const std::share switch(sortBy) { case _numOfMaps: //by number of maps in campaign - return CGI->generaltexth->getCampaignLength(aaa->campaignHeader->mapVersion) < - CGI->generaltexth->getCampaignLength(bbb->campaignHeader->mapVersion); + return aaa->campaignHeader->numberOfScenarios < + bbb->campaignHeader->numberOfScenarios; break; case _name: //by name return boost::ilexicographical_compare(aaa->campaignHeader->name, bbb->campaignHeader->name); @@ -595,7 +595,8 @@ void SelectionTab::parseCampaigns(const std::unordered_set & files) //allItems[i].date = std::asctime(std::localtime(&files[i].date)); info->fileURI = file.getName(); info->campaignInit(); - allItems.push_back(info); + if(info->campaignHeader) + allItems.push_back(info); } } @@ -657,7 +658,7 @@ void SelectionTab::ListItem::updateItem(std::shared_ptr info, bool sel iconLossCondition->disable(); labelNumberOfCampaignMaps->enable(); std::ostringstream ostr(std::ostringstream::out); - ostr << CGI->generaltexth->getCampaignLength(info->campaignHeader->mapVersion); + ostr << info->campaignHeader->numberOfScenarios; labelNumberOfCampaignMaps->setText(ostr.str()); labelNumberOfCampaignMaps->setColor(color); } diff --git a/client/mainmenu/CPrologEpilogVideo.cpp b/client/mainmenu/CPrologEpilogVideo.cpp index 5161c74ed..e496b00f7 100644 --- a/client/mainmenu/CPrologEpilogVideo.cpp +++ b/client/mainmenu/CPrologEpilogVideo.cpp @@ -29,8 +29,8 @@ CPrologEpilogVideo::CPrologEpilogVideo(CCampaignScenario::SScenarioPrologEpilog pos = center(Rect(0, 0, 800, 600)); updateShadow(); - CCS->videoh->open(CCampaignHandler::prologVideoName(spe.prologVideo)); - CCS->musich->playMusic("Music/" + CCampaignHandler::prologMusicName(spe.prologMusic), true, true); + CCS->videoh->open(spe.prologVideo); + CCS->musich->playMusic("Music/" + spe.prologMusic, true, true); // MPTODO: Custom campaign crashing on this? // voiceSoundHandle = CCS->soundh->playSound(CCampaignHandler::prologVoiceName(spe.prologVideo)); diff --git a/lib/CGameState.cpp b/lib/CGameState.cpp index 899307f9f..980389161 100644 --- a/lib/CGameState.cpp +++ b/lib/CGameState.cpp @@ -722,7 +722,7 @@ void CGameState::preInit(Services * services) this->services = services; } -void CGameState::init(const IMapService * mapService, StartInfo * si, bool allowSavingRandomMap) +void CGameState::init(const CMapService * mapService, StartInfo * si, bool allowSavingRandomMap) { preInitAuto(); logGlobal->info("\tUsing random seed: %d", si->seedToBeUsed); @@ -851,7 +851,7 @@ void CGameState::preInitAuto() } } -void CGameState::initNewGame(const IMapService * mapService, bool allowSavingRandomMap) +void CGameState::initNewGame(const CMapService * mapService, bool allowSavingRandomMap) { if(scenarioOps->createRandomMap()) { @@ -1266,7 +1266,7 @@ void CGameState::prepareCrossoverHeroes(std::vectorartType->getId(); - assert( 8*18 > id );//number of arts that fits into h3m format - bool takeable = travelOptions.artifsKeptByHero[id / 8] & ( 1 << (id%8) ); + bool takeable = travelOptions.artifactsKeptByHero.count(art->artType->getId()); ArtifactLocation al(hero, artifactPosition); if(!takeable && !al.getSlot()->locked) //don't try removing locked artifacts -> it crashes #1719 @@ -1346,7 +1344,7 @@ void CGameState::prepareCrossoverHeroes(std::vector & j) -> bool { CreatureID::ECreatureID crid = j.second->getCreatureID().toEnum(); - return !(travelOptions.monstersKeptByHero[crid / 8] & (1 << (crid % 8))); + return !travelOptions.monstersKeptByHero.count(crid); }; auto stacksCopy = cgh->stacks; //copy of the map, so we can iterate iover it and remove stacks @@ -1725,8 +1723,13 @@ void CGameState::initTowns() if (owner->human && //human-owned map->towns[g]->pos == pi.posOfMainTown) { - map->towns[g]->builtBuildings.insert( - CBuildingHandler::campToERMU(chosenBonus->info1, map->towns[g]->subID, map->towns[g]->builtBuildings)); + BuildingID buildingId; + if(scenarioOps->campState->camp->header.version == CampaignVersion::VCMI) + buildingId = BuildingID(chosenBonus->info1); + else + buildingId = CBuildingHandler::campToERMU(chosenBonus->info1, map->towns[g]->subID, map->towns[g]->builtBuildings); + + map->towns[g]->builtBuildings.insert(buildingId); break; } } diff --git a/lib/CGameState.h b/lib/CGameState.h index e2d17250b..92c7427e8 100644 --- a/lib/CGameState.h +++ b/lib/CGameState.h @@ -57,7 +57,7 @@ class CQuest; class CCampaignScenario; struct EventCondition; class CScenarioTravel; -class IMapService; +class CMapService; template class CApplier; @@ -161,7 +161,7 @@ public: void preInit(Services * services); - void init(const IMapService * mapService, StartInfo * si, bool allowSavingRandomMap = false); + void init(const CMapService * mapService, StartInfo * si, bool allowSavingRandomMap = false); void updateOnLoad(StartInfo * si); ConstTransitivePtr scenarioOps, initialOpts; //second one is a copy of settings received from pregame (not randomized) @@ -252,7 +252,7 @@ private: // ----- initialization ----- void preInitAuto(); - void initNewGame(const IMapService * mapService, bool allowSavingRandomMap); + void initNewGame(const CMapService * mapService, bool allowSavingRandomMap); void initCampaign(); void checkMapChecksum(); void initGlobalBonuses(); diff --git a/lib/CModHandler.cpp b/lib/CModHandler.cpp index 7b4f8f1a0..410e0ac47 100644 --- a/lib/CModHandler.cpp +++ b/lib/CModHandler.cpp @@ -967,6 +967,11 @@ std::vector CModHandler::getActiveMods() return activeMods; } +const CModInfo & CModHandler::getModInfo(const TModID & modId) const +{ + return allMods.at(modId); +} + static JsonNode genDefaultFS() { // default FS config for mods: directory "Content" that acts as H3 root directory diff --git a/lib/CModHandler.h b/lib/CModHandler.h index 0e73767d7..e1a8bdec2 100644 --- a/lib/CModHandler.h +++ b/lib/CModHandler.h @@ -174,7 +174,7 @@ public: const ContentTypeHandler & operator[] (const std::string & name) const; }; -typedef std::string TModID; +using TModID = std::string; class DLL_LINKAGE CModInfo { @@ -186,7 +186,7 @@ public: PASSED }; - struct Version + struct DLL_LINKAGE Version { int major = 0; int minor = 0; @@ -347,6 +347,8 @@ public: /// returns list of all (active) mods std::vector getAllMods(); std::vector getActiveMods(); + + const CModInfo & getModInfo(const TModID & modId) const; /// load content from all available mods void load(); diff --git a/lib/StartInfo.cpp b/lib/StartInfo.cpp index 353e6a495..57c35f4f2 100644 --- a/lib/StartInfo.cpp +++ b/lib/StartInfo.cpp @@ -15,6 +15,7 @@ #include "mapping/CMapInfo.h" #include "mapping/CCampaignHandler.h" #include "mapping/CMap.h" +#include "mapping/CMapService.h" VCMI_LIB_NAMESPACE_BEGIN @@ -67,8 +68,16 @@ std::string StartInfo::getCampaignName() const void LobbyInfo::verifyStateBeforeStart(bool ignoreNoHuman) const { - if(!mi) + if(!mi || !mi->mapHeader) throw std::domain_error("ExceptionMapMissing"); + + auto missingMods = CMapService::verifyMapHeaderMods(*mi->mapHeader); + CModHandler::Incompatibility::ModList modList; + for(const auto & m : missingMods) + modList.push_back({m.first, m.second.toString()}); + + if(!modList.empty()) + throw CModHandler::Incompatibility(std::move(modList)); //there must be at least one human player before game can be started std::map::const_iterator i; diff --git a/lib/filesystem/MinizipExtensions.cpp b/lib/filesystem/MinizipExtensions.cpp index c98eccdfd..e58fb3a9d 100644 --- a/lib/filesystem/MinizipExtensions.cpp +++ b/lib/filesystem/MinizipExtensions.cpp @@ -67,7 +67,7 @@ inline long streamSeek(voidpf opaque, voidpf stream, ZPOS64_T offset, int origin } if(ret == -1) logGlobal->error("Stream seek failed"); - return ret; + return 0; } template diff --git a/lib/filesystem/ResourceID.cpp b/lib/filesystem/ResourceID.cpp index 63f61afca..db7f2118b 100644 --- a/lib/filesystem/ResourceID.cpp +++ b/lib/filesystem/ResourceID.cpp @@ -159,6 +159,7 @@ EResType::Type EResTypeHelper::getTypeFromExtension(std::string extension) {".ERT", EResType::ERT}, {".ERS", EResType::ERS}, {".VMAP", EResType::MAP}, + {".VCMP", EResType::CAMPAIGN}, {".VERM", EResType::ERM}, {".LUA", EResType::LUA} }; diff --git a/lib/mapObjects/CGHeroInstance.cpp b/lib/mapObjects/CGHeroInstance.cpp index fcfebeb5c..8e7079229 100644 --- a/lib/mapObjects/CGHeroInstance.cpp +++ b/lib/mapObjects/CGHeroInstance.cpp @@ -1477,7 +1477,10 @@ void CGHeroInstance::serializeCommonOptions(JsonSerializeFormat & handler) if(portrait >= 0) { if(portrait < legacyHeroes || portrait >= moddedStart) - handler.serializeId("portrait", portrait, -1); + { + int tempPortrait = portrait - GameConstants::HERO_PORTRAIT_SHIFT; + handler.serializeId("portrait", tempPortrait, -1); + } else handler.serializeInt("portrait", portrait, -1); } @@ -1487,7 +1490,10 @@ void CGHeroInstance::serializeCommonOptions(JsonSerializeFormat & handler) const JsonNode & portraitNode = handler.getCurrent()["portrait"]; if(portraitNode.getType() == JsonNode::JsonType::DATA_STRING) + { handler.serializeId("portrait", portrait, -1); + portrait += GameConstants::HERO_PORTRAIT_SHIFT; + } else handler.serializeInt("portrait", portrait, -1); } diff --git a/lib/mapping/CCampaignHandler.cpp b/lib/mapping/CCampaignHandler.cpp index 59b9b3cfc..9513a5320 100644 --- a/lib/mapping/CCampaignHandler.cpp +++ b/lib/mapping/CCampaignHandler.cpp @@ -24,6 +24,7 @@ #include "../mapObjects/CGHeroInstance.h"//for hero crossover #include "../CHeroHandler.h" #include "../Languages.h" +#include "../StringConstants.h" #include "CMapService.h" #include "CMap.h" #include "CMapInfo.h" @@ -35,27 +36,73 @@ VCMI_LIB_NAMESPACE_BEGIN +CampaignRegions::RegionDescription CampaignRegions::RegionDescription::fromJson(const JsonNode & node) +{ + CampaignRegions::RegionDescription rd; + rd.infix = node["infix"].String(); + rd.xpos = static_cast(node["x"].Float()); + rd.ypos = static_cast(node["y"].Float()); + return rd; +} + +CampaignRegions CampaignRegions::fromJson(const JsonNode & node) +{ + CampaignRegions cr; + cr.campPrefix = node["prefix"].String(); + cr.colorSuffixLength = static_cast(node["color_suffix_length"].Float()); + + for(const JsonNode & desc : node["desc"].Vector()) + cr.regions.push_back(CampaignRegions::RegionDescription::fromJson(desc)); + + return cr; +} + +CampaignRegions CampaignRegions::getLegacy(int campId) +{ + static std::vector campDescriptions; + if(campDescriptions.empty()) //read once + { + const JsonNode config(ResourceID("config/campaign_regions.json")); + for(const JsonNode & campaign : config["campaign_regions"].Vector()) + campDescriptions.push_back(CampaignRegions::fromJson(campaign)); + } + + return campDescriptions.at(campId); +} + + bool CScenarioTravel::STravelBonus::isBonusForHero() const { return type == SPELL || type == MONSTER || type == ARTIFACT || type == SPELL_SCROLL || type == PRIMARY_SKILL || type == SECONDARY_SKILL; } +void CCampaignHeader::loadLegacyData(ui8 campId) +{ + campaignRegions = CampaignRegions::getLegacy(campId); + numberOfScenarios = VLC->generaltexth->getCampaignLength(campId); +} + CCampaignHeader CCampaignHandler::getHeader( const std::string & name) { ResourceID resourceID(name, EResType::CAMPAIGN); std::string modName = VLC->modh->findResourceOrigin(resourceID); std::string language = VLC->modh->getModLanguage(modName); std::string encoding = Languages::getLanguageOptions(language).encoding; + auto fileStream = CResourceHandler::get(modName)->load(resourceID); - std::vector cmpgn = getFile(std::move(fileStream), true)[0]; - - CMemoryStream stream(cmpgn.data(), cmpgn.size()); - CBinaryReader reader(&stream); - CCampaignHeader ret = readHeaderFromMemory(reader, resourceID.getName(), modName, encoding); - - return ret; + JsonNode jsonCampaign((const char*)cmpgn.data(), cmpgn.size()); + if(jsonCampaign.isNull()) + { + //legacy OH3 campaign (*.h3c) + CMemoryStream stream(cmpgn.data(), cmpgn.size()); + CBinaryReader reader(&stream); + return readHeaderFromMemory(reader, resourceID.getName(), modName, encoding); + } + + //VCMI (*.vcmp) + return readHeaderFromJson(jsonCampaign, resourceID.getName(), modName, encoding); } std::unique_ptr CCampaignHandler::getCampaign( const std::string & name ) @@ -64,39 +111,42 @@ std::unique_ptr CCampaignHandler::getCampaign( const std::string & na std::string modName = VLC->modh->findResourceOrigin(resourceID); std::string language = VLC->modh->getModLanguage(modName); std::string encoding = Languages::getLanguageOptions(language).encoding; + + auto ret = std::make_unique(); + auto fileStream = CResourceHandler::get(modName)->load(resourceID); - auto ret = std::make_unique(); - - std::vector> file = getFile(std::move(fileStream), false); - - CMemoryStream stream(file[0].data(), file[0].size()); - CBinaryReader reader(&stream); - ret->header = readHeaderFromMemory(reader, resourceID.getName(), modName, encoding); - - int howManyScenarios = static_cast(VLC->generaltexth->getCampaignLength(ret->header.mapVersion)); - for(int g=0; g> files = getFile(std::move(fileStream), false); + + JsonNode jsonCampaign((const char*)files[0].data(), files[0].size()); + if(jsonCampaign.isNull()) { - CCampaignScenario sc = readScenarioFromMemory(reader, resourceID.getName(), modName, encoding, ret->header.version, ret->header.mapVersion); - ret->scenarios.push_back(sc); + CMemoryStream stream(files[0].data(), files[0].size()); + CBinaryReader reader(&stream); + ret->header = readHeaderFromMemory(reader, resourceID.getName(), modName, encoding); + + for(int g = 0; g < ret->header.numberOfScenarios; ++g) + ret->scenarios.emplace_back(readScenarioFromMemory(reader, ret->header)); } - - int scenarioID = 0; - + else + { + ret->header = readHeaderFromJson(jsonCampaign, resourceID.getName(), modName, encoding); + for(auto & scenario : jsonCampaign["scenarios"].Vector()) + ret->scenarios.emplace_back(readScenarioFromJson(scenario)); + } + //first entry is campaign header. start loop from 1 - for (int g=1; gheader.numberOfScenarios; ++g) { while(!ret->scenarios[scenarioID].isNotVoid()) //skip void scenarios - { scenarioID++; - } std::string scenarioName = resourceID.getName(); boost::to_lower(scenarioName); scenarioName += ':' + std::to_string(g - 1); //set map piece appropriately, convert vector to string - ret->mapPieces[scenarioID].assign(reinterpret_cast< const char* >(file[g].data()), file[g].size()); + ret->mapPieces[scenarioID].assign(reinterpret_cast(files[g].data()), files[g].size()); CMapService mapService; auto hdr = mapService.loadMapHeader( reinterpret_cast(ret->mapPieces[scenarioID].c_str()), @@ -151,26 +201,273 @@ std::string CCampaignHandler::readLocalizedString(CBinaryReader & reader, std::s return VLC->generaltexth->translate(stringID.get()); } +CCampaignHeader CCampaignHandler::readHeaderFromJson(JsonNode & reader, std::string filename, std::string modName, std::string encoding) +{ + CCampaignHeader ret; + + ret.version = reader["version"].Integer(); + if(ret.version < CampaignVersion::VCMI_MIN || ret.version > CampaignVersion::VCMI_MAX) + { + logGlobal->info("VCMP Loading: Unsupported campaign %s version %d", filename, ret.version); + return ret; + } + + ret.version = CampaignVersion::VCMI; + ret.campaignRegions = CampaignRegions::fromJson(reader["regions"]); + ret.numberOfScenarios = reader["scenarios"].Vector().size(); + ret.name = reader["name"].String(); + ret.description = reader["description"].String(); + ret.difficultyChoosenByPlayer = reader["allowDifficultySelection"].Bool(); + //skip ret.music because it's unused in vcmi + ret.filename = filename; + ret.modName = modName; + ret.encoding = encoding; + ret.valid = true; + return ret; +} + +CCampaignScenario CCampaignHandler::readScenarioFromJson(JsonNode & reader) +{ + auto prologEpilogReader = [](JsonNode & identifier) -> CCampaignScenario::SScenarioPrologEpilog + { + CCampaignScenario::SScenarioPrologEpilog ret; + ret.hasPrologEpilog = !identifier.isNull(); + if(ret.hasPrologEpilog) + { + ret.prologVideo = identifier["video"].String(); + ret.prologMusic = identifier["music"].String(); + ret.prologText = identifier["text"].String(); + } + return ret; + }; + + CCampaignScenario ret; + ret.conquered = false; + ret.mapName = reader["map"].String(); + for(auto & g : reader["preconditions"].Vector()) + ret.preconditionRegions.insert(g.Integer()); + + ret.regionColor = reader["color"].Integer(); + ret.difficulty = reader["difficulty"].Integer(); + ret.regionText = reader["regionText"].String(); + ret.prolog = prologEpilogReader(reader["prolog"]); + ret.epilog = prologEpilogReader(reader["epilog"]); + + ret.travelOptions = readScenarioTravelFromJson(reader); + + return ret; +} + +CScenarioTravel CCampaignHandler::readScenarioTravelFromJson(JsonNode & reader) +{ + CScenarioTravel ret; + + std::map startOptionsMap = { + {"none", 0}, + {"bonus", 1}, + {"crossover", 2}, + {"hero", 3} + }; + + std::map bonusTypeMap = { + {"spell", CScenarioTravel::STravelBonus::EBonusType::SPELL}, + {"creature", CScenarioTravel::STravelBonus::EBonusType::MONSTER}, + {"building", CScenarioTravel::STravelBonus::EBonusType::BUILDING}, + {"artifact", CScenarioTravel::STravelBonus::EBonusType::ARTIFACT}, + {"scroll", CScenarioTravel::STravelBonus::EBonusType::SPELL_SCROLL}, + {"primarySkill", CScenarioTravel::STravelBonus::EBonusType::PRIMARY_SKILL}, + {"secondarySkill", CScenarioTravel::STravelBonus::EBonusType::SECONDARY_SKILL}, + {"resource", CScenarioTravel::STravelBonus::EBonusType::RESOURCE}, + //{"prevHero", CScenarioTravel::STravelBonus::EBonusType::HEROES_FROM_PREVIOUS_SCENARIO}, + //{"hero", CScenarioTravel::STravelBonus::EBonusType::HERO}, + }; + + std::map primarySkillsMap = { + {"attack", 0}, + {"defence", 8}, + {"spellpower", 16}, + {"knowledge", 24}, + }; + + std::map heroSpecialMap = { + {"strongest", 0xFFFD}, + {"generated", 0xFFFE}, + {"random", 0xFFFF} + }; + + std::map 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()) + { + if(k.String() == "experience") ret.whatHeroKeeps.experience = true; + if(k.String() == "primarySkills") ret.whatHeroKeeps.primarySkills = true; + if(k.String() == "secondarySkills") ret.whatHeroKeeps.secondarySkills = true; + if(k.String() == "spells") ret.whatHeroKeeps.spells = true; + if(k.String() == "artifacts") ret.whatHeroKeeps.artifacts = true; + } + + for(auto & k : reader["keepCreatures"].Vector()) + { + if(auto identifier = VLC->modh->identifiers.getIdentifier(CModHandler::scopeMap(), "creature", k.String())) + ret.monstersKeptByHero.insert(CreatureID(identifier.value())); + else + logGlobal->warn("VCMP Loading: keepCreatures contains unresolved identifier %s", k.String()); + } + for(auto & k : reader["keepArtifacts"].Vector()) + { + if(auto identifier = VLC->modh->identifiers.getIdentifier(CModHandler::scopeMap(), "artifact", k.String())) + ret.artifactsKeptByHero.insert(ArtifactID(identifier.value())); + else + logGlobal->warn("VCMP Loading: keepArtifacts contains unresolved identifier %s", k.String()); + } + + ret.startOptions = startOptionsMap[reader["startOptions"].String()]; + switch(ret.startOptions) + { + case 0: + //no bonuses. Seems to be OK + break; + case 1: //reading of bonuses player can choose + { + ret.playerColor = reader["playerColor"].Integer(); + for(auto & bjson : reader["bonuses"].Vector()) + { + CScenarioTravel::STravelBonus bonus; + bonus.type = bonusTypeMap[bjson["what"].String()]; + switch (bonus.type) + { + case CScenarioTravel::STravelBonus::EBonusType::RESOURCE: + bonus.info1 = resourceTypeMap[bjson["type"].String()]; + bonus.info2 = bjson["amount"].Integer(); + break; + + case CScenarioTravel::STravelBonus::EBonusType::BUILDING: + bonus.info1 = vstd::find_pos(EBuildingType::names, bjson["type"].String()); + if(bonus.info1 == -1) + logGlobal->warn("VCMP Loading: unresolved building identifier %s", bjson["type"].String()); + break; + + default: + if(int heroId = heroSpecialMap[bjson["hero"].String()]) + bonus.info1 = heroId; + else + if(auto identifier = VLC->modh->identifiers.getIdentifier(CModHandler::scopeMap(), "hero", bjson["hero"].String())) + bonus.info1 = identifier.value(); + else + logGlobal->warn("VCMP Loading: unresolved hero identifier %s", bjson["hero"].String()); + + bonus.info3 = bjson["amount"].Integer(); + + switch(bonus.type) + { + case CScenarioTravel::STravelBonus::EBonusType::SPELL: + case CScenarioTravel::STravelBonus::EBonusType::MONSTER: + case CScenarioTravel::STravelBonus::EBonusType::SECONDARY_SKILL: + case CScenarioTravel::STravelBonus::EBonusType::ARTIFACT: + if(auto identifier = VLC->modh->identifiers.getIdentifier(CModHandler::scopeMap(), bjson["what"].String(), bjson["type"].String())) + bonus.info2 = identifier.value(); + else + logGlobal->warn("VCMP Loading: unresolved %s identifier %s", bjson["what"].String(), bjson["type"].String()); + break; + + case CScenarioTravel::STravelBonus::EBonusType::SPELL_SCROLL: + if(auto Identifier = VLC->modh->identifiers.getIdentifier(CModHandler::scopeMap(), "spell", bjson["type"].String())) + bonus.info2 = Identifier.value(); + else + logGlobal->warn("VCMP Loading: unresolved spell scroll identifier %s", bjson["type"].String()); + break; + + case CScenarioTravel::STravelBonus::EBonusType::PRIMARY_SKILL: + for(auto & ps : primarySkillsMap) + bonus.info2 |= bjson[ps.first].Integer() << ps.second; + break; + + default: + bonus.info2 = bjson["type"].Integer(); + } + break; + } + ret.bonusesToChoose.push_back(bonus); + } + break; + } + case 2: //reading of players (colors / scenarios ?) player can choose + { + for(auto & bjson : reader["bonuses"].Vector()) + { + CScenarioTravel::STravelBonus bonus; + bonus.type = CScenarioTravel::STravelBonus::HEROES_FROM_PREVIOUS_SCENARIO; + bonus.info1 = bjson["playerColor"].Integer(); //player color + bonus.info2 = bjson["scenario"].Integer(); //from what scenario + ret.bonusesToChoose.push_back(bonus); + } + break; + } + case 3: //heroes player can choose between + { + for(auto & bjson : reader["bonuses"].Vector()) + { + CScenarioTravel::STravelBonus bonus; + bonus.type = CScenarioTravel::STravelBonus::HERO; + bonus.info1 = bjson["playerColor"].Integer(); //player color + + if(int heroId = heroSpecialMap[bjson["hero"].String()]) + bonus.info2 = heroId; + else + if (auto identifier = VLC->modh->identifiers.getIdentifier(CModHandler::scopeMap(), "hero", bjson["hero"].String())) + bonus.info2 = identifier.value(); + else + logGlobal->warn("VCMP Loading: unresolved hero identifier %s", bjson["hero"].String()); + + ret.bonusesToChoose.push_back(bonus); + } + break; + } + default: + { + logGlobal->warn("VCMP Loading: Unsupported start options value"); + break; + } + } + + return ret; +} + + CCampaignHeader CCampaignHandler::readHeaderFromMemory( CBinaryReader & reader, std::string filename, std::string modName, std::string encoding ) { CCampaignHeader ret; ret.version = reader.readUInt32(); - ret.mapVersion = reader.readUInt8() - 1;//change range of it from [1, 20] to [0, 19] + ui8 campId = reader.readUInt8() - 1;//change range of it from [1, 20] to [0, 19] + ret.loadLegacyData(campId); ret.name = readLocalizedString(reader, filename, modName, encoding, "name"); ret.description = readLocalizedString(reader, filename, modName, encoding, "description"); if (ret.version > CampaignVersion::RoE) ret.difficultyChoosenByPlayer = reader.readInt8(); else - ret.difficultyChoosenByPlayer = 0; - ret.music = reader.readInt8(); + ret.difficultyChoosenByPlayer = false; + reader.readInt8(); //music - skip as unused ret.filename = filename; ret.modName = modName; ret.encoding = encoding; + ret.valid = true; return ret; } -CCampaignScenario CCampaignHandler::readScenarioFromMemory( CBinaryReader & reader, std::string filename, std::string modName, std::string encoding, int version, int mapVersion ) +CCampaignScenario CCampaignHandler::readScenarioFromMemory( CBinaryReader & reader, const CCampaignHeader & header) { auto prologEpilogReader = [&](const std::string & identifier) -> CCampaignScenario::SScenarioPrologEpilog { @@ -178,9 +475,9 @@ CCampaignScenario CCampaignHandler::readScenarioFromMemory( CBinaryReader & read ret.hasPrologEpilog = reader.readUInt8(); if(ret.hasPrologEpilog) { - ret.prologVideo = reader.readUInt8(); - ret.prologMusic = reader.readUInt8(); - ret.prologText = readLocalizedString(reader, filename, modName, encoding, identifier); + ret.prologVideo = CCampaignHandler::prologVideoName(reader.readUInt8()); + ret.prologMusic = CCampaignHandler::prologMusicName(reader.readUInt8()); + ret.prologText = readLocalizedString(reader, header.filename, header.modName, header.encoding, identifier); } return ret; }; @@ -188,8 +485,8 @@ CCampaignScenario CCampaignHandler::readScenarioFromMemory( CBinaryReader & read CCampaignScenario ret; ret.conquered = false; ret.mapName = reader.readBaseString(); - ret.packedMapSize = reader.readUInt32(); - if(mapVersion == 18)//unholy alliance + reader.readUInt32(); //packedMapSize - not used + if(header.numberOfScenarios > 8) //unholy alliance { ret.loadPreconditionRegions(reader.readUInt16()); } @@ -199,11 +496,11 @@ CCampaignScenario CCampaignHandler::readScenarioFromMemory( CBinaryReader & read } ret.regionColor = reader.readUInt8(); ret.difficulty = reader.readUInt8(); - ret.regionText = readLocalizedString(reader, filename, modName, encoding, ret.mapName + ".region"); + ret.regionText = readLocalizedString(reader, header.filename, header.modName, header.encoding, ret.mapName + ".region"); ret.prolog = prologEpilogReader(ret.mapName + ".prolog"); ret.epilog = prologEpilogReader(ret.mapName + ".epilog"); - ret.travelOptions = readScenarioTravelFromMemory(reader, version); + ret.travelOptions = readScenarioTravelFromMemory(reader, header.version); return ret; } @@ -221,18 +518,27 @@ CScenarioTravel CCampaignHandler::readScenarioTravelFromMemory(CBinaryReader & r { CScenarioTravel ret; - ret.whatHeroKeeps = reader.readUInt8(); - reader.getStream()->read(ret.monstersKeptByHero.data(), ret.monstersKeptByHero.size()); - - if (version < CampaignVersion::SoD) + ui8 whatHeroKeeps = reader.readUInt8(); + ret.whatHeroKeeps.experience = whatHeroKeeps & 1; + ret.whatHeroKeeps.primarySkills = whatHeroKeeps & 2; + ret.whatHeroKeeps.secondarySkills = whatHeroKeeps & 4; + ret.whatHeroKeeps.spells = whatHeroKeeps & 8; + ret.whatHeroKeeps.artifacts = whatHeroKeeps & 16; + + //TODO: replace with template lambda form C++20 and make typed containers + auto bitMaskToId = [&reader](std::set & container, int size) { - ret.artifsKeptByHero.fill(0); - reader.getStream()->read(ret.artifsKeptByHero.data(), ret.artifsKeptByHero.size() - 1); - } - else - { - reader.getStream()->read(ret.artifsKeptByHero.data(), ret.artifsKeptByHero.size()); - } + for(int iId = 0, byte = 0; iId < size * 8; ++iId) + { + if(iId % 8 == 0) + byte = reader.readUInt8(); + if(byte & (1 << iId % 8)) + container.insert(iId); + } + }; + + bitMaskToId(ret.monstersKeptByHero, 19); + bitMaskToId(ret.artifactsKeptByHero, version < CampaignVersion::SoD ? 17 : 18); ret.startOptions = reader.readUInt8(); @@ -490,12 +796,13 @@ CMap * CCampaignState::getMap(int scenarioId) const // FIXME: there is certainly better way to handle maps inside campaigns if(scenarioId == -1) scenarioId = currentMap.value(); + + CMapService mapService; std::string scenarioName = camp->header.filename.substr(0, camp->header.filename.find('.')); boost::to_lower(scenarioName); scenarioName += ':' + std::to_string(scenarioId); std::string & mapContent = camp->mapPieces.find(scenarioId)->second; const auto * buffer = reinterpret_cast(mapContent.data()); - CMapService mapService; return mapService.loadMap(buffer, static_cast(mapContent.size()), scenarioName, camp->header.modName, camp->header.encoding).release(); } @@ -503,13 +810,13 @@ std::unique_ptr CCampaignState::getHeader(int scenarioId) const { if(scenarioId == -1) scenarioId = currentMap.value(); - + + CMapService mapService; std::string scenarioName = camp->header.filename.substr(0, camp->header.filename.find('.')); boost::to_lower(scenarioName); scenarioName += ':' + std::to_string(scenarioId); std::string & mapContent = camp->mapPieces.find(scenarioId)->second; const auto * buffer = reinterpret_cast(mapContent.data()); - CMapService mapService; return mapService.loadMapHeader(buffer, static_cast(mapContent.size()), scenarioName, camp->header.modName, camp->header.encoding); } diff --git a/lib/mapping/CCampaignHandler.h b/lib/mapping/CCampaignHandler.h index 0ece9d3da..ca6c2fbfc 100644 --- a/lib/mapping/CCampaignHandler.h +++ b/lib/mapping/CCampaignHandler.h @@ -29,43 +29,105 @@ namespace CampaignVersion RoE = 4, AB = 5, SoD = 6, - WoG = 6 + WoG = 6, + VCMI = 1 }; + + const int VCMI_MIN = 1; + const int VCMI_MAX = 1; } +struct DLL_LINKAGE CampaignRegions +{ + std::string campPrefix; + int colorSuffixLength; + + struct DLL_LINKAGE RegionDescription + { + std::string infix; + int xpos, ypos; + + template void serialize(Handler &h, const int formatVersion) + { + h & infix; + h & xpos; + h & ypos; + } + + static CampaignRegions::RegionDescription fromJson(const JsonNode & node); + }; + + std::vector regions; + + template void serialize(Handler &h, const int formatVersion) + { + h & campPrefix; + h & colorSuffixLength; + h & regions; + } + + static CampaignRegions fromJson(const JsonNode & node); + static CampaignRegions getLegacy(int campId); +}; + class DLL_LINKAGE CCampaignHeader { public: si32 version = 0; //4 - RoE, 5 - AB, 6 - SoD and WoG - ui8 mapVersion = 0; //CampText.txt's format + CampaignRegions campaignRegions; + int numberOfScenarios = 0; std::string name, description; - ui8 difficultyChoosenByPlayer = 0; - ui8 music = 0; //CmpMusic.txt, start from 0 + bool difficultyChoosenByPlayer = false; + bool valid = false; std::string filename; std::string modName; std::string encoding; + + void loadLegacyData(ui8 campId); template void serialize(Handler &h, const int formatVersion) { h & version; - h & mapVersion; + h & campaignRegions; + h & numberOfScenarios; h & name; h & description; h & difficultyChoosenByPlayer; - h & music; h & filename; h & modName; h & encoding; + h & valid; } }; class DLL_LINKAGE CScenarioTravel { public: - ui8 whatHeroKeeps = 0; //bitfield [0] - experience, [1] - prim skills, [2] - sec skills, [3] - spells, [4] - artifacts - std::array monstersKeptByHero; - std::array artifsKeptByHero; + + struct DLL_LINKAGE WhatHeroKeeps + { + bool experience = false; + bool primarySkills = false; + bool secondarySkills = false; + bool spells = false; + bool artifacts = false; + + template void serialize(Handler &h, const int formatVersion) + { + h & experience; + h & primarySkills; + h & secondarySkills; + h & spells; + h & artifacts; + } + }; + + WhatHeroKeeps whatHeroKeeps; + + //TODO: use typed containers + std::set monstersKeptByHero; + std::set artifactsKeptByHero; ui8 startOptions = 0; //1 - start bonus, 2 - traveling hero, 3 - hero options @@ -95,7 +157,7 @@ public: { h & whatHeroKeeps; h & monstersKeptByHero; - h & artifsKeptByHero; + h & artifactsKeptByHero; h & startOptions; h & playerColor; h & bonusesToChoose; @@ -109,8 +171,8 @@ public: struct DLL_LINKAGE SScenarioPrologEpilog { bool hasPrologEpilog = false; - ui8 prologVideo = 0; // from CmpMovie.txt - ui8 prologMusic = 0; // from CmpMusic.txt + std::string prologVideo; // from CmpMovie.txt + std::string prologMusic; // from CmpMusic.txt std::string prologText; template void serialize(Handler &h, const int formatVersion) @@ -124,7 +186,6 @@ public: std::string mapName; //*.h3m std::string scenarioName; //from header. human-readble - ui32 packedMapSize = 0; //generally not used std::set preconditionRegions; //what we need to conquer to conquer this one (stored as bitfield in h3c) ui8 regionColor = 0; ui8 difficulty = 0; @@ -148,7 +209,6 @@ public: { h & mapName; h & scenarioName; - h & packedMapSize; h & preconditionRegions; h & regionColor; h & difficulty; @@ -218,22 +278,26 @@ public: class DLL_LINKAGE CCampaignHandler { - std::vector scenariosCountPerCampaign; - static std::string readLocalizedString(CBinaryReader & reader, std::string filename, std::string modName, std::string encoding, std::string identifier); + + //parsers for VCMI campaigns (*.vcmp) + static CCampaignHeader readHeaderFromJson(JsonNode & reader, std::string filename, std::string modName, std::string encoding); + static CCampaignScenario readScenarioFromJson(JsonNode & reader); + static CScenarioTravel readScenarioTravelFromJson(JsonNode & reader); + //parsers for original H3C campaigns static CCampaignHeader readHeaderFromMemory(CBinaryReader & reader, std::string filename, std::string modName, std::string encoding); - static CCampaignScenario readScenarioFromMemory(CBinaryReader & reader, std::string filename, std::string modName, std::string encoding, int version, int mapVersion ); + static CCampaignScenario readScenarioFromMemory(CBinaryReader & reader, const CCampaignHeader & header); static CScenarioTravel readScenarioTravelFromMemory(CBinaryReader & reader, int version); /// returns h3c split in parts. 0 = h3c header, 1-end - maps (binary h3m) /// headerOnly - only header will be decompressed, returned vector wont have any maps static std::vector> getFile(std::unique_ptr file, bool headerOnly); -public: static std::string prologVideoName(ui8 index); static std::string prologMusicName(ui8 index); static std::string prologVoiceName(ui8 index); +public: static CCampaignHeader getHeader( const std::string & name); //name - name of appropriate file static std::unique_ptr getCampaign(const std::string & name); //name - name of appropriate file diff --git a/lib/mapping/CMap.h b/lib/mapping/CMap.h index ec6fb88b4..2c5aa3596 100644 --- a/lib/mapping/CMap.h +++ b/lib/mapping/CMap.h @@ -18,6 +18,7 @@ #include "../int3.h" #include "../GameConstants.h" #include "../LogicalExpression.h" +#include "../CModHandler.h" #include "CMapDefines.h" VCMI_LIB_NAMESPACE_BEGIN @@ -262,6 +263,10 @@ enum class EMapFormat: uint8_t VCMI = 0xF0 }; +// Inherit from container to enable forward declaration +class ModCompatibilityInfo: public std::map +{}; + /// The map header holds information about loss/victory condition,map format, version, players, height, width,... class DLL_LINKAGE CMapHeader { @@ -282,6 +287,8 @@ public: ui8 levels() const; EMapFormat version; /// The default value is EMapFormat::SOD. + ModCompatibilityInfo mods; /// set of mods required to play a map + si32 height; /// The default value is 72. si32 width; /// The default value is 72. bool twoLevel; /// The default value is true. @@ -310,6 +317,8 @@ public: void serialize(Handler & h, const int Version) { h & version; + if(Version >= 821) + h & mods; h & name; h & description; h & width; diff --git a/lib/mapping/CMapInfo.cpp b/lib/mapping/CMapInfo.cpp index fe8864890..e956fc6c6 100644 --- a/lib/mapping/CMapInfo.cpp +++ b/lib/mapping/CMapInfo.cpp @@ -65,6 +65,8 @@ void CMapInfo::saveInit(const ResourceID & file) void CMapInfo::campaignInit() { campaignHeader = std::make_unique(CCampaignHandler::getHeader(fileURI)); + if(!campaignHeader->valid) + campaignHeader.reset(); } void CMapInfo::countPlayers() diff --git a/lib/mapping/CMapService.cpp b/lib/mapping/CMapService.cpp index 6fb870d3a..487c302ab 100644 --- a/lib/mapping/CMapService.cpp +++ b/lib/mapping/CMapService.cpp @@ -86,6 +86,24 @@ void CMapService::saveMap(const std::unique_ptr & map, boost::filesystem:: } } +ModCompatibilityInfo CMapService::verifyMapHeaderMods(const CMapHeader & map) +{ + ModCompatibilityInfo modCompatibilityInfo; + const auto & activeMods = VLC->modh->getActiveMods(); + for(const auto & mapMod : map.mods) + { + if(vstd::contains(activeMods, mapMod.first)) + { + const auto & modInfo = VLC->modh->getModInfo(mapMod.first); + if(modInfo.version.compatible(mapMod.second)) + continue; + } + + modCompatibilityInfo[mapMod.first] = mapMod.second; + } + return modCompatibilityInfo; +} + std::unique_ptr CMapService::getStreamFromFS(const ResourceID & name) { return CResourceHandler::get()->load(name); diff --git a/lib/mapping/CMapService.h b/lib/mapping/CMapService.h index ab671c8ba..e408a46e0 100644 --- a/lib/mapping/CMapService.h +++ b/lib/mapping/CMapService.h @@ -21,31 +21,33 @@ class CInputStream; class IMapLoader; class IMapPatcher; +class ModCompatibilityInfo; + /** - * The map service provides loading of VCMI/H3 map files. It can - * be extended to save maps later as well. + * The map service provides loading and saving of VCMI/H3 map files. */ -class DLL_LINKAGE IMapService +class DLL_LINKAGE CMapService { public: - IMapService() = default; - virtual ~IMapService() = default; + CMapService() = default; + virtual ~CMapService() = default; + /** * Loads the VCMI/H3 map file specified by the name. * * @param name the name of the map * @return a unique ptr to the loaded map class */ - virtual std::unique_ptr loadMap(const ResourceID & name) const = 0; - + std::unique_ptr loadMap(const ResourceID & name) const; + /** * Loads the VCMI/H3 map header specified by the name. * * @param name the name of the map * @return a unique ptr to the loaded map header class */ - virtual std::unique_ptr loadMapHeader(const ResourceID & name) const = 0; - + std::unique_ptr loadMapHeader(const ResourceID & name) const; + /** * Loads the VCMI/H3 map file from a buffer. This method is temporarily * in use to ease the transition to use the new map service. @@ -58,8 +60,8 @@ public: * @param name indicates name of file that will be used during map header patching * @return a unique ptr to the loaded map class */ - virtual std::unique_ptr loadMap(const ui8 * buffer, int size, const std::string & name, const std::string & modName, const std::string & encoding) const = 0; - + std::unique_ptr loadMap(const ui8 * buffer, int size, const std::string & name, const std::string & modName, const std::string & encoding) const; + /** * Loads the VCMI/H3 map header from a buffer. This method is temporarily * in use to ease the transition to use the new map service. @@ -72,22 +74,22 @@ public: * @param name indicates name of file that will be used during map header patching * @return a unique ptr to the loaded map class */ - virtual std::unique_ptr loadMapHeader(const ui8 * buffer, int size, const std::string & name, const std::string & modName, const std::string & encoding) const = 0; - - virtual void saveMap(const std::unique_ptr & map, boost::filesystem::path fullPath) const = 0; -}; - -class DLL_LINKAGE CMapService : public IMapService -{ -public: - CMapService() = default; - virtual ~CMapService() = default; - - std::unique_ptr loadMap(const ResourceID & name) const override; - std::unique_ptr loadMapHeader(const ResourceID & name) const override; - std::unique_ptr loadMap(const ui8 * buffer, int size, const std::string & name, const std::string & modName, const std::string & encoding) const override; - std::unique_ptr loadMapHeader(const ui8 * buffer, int size, const std::string & name, const std::string & modName, const std::string & encoding) const override; - void saveMap(const std::unique_ptr & map, boost::filesystem::path fullPath) const override; + std::unique_ptr loadMapHeader(const ui8 * buffer, int size, const std::string & name, const std::string & modName, const std::string & encoding) const; + + /** + * Tests if mods used in the map are currently loaded + * @param map const reference to map header + * @return data structure representing missing or incompatible mods (those which are needed from map but not loaded) + */ + static ModCompatibilityInfo verifyMapHeaderMods(const CMapHeader & map); + + /** + * Saves map into VCMI format with name specified + * @param map to save + * @param fullPath full path to file to write, including extension + */ + void saveMap(const std::unique_ptr & map, boost::filesystem::path fullPath) const; + private: /** * Gets a map input stream object specified by a map name. diff --git a/lib/mapping/MapFormatH3M.cpp b/lib/mapping/MapFormatH3M.cpp index 454e9ce8e..1d85427ce 100644 --- a/lib/mapping/MapFormatH3M.cpp +++ b/lib/mapping/MapFormatH3M.cpp @@ -147,6 +147,10 @@ void CMapLoaderH3M::readHeader() features = MapFormatFeaturesH3M::find(mapHeader->version, 0); reader->setFormatLevel(mapHeader->version, 0); } + + // include basic mod + if(mapHeader->version == EMapFormat::WOG) + mapHeader->mods["wake-of-gods"]; // Read map name, description, dimensions,... mapHeader->areAnyPlayers = reader->readBool(); diff --git a/lib/mapping/MapFormatJson.cpp b/lib/mapping/MapFormatJson.cpp index 6737c4a9b..8ddfbb75c 100644 --- a/lib/mapping/MapFormatJson.cpp +++ b/lib/mapping/MapFormatJson.cpp @@ -339,7 +339,7 @@ namespace TerrainDetail ///CMapFormatJson const int CMapFormatJson::VERSION_MAJOR = 1; -const int CMapFormatJson::VERSION_MINOR = 0; +const int CMapFormatJson::VERSION_MINOR = 1; const std::string CMapFormatJson::HEADER_FILE_NAME = "header.json"; const std::string CMapFormatJson::OBJECTS_FILE_NAME = "objects.json"; @@ -947,6 +947,13 @@ void CMapLoaderJson::readHeader(const bool complete) JsonDeserializer handler(mapObjectResolver.get(), header); mapHeader->version = EMapFormat::VCMI;//todo: new version field + + //loading mods + if(!header["mods"].isNull()) + { + for(auto & mod : header["mods"].Vector()) + mapHeader->mods[mod["name"].String()] = CModInfo::Version::fromString(mod["version"].String()); + } //todo: multilevel map load support { @@ -1279,6 +1286,16 @@ void CMapSaverJson::writeHeader() header["versionMajor"].Float() = VERSION_MAJOR; header["versionMinor"].Float() = VERSION_MINOR; + + //write mods + JsonNode & mods = header["mods"]; + for(const auto & mod : mapHeader->mods) + { + JsonNode modWriter; + modWriter["name"].String() = mod.first; + modWriter["version"].String() = mod.second.toString(); + mods.Vector().push_back(modWriter); + } //todo: multilevel map save support JsonNode & levels = header["mapLevels"]; diff --git a/mapeditor/mainwindow.cpp b/mapeditor/mainwindow.cpp index 329cb9176..5f3a141c5 100644 --- a/mapeditor/mainwindow.cpp +++ b/mapeditor/mainwindow.cpp @@ -333,7 +333,23 @@ bool MainWindow::openMap(const QString & filenameSelect) CMapService mapService; try { - controller.setMap(mapService.loadMap(resId)); + if(auto header = mapService.loadMapHeader(resId)) + { + auto missingMods = CMapService::verifyMapHeaderMods(*header); + CModHandler::Incompatibility::ModList modList; + for(const auto & m : missingMods) + modList.push_back({m.first, m.second.toString()}); + + if(!modList.empty()) + throw CModHandler::Incompatibility(std::move(modList)); + + controller.setMap(mapService.loadMap(resId)); + } + } + catch(const CModHandler::Incompatibility & e) + { + QMessageBox::warning(this, "Mods requiered", e.what()); + return false; } catch(const std::exception & e) { diff --git a/mapeditor/mapcontroller.cpp b/mapeditor/mapcontroller.cpp index e992e40c3..e4faf8d5f 100644 --- a/mapeditor/mapcontroller.cpp +++ b/mapeditor/mapcontroller.cpp @@ -554,3 +554,36 @@ void MapController::redo() sceneForceUpdate(); //TODO: use smart invalidation (setDirty) main->mapChanged(); } + +ModCompatibilityInfo MapController::modAssessmentAll() +{ + ModCompatibilityInfo result; + for(auto primaryID : VLC->objtypeh->knownObjects()) + { + for(auto secondaryID : VLC->objtypeh->knownSubObjects(primaryID)) + { + auto handler = VLC->objtypeh->getHandlerFor(primaryID, secondaryID); + auto modName = QString::fromStdString(handler->getJsonKey()).split(":").at(0).toStdString(); + if(modName != "core") + result[modName] = VLC->modh->getModInfo(modName).version; + } + } + return result; +} + +ModCompatibilityInfo MapController::modAssessmentMap(const CMap & map) +{ + ModCompatibilityInfo result; + for(auto obj : map.objects) + { + if(obj->ID == Obj::HERO) + continue; //stub! + + auto handler = VLC->objtypeh->getHandlerFor(obj->ID, obj->subID); + auto modName = QString::fromStdString(handler->getJsonKey()).split(":").at(0).toStdString(); + if(modName != "core") + result[modName] = VLC->modh->getModInfo(modName).version; + } + //TODO: terrains? + return result; +} diff --git a/mapeditor/mapcontroller.h b/mapeditor/mapcontroller.h index 338798201..892a89a1a 100644 --- a/mapeditor/mapcontroller.h +++ b/mapeditor/mapcontroller.h @@ -54,6 +54,9 @@ public: bool discardObject(int level) const; void createObject(int level, CGObjectInstance * obj) const; bool canPlaceObject(int level, CGObjectInstance * obj, QString & error) const; + + static ModCompatibilityInfo modAssessmentAll(); + static ModCompatibilityInfo modAssessmentMap(const CMap & map); void undo(); void redo(); diff --git a/mapeditor/mapsettings.cpp b/mapeditor/mapsettings.cpp index 46321b42f..5fc4df355 100644 --- a/mapeditor/mapsettings.cpp +++ b/mapeditor/mapsettings.cpp @@ -17,8 +17,10 @@ #include "../lib/CArtHandler.h" #include "../lib/CHeroHandler.h" #include "../lib/CGeneralTextHandler.h" +#include "../lib/CModHandler.h" #include "../lib/mapObjects/CGHeroInstance.h" #include "../lib/mapObjects/MiscObjects.h" +#include "../lib/mapping/CMapService.h" #include "../lib/StringConstants.h" #include "inspector/townbulidingswidget.h" //to convert BuildingID to string @@ -82,6 +84,14 @@ std::vector linearJsonArray(const JsonNode & json) return result; } +void traverseNode(QTreeWidgetItem * item, std::function action) +{ + // Do something with item + action(item); + for (int i = 0; i < item->childCount(); ++i) + traverseNode(item->child(i), action); +} + MapSettings::MapSettings(MapController & ctrl, QWidget *parent) : QDialog(parent), ui(new Ui::MapSettings), @@ -93,10 +103,11 @@ MapSettings::MapSettings(MapController & ctrl, QWidget *parent) : ui->mapNameEdit->setText(tr(controller.map()->name.c_str())); ui->mapDescriptionEdit->setPlainText(tr(controller.map()->description.c_str())); + ui->heroLevelLimit->setValue(controller.map()->levelLimit); + ui->heroLevelLimitCheck->setChecked(controller.map()->levelLimit); show(); - for(int i = 0; i < controller.map()->allowedAbilities.size(); ++i) { auto * item = new QListWidgetItem(QString::fromStdString(VLC->skillh->objects[i]->getNameTranslated())); @@ -385,6 +396,61 @@ MapSettings::MapSettings(MapController & ctrl, QWidget *parent) : } } } + + //mods management + //collect all active mods + QMap addedMods; + QSet modsToProcess; + ui->treeMods->blockSignals(true); + + auto createModTreeWidgetItem = [&](QTreeWidgetItem * parent, const CModInfo & modInfo) + { + auto item = new QTreeWidgetItem(parent, {QString::fromStdString(modInfo.name), QString::fromStdString(modInfo.version.toString())}); + item->setData(0, Qt::UserRole, QVariant(QString::fromStdString(modInfo.identifier))); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + item->setCheckState(0, controller.map()->mods.count(modInfo.identifier) ? Qt::Checked : Qt::Unchecked); + //set parent check + if(parent && item->checkState(0) == Qt::Checked) + parent->setCheckState(0, Qt::Checked); + return item; + }; + + for(const auto & modName : VLC->modh->getActiveMods()) + { + QString qmodName = QString::fromStdString(modName); + if(qmodName.split(".").size() == 1) + { + const auto & modInfo = VLC->modh->getModInfo(modName); + addedMods[qmodName] = createModTreeWidgetItem(nullptr, modInfo); + ui->treeMods->addTopLevelItem(addedMods[qmodName]); + } + else + { + modsToProcess.insert(qmodName); + } + } + + for(auto qmodIter = modsToProcess.begin(); qmodIter != modsToProcess.end();) + { + auto qmodName = *qmodIter; + auto pieces = qmodName.split("."); + assert(pieces.size() > 1); + + QString qs; + for(int i = 0; i < pieces.size() - 1; ++i) + qs += pieces[i]; + + if(addedMods.count(qs)) + { + const auto & modInfo = VLC->modh->getModInfo(qmodName.toStdString()); + addedMods[qmodName] = createModTreeWidgetItem(addedMods[qs], modInfo); + modsToProcess.erase(qmodIter); + qmodIter = modsToProcess.begin(); + } + else + ++qmodIter; + } + ui->treeMods->blockSignals(false); } MapSettings::~MapSettings() @@ -428,10 +494,30 @@ std::string MapSettings::getMonsterName(int monsterObjectIdx) return name; } +void MapSettings::updateModWidgetBasedOnMods(const ModCompatibilityInfo & mods) +{ + //Mod management + auto widgetAction = [&](QTreeWidgetItem * item) + { + auto modName = item->data(0, Qt::UserRole).toString().toStdString(); + item->setCheckState(0, mods.count(modName) ? Qt::Checked : Qt::Unchecked); + }; + + for (int i = 0; i < ui->treeMods->topLevelItemCount(); ++i) + { + QTreeWidgetItem *item = ui->treeMods->topLevelItem(i); + traverseNode(item, widgetAction); + } +} + void MapSettings::on_pushButton_clicked() { controller.map()->name = ui->mapNameEdit->text().toStdString(); controller.map()->description = ui->mapDescriptionEdit->toPlainText().toStdString(); + if(ui->heroLevelLimitCheck->isChecked()) + controller.map()->levelLimit = ui->heroLevelLimit->value(); + else + controller.map()->levelLimit = 0; controller.commitChangeWithoutRedraw(); for(int i = 0; i < controller.map()->allowedAbilities.size(); ++i) @@ -699,6 +785,23 @@ void MapSettings::on_pushButton_clicked() controller.map()->triggeredEvents.push_back(specialDefeat); } + //Mod management + auto widgetAction = [&](QTreeWidgetItem * item) + { + if(item->checkState(0) == Qt::Checked) + { + auto modName = item->data(0, Qt::UserRole).toString().toStdString(); + controller.map()->mods[modName] = VLC->modh->getModInfo(modName).version; + } + }; + + controller.map()->mods.clear(); + for (int i = 0; i < ui->treeMods->topLevelItemCount(); ++i) + { + QTreeWidgetItem *item = ui->treeMods->topLevelItem(i); + traverseNode(item, widgetAction); + } + close(); } @@ -875,3 +978,39 @@ void MapSettings::on_loseComboBox_currentIndexChanged(int index) } } + +void MapSettings::on_heroLevelLimitCheck_toggled(bool checked) +{ + ui->heroLevelLimit->setEnabled(checked); +} + +void MapSettings::on_modResolution_map_clicked() +{ + updateModWidgetBasedOnMods(MapController::modAssessmentMap(*controller.map())); +} + + +void MapSettings::on_modResolution_full_clicked() +{ + updateModWidgetBasedOnMods(MapController::modAssessmentAll()); +} + +void MapSettings::on_treeMods_itemChanged(QTreeWidgetItem *item, int column) +{ + //set state for children + for (int i = 0; i < item->childCount(); ++i) + item->child(i)->setCheckState(0, item->checkState(0)); + + //set state for parent + ui->treeMods->blockSignals(true); + if(item->checkState(0) == Qt::Checked) + { + while(item->parent()) + { + item->parent()->setCheckState(0, Qt::Checked); + item = item->parent(); + } + } + ui->treeMods->blockSignals(false); +} + diff --git a/mapeditor/mapsettings.h b/mapeditor/mapsettings.h index de8edbe50..bd7a79826 100644 --- a/mapeditor/mapsettings.h +++ b/mapeditor/mapsettings.h @@ -32,12 +32,22 @@ private slots: void on_loseComboBox_currentIndexChanged(int index); + void on_heroLevelLimitCheck_toggled(bool checked); + + void on_modResolution_map_clicked(); + + void on_modResolution_full_clicked(); + + void on_treeMods_itemChanged(QTreeWidgetItem *item, int column); + private: std::string getTownName(int townObjectIdx); std::string getHeroName(int townObjectIdx); std::string getMonsterName(int townObjectIdx); + void updateModWidgetBasedOnMods(const ModCompatibilityInfo & mods); + template std::vector getObjectIndexes() const { diff --git a/mapeditor/mapsettings.ui b/mapeditor/mapsettings.ui index ebea4b77c..5a314b917 100644 --- a/mapeditor/mapsettings.ui +++ b/mapeditor/mapsettings.ui @@ -9,8 +9,8 @@ 0 0 - 470 - 480 + 543 + 494 @@ -23,14 +23,7 @@ Map settings - - - - Ok - - - - + 0 @@ -60,6 +53,39 @@ + + + + 10 + + + + + false + + + + 48 + 0 + + + + + + + + + 0 + 0 + + + + Limit maximum heroes level + + + + + @@ -106,6 +132,77 @@ + + + Mods + + + + + + Mandatory mods for playing this map + + + + + + + QAbstractScrollArea::AdjustIgnored + + + 320 + + + + Mod name + + + + + Version + + + + + + + + + + Automatic assignment + + + + + + + Set required mods based on objects placed on the map. This method may cause problems if you have customized rewards, garrisons, etc from mods + + + Map objects mods + + + false + + + + + + + Set all mods having a game content as mandatory + + + Full content mods + + + false + + + + + + + Events @@ -336,6 +433,13 @@ + + + + Ok + + + diff --git a/mapeditor/validator.cpp b/mapeditor/validator.cpp index 2fd622d69..c7a342542 100644 --- a/mapeditor/validator.cpp +++ b/mapeditor/validator.cpp @@ -10,6 +10,7 @@ #include "StdInc.h" #include "validator.h" +#include "mapcontroller.h" #include "ui_validator.h" #include "../lib/mapObjects/MapObjects.h" #include "../lib/CHeroHandler.h" @@ -158,6 +159,15 @@ std::list Validator::validate(const CMap * map) issues.emplace_back("Map name is not specified", false); if(map->description.empty()) issues.emplace_back("Map description is not specified", false); + + //verificationfor mods + for(auto & mod : MapController::modAssessmentMap(*map)) + { + if(!map->mods.count(mod.first)) + { + issues.emplace_back(QString("Map contains object from mod \"%1\", but doesn't require it").arg(QString::fromStdString(VLC->modh->getModInfo(mod.first).name)), true); + } + } } catch(const std::exception & e) { diff --git a/mapeditor/windownewmap.cpp b/mapeditor/windownewmap.cpp index 59fe1b2be..714012d9a 100644 --- a/mapeditor/windownewmap.cpp +++ b/mapeditor/windownewmap.cpp @@ -264,7 +264,7 @@ void WindowNewMap::on_okButton_clicked() nmap = f.get(); } - + nmap->mods = MapController::modAssessmentAll(); static_cast(parent())->controller.setMap(std::move(nmap)); static_cast(parent())->initializeMap(true); close();