From d996726fe72aa98f5eddb626d0a10b8f0167be56 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Wed, 12 Feb 2025 16:44:22 +0000 Subject: [PATCH] Support for parsing HotA 1.7.0 maps --- lib/constants/EntityIdentifiers.h | 12 +- lib/mapping/MapFeaturesH3M.cpp | 18 +- lib/mapping/MapFeaturesH3M.h | 1 + lib/mapping/MapFormatH3M.cpp | 453 ++++++++++++++++++++++++++---- lib/mapping/MapFormatH3M.h | 18 +- lib/mapping/MapReaderH3M.cpp | 41 ++- lib/mapping/MapReaderH3M.h | 4 + 7 files changed, 479 insertions(+), 68 deletions(-) diff --git a/lib/constants/EntityIdentifiers.h b/lib/constants/EntityIdentifiers.h index b1adc4354..126e10f4c 100644 --- a/lib/constants/EntityIdentifiers.h +++ b/lib/constants/EntityIdentifiers.h @@ -407,12 +407,12 @@ public: NO_OBJ = -1, NOTHING = 0, - ALTAR_OF_SACRIFICE [[deprecated]] = 2, + ALTAR_OF_SACRIFICE = 2, ANCHOR_POINT = 3, ARENA = 4, ARTIFACT = 5, PANDORAS_BOX = 6, - BLACK_MARKET [[deprecated]] = 7, + BLACK_MARKET = 7, BOAT = 8, BORDERGUARD = 9, KEYMASTER = 10, @@ -504,12 +504,12 @@ public: TEMPLE = 96, DEN_OF_THIEVES = 97, TOWN = 98, - TRADING_POST [[deprecated]] = 99, + TRADING_POST = 99, LEARNING_STONE = 100, TREASURE_CHEST = 101, TREE_OF_KNOWLEDGE = 102, SUBTERRANEAN_GATE = 103, - UNIVERSITY [[deprecated]] = 104, + UNIVERSITY = 104, WAGON = 105, WAR_MACHINE_FACTORY = 106, SCHOOL_OF_WAR = 107, @@ -564,7 +564,7 @@ public: RANDOM_MONSTER_L6 = 163, RANDOM_MONSTER_L7 = 164, BORDER_GATE = 212, - FREELANCERS_GUILD [[deprecated]] = 213, + FREELANCERS_GUILD = 213, HERO_PLACEHOLDER = 214, QUEST_GUARD = 215, RANDOM_DWELLING = 216, @@ -572,7 +572,7 @@ public: RANDOM_DWELLING_FACTION = 218, //subtype = faction GARRISON2 = 219, ABANDONED_MINE = 220, - TRADING_POST_SNOW [[deprecated]] = 221, + TRADING_POST_SNOW = 221, CLOVER_FIELD = 222, CURSED_GROUND2 = 223, EVIL_FOG = 224, diff --git a/lib/mapping/MapFeaturesH3M.cpp b/lib/mapping/MapFeaturesH3M.cpp index df4eb6c4c..111d85400 100644 --- a/lib/mapping/MapFeaturesH3M.cpp +++ b/lib/mapping/MapFeaturesH3M.cpp @@ -131,14 +131,14 @@ MapFormatFeaturesH3M MapFormatFeaturesH3M::getFeaturesHOTA(uint32_t hotaVersion) { // even if changes are minimal, we might not be able to parse map header in map selection screen // throw exception - to be caught by map selection screen & excluded as invalid - if(hotaVersion > 3) + if(hotaVersion > 5) throw std::runtime_error("Invalid map format!"); MapFormatFeaturesH3M result = getFeaturesSOD(); result.levelHOTA0 = true; result.levelHOTA1 = hotaVersion > 0; - //result.levelHOTA2 = hotaVersion > 1; // HOTA2 seems to be identical to HOTA1 so far result.levelHOTA3 = hotaVersion > 2; + result.levelHOTA5 = hotaVersion > 4; result.artifactsBytes = 21; result.heroesBytes = 23; @@ -157,8 +157,18 @@ MapFormatFeaturesH3M MapFormatFeaturesH3M::getFeaturesHOTA(uint32_t hotaVersion) if(hotaVersion == 3) { result.artifactsCount = 165; // + HotA artifacts - result.heroesCount = 179; // + Cove + Giselle - result.heroesPortraitsCount = 188; // + Cove + Giselle + result.heroesCount = 179; // + Giselle + result.heroesPortraitsCount = 188; // + campaign portrait + Giselle + } + if (hotaVersion == 5) + { + result.factionsCount = 11; // + Factory + result.creaturesCount = 186; // + 16 Factory + result.artifactsCount = 166; // +pendant of reflection, +sleepkeeper + result.heroesCount = 198; // + 16 Factory, +3 campaign + result.heroesPortraitsCount = 208; // + 16 Factory, +10 campaign + + result.heroesBytes = 25; } assert((result.heroesCount + 7) / 8 == result.heroesBytes); diff --git a/lib/mapping/MapFeaturesH3M.h b/lib/mapping/MapFeaturesH3M.h index 4768f0746..7633ee306 100644 --- a/lib/mapping/MapFeaturesH3M.h +++ b/lib/mapping/MapFeaturesH3M.h @@ -70,6 +70,7 @@ public: bool levelHOTA0 = false; bool levelHOTA1 = false; bool levelHOTA3 = false; + bool levelHOTA5 = false; }; VCMI_LIB_NAMESPACE_END diff --git a/lib/mapping/MapFormatH3M.cpp b/lib/mapping/MapFormatH3M.cpp index b835450c2..c79677728 100644 --- a/lib/mapping/MapFormatH3M.cpp +++ b/lib/mapping/MapFormatH3M.cpp @@ -183,8 +183,26 @@ void CMapLoaderH3M::readHeader() if(hotaVersion > 1) { - [[maybe_unused]] uint8_t unknown = reader->readUInt32(); - assert(unknown == 12); + int32_t terrainTypesCount = reader->readUInt32(); + assert(features.terrainsCount == terrainTypesCount); + + if (features.terrainsCount != terrainTypesCount) + logGlobal->warn("Map '%s': Expected %d terrains, but %d found!", mapName, features.terrainsCount, terrainTypesCount); + } + + if(hotaVersion > 4) + { + int32_t townTypesCount = reader->readUInt32(); + uint8_t allowedDifficultiesMask = reader->readUInt8(); + + assert(features.factionsCount == townTypesCount); + assert(allowedDifficultiesMask < 32); + + if (features.factionsCount != townTypesCount) + logGlobal->warn("Map '%s': Expected %d factions, but %d found!", mapName, features.factionsCount, townTypesCount); + + if (allowedDifficultiesMask != 0) + logGlobal->warn("Map '%s': List of allowed difficulties (%d) is not implemented!", mapName, allowedDifficultiesMask); } } else @@ -249,14 +267,14 @@ void CMapLoaderH3M::readPlayerInfo() playerInfo.aiTactic = static_cast(reader->readInt8Checked(-1, 3)); if(features.levelSOD) - reader->skipUnused(1); //TODO: check meaning? + reader->skipUnused(1); //faction is selectable std::set allowedFactions; reader->readBitmaskFactions(allowedFactions, false); - const bool isFactionRandom = playerInfo.isFactionRandom = reader->readBool(); - const bool allFactionsAllowed = isFactionRandom && allowedFactions.size() == features.factionsCount; + playerInfo.isFactionRandom = reader->readBool(); + const bool allFactionsAllowed = playerInfo.isFactionRandom && allowedFactions.size() == features.factionsCount; if(!allFactionsAllowed) playerInfo.allowedFactions = allowedFactions; @@ -267,7 +285,7 @@ void CMapLoaderH3M::readPlayerInfo() if(features.levelAB) { playerInfo.generateHeroAtMainTown = reader->readBool(); - reader->skipUnused(1); //TODO: check meaning? + reader->skipUnused(1); // starting town type, unused } else { @@ -721,7 +739,6 @@ void CMapLoaderH3M::readMapOptions() if(features.levelHOTA0) { - //TODO: HotA bool allowSpecialMonths = reader->readBool(); map->overrideGameSetting(EGameSettings::CREATURES_ALLOW_RANDOM_SPECIAL_WEEKS, JsonNode(allowSpecialMonths)); reader->skipZero(3); @@ -732,6 +749,7 @@ void CMapLoaderH3M::readMapOptions() // Unknown, may be another "sized bitmap", e.g // 4 bytes - size of bitmap (16) // 2 bytes - bitmap data (16 bits / 2 bytes) + // potentially - combo_artifact_count / combo_artifacts [[maybe_unused]] uint8_t unknownConstant = reader->readUInt8(); assert(unknownConstant == 16); reader->skipZero(5); @@ -744,6 +762,18 @@ void CMapLoaderH3M::readMapOptions() if(roundLimit != -1) logGlobal->warn("Map '%s': roundLimit of %d is not implemented!", mapName, roundLimit); } + + if(features.levelHOTA5) + { + int32_t unknownA = reader->readInt32(); + int32_t unknownB = reader->readInt32(); + + if (unknownA != 0) + logGlobal->warn("Map '%s': unknown option A has been set to %d!!", mapName, unknownA); + + if (unknownB != 0) + logGlobal->warn("Map '%s': unknown option B has been set to %d!!", mapName, unknownB); + } } void CMapLoaderH3M::readAllowedArtifacts() @@ -884,6 +914,26 @@ void CMapLoaderH3M::readPredefinedHeroes() logGlobal->debug("Map '%s': Hero predefined in map: %s", mapName, hero->getHeroType()->getJsonKey()); } + + if(features.levelHOTA5) + { + for(int heroID = 0; heroID < heroesCount; heroID++) + { + bool alwaysAddSkills = reader->readBool(); // prevent heroes from receiving additional random secondary skills at the start of the map if they are not of the first level + bool cannotGainXP = reader->readBool(); + int32_t level = reader->readInt32(); // Needs investigation how this interacts with usual setting of level via experience + assert(level > 0); + + if (!alwaysAddSkills) + logGlobal->warn("Map '%s': Option to prevent hero %d from gaining skills on map start is not implemented!", mapName, heroID); + + if (cannotGainXP) + logGlobal->warn("Map '%s': Option to prevent hero %d from receiveing experience is not implemented!", mapName, heroID); + + if (level > 1) + logGlobal->warn("Map '%s': Option to set level of hero %d to %d is not implemented!", mapName, heroID, level); + } + } } void CMapLoaderH3M::loadArtifactsOfHero(CGHeroInstance * hero) @@ -921,6 +971,9 @@ void CMapLoaderH3M::loadArtifactsOfHero(CGHeroInstance * hero) bool CMapLoaderH3M::loadArtifactToSlot(CGHeroInstance * hero, int slot) { ArtifactID artifactID = reader->readArtifact(); + SpellID scrollSpell = SpellID::NONE; + if (features.levelHOTA5) + scrollSpell = reader->readSpell16(); if(artifactID == ArtifactID::NONE) return false; @@ -944,7 +997,7 @@ bool CMapLoaderH3M::loadArtifactToSlot(CGHeroInstance * hero, int slot) // Artifact seems to be missing in game, so skip artifacts that don't fit target slot if(ArtifactID(artifactID).toArtifact()->canBePutAt(hero, ArtifactPosition(slot))) { - auto * artifact = ArtifactUtils::createArtifact(artifactID); + auto * artifact = ArtifactUtils::createArtifact(artifactID, scrollSpell); map->putArtifactInstance(*hero, artifact, slot); map->addNewArtifactInstance(artifact); } @@ -988,16 +1041,21 @@ void CMapLoaderH3M::readObjectTemplates() { uint32_t defAmount = reader->readUInt32(); - templates.reserve(defAmount); + originalTemplates.reserve(defAmount); + remappedTemplates.reserve(defAmount); // Read custom defs for(int defID = 0; defID < defAmount; ++defID) { auto tmpl = reader->readObjectTemplate(); - templates.push_back(tmpl); + originalTemplates.push_back(tmpl); - if (!CResourceHandler::get()->existsResource(tmpl->animationFile.addPrefix("SPRITES/"))) - logMod->warn("Template animation %s of type (%d %d) is missing!", tmpl->animationFile.getOriginalName(), tmpl->id, tmpl->subid ); + auto remapped = std::make_shared(*tmpl); + reader->remapTemplate(*remapped); + remappedTemplates.push_back(remapped); + + if (!CResourceHandler::get()->existsResource(remapped->animationFile.addPrefix("SPRITES/"))) + logMod->warn("Template animation %s of type (%d %d) is missing!", remapped->animationFile.getOriginalName(), remapped->id, remapped->subid ); } } @@ -1018,6 +1076,16 @@ CGObjectInstance * CMapLoaderH3M::readEvent(const int3 & mapPosition, const Obje else object->humanActivate = true; + if(features.levelHOTA5) + { + int32_t movementMode = reader->readInt32(); // Give, Take, Nullify, Set, Replenish + int32_t movementAmount = reader->readInt32(); + + assert(movementMode >= 0 && movementMode <= 4); + if (movementMode != 0 || movementAmount != 0) + logGlobal->warn("Map '%s': Option to modify (mode %d) movement points by %d in event is not implemented!", mapName, movementMode, movementAmount); + } + return object; } @@ -1025,6 +1093,21 @@ CGObjectInstance * CMapLoaderH3M::readPandora(const int3 & mapPosition, const Ob { auto * object = new CGPandoraBox(map->cb); readBoxContent(object, mapPosition, idToBeGiven); + + if(features.levelHOTA5) + { + uint8_t unknown = reader->readUInt8(); + int32_t movementMode = reader->readInt32(); // Give, Take, Nullify, Set, Replenish + int32_t movementAmount = reader->readInt32(); + + assert(movementMode >= 0 && movementMode <= 4); + assert(unknown == 0); + if (unknown != 0) + logGlobal->warn("Map '%s': Unknown option in pandora box has been set!", mapName); + if (movementMode != 0 || movementAmount != 0) + logGlobal->warn("Map '%s': Option to modify (mode %d) movement points by %d in event is not implemented!", mapName, movementMode, movementAmount); + } + return object; } @@ -1055,7 +1138,15 @@ void CMapLoaderH3M::readBoxContent(CGPandoraBox * object, const int3 & mapPositi } size_t gart = reader->readUInt8(); //number of gained artifacts for(size_t oo = 0; oo < gart; ++oo) + { reward.artifacts.push_back(reader->readArtifact()); + if (features.levelHOTA5) + { + SpellID scrollSpell = reader->readSpell16(); + if (reward.artifacts.back() == ArtifactID::SPELL_SCROLL) + logGlobal->warn("Map '%s': Pandora/Event at %s Option to give spell scroll (%s) via event or pandora is not implemented!", mapName, mapPosition.toString(), scrollSpell.toEntity(VLC)->getJsonKey()); + } + } size_t gspel = reader->readUInt8(); //number of gained spells for(size_t oo = 0; oo < gspel; ++oo) @@ -1127,6 +1218,14 @@ CGObjectInstance * CMapLoaderH3M::readMonster(const int3 & mapPosition, const Ob ); } + if (features.levelHOTA5) + { + [[maybe_unused]] int8_t unknownA = reader->readInt8(); + [[maybe_unused]] int32_t unknownB = reader->readInt32(); + assert(unknownA == 0); + assert(unknownB == 0); + } + return object; } @@ -1272,6 +1371,34 @@ CGObjectInstance * CMapLoaderH3M::readGarrison(const int3 & mapPosition) } CGObjectInstance * CMapLoaderH3M::readArtifact(const int3 & mapPosition, std::shared_ptr objectTemplate) +{ + ArtifactID artID = ArtifactID::NONE; //random, set later + auto * object = new CGArtifact(map->cb); + + readMessageAndGuards(object->message, object, mapPosition); + + //specific artifact + if(objectTemplate->id == Obj::ARTIFACT) + artID = ArtifactID(objectTemplate->subid); + + if(features.levelHOTA5 && objectTemplate->id != Obj::SPELL_SCROLL) + { + uint32_t pickupMode = reader->readUInt32(); + uint8_t pickupFlags = reader->readUInt8(); + + assert(pickupMode == 0 || pickupMode == 1 || pickupMode == 2); // DISABLED, RANDOM, CUSTOM + + if (pickupMode != 0) + logGlobal->debug("Map '%s': Artifact %s: not implemented pickup mode %d (flags: %d)", mapName, mapPosition.toString(), pickupMode, pickupFlags); + + } + + object->storedArtifact = ArtifactUtils::createArtifact(artID, SpellID::NONE); + map->addNewArtifactInstance(object->storedArtifact); + return object; +} + +CGObjectInstance * CMapLoaderH3M::readScroll(const int3 & mapPosition, std::shared_ptr objectTemplate) { ArtifactID artID = ArtifactID::NONE; //random, set later SpellID spellID = SpellID::NONE; @@ -1290,6 +1417,16 @@ CGObjectInstance * CMapLoaderH3M::readArtifact(const int3 & mapPosition, std::sh artID = ArtifactID(objectTemplate->subid); } + if(features.levelHOTA5 && objectTemplate->id != Obj::SPELL_SCROLL) + { + [[maybe_unused]] uint32_t unknownA = reader->readUInt32(); + [[maybe_unused]] uint8_t unknownB = reader->readUInt8(); + + // TODO + //assert(unknownA == 0);//pickup_mode (DISABLED=0, RANDOM=1, CUSTOM=2) + //assert(unknownB == 127);//pickup_conditions + } + object->storedArtifact = ArtifactUtils::createArtifact(artID, spellID.getNum()); map->addNewArtifactInstance(object->storedArtifact); return object; @@ -1315,17 +1452,32 @@ CGObjectInstance * CMapLoaderH3M::readResource(const int3 & mapPosition, std::sh return object; } -CGObjectInstance * CMapLoaderH3M::readMine(const int3 & mapPosition, std::shared_ptr objectTemplate) +CGObjectInstance * CMapLoaderH3M::readMine(const int3 & mapPosition) { auto * object = new CGMine(map->cb); - if(objectTemplate->subid < 7) + setOwnerAndValidate(mapPosition, object, reader->readPlayer32()); + return object; +} + +CGObjectInstance * CMapLoaderH3M::readAbandonedMine(const int3 & mapPosition) +{ + auto * object = new CGMine(map->cb); + object->setOwner(PlayerColor::NEUTRAL); + reader->readBitmaskResources(object->abandonedMineResources, false); + + if(features.levelHOTA5) { - setOwnerAndValidate(mapPosition, object, reader->readPlayer32()); - } - else - { - object->setOwner(PlayerColor::NEUTRAL); - reader->readBitmaskResources(object->abandonedMineResources, false); + bool customGuards = reader->readBool(); + CreatureID guardsUnit = reader->readCreature32(); + int32_t guardsMin = reader->readInt32(); + int32_t guardsMax = reader->readInt32(); + + if (customGuards) + { + assert(guardsMin <= guardsMax); + assert(guardsUnit.hasValue()); + logGlobal->debug("Map '%s': Abandoned Mine %s: not implemented guards of %d-%d %s", mapName, mapPosition.toString(), guardsMin, guardsMax, guardsUnit.toEntity(VLC)->getJsonKey()); + } } return object; } @@ -1415,21 +1567,49 @@ CGObjectInstance * CMapLoaderH3M::readHeroPlaceholder(const int3 & mapPosition) logGlobal->debug("Map '%s': Hero placeholder: %s at %s, owned by %s", mapName, VLC->heroh->getById(htid)->getJsonKey(), mapPosition.toString(), object->getOwner().toString()); } + if(features.levelHOTA5) + { + bool customizedStatingUnits = reader->readBool(); + + if (customizedStatingUnits) + logGlobal->debug("Map '%s': Hero placeholder: not implemented option to customize starting units", mapName); + + for (int i = 0; i < 7; ++i) + { + int32_t unitAmount = reader->readInt32(); + int32_t unitToGive = reader->readInt32(); + + if (unitToGive != -1) + logGlobal->debug("Map '%s': Hero placeholder: not implemented option to give %d units of type %d on map start to slot %d is not implemented!", mapName, unitAmount, unitToGive, i); + } + + int32_t artifactsToGive = reader->readInt32(); + assert(artifactsToGive >= 0); + assert(artifactsToGive < 100); // technically legal, but not possible in h3 + + for (int i = 0; i < artifactsToGive; ++i) + { + int32_t possiblyArtifactCustom = reader->readInt32(); + if (possiblyArtifactCustom != 0) + logGlobal->debug("Map '%s': Hero placeholder: not implemented option to give hero artifact %d", mapName, possiblyArtifactCustom); + } + + } + return object; } -CGObjectInstance * CMapLoaderH3M::readGrail(const int3 & mapPosition, std::shared_ptr objectTemplate) +CGObjectInstance * CMapLoaderH3M::readGrail(const int3 & mapPosition) { - if (objectTemplate->subid < 1000) - { - map->grailPos = mapPosition; - map->grailRadius = reader->readInt32(); - } - else - { - // Battle location for arena mode in HotA - logGlobal->warn("Map '%s': Arena mode is not supported!", mapName); - } + map->grailPos = mapPosition; + map->grailRadius = reader->readInt32(); + return nullptr; +} + +CGObjectInstance * CMapLoaderH3M::readHotaBattleLocation(const int3 & mapPosition) +{ + // Battle location for arena mode in HotA + logGlobal->warn("Map '%s': Arena mode is not supported!", mapName); return nullptr; } @@ -1442,14 +1622,6 @@ CGObjectInstance * CMapLoaderH3M::readGeneric(const int3 & mapPosition, std::sha return new CGObjectInstance(map->cb); } -CGObjectInstance * CMapLoaderH3M::readPyramid(const int3 & mapPosition, std::shared_ptr objectTemplate) -{ - if(objectTemplate->subid == 0) - return readGeneric(mapPosition, objectTemplate); - - return new CGObjectInstance(map->cb); -} - CGObjectInstance * CMapLoaderH3M::readQuestGuard(const int3 & mapPosition) { auto * guard = new CGQuestGuard(map->cb); @@ -1510,9 +1682,76 @@ CGObjectInstance * CMapLoaderH3M::readBank(const int3 & mapPosition, std::shared return readGeneric(mapPosition, objectTemplate); } -CGObjectInstance * CMapLoaderH3M::readObject(std::shared_ptr objectTemplate, const int3 & mapPosition, const ObjectInstanceID & objectInstanceID) +CGObjectInstance * CMapLoaderH3M::readTreasureChest(const int3 & mapPosition, std::shared_ptr objectTemplate) { - switch(objectTemplate->id.toEnum()) + if(features.levelHOTA5) + { + int32_t content = reader->readInt32(); + int32_t artifact = reader->readInt32(); + logGlobal->warn("Map '%s: Object (%d) %s settings %d %d are not implemented!", mapName, objectTemplate->id, mapPosition.toString(), content, artifact); + } + + return readGeneric(mapPosition, objectTemplate); +} + +CGObjectInstance * CMapLoaderH3M::readBlackMarket(const int3 & mapPosition, std::shared_ptr objectTemplate) +{ + if(features.levelHOTA5) + { + for (int i = 0; i < 7; ++i) + { + ArtifactID artifact = reader->readArtifact(); + SpellID spellID = reader->readSpell16(); + + if (artifact.hasValue()) + { + if (artifact != ArtifactID::SPELL_SCROLL) + logGlobal->debug("Map '%s': Black Market at %s: option to sell artifact %s is not implemented", mapName, mapPosition.toString(), artifact.toEntity(VLC)->getJsonKey()); + else + logGlobal->debug("Map '%s': Black Market at %s: option to sell scroll %s is not implemented", mapName, mapPosition.toString(), spellID.toEntity(VLC)->getJsonKey()); + } + } + } + return readGeneric(mapPosition, objectTemplate); +} + +CGObjectInstance * CMapLoaderH3M::readUniversity(const int3 & mapPosition, std::shared_ptr objectTemplate) +{ + if(features.levelHOTA5) + { + int32_t customized = reader->readInt32(); + + std::set allowedSkills; + reader->readBitmaskSkills(allowedSkills, false); + + assert(customized == -1 || customized == 0); + if (customized != -1) + logGlobal->debug("Map '%s': University at %s: option to give specific skills out of %d is not implemented", mapName, mapPosition.toString(), allowedSkills.size()); + } + + return readGeneric(mapPosition, objectTemplate); +} + +CGObjectInstance * CMapLoaderH3M::readCampfire(const int3 & mapPosition, std::shared_ptr objectTemplate) +{ + if(features.levelHOTA5) + { + [[maybe_unused]] int32_t content = reader->readInt32(); + [[maybe_unused]] int32_t artifact = reader->readInt32(); + [[maybe_unused]] int32_t amount = reader->readInt32(); + [[maybe_unused]] int32_t resourceID = reader->readInt32(); + [[maybe_unused]] uint16_t unknown = reader->readUInt16(); + + logGlobal->warn("Map '%s: Object (%d) %s settings %d %d are not implemented!", mapName, objectTemplate->id, mapPosition.toString(), content, artifact); + + } + + return readGeneric(mapPosition, objectTemplate); +} + +CGObjectInstance * CMapLoaderH3M::readObject(MapObjectID id, MapObjectSubID subid, std::shared_ptr objectTemplate, const int3 & mapPosition, const ObjectInstanceID & objectInstanceID) +{ + switch(id.toEnum()) { case Obj::EVENT: return readEvent(mapPosition, objectInstanceID); @@ -1555,8 +1794,9 @@ CGObjectInstance * CMapLoaderH3M::readObject(std::shared_ptr Ancient Lamp + // 1 -> Sea Barrel + // 2 -> Jetsam + // 3 -> Vial of Mana + if (subid == 0 || subid == 1) + return readCampfire(mapPosition, objectTemplate); + else + return readTreasureChest(mapPosition, objectTemplate); + + case Obj(146):// HOTA_CUSTOM_2 + if (subid == 0) // Seafaring Academy + return readUniversity(mapPosition, objectTemplate); + else + return readGeneric(mapPosition, objectTemplate); + + case Obj::BLACK_MARKET: + return readBlackMarket(mapPosition, objectTemplate); + + case Obj::UNIVERSITY: + return readUniversity(mapPosition, objectTemplate); + default: //any other object return readGeneric(mapPosition, objectTemplate); } @@ -1625,26 +1912,30 @@ void CMapLoaderH3M::readObjects() for(uint32_t i = 0; i < objectsCount; ++i) { int3 mapPosition = reader->readInt3(); + assert(map->isInTheMap(mapPosition) || map->isInTheMap(mapPosition - int3(0,8,0)) || map->isInTheMap(mapPosition - int3(8,0,0)) || map->isInTheMap(mapPosition - int3(8,8,0))); uint32_t defIndex = reader->readUInt32(); ObjectInstanceID objectInstanceID = ObjectInstanceID(static_cast(map->objects.size())); - std::shared_ptr objectTemplate = templates.at(defIndex); + std::shared_ptr originalTemplate = originalTemplates.at(defIndex); + std::shared_ptr remappedTemplate = remappedTemplates.at(defIndex); + auto originalID = originalTemplate->id; + auto originalSubID = originalTemplate->subid; reader->skipZero(5); - CGObjectInstance * newObject = readObject(objectTemplate, mapPosition, objectInstanceID); + CGObjectInstance * newObject = readObject(originalID, originalSubID, remappedTemplate, mapPosition, objectInstanceID); if(!newObject) continue; newObject->setAnchorPos(mapPosition); - newObject->ID = objectTemplate->id; + newObject->ID = remappedTemplate->id; newObject->id = objectInstanceID; if(newObject->ID != Obj::HERO && newObject->ID != Obj::HERO_PLACEHOLDER && newObject->ID != Obj::PRISON) { - newObject->subID = objectTemplate->subid; + newObject->subID = remappedTemplate->subid; } - newObject->appearance = objectTemplate; + newObject->appearance = remappedTemplate; assert(objectInstanceID == ObjectInstanceID((si32)map->objects.size())); if (newObject->isVisitable() && !map->isInTheMap(newObject->visitablePos())) @@ -1895,6 +2186,23 @@ CGObjectInstance * CMapLoaderH3M::readHero(const int3 & mapPosition, const Objec logGlobal->debug("Map '%s': Hero on map: (random) at %s, owned by %s", mapName, mapPosition.toString(), object->getOwner().toString()); reader->skipZero(16); + + if(features.levelHOTA5) + { + bool alwaysAddSkills = reader->readBool(); // prevent heroes from receiving additional random secondary skills at the start of the map if they are not of the first level + bool cannotGainXP = reader->readBool(); + int32_t level = reader->readInt32(); // Needs investigation how this interacts with usual setting of level via experience + assert(level > 0); + + if (!alwaysAddSkills) + logGlobal->warn("Map '%s': Option to prevent hero %d from gaining skills on map start is not implemented!", mapName, object->subID.num); + + if (cannotGainXP) + logGlobal->warn("Map '%s': Option to prevent hero %d from receiveing experience is not implemented!", mapName, object->subID.num); + + if (level > 1) + logGlobal->warn("Map '%s': Option to set level of hero %d to %d is not implemented!", mapName, object->subID.num, level); + } return object; } @@ -2028,6 +2336,14 @@ void CMapLoaderH3M::readSeerHutQuest(CGSeerHut * hut, const int3 & position, con case ESeerHutRewardType::ARTIFACT: { reward.artifacts.push_back(reader->readArtifact()); + if (features.levelHOTA5) + { + SpellID scrollSpell = reader->readSpell16(); + if (reward.artifacts.back() == ArtifactID::SPELL_SCROLL) + logGlobal->warn("Map '%s': Seer Hut at %s: Option to give spell scroll (%s) as a reward is not implemented!", mapName, position.toString(), scrollSpell.toEntity(VLC)->getJsonKey()); + + } + break; } case ESeerHutRewardType::SPELL: @@ -2092,6 +2408,13 @@ EQuestMission CMapLoaderH3M::readQuest(IQuestObject * guard, const int3 & positi for(size_t yy = 0; yy < artNumber; ++yy) { auto artid = reader->readArtifact(); + if (features.levelHOTA5) + { + SpellID scrollSpell = reader->readSpell16(); + if (artid == ArtifactID::SPELL_SCROLL) + logGlobal->warn("Map '%s': Seer Hut at %s: Quest to find scroll '%s' is not implemented!", mapName, position.toString(), scrollSpell.toEntity(VLC)->getJsonKey()); + + } guard->quest->mission.artifacts.push_back(artid); map->allowedArtifact.erase(artid); //these are unavailable for random generation } @@ -2223,11 +2546,19 @@ CGObjectInstance * CMapLoaderH3M::readTown(const int3 & position, std::shared_pt if(features.levelHOTA1) object->spellResearchAllowed = reader->readBool(); + if(features.levelHOTA5) + { + uint32_t unknownSize = reader->readUInt32(); + assert(unknownSize == 44); // buildings? + reader->skipUnused(unknownSize); + } + // Read castle events uint32_t eventsCount = reader->readUInt32(); for(int eventID = 0; eventID < eventsCount; ++eventID) { + // TODO: a lot of copy-pasted code with map event CCastleEvent event; event.name = readBasicString(); event.message.appendTextID(readLocalizedString(TextIdentifier("town", position.x, position.y, position.z, "event", eventID, "description"))); @@ -2246,6 +2577,14 @@ CGObjectInstance * CMapLoaderH3M::readTown(const int3 & position, std::shared_pt reader->skipZero(17); + if(features.levelHOTA5) + { + [[maybe_unused]] int32_t allowedDifficulties = reader->readInt32(); + [[maybe_unused]] int32_t hota_level_7b = reader->readInt32(); + [[maybe_unused]] int32_t hota_amount = reader->readInt32(); + [[maybe_unused]] int16_t apply_neutral_towns = reader->readInt16(); + } + // New buildings reader->readBitmaskBuildings(event.buildings, faction); @@ -2298,6 +2637,7 @@ void CMapLoaderH3M::readEvents() uint32_t eventsCount = reader->readUInt32(); for(int eventID = 0; eventID < eventsCount; ++eventID) { + // FIXME: a lot of copy-pasted code with town event CMapEvent event; event.name = readBasicString(); event.message.appendTextID(readLocalizedString(TextIdentifier("event", eventID, "description"))); @@ -2318,6 +2658,15 @@ void CMapLoaderH3M::readEvents() reader->skipZero(17); + if (features.levelHOTA5) + { + [[maybe_unused]] int32_t difficulties = reader->readInt32(); + [[maybe_unused]] int32_t unknownA= reader->readInt32(); + [[maybe_unused]] int32_t unknownB= reader->readInt32(); + [[maybe_unused]] int16_t unknownC= reader->readInt16(); + + } + map->events.push_back(event); } } diff --git a/lib/mapping/MapFormatH3M.h b/lib/mapping/MapFormatH3M.h index 7196edbe0..ffa5abf14 100644 --- a/lib/mapping/MapFormatH3M.h +++ b/lib/mapping/MapFormatH3M.h @@ -12,6 +12,7 @@ #include "CMapService.h" #include "MapFeaturesH3M.h" +#include "../constants/EntityIdentifiers.h" VCMI_LIB_NAMESPACE_BEGIN @@ -185,7 +186,7 @@ private: void readObjects(); /// Reads single object from input stream based on template - CGObjectInstance * readObject(std::shared_ptr objectTemplate, const int3 & objectPosition, const ObjectInstanceID & idToBeGiven); + CGObjectInstance * readObject(MapObjectID id, MapObjectSubID subid, std::shared_ptr objectTemplate, const int3 & objectPosition, const ObjectInstanceID & idToBeGiven); CGObjectInstance * readEvent(const int3 & objectPosition, const ObjectInstanceID & idToBeGiven); CGObjectInstance * readMonster(const int3 & objectPosition, const ObjectInstanceID & idToBeGiven); @@ -197,20 +198,26 @@ private: CGObjectInstance * readScholar(const int3 & position, std::shared_ptr objectTemplate); CGObjectInstance * readGarrison(const int3 & mapPosition); CGObjectInstance * readArtifact(const int3 & position, std::shared_ptr objTempl); + CGObjectInstance * readScroll(const int3 & position, std::shared_ptr objTempl); CGObjectInstance * readResource(const int3 & position, std::shared_ptr objTempl); - CGObjectInstance * readMine(const int3 & position, std::shared_ptr objTempl); + CGObjectInstance * readMine(const int3 & position); + CGObjectInstance * readAbandonedMine(const int3 & position); CGObjectInstance * readPandora(const int3 & position, const ObjectInstanceID & idToBeGiven); CGObjectInstance * readDwelling(const int3 & position); CGObjectInstance * readDwellingRandom(const int3 & position, std::shared_ptr objTempl); CGObjectInstance * readShrine(const int3 & position, std::shared_ptr objectTemplate); CGObjectInstance * readHeroPlaceholder(const int3 & position); - CGObjectInstance * readGrail(const int3 & position, std::shared_ptr objectTemplate); - CGObjectInstance * readPyramid(const int3 & position, std::shared_ptr objTempl); + CGObjectInstance * readGrail(const int3 & position); + CGObjectInstance * readHotaBattleLocation(const int3 & position); CGObjectInstance * readQuestGuard(const int3 & position); CGObjectInstance * readShipyard(const int3 & mapPosition, std::shared_ptr objectTemplate); CGObjectInstance * readLighthouse(const int3 & mapPosition, std::shared_ptr objectTemplate); CGObjectInstance * readGeneric(const int3 & position, std::shared_ptr objectTemplate); CGObjectInstance * readBank(const int3 & position, std::shared_ptr objectTemplate); + CGObjectInstance * readTreasureChest(const int3 & position, std::shared_ptr objectTemplate); + CGObjectInstance * readCampfire(const int3 & position, std::shared_ptr objectTemplate); + CGObjectInstance * readBlackMarket(const int3 & position, std::shared_ptr objectTemplate); + CGObjectInstance * readUniversity(const int3 & position, std::shared_ptr objectTemplate); /** * Reads a creature set. @@ -260,7 +267,8 @@ private: /** List of templates loaded from the map, used on later stage to create * objects but not needed for fully functional CMap */ - std::vector> templates; + std::vector> originalTemplates; + std::vector> remappedTemplates; /** ptr to the map object which gets filled by data from the buffer */ CMap * map; diff --git a/lib/mapping/MapReaderH3M.cpp b/lib/mapping/MapReaderH3M.cpp index 21c2a8771..0b562021d 100644 --- a/lib/mapping/MapReaderH3M.cpp +++ b/lib/mapping/MapReaderH3M.cpp @@ -136,6 +136,27 @@ HeroTypeID MapReaderH3M::readHeroPortrait() return remapper.remapPortrait(result); } +CreatureID MapReaderH3M::readCreature32() +{ + CreatureID result= CreatureID(reader->readUInt32()); + + if(result.getNum() == features.creatureIdentifierInvalid) + return CreatureID::NONE; + + if(result.getNum() < features.creaturesCount) + return remapIdentifier(result); + + // this may be random creature in army/town, to be randomized later + CreatureID randomIndex(result.getNum() - features.creatureIdentifierInvalid - 1); + assert(randomIndex < CreatureID::NONE); + + if (randomIndex.getNum() > -16) + return randomIndex; + + logGlobal->warn("Map contains invalid creature %d. Will be removed!", result.getNum()); + return CreatureID::NONE; +} + CreatureID MapReaderH3M::readCreature() { CreatureID result; @@ -209,6 +230,15 @@ SpellID MapReaderH3M::readSpell() return remapIdentifier(result); } +SpellID MapReaderH3M::readSpell16() +{ + SpellID result(readInt16()); + if(result.getNum() == features.spellIdentifierInvalid) + return SpellID::NONE; + assert(result.getNum() < features.spellsCount); + return result; +} + SpellID MapReaderH3M::readSpell32() { SpellID result(readInt32()); @@ -371,10 +401,14 @@ std::shared_ptr MapReaderH3M::readObjectTemplate() { auto tmpl = std::make_shared(); tmpl->readMap(*reader); - remapper.remapTemplate(*tmpl); return tmpl; } +void MapReaderH3M::remapTemplate(ObjectTemplate & tmpl) +{ + remapper.remapTemplate(tmpl); +} + void MapReaderH3M::skipUnused(size_t amount) { reader->skip(amount); @@ -432,6 +466,11 @@ uint16_t MapReaderH3M::readUInt16() return reader->readUInt16(); } +int16_t MapReaderH3M::readInt16() +{ + return reader->readInt16(); +} + uint32_t MapReaderH3M::readUInt32() { return reader->readUInt32(); diff --git a/lib/mapping/MapReaderH3M.h b/lib/mapping/MapReaderH3M.h index d6312a80d..04468d18d 100644 --- a/lib/mapping/MapReaderH3M.h +++ b/lib/mapping/MapReaderH3M.h @@ -34,6 +34,7 @@ public: ArtifactID readArtifact(); ArtifactID readArtifact8(); ArtifactID readArtifact32(); + CreatureID readCreature32(); CreatureID readCreature(); HeroTypeID readHero(); HeroTypeID readHeroPortrait(); @@ -43,6 +44,7 @@ public: PrimarySkill readPrimary(); SecondarySkill readSkill(); SpellID readSpell(); + SpellID readSpell16(); SpellID readSpell32(); GameResID readGameResID(); PlayerColor readPlayer(); @@ -63,6 +65,7 @@ public: int3 readInt3(); std::shared_ptr readObjectTemplate(); + void remapTemplate(ObjectTemplate & tmpl); void skipUnused(size_t amount); void skipZero(size_t amount); @@ -75,6 +78,7 @@ public: int8_t readInt8(); int8_t readInt8Checked(int8_t lowerLimit, int8_t upperLimit); + int16_t readInt16(); uint16_t readUInt16(); uint32_t readUInt32();