mirror of
https://github.com/vcmi/vcmi.git
synced 2025-11-25 22:42:04 +02:00
Tomes of X Spells and Spellbinder's Hat (and any other sources for such bonuses from mods) will no longer provide spells that are banned on map. Option is only active for random maps and for HotA h3m's. RoE-SoD .h3m's work as before. If needed, behavior can be changed in config
2737 lines
86 KiB
C++
2737 lines
86 KiB
C++
/*
|
|
* MapFormatH3M.cpp, part of VCMI engine
|
|
*
|
|
* Authors: listed in file AUTHORS in main folder
|
|
*
|
|
* License: GNU General Public License v2.0 or later
|
|
* Full text of license available in license.txt file, in main folder
|
|
*
|
|
*/
|
|
|
|
#include "StdInc.h"
|
|
#include "MapFormatH3M.h"
|
|
|
|
#include "CCastleEvent.h"
|
|
#include "CMap.h"
|
|
#include "MapReaderH3M.h"
|
|
#include "MapFormatSettings.h"
|
|
|
|
#include "../CCreatureHandler.h"
|
|
#include "../texts/CGeneralTextHandler.h"
|
|
#include "../CSkillHandler.h"
|
|
#include "../CStopWatch.h"
|
|
#include "../IGameSettings.h"
|
|
#include "../RiverHandler.h"
|
|
#include "../RoadHandler.h"
|
|
#include "../TerrainHandler.h"
|
|
#include "../GameLibrary.h"
|
|
#include "../constants/StringConstants.h"
|
|
#include "../entities/artifact/CArtHandler.h"
|
|
#include "../entities/hero/CHeroHandler.h"
|
|
#include "../filesystem/CBinaryReader.h"
|
|
#include "../filesystem/Filesystem.h"
|
|
#include "../mapObjectConstructors/AObjectTypeHandler.h"
|
|
#include "../mapObjectConstructors/CObjectClassesHandler.h"
|
|
#include "../mapObjectConstructors/CommonConstructors.h"
|
|
#include "../mapObjects/CGCreature.h"
|
|
#include "../mapObjects/CGResource.h"
|
|
#include "../mapObjects/CQuest.h"
|
|
#include "../mapObjects/MapObjects.h"
|
|
#include "../mapObjects/ObjectTemplate.h"
|
|
#include "../modding/ModScope.h"
|
|
#include "../networkPacks/Component.h"
|
|
#include "../networkPacks/ArtifactLocation.h"
|
|
#include "../spells/CSpellHandler.h"
|
|
#include "../texts/TextOperations.h"
|
|
#include "entities/hero/CHeroClass.h"
|
|
|
|
VCMI_LIB_NAMESPACE_BEGIN
|
|
|
|
CMapLoaderH3M::CMapLoaderH3M(const std::string & mapName, const std::string & modName, const std::string & encodingName, CInputStream * stream)
|
|
: map(nullptr)
|
|
, reader(new MapReaderH3M(stream))
|
|
, inputStream(stream)
|
|
, mapName(TextOperations::convertMapName(mapName))
|
|
, modName(modName)
|
|
, fileEncoding(encodingName)
|
|
{
|
|
}
|
|
|
|
//must be instantiated in .cpp file for access to complete types of all member fields
|
|
CMapLoaderH3M::~CMapLoaderH3M() = default;
|
|
|
|
std::unique_ptr<CMap> CMapLoaderH3M::loadMap(IGameInfoCallback * cb)
|
|
{
|
|
// Init map object by parsing the input buffer
|
|
map = new CMap(cb);
|
|
mapHeader = std::unique_ptr<CMapHeader>(dynamic_cast<CMapHeader *>(map));
|
|
init();
|
|
|
|
return std::unique_ptr<CMap>(dynamic_cast<CMap *>(mapHeader.release()));
|
|
}
|
|
|
|
std::unique_ptr<CMapHeader> CMapLoaderH3M::loadMapHeader()
|
|
{
|
|
// Read header
|
|
mapHeader = std::make_unique<CMapHeader>();
|
|
readHeader();
|
|
|
|
return std::move(mapHeader);
|
|
}
|
|
|
|
void CMapLoaderH3M::init()
|
|
{
|
|
inputStream->seek(0);
|
|
|
|
readHeader();
|
|
readMapOptions();
|
|
readAllowedArtifacts();
|
|
readAllowedSpellsAbilities();
|
|
readRumors();
|
|
readPredefinedHeroes();
|
|
readTerrain();
|
|
readObjectTemplates();
|
|
readObjects();
|
|
readEvents();
|
|
|
|
map->calculateGuardingGreaturePositions();
|
|
afterRead();
|
|
//map->banWaterContent(); //Not sure if force this for custom scenarios
|
|
}
|
|
|
|
void CMapLoaderH3M::readHeader()
|
|
{
|
|
// Map version
|
|
mapHeader->version = static_cast<EMapFormat>(reader->readUInt32());
|
|
|
|
if(mapHeader->version == EMapFormat::HOTA)
|
|
{
|
|
uint32_t hotaVersion = reader->readUInt32();
|
|
features = MapFormatFeaturesH3M::find(mapHeader->version, hotaVersion);
|
|
reader->setFormatLevel(features);
|
|
|
|
if(features.levelHOTA8)
|
|
{
|
|
int hotaVersionMajor = reader->readUInt32();
|
|
int hotaVersionMinor = reader->readUInt32();
|
|
int hotaVersionPatch = reader->readUInt32();
|
|
logGlobal->trace("Loading HotA map, version %d.%d.%d", hotaVersionMajor, hotaVersionMinor, hotaVersionPatch);
|
|
}
|
|
|
|
if(features.levelHOTA1)
|
|
{
|
|
bool isMirrorMap = reader->readBool();
|
|
bool isArenaMap = reader->readBool();
|
|
|
|
//TODO: HotA
|
|
if (isMirrorMap)
|
|
logGlobal->warn("Map '%s': Mirror maps are not yet supported!", mapName);
|
|
|
|
if (isArenaMap)
|
|
logGlobal->warn("Map '%s': Arena maps are not supported!", mapName);
|
|
}
|
|
|
|
if(features.levelHOTA2)
|
|
{
|
|
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(features.levelHOTA5)
|
|
{
|
|
int32_t townTypesCount = reader->readUInt32();
|
|
int8_t allowedDifficultiesMask = reader->readInt8Checked(0, 31);
|
|
|
|
assert(features.factionsCount == townTypesCount);
|
|
|
|
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, static_cast<int>(allowedDifficultiesMask));
|
|
}
|
|
|
|
if(features.levelHOTA7)
|
|
{
|
|
bool canHireDefeatedHeroes = reader->readBool();
|
|
|
|
if (!canHireDefeatedHeroes)
|
|
logGlobal->warn("Map '%s': Option to block hiring of defeated heroes is not implemented!", mapName);
|
|
}
|
|
|
|
if(features.levelHOTA8)
|
|
{
|
|
bool forceMatchingVersion = reader->readBool();
|
|
if (forceMatchingVersion)
|
|
logGlobal->warn("Map '%s': This map is forced to use specific hota version!", mapName);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
features = MapFormatFeaturesH3M::find(mapHeader->version, 0);
|
|
reader->setFormatLevel(features);
|
|
}
|
|
|
|
if (!LIBRARY->mapFormat->isSupported(mapHeader->version))
|
|
throw std::runtime_error("Unsupported map format! Format ID " + std::to_string(static_cast<int>(mapHeader->version)));
|
|
|
|
const MapIdentifiersH3M & identifierMapper = LIBRARY->mapFormat->getMapping(mapHeader->version);
|
|
|
|
reader->setIdentifierRemapper(identifierMapper);
|
|
|
|
// Read map name, description, dimensions,...
|
|
mapHeader->areAnyPlayers = reader->readBool();
|
|
mapHeader->height = mapHeader->width = reader->readInt32();
|
|
mapHeader->twoLevel = reader->readBool();
|
|
mapHeader->name.appendTextID(readLocalizedString("header.name"));
|
|
mapHeader->description.appendTextID(readLocalizedString("header.description"));
|
|
mapHeader->author.appendRawString("");
|
|
mapHeader->authorContact.appendRawString("");
|
|
mapHeader->mapVersion.appendRawString("");
|
|
mapHeader->creationDateTime = 0;
|
|
mapHeader->difficulty = static_cast<EMapDifficulty>(reader->readInt8Checked(0, 4));
|
|
|
|
if(features.levelAB)
|
|
mapHeader->levelLimit = reader->readInt8Checked(0, std::min(100u, LIBRARY->heroh->maxSupportedLevel()));
|
|
else
|
|
mapHeader->levelLimit = 0;
|
|
|
|
readPlayerInfo();
|
|
readVictoryLossConditions();
|
|
readTeamInfo();
|
|
readAllowedHeroes();
|
|
readDisposedHeroes();
|
|
}
|
|
|
|
void CMapLoaderH3M::readPlayerInfo()
|
|
{
|
|
for(int i = 0; i < mapHeader->players.size(); ++i)
|
|
{
|
|
auto & playerInfo = mapHeader->players[i];
|
|
|
|
playerInfo.canHumanPlay = reader->readBool();
|
|
playerInfo.canComputerPlay = reader->readBool();
|
|
|
|
// If nobody can play with this player - skip loading of these properties
|
|
if((!(playerInfo.canHumanPlay || playerInfo.canComputerPlay)))
|
|
{
|
|
if(features.levelROE)
|
|
reader->skipUnused(6);
|
|
if(features.levelAB)
|
|
reader->skipUnused(6);
|
|
if(features.levelSOD)
|
|
reader->skipUnused(1);
|
|
continue;
|
|
}
|
|
|
|
playerInfo.aiTactic = static_cast<EAiTactic>(reader->readInt8Checked(-1, 3));
|
|
|
|
if(features.levelSOD)
|
|
reader->skipUnused(1); //faction is selectable
|
|
|
|
std::set<FactionID> allowedFactions;
|
|
|
|
reader->readBitmaskFactions(allowedFactions, false);
|
|
|
|
playerInfo.isFactionRandom = reader->readBool();
|
|
const bool allFactionsAllowed = playerInfo.isFactionRandom && allowedFactions.size() == features.factionsCount;
|
|
|
|
if(!allFactionsAllowed)
|
|
{
|
|
if (!allowedFactions.empty())
|
|
playerInfo.allowedFactions = allowedFactions;
|
|
else
|
|
logGlobal->warn("Map '%s': Player %d has no allowed factions to play! Ignoring.", mapName, i);
|
|
}
|
|
|
|
playerInfo.hasMainTown = reader->readBool();
|
|
if(playerInfo.hasMainTown)
|
|
{
|
|
if(features.levelAB)
|
|
{
|
|
playerInfo.generateHeroAtMainTown = reader->readBool();
|
|
reader->skipUnused(1); // starting town type, unused
|
|
}
|
|
else
|
|
{
|
|
playerInfo.generateHeroAtMainTown = true;
|
|
}
|
|
|
|
playerInfo.posOfMainTown = reader->readInt3();
|
|
}
|
|
|
|
playerInfo.hasRandomHero = reader->readBool();
|
|
playerInfo.mainCustomHeroId = reader->readHero();
|
|
|
|
if(playerInfo.mainCustomHeroId != HeroTypeID::NONE)
|
|
{
|
|
playerInfo.mainCustomHeroPortrait = reader->readHeroPortrait();
|
|
playerInfo.mainCustomHeroNameTextId = readLocalizedString(TextIdentifier("header", "player", i, "mainHeroName"));
|
|
}
|
|
|
|
if(features.levelAB)
|
|
{
|
|
reader->skipUnused(1); //TODO: check meaning?
|
|
size_t heroCount = reader->readUInt32();
|
|
for(size_t pp = 0; pp < heroCount; ++pp)
|
|
{
|
|
SHeroName vv;
|
|
vv.heroId = reader->readHero();
|
|
vv.heroName = readLocalizedString(TextIdentifier("header", "heroNames", vv.heroId.getNum()));
|
|
|
|
playerInfo.heroesNames.push_back(vv);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void CMapLoaderH3M::readVictoryLossConditions()
|
|
{
|
|
mapHeader->triggeredEvents.clear();
|
|
mapHeader->victoryMessage.clear();
|
|
mapHeader->defeatMessage.clear();
|
|
|
|
auto vicCondition = static_cast<EVictoryConditionType>(reader->readInt8Checked(-1, 12));
|
|
|
|
EventCondition victoryCondition(EventCondition::STANDARD_WIN);
|
|
EventCondition defeatCondition(EventCondition::DAYS_WITHOUT_TOWN);
|
|
defeatCondition.value = 7;
|
|
|
|
TriggeredEvent standardVictory;
|
|
standardVictory.effect.type = EventEffect::VICTORY;
|
|
standardVictory.effect.toOtherMessage.appendTextID("core.genrltxt.5");
|
|
standardVictory.identifier = "standardVictory";
|
|
standardVictory.description.clear(); // TODO: display in quest window
|
|
standardVictory.onFulfill.appendTextID("core.genrltxt.659");
|
|
standardVictory.trigger = EventExpression(victoryCondition);
|
|
|
|
TriggeredEvent standardDefeat;
|
|
standardDefeat.effect.type = EventEffect::DEFEAT;
|
|
standardDefeat.effect.toOtherMessage.appendTextID("core.genrltxt.8");
|
|
standardDefeat.identifier = "standardDefeat";
|
|
standardDefeat.description.clear(); // TODO: display in quest window
|
|
standardDefeat.onFulfill.appendTextID("core.genrltxt.7");
|
|
standardDefeat.trigger = EventExpression(defeatCondition);
|
|
|
|
// Specific victory conditions
|
|
if(vicCondition == EVictoryConditionType::WINSTANDARD)
|
|
{
|
|
// create normal condition
|
|
mapHeader->triggeredEvents.push_back(standardVictory);
|
|
mapHeader->victoryIconIndex = 11;
|
|
mapHeader->victoryMessage.appendTextID("core.vcdesc.0");
|
|
}
|
|
else
|
|
{
|
|
TriggeredEvent specialVictory;
|
|
specialVictory.effect.type = EventEffect::VICTORY;
|
|
specialVictory.identifier = "specialVictory";
|
|
specialVictory.description.clear(); // TODO: display in quest window
|
|
|
|
mapHeader->victoryIconIndex = static_cast<ui16>(vicCondition);
|
|
|
|
bool allowNormalVictory = reader->readBool();
|
|
bool appliesToAI = reader->readBool();
|
|
|
|
switch(vicCondition)
|
|
{
|
|
case EVictoryConditionType::ARTIFACT:
|
|
{
|
|
assert(allowNormalVictory == true); // not selectable in editor
|
|
EventCondition cond(EventCondition::HAVE_ARTIFACT);
|
|
cond.objectType = reader->readArtifact();
|
|
|
|
specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.281");
|
|
specialVictory.onFulfill.appendTextID("core.genrltxt.280");
|
|
specialVictory.trigger = EventExpression(cond);
|
|
|
|
mapHeader->victoryMessage.appendTextID("core.vcdesc.1");
|
|
break;
|
|
}
|
|
case EVictoryConditionType::GATHERTROOP:
|
|
{
|
|
EventCondition cond(EventCondition::HAVE_CREATURES);
|
|
cond.objectType = reader->readCreature();
|
|
cond.value = reader->readInt32();
|
|
|
|
specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.277");
|
|
specialVictory.onFulfill.appendTextID("core.genrltxt.276");
|
|
specialVictory.trigger = EventExpression(cond);
|
|
|
|
mapHeader->victoryMessage.appendTextID("core.vcdesc.2");
|
|
break;
|
|
}
|
|
case EVictoryConditionType::GATHERRESOURCE:
|
|
{
|
|
EventCondition cond(EventCondition::HAVE_RESOURCES);
|
|
cond.objectType = reader->readGameResID();
|
|
cond.value = reader->readInt32();
|
|
|
|
specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.279");
|
|
specialVictory.onFulfill.appendTextID("core.genrltxt.278");
|
|
specialVictory.trigger = EventExpression(cond);
|
|
|
|
mapHeader->victoryMessage.appendTextID("core.vcdesc.3");
|
|
break;
|
|
}
|
|
case EVictoryConditionType::BUILDCITY:
|
|
{
|
|
assert(appliesToAI == true); // not selectable in editor
|
|
EventExpression::OperatorAll oper;
|
|
EventCondition cond(EventCondition::HAVE_BUILDING);
|
|
cond.position = reader->readInt3();
|
|
cond.objectType = BuildingID::HALL_LEVEL(reader->readInt8Checked(0,3) + 1);
|
|
oper.expressions.emplace_back(cond);
|
|
cond.objectType = BuildingID::FORT_LEVEL(reader->readInt8Checked(0, 2));
|
|
oper.expressions.emplace_back(cond);
|
|
|
|
specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.283");
|
|
specialVictory.onFulfill.appendTextID("core.genrltxt.282");
|
|
specialVictory.trigger = EventExpression(oper);
|
|
|
|
mapHeader->victoryMessage.appendTextID("core.vcdesc.4");
|
|
break;
|
|
}
|
|
case EVictoryConditionType::BUILDGRAIL:
|
|
{
|
|
assert(allowNormalVictory == true); // not selectable in editor
|
|
assert(appliesToAI == true); // not selectable in editor
|
|
EventCondition cond(EventCondition::HAVE_BUILDING);
|
|
cond.objectType = BuildingID(BuildingID::GRAIL);
|
|
cond.position = reader->readInt3();
|
|
if(cond.position.z > 2)
|
|
cond.position = int3(-1, -1, -1);
|
|
|
|
specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.285");
|
|
specialVictory.onFulfill.appendTextID("core.genrltxt.284");
|
|
specialVictory.trigger = EventExpression(cond);
|
|
|
|
mapHeader->victoryMessage.appendTextID("core.vcdesc.5");
|
|
break;
|
|
}
|
|
case EVictoryConditionType::BEATHERO:
|
|
{
|
|
if (!allowNormalVictory)
|
|
logGlobal->debug("Map %s: Has 'beat hero' as victory condition, but 'allow normal victory' not set. Ignoring", mapName);
|
|
allowNormalVictory = true; // H3 behavior
|
|
assert(appliesToAI == false); // not selectable in editor
|
|
EventCondition cond(EventCondition::DESTROY);
|
|
cond.objectType = MapObjectID(MapObjectID::HERO);
|
|
cond.position = reader->readInt3();
|
|
|
|
specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.253");
|
|
specialVictory.onFulfill.appendTextID("core.genrltxt.252");
|
|
specialVictory.trigger = EventExpression(cond);
|
|
|
|
mapHeader->victoryMessage.appendTextID("core.vcdesc.6");
|
|
break;
|
|
}
|
|
case EVictoryConditionType::CAPTURECITY:
|
|
{
|
|
EventCondition cond(EventCondition::CONTROL);
|
|
cond.objectType = MapObjectID(MapObjectID::TOWN);
|
|
cond.position = reader->readInt3();
|
|
|
|
specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.250");
|
|
specialVictory.onFulfill.appendTextID("core.genrltxt.249");
|
|
specialVictory.trigger = EventExpression(cond);
|
|
|
|
mapHeader->victoryMessage.appendTextID("core.vcdesc.7");
|
|
break;
|
|
}
|
|
case EVictoryConditionType::BEATMONSTER:
|
|
{
|
|
assert(appliesToAI == true); // not selectable in editor
|
|
EventCondition cond(EventCondition::DESTROY);
|
|
cond.objectType = MapObjectID(MapObjectID::MONSTER);
|
|
cond.position = reader->readInt3();
|
|
|
|
specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.287");
|
|
specialVictory.onFulfill.appendTextID("core.genrltxt.286");
|
|
specialVictory.trigger = EventExpression(cond);
|
|
|
|
mapHeader->victoryMessage.appendTextID("core.vcdesc.8");
|
|
break;
|
|
}
|
|
case EVictoryConditionType::TAKEDWELLINGS:
|
|
{
|
|
EventExpression::OperatorAll oper;
|
|
oper.expressions.emplace_back(EventCondition(EventCondition::CONTROL, 0, Obj(Obj::CREATURE_GENERATOR1)));
|
|
oper.expressions.emplace_back(EventCondition(EventCondition::CONTROL, 0, Obj(Obj::CREATURE_GENERATOR4)));
|
|
|
|
specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.289");
|
|
specialVictory.onFulfill.appendTextID("core.genrltxt.288");
|
|
specialVictory.trigger = EventExpression(oper);
|
|
|
|
mapHeader->victoryMessage.appendTextID("core.vcdesc.9");
|
|
break;
|
|
}
|
|
case EVictoryConditionType::TAKEMINES:
|
|
{
|
|
EventCondition cond(EventCondition::CONTROL);
|
|
cond.objectType = MapObjectID(MapObjectID::MINE);
|
|
|
|
specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.291");
|
|
specialVictory.onFulfill.appendTextID("core.genrltxt.290");
|
|
specialVictory.trigger = EventExpression(cond);
|
|
|
|
mapHeader->victoryMessage.appendTextID("core.vcdesc.10");
|
|
break;
|
|
}
|
|
case EVictoryConditionType::TRANSPORTITEM:
|
|
{
|
|
assert(allowNormalVictory == true); // not selectable in editor
|
|
EventCondition cond(EventCondition::TRANSPORT);
|
|
cond.objectType = reader->readArtifact8();
|
|
cond.position = reader->readInt3();
|
|
|
|
specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.293");
|
|
specialVictory.onFulfill.appendTextID("core.genrltxt.292");
|
|
specialVictory.trigger = EventExpression(cond);
|
|
|
|
mapHeader->victoryMessage.appendTextID("core.vcdesc.11");
|
|
break;
|
|
}
|
|
case EVictoryConditionType::HOTA_ELIMINATE_ALL_MONSTERS:
|
|
{
|
|
assert(appliesToAI == false); // not selectable in editor
|
|
EventCondition cond(EventCondition::DESTROY);
|
|
cond.objectType = MapObjectID(MapObjectID::MONSTER);
|
|
|
|
specialVictory.effect.toOtherMessage.appendTextID("vcmi.map.victoryCondition.eliminateMonsters.toOthers");
|
|
specialVictory.onFulfill.appendTextID("vcmi.map.victoryCondition.eliminateMonsters.toSelf");
|
|
specialVictory.trigger = EventExpression(cond);
|
|
|
|
mapHeader->victoryMessage.appendTextID("core.vcdesc.12");
|
|
mapHeader->victoryIconIndex = 12;
|
|
break;
|
|
}
|
|
case EVictoryConditionType::HOTA_SURVIVE_FOR_DAYS:
|
|
{
|
|
assert(appliesToAI == false); // not selectable in editor
|
|
EventCondition cond(EventCondition::DAYS_PASSED);
|
|
cond.value = reader->readUInt32();
|
|
|
|
specialVictory.effect.toOtherMessage.appendTextID("vcmi.map.victoryCondition.daysPassed.toOthers");
|
|
specialVictory.onFulfill.appendTextID("vcmi.map.victoryCondition.daysPassed.toSelf");
|
|
specialVictory.trigger = EventExpression(cond);
|
|
|
|
mapHeader->victoryMessage.appendTextID("core.vcdesc.13");
|
|
mapHeader->victoryIconIndex = 13;
|
|
break;
|
|
}
|
|
default:
|
|
assert(0);
|
|
}
|
|
|
|
if(allowNormalVictory)
|
|
{
|
|
size_t playersOnMap = boost::range::count_if(
|
|
mapHeader->players,
|
|
[](const PlayerInfo & info)
|
|
{
|
|
return info.canAnyonePlay();
|
|
}
|
|
);
|
|
|
|
if(playersOnMap == 1)
|
|
{
|
|
logGlobal->warn("Map %s: Only one player exists, but normal victory allowed!", mapName);
|
|
allowNormalVictory = false; // makes sense? Not much. Works as H3? Yes!
|
|
}
|
|
}
|
|
|
|
// if condition is human-only turn it into following construction: AllOf(human, condition)
|
|
if(!appliesToAI)
|
|
{
|
|
EventExpression::OperatorAll oper;
|
|
EventCondition notAI(EventCondition::IS_HUMAN);
|
|
notAI.value = 1;
|
|
oper.expressions.emplace_back(notAI);
|
|
oper.expressions.push_back(specialVictory.trigger.get());
|
|
specialVictory.trigger = EventExpression(oper);
|
|
}
|
|
|
|
// if normal victory allowed - add one more quest
|
|
if(allowNormalVictory)
|
|
{
|
|
mapHeader->victoryMessage.appendRawString(" / ");
|
|
mapHeader->victoryMessage.appendTextID("core.vcdesc.0");
|
|
mapHeader->triggeredEvents.push_back(standardVictory);
|
|
}
|
|
mapHeader->triggeredEvents.push_back(specialVictory);
|
|
}
|
|
|
|
// Read loss conditions
|
|
auto lossCond = static_cast<ELossConditionType>(reader->readInt8Checked(-1, 2));
|
|
if(lossCond == ELossConditionType::LOSSSTANDARD)
|
|
{
|
|
mapHeader->defeatIconIndex = 3;
|
|
mapHeader->defeatMessage.appendTextID("core.lcdesc.0");
|
|
}
|
|
else
|
|
{
|
|
TriggeredEvent specialDefeat;
|
|
specialDefeat.effect.type = EventEffect::DEFEAT;
|
|
specialDefeat.effect.toOtherMessage.appendTextID("core.genrltxt.5");
|
|
specialDefeat.identifier = "specialDefeat";
|
|
specialDefeat.description.clear(); // TODO: display in quest window
|
|
|
|
mapHeader->defeatIconIndex = static_cast<ui16>(lossCond);
|
|
|
|
switch(lossCond)
|
|
{
|
|
case ELossConditionType::LOSSCASTLE:
|
|
{
|
|
EventExpression::OperatorNone noneOf;
|
|
EventCondition cond(EventCondition::CONTROL);
|
|
cond.objectType = Obj(Obj::TOWN);
|
|
cond.position = reader->readInt3();
|
|
|
|
noneOf.expressions.emplace_back(cond);
|
|
specialDefeat.onFulfill.appendTextID("core.genrltxt.251");
|
|
specialDefeat.trigger = EventExpression(noneOf);
|
|
|
|
mapHeader->defeatMessage.appendTextID("core.lcdesc.1");
|
|
break;
|
|
}
|
|
case ELossConditionType::LOSSHERO:
|
|
{
|
|
EventExpression::OperatorNone noneOf;
|
|
EventCondition cond(EventCondition::CONTROL);
|
|
cond.objectType = Obj(Obj::HERO);
|
|
cond.position = reader->readInt3();
|
|
|
|
noneOf.expressions.emplace_back(cond);
|
|
specialDefeat.onFulfill.appendTextID("core.genrltxt.253");
|
|
specialDefeat.trigger = EventExpression(noneOf);
|
|
|
|
mapHeader->defeatMessage.appendTextID("core.lcdesc.2");
|
|
break;
|
|
}
|
|
case ELossConditionType::TIMEEXPIRES:
|
|
{
|
|
EventCondition cond(EventCondition::DAYS_PASSED);
|
|
cond.value = reader->readUInt16();
|
|
|
|
specialDefeat.onFulfill.appendTextID("core.genrltxt.254");
|
|
specialDefeat.trigger = EventExpression(cond);
|
|
|
|
mapHeader->defeatMessage.appendTextID("core.lcdesc.3");
|
|
break;
|
|
}
|
|
}
|
|
// turn simple loss condition into complete one that can be evaluated later:
|
|
// - any of :
|
|
// - days without town: 7
|
|
// - all of:
|
|
// - is human
|
|
// - (expression)
|
|
|
|
EventExpression::OperatorAll allOf;
|
|
EventCondition isHuman(EventCondition::IS_HUMAN);
|
|
isHuman.value = 1;
|
|
|
|
allOf.expressions.emplace_back(isHuman);
|
|
allOf.expressions.push_back(specialDefeat.trigger.get());
|
|
specialDefeat.trigger = EventExpression(allOf);
|
|
|
|
mapHeader->triggeredEvents.push_back(specialDefeat);
|
|
}
|
|
mapHeader->triggeredEvents.push_back(standardDefeat);
|
|
}
|
|
|
|
void CMapLoaderH3M::readTeamInfo()
|
|
{
|
|
mapHeader->howManyTeams = reader->readUInt8();
|
|
if(mapHeader->howManyTeams > 0)
|
|
{
|
|
// Teams
|
|
for(int i = 0; i < PlayerColor::PLAYER_LIMIT_I; ++i)
|
|
mapHeader->players[i].team = TeamID(reader->readUInt8());
|
|
}
|
|
else
|
|
{
|
|
// No alliances
|
|
for(int i = 0; i < PlayerColor::PLAYER_LIMIT_I; i++)
|
|
if(mapHeader->players[i].canComputerPlay || mapHeader->players[i].canHumanPlay)
|
|
mapHeader->players[i].team = TeamID(mapHeader->howManyTeams++);
|
|
}
|
|
}
|
|
|
|
void CMapLoaderH3M::readAllowedHeroes()
|
|
{
|
|
mapHeader->allowedHeroes = LIBRARY->heroh->getDefaultAllowed();
|
|
|
|
if(features.levelHOTA0)
|
|
reader->readBitmaskHeroesSized(mapHeader->allowedHeroes, false);
|
|
else
|
|
reader->readBitmaskHeroes(mapHeader->allowedHeroes, false);
|
|
|
|
if(features.levelAB)
|
|
{
|
|
size_t placeholdersQty = reader->readUInt32();
|
|
|
|
for (size_t i = 0; i < placeholdersQty; ++i)
|
|
{
|
|
auto heroID = reader->readHero();
|
|
mapHeader->reservedCampaignHeroes.insert(heroID);
|
|
}
|
|
}
|
|
}
|
|
|
|
void CMapLoaderH3M::readDisposedHeroes()
|
|
{
|
|
// Reading disposed heroes (20 bytes)
|
|
if(features.levelSOD)
|
|
{
|
|
size_t disp = reader->readUInt8();
|
|
mapHeader->disposedHeroes.resize(disp);
|
|
for(size_t g = 0; g < disp; ++g)
|
|
{
|
|
mapHeader->disposedHeroes[g].heroId = reader->readHero();
|
|
mapHeader->disposedHeroes[g].portrait = reader->readHeroPortrait();
|
|
mapHeader->disposedHeroes[g].name = readLocalizedString(TextIdentifier("header", "heroes", mapHeader->disposedHeroes[g].heroId.getNum()));
|
|
reader->readBitmaskPlayers(mapHeader->disposedHeroes[g].players, false);
|
|
}
|
|
}
|
|
}
|
|
|
|
void CMapLoaderH3M::readMapOptions()
|
|
{
|
|
//omitting NULLS
|
|
reader->skipZero(31);
|
|
|
|
if(features.levelHOTA0)
|
|
{
|
|
bool allowSpecialMonths = reader->readBool();
|
|
map->overrideGameSetting(EGameSettings::CREATURES_ALLOW_RANDOM_SPECIAL_WEEKS, JsonNode(allowSpecialMonths));
|
|
reader->skipZero(3);
|
|
}
|
|
|
|
if(features.levelHOTA1)
|
|
{
|
|
int32_t combinedArtifactsCount = reader->readInt32();
|
|
int32_t combinedArtifactsBytes = (combinedArtifactsCount + 7) / 8;
|
|
|
|
for (int i = 0; i < combinedArtifactsBytes; ++i)
|
|
{
|
|
uint8_t mask = reader->readUInt8();
|
|
if (mask != 0)
|
|
logGlobal->warn("Map '%s': Option to ban specific combined artifacts is not implemented!", mapName);
|
|
}
|
|
}
|
|
|
|
if(features.levelHOTA3)
|
|
{
|
|
//TODO: HotA
|
|
int32_t roundLimit = reader->readInt32();
|
|
if(roundLimit != -1)
|
|
logGlobal->warn("Map '%s': roundLimit of %d is not implemented!", mapName, roundLimit);
|
|
}
|
|
|
|
if(features.levelHOTA5)
|
|
{
|
|
for (int i = 0; i < PlayerColor::PLAYER_LIMIT_I; ++i)
|
|
{
|
|
// unconfirmed, but only remainig option according to changelog
|
|
bool heroRecruitmentBlocked = reader->readBool();
|
|
if (heroRecruitmentBlocked)
|
|
logGlobal->warn("Map '%s': option to ban hero recruitment for %s is not implemented!!", mapName, PlayerColor(i).toString());
|
|
}
|
|
}
|
|
|
|
const MapIdentifiersH3M & identifierMapper = LIBRARY->mapFormat->getMapping(mapHeader->version);
|
|
map->overrideGameSettings(identifierMapper.getFormatSettings());
|
|
}
|
|
|
|
void CMapLoaderH3M::readAllowedArtifacts()
|
|
{
|
|
map->allowedArtifact = LIBRARY->arth->getDefaultAllowed();
|
|
|
|
if(features.levelAB)
|
|
{
|
|
if(features.levelHOTA0)
|
|
reader->readBitmaskArtifactsSized(map->allowedArtifact, true);
|
|
else
|
|
reader->readBitmaskArtifacts(map->allowedArtifact, true);
|
|
}
|
|
|
|
// ban combo artifacts
|
|
if(!features.levelSOD)
|
|
{
|
|
for(auto const & artifact : LIBRARY->arth->objects)
|
|
if(artifact->isCombined())
|
|
map->allowedArtifact.erase(artifact->getId());
|
|
}
|
|
|
|
if(!features.levelAB)
|
|
{
|
|
map->allowedArtifact.erase(ArtifactID::VIAL_OF_DRAGON_BLOOD);
|
|
map->allowedArtifact.erase(ArtifactID::ARMAGEDDONS_BLADE);
|
|
}
|
|
|
|
// Messy, but needed
|
|
for(TriggeredEvent & event : map->triggeredEvents)
|
|
{
|
|
auto patcher = [&](EventCondition cond) -> EventExpression::Variant
|
|
{
|
|
if(cond.condition == EventCondition::HAVE_ARTIFACT || cond.condition == EventCondition::TRANSPORT)
|
|
{
|
|
map->allowedArtifact.erase(cond.objectType.as<ArtifactID>());
|
|
}
|
|
return cond;
|
|
};
|
|
|
|
event.trigger = event.trigger.morph(patcher);
|
|
}
|
|
}
|
|
|
|
void CMapLoaderH3M::readAllowedSpellsAbilities()
|
|
{
|
|
map->allowedSpells = LIBRARY->spellh->getDefaultAllowed();
|
|
map->allowedAbilities = LIBRARY->skillh->getDefaultAllowed();
|
|
|
|
if(features.levelSOD)
|
|
{
|
|
reader->readBitmaskSpells(map->allowedSpells, true);
|
|
reader->readBitmaskSkills(map->allowedAbilities, true);
|
|
}
|
|
}
|
|
|
|
void CMapLoaderH3M::readRumors()
|
|
{
|
|
size_t rumorsCount = reader->readUInt32();
|
|
assert(rumorsCount < 1000); // sanity check
|
|
|
|
for(size_t it = 0; it < rumorsCount; it++)
|
|
{
|
|
Rumor ourRumor;
|
|
ourRumor.name = readBasicString();
|
|
ourRumor.text.appendTextID(readLocalizedString(TextIdentifier("header", "rumor", it, "text")));
|
|
map->rumors.push_back(ourRumor);
|
|
}
|
|
}
|
|
|
|
void CMapLoaderH3M::readPredefinedHeroes()
|
|
{
|
|
if(!features.levelSOD)
|
|
return;
|
|
|
|
uint32_t heroesCount = features.heroesCount;
|
|
|
|
if(features.levelHOTA0)
|
|
heroesCount = reader->readUInt32();
|
|
|
|
assert(heroesCount <= features.heroesCount);
|
|
|
|
for(int heroID = 0; heroID < heroesCount; heroID++)
|
|
{
|
|
bool custom = reader->readBool();
|
|
if(!custom)
|
|
continue;
|
|
|
|
auto handler = LIBRARY->objtypeh->getHandlerFor(Obj::HERO, HeroTypeID(heroID).toHeroType()->heroClass->getIndex());
|
|
auto object = handler->create(map->cb, handler->getTemplates().front());
|
|
auto hero = std::dynamic_pointer_cast<CGHeroInstance>(object);
|
|
hero->subID = heroID;
|
|
|
|
bool hasExp = reader->readBool();
|
|
if(hasExp)
|
|
{
|
|
hero->exp = reader->readUInt32();
|
|
}
|
|
else
|
|
{
|
|
hero->exp = 0;
|
|
}
|
|
|
|
bool hasSecSkills = reader->readBool();
|
|
if(hasSecSkills)
|
|
{
|
|
uint32_t howMany = reader->readUInt32();
|
|
hero->secSkills.resize(howMany);
|
|
for(int yy = 0; yy < howMany; ++yy)
|
|
{
|
|
hero->secSkills[yy].first = reader->readSkill();
|
|
hero->secSkills[yy].second = reader->readInt8Checked(1,3);
|
|
}
|
|
}
|
|
|
|
loadArtifactsOfHero(hero.get());
|
|
|
|
bool hasCustomBio = reader->readBool();
|
|
if(hasCustomBio)
|
|
hero->biographyCustomTextId = readLocalizedString(TextIdentifier("heroes", heroID, "biography"));
|
|
|
|
// 0xFF is default, 00 male, 01 female
|
|
hero->gender = static_cast<EHeroGender>(reader->readInt8Checked(-1, 1));
|
|
assert(hero->gender == EHeroGender::MALE || hero->gender == EHeroGender::FEMALE || hero->gender == EHeroGender::DEFAULT);
|
|
|
|
bool hasCustomSpells = reader->readBool();
|
|
if(hasCustomSpells)
|
|
reader->readBitmaskSpells(hero->spells, false);
|
|
|
|
bool hasCustomPrimSkills = reader->readBool();
|
|
if(hasCustomPrimSkills)
|
|
{
|
|
for(int skillID = 0; skillID < GameConstants::PRIMARY_SKILLS; skillID++)
|
|
{
|
|
hero->pushPrimSkill(static_cast<PrimarySkill>(skillID), reader->readUInt8());
|
|
}
|
|
}
|
|
map->addToHeroPool(hero);
|
|
|
|
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)
|
|
{
|
|
bool hasArtSet = reader->readBool();
|
|
|
|
// True if artifact set is not default (hero has some artifacts)
|
|
if(!hasArtSet)
|
|
return;
|
|
|
|
// Workaround - if hero has customized artifacts game should not attempt to add spellbook based on hero type
|
|
hero->spells.insert(SpellID::SPELLBOOK_PRESET);
|
|
|
|
if(!hero->artifactsWorn.empty() || !hero->artifactsInBackpack.empty())
|
|
{
|
|
logGlobal->debug("Hero %d at %s has set artifacts twice (in map properties and on adventure map instance). Using the latter set...", hero->getHeroTypeID().getNum(), hero->anchorPos().toString());
|
|
|
|
hero->artifactsInBackpack.clear();
|
|
while(!hero->artifactsWorn.empty())
|
|
hero->removeArtifact(hero->artifactsWorn.begin()->first);
|
|
}
|
|
|
|
for(int i = 0; i < features.artifactSlotsCount; i++)
|
|
loadArtifactToSlot(hero, i);
|
|
|
|
// bag artifacts
|
|
// number of artifacts in hero's bag
|
|
size_t amount = reader->readUInt16();
|
|
for(size_t i = 0; i < amount; ++i)
|
|
{
|
|
loadArtifactToSlot(hero, ArtifactPosition::BACKPACK_START + static_cast<int>(hero->artifactsInBackpack.size()));
|
|
}
|
|
}
|
|
|
|
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;
|
|
|
|
const Artifact * art = artifactID.toEntity(LIBRARY);
|
|
|
|
if(!art)
|
|
{
|
|
logGlobal->warn("Map '%s': Invalid artifact in hero's backpack, ignoring...", mapName);
|
|
return false;
|
|
}
|
|
|
|
if(art->isBig() && slot >= ArtifactPosition::BACKPACK_START)
|
|
{
|
|
logGlobal->warn("Map '%s': A big artifact (war machine) in hero's backpack, ignoring...", mapName);
|
|
return false;
|
|
}
|
|
|
|
// H3 bug workaround - Enemy hero on 3rd scenario of Good1.h3c campaign ("Long Live The Queen")
|
|
// He has Shackles of War (normally - MISC slot artifact) in LEFT_HAND slot set in editor
|
|
// 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 = map->createArtifact(artifactID, scrollSpell);
|
|
map->putArtifactInstance(*hero, artifact->getId(), slot);
|
|
}
|
|
else
|
|
{
|
|
logGlobal->warn("Map '%s': Artifact '%s' can't be put at the slot %d", mapName, ArtifactID(artifactID).toArtifact()->getNameTranslated(), slot);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void CMapLoaderH3M::readTerrain()
|
|
{
|
|
map->initTerrain();
|
|
|
|
// Read terrain
|
|
int3 pos;
|
|
for(pos.z = 0; pos.z < map->levels(); ++pos.z)
|
|
{
|
|
//OH3 format is [z][y][x]
|
|
for(pos.y = 0; pos.y < map->height; pos.y++)
|
|
{
|
|
for(pos.x = 0; pos.x < map->width; pos.x++)
|
|
{
|
|
auto & tile = map->getTile(pos);
|
|
tile.terrainType = reader->readTerrain();
|
|
tile.terView = reader->readUInt8();
|
|
tile.riverType = reader->readRiver();
|
|
tile.riverDir = reader->readUInt8();
|
|
tile.roadType = reader->readRoad();
|
|
tile.roadDir = reader->readUInt8();
|
|
tile.extTileFlags = reader->readUInt8();
|
|
}
|
|
}
|
|
}
|
|
map->calculateWaterContent();
|
|
}
|
|
|
|
void CMapLoaderH3M::readObjectTemplates()
|
|
{
|
|
uint32_t defAmount = reader->readUInt32();
|
|
|
|
originalTemplates.reserve(defAmount);
|
|
remappedTemplates.reserve(defAmount);
|
|
|
|
// Read custom defs
|
|
for(int defID = 0; defID < defAmount; ++defID)
|
|
{
|
|
auto tmpl = reader->readObjectTemplate();
|
|
originalTemplates.push_back(tmpl);
|
|
|
|
auto remapped = std::make_shared<ObjectTemplate>(*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 );
|
|
}
|
|
}
|
|
|
|
std::shared_ptr<CGObjectInstance> CMapLoaderH3M::readEvent(const int3 & mapPosition, const ObjectInstanceID & idToBeGiven)
|
|
{
|
|
auto object = std::make_shared<CGEvent>(map->cb);
|
|
|
|
readBoxContent(object.get(), mapPosition, idToBeGiven);
|
|
|
|
reader->readBitmaskPlayers(object->availableFor, false);
|
|
object->computerActivate = reader->readBool();
|
|
object->removeAfterVisit = reader->readBool();
|
|
|
|
reader->skipZero(4);
|
|
|
|
if(features.levelHOTA3)
|
|
object->humanActivate = reader->readBool();
|
|
else
|
|
object->humanActivate = true;
|
|
|
|
readBoxHotaContent(object.get(), mapPosition, idToBeGiven);
|
|
|
|
return object;
|
|
}
|
|
|
|
std::shared_ptr<CGObjectInstance> CMapLoaderH3M::readPandora(const int3 & mapPosition, const ObjectInstanceID & idToBeGiven)
|
|
{
|
|
auto object = std::make_shared<CGPandoraBox>(map->cb);
|
|
readBoxContent(object.get(), mapPosition, idToBeGiven);
|
|
|
|
if(features.levelHOTA5)
|
|
reader->skipZero(1); // Unknown value, always 0 so far
|
|
|
|
readBoxHotaContent(object.get(), mapPosition, idToBeGiven);
|
|
|
|
return object;
|
|
}
|
|
|
|
void CMapLoaderH3M::readBoxContent(CGPandoraBox * object, const int3 & mapPosition, const ObjectInstanceID & idToBeGiven)
|
|
{
|
|
readMessageAndGuards(object->message, object, mapPosition, idToBeGiven);
|
|
Rewardable::VisitInfo vinfo;
|
|
auto & reward = vinfo.reward;
|
|
|
|
reward.heroExperience = reader->readUInt32();
|
|
reward.manaDiff = reader->readInt32();
|
|
if(auto val = reader->readInt8Checked(-3, 3))
|
|
reward.heroBonuses.push_back(std::make_shared<Bonus>(BonusDuration::ONE_BATTLE, BonusType::MORALE, BonusSource::OBJECT_INSTANCE, val, BonusSourceID(idToBeGiven)));
|
|
if(auto val = reader->readInt8Checked(-3, 3))
|
|
reward.heroBonuses.push_back(std::make_shared<Bonus>(BonusDuration::ONE_BATTLE, BonusType::LUCK, BonusSource::OBJECT_INSTANCE, val, BonusSourceID(idToBeGiven)));
|
|
|
|
reader->readResources(reward.resources);
|
|
for(int x = 0; x < GameConstants::PRIMARY_SKILLS; ++x)
|
|
reward.primary.at(x) = reader->readUInt8();
|
|
|
|
size_t gabn = reader->readUInt8(); //number of gained abilities
|
|
for(size_t oo = 0; oo < gabn; ++oo)
|
|
{
|
|
auto rId = reader->readSkill();
|
|
auto rVal = reader->readInt8Checked(1,3);
|
|
|
|
reward.secondary[rId] = rVal;
|
|
}
|
|
size_t gart = reader->readUInt8(); //number of gained artifacts
|
|
for(size_t oo = 0; oo < gart; ++oo)
|
|
{
|
|
ArtifactID grantedArtifact = reader->readArtifact();
|
|
|
|
if (features.levelHOTA5)
|
|
{
|
|
SpellID scrollSpell = reader->readSpell16();
|
|
if (grantedArtifact == ArtifactID::SPELL_SCROLL)
|
|
reward.grantedScrolls.push_back(scrollSpell);
|
|
}
|
|
else
|
|
reward.grantedArtifacts.push_back(grantedArtifact);
|
|
}
|
|
|
|
size_t gspel = reader->readUInt8(); //number of gained spells
|
|
for(size_t oo = 0; oo < gspel; ++oo)
|
|
reward.spells.push_back(reader->readSpell());
|
|
|
|
size_t gcre = reader->readUInt8(); //number of gained creatures
|
|
for(size_t oo = 0; oo < gcre; ++oo)
|
|
{
|
|
auto rId = reader->readCreature();
|
|
auto rVal = reader->readUInt16();
|
|
|
|
reward.creatures.emplace_back(rId, rVal);
|
|
}
|
|
|
|
vinfo.visitType = Rewardable::EEventType::EVENT_FIRST_VISIT;
|
|
object->configuration.info.push_back(vinfo);
|
|
|
|
reader->skipZero(8);
|
|
}
|
|
|
|
void CMapLoaderH3M::readBoxHotaContent(CGPandoraBox * object, const int3 & mapPosition, const ObjectInstanceID & idToBeGiven)
|
|
{
|
|
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': Event/Pandora %s option to modify (mode %d) movement points by %d is not implemented!", mapName, mapPosition.toString(), movementMode, movementAmount);
|
|
}
|
|
|
|
if(features.levelHOTA6)
|
|
{
|
|
int32_t allowedDifficultiesMask = reader->readInt32();
|
|
assert(allowedDifficultiesMask > 0 && allowedDifficultiesMask < 32);
|
|
if (allowedDifficultiesMask != 31)
|
|
logGlobal->warn("Map '%s': Event/Pandora %s availability by difficulty (mode %d) is not implemented!", mapName, mapPosition.toString(), allowedDifficultiesMask);
|
|
}
|
|
}
|
|
|
|
std::shared_ptr<CGObjectInstance> CMapLoaderH3M::readMonster(const int3 & mapPosition, const ObjectInstanceID & objectInstanceID)
|
|
{
|
|
auto object = std::make_shared<CGCreature>(map->cb);
|
|
object->id = objectInstanceID;
|
|
|
|
if(features.levelAB)
|
|
{
|
|
object->identifier = reader->readUInt32();
|
|
questIdentifierToId[object->identifier] = objectInstanceID;
|
|
}
|
|
|
|
auto hlp = std::make_unique<CStackInstance>(map->cb);
|
|
hlp->setCount(reader->readUInt16());
|
|
|
|
//type will be set during initialization
|
|
object->putStack(SlotID(0), std::move(hlp));
|
|
|
|
//TODO: 0-4 is h3 range. 5 is hota extension for exact aggression?
|
|
object->character = reader->readInt8Checked(0, 5);
|
|
|
|
bool hasMessage = reader->readBool();
|
|
if(hasMessage)
|
|
{
|
|
object->message.appendTextID(readLocalizedString(TextIdentifier("monster", mapPosition.x, mapPosition.y, mapPosition.z, "message")));
|
|
reader->readResources(object->resources);
|
|
object->gainedArtifact = reader->readArtifact();
|
|
}
|
|
object->neverFlees = reader->readBool();
|
|
object->notGrowingTeam = reader->readBool();
|
|
reader->skipZero(2);
|
|
|
|
if(features.levelHOTA3)
|
|
{
|
|
//TODO: HotA
|
|
int32_t aggressionExact = reader->readInt32(); // -1 = default, 1-10 = possible values range
|
|
bool joinOnlyForMoney = reader->readBool(); // if true, monsters will only join for money
|
|
int32_t joinPercent = reader->readInt32(); // 100 = default, percent of monsters that will join on successful aggression check
|
|
int32_t upgradedStack = reader->readInt32(); // Presence of upgraded stack, -1 = random, 0 = never, 1 = always
|
|
int32_t stacksCount = reader->readInt32(); // TODO: check possible values. How many creature stacks will be present on battlefield, -1 = default
|
|
|
|
if(aggressionExact != -1 || joinOnlyForMoney || joinPercent != 100 || upgradedStack != -1 || stacksCount != -1)
|
|
logGlobal->warn(
|
|
"Map '%s': Wandering monsters %s settings %d %d %d %d %d are not implemented!",
|
|
mapName,
|
|
mapPosition.toString(),
|
|
aggressionExact,
|
|
int(joinOnlyForMoney),
|
|
joinPercent,
|
|
upgradedStack,
|
|
stacksCount
|
|
);
|
|
}
|
|
|
|
if (features.levelHOTA5)
|
|
{
|
|
bool sizeByValue = reader->readBool();//FIXME: double-check this flag effect
|
|
int32_t targetValue = reader->readInt32();
|
|
|
|
if (sizeByValue || targetValue)
|
|
logGlobal->warn( "Map '%s': Wandering monsters %s option to set unit size to %d (%d) AI value is not implemented!", mapName, mapPosition.toString(), targetValue, sizeByValue);
|
|
}
|
|
|
|
return object;
|
|
}
|
|
|
|
std::shared_ptr<CGObjectInstance> CMapLoaderH3M::readSign(const int3 & mapPosition)
|
|
{
|
|
auto object = std::make_shared<CGSignBottle>(map->cb);
|
|
object->message.appendTextID(readLocalizedString(TextIdentifier("sign", mapPosition.x, mapPosition.y, mapPosition.z, "message")));
|
|
reader->skipZero(4);
|
|
return object;
|
|
}
|
|
|
|
std::shared_ptr<CGObjectInstance> CMapLoaderH3M::readWitchHut(const int3 & position, std::shared_ptr<const ObjectTemplate> objectTemplate)
|
|
{
|
|
auto object = readGeneric(position, objectTemplate);
|
|
auto rewardable = std::dynamic_pointer_cast<CRewardableObject>(object);
|
|
|
|
// AB and later maps have allowed abilities defined in H3M
|
|
if(features.levelAB)
|
|
{
|
|
std::set<SecondarySkill> allowedAbilities;
|
|
reader->readBitmaskSkills(allowedAbilities, false);
|
|
|
|
if (rewardable)
|
|
{
|
|
if(allowedAbilities.size() != 1)
|
|
{
|
|
auto defaultAllowed = LIBRARY->skillh->getDefaultAllowed();
|
|
|
|
for(int skillID = features.skillsCount; skillID < defaultAllowed.size(); ++skillID)
|
|
if(defaultAllowed.count(skillID))
|
|
allowedAbilities.insert(SecondarySkill(skillID));
|
|
}
|
|
|
|
JsonNode variable;
|
|
if (allowedAbilities.size() == 1)
|
|
{
|
|
variable.String() = LIBRARY->skills()->getById(*allowedAbilities.begin())->getJsonKey();
|
|
}
|
|
else
|
|
{
|
|
JsonVector anyOfList;
|
|
for (auto const & skill : allowedAbilities)
|
|
{
|
|
JsonNode entry;
|
|
entry.String() = LIBRARY->skills()->getById(skill)->getJsonKey();
|
|
anyOfList.push_back(entry);
|
|
}
|
|
variable["anyOf"].Vector() = anyOfList;
|
|
}
|
|
|
|
variable.setModScope(ModScope::scopeGame()); // list may include skills from all mods
|
|
rewardable->configuration.presetVariable("secondarySkill", "gainedSkill", variable);
|
|
}
|
|
else
|
|
{
|
|
logGlobal->warn("Failed to set allowed secondary skills to a Witch Hut! Object is not rewardable!");
|
|
}
|
|
}
|
|
return object;
|
|
}
|
|
|
|
std::shared_ptr<CGObjectInstance> CMapLoaderH3M::readScholar(const int3 & position, std::shared_ptr<const ObjectTemplate> objectTemplate)
|
|
{
|
|
enum class ScholarBonusType : int8_t {
|
|
RANDOM = -1,
|
|
PRIM_SKILL = 0,
|
|
SECONDARY_SKILL = 1,
|
|
SPELL = 2,
|
|
};
|
|
|
|
auto object = readGeneric(position, objectTemplate);
|
|
auto rewardable = std::dynamic_pointer_cast<CRewardableObject>(object);
|
|
|
|
uint8_t bonusTypeRaw = reader->readInt8Checked(-1, 2);
|
|
auto bonusType = static_cast<ScholarBonusType>(bonusTypeRaw);
|
|
auto bonusID = reader->readUInt8();
|
|
|
|
if (rewardable)
|
|
{
|
|
switch (bonusType)
|
|
{
|
|
case ScholarBonusType::PRIM_SKILL:
|
|
{
|
|
JsonNode variable;
|
|
JsonNode dice;
|
|
variable.String() = NPrimarySkill::names[bonusID];
|
|
variable.setModScope(ModScope::scopeGame());
|
|
dice.Integer() = 80;
|
|
rewardable->configuration.presetVariable("primarySkill", "gainedStat", variable);
|
|
rewardable->configuration.presetVariable("dice", "0", dice);
|
|
break;
|
|
}
|
|
case ScholarBonusType::SECONDARY_SKILL:
|
|
{
|
|
JsonNode variable;
|
|
JsonNode dice;
|
|
variable.String() = LIBRARY->skills()->getByIndex(bonusID)->getJsonKey();
|
|
variable.setModScope(ModScope::scopeGame());
|
|
dice.Integer() = 50;
|
|
rewardable->configuration.presetVariable("secondarySkill", "gainedSkill", variable);
|
|
rewardable->configuration.presetVariable("dice", "0", dice);
|
|
break;
|
|
}
|
|
case ScholarBonusType::SPELL:
|
|
{
|
|
JsonNode variable;
|
|
JsonNode dice;
|
|
variable.String() = LIBRARY->spells()->getByIndex(bonusID)->getJsonKey();
|
|
variable.setModScope(ModScope::scopeGame());
|
|
dice.Integer() = 20;
|
|
rewardable->configuration.presetVariable("spell", "gainedSpell", variable);
|
|
rewardable->configuration.presetVariable("dice", "0", dice);
|
|
break;
|
|
}
|
|
case ScholarBonusType::RANDOM:
|
|
break;// No-op
|
|
default:
|
|
logGlobal->warn("Map '%s': Invalid Scholar settings! Ignoring...", mapName);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
logGlobal->warn("Failed to set reward parameters for a Scholar! Object is not rewardable!");
|
|
}
|
|
|
|
reader->skipZero(6);
|
|
return object;
|
|
}
|
|
|
|
std::shared_ptr<CGObjectInstance> CMapLoaderH3M::readGarrison(const int3 & mapPosition, const ObjectInstanceID & idToBeGiven)
|
|
{
|
|
auto object = std::make_shared<CGGarrison>(map->cb);
|
|
|
|
setOwnerAndValidate(mapPosition, object.get(), reader->readPlayer32());
|
|
readCreatureSet(object.get(), idToBeGiven);
|
|
if(features.levelAB)
|
|
object->removableUnits = reader->readBool();
|
|
else
|
|
object->removableUnits = true;
|
|
|
|
reader->skipZero(8);
|
|
return object;
|
|
}
|
|
|
|
std::shared_ptr<CGObjectInstance> CMapLoaderH3M::readArtifact(const int3 & mapPosition, std::shared_ptr<const ObjectTemplate> objectTemplate, const ObjectInstanceID & idToBeGiven)
|
|
{
|
|
ArtifactID artID = ArtifactID::NONE; //random, set later
|
|
auto object = std::make_shared<CGArtifact>(map->cb);
|
|
|
|
readMessageAndGuards(object->message, object.get(), mapPosition, idToBeGiven);
|
|
|
|
//specific artifact
|
|
if(objectTemplate->id == Obj::ARTIFACT)
|
|
artID = ArtifactID(objectTemplate->subid);
|
|
|
|
if(features.levelHOTA5)
|
|
{
|
|
uint32_t pickupMode = reader->readUInt32();
|
|
uint8_t pickupFlags = reader->readUInt8();
|
|
|
|
assert(pickupMode == 0 || pickupMode == 1 || pickupMode == 2); // DISABLED, RANDOM, CUSTOM
|
|
|
|
if (pickupMode != 0)
|
|
logGlobal->warn("Map '%s': Artifact %s: not implemented pickup mode %d (flags: %d)", mapName, mapPosition.toString(), pickupMode, static_cast<int>(pickupFlags));
|
|
}
|
|
|
|
if (artID.hasValue())
|
|
object->setArtifactInstance(map->createArtifact(artID, SpellID::NONE));
|
|
// else - random, will be initialized later
|
|
return object;
|
|
}
|
|
|
|
std::shared_ptr<CGObjectInstance> CMapLoaderH3M::readScroll(const int3 & mapPosition, std::shared_ptr<const ObjectTemplate> objectTemplate, const ObjectInstanceID & idToBeGiven)
|
|
{
|
|
auto object = std::make_shared<CGArtifact>(map->cb);
|
|
readMessageAndGuards(object->message, object.get(), mapPosition, idToBeGiven);
|
|
SpellID spellID = reader->readSpell32();
|
|
|
|
object->setArtifactInstance(map->createArtifact(ArtifactID::SPELL_SCROLL, spellID.getNum()));
|
|
return object;
|
|
}
|
|
|
|
std::shared_ptr<CGObjectInstance> CMapLoaderH3M::readResource(const int3 & mapPosition, std::shared_ptr<const ObjectTemplate> objectTemplate, const ObjectInstanceID & idToBeGiven)
|
|
{
|
|
auto object = std::make_shared<CGResource>(map->cb);
|
|
|
|
readMessageAndGuards(object->message, object.get(), mapPosition, idToBeGiven);
|
|
|
|
object->amount = reader->readUInt32();
|
|
|
|
if (objectTemplate->id != Obj::RANDOM_RESOURCE)
|
|
{
|
|
const auto & baseHandler = LIBRARY->objtypeh->getHandlerFor(objectTemplate->id, objectTemplate->subid);
|
|
const auto & ourHandler = std::dynamic_pointer_cast<ResourceInstanceConstructor>(baseHandler);
|
|
|
|
object->amount *= ourHandler->getAmountMultiplier();
|
|
}
|
|
|
|
reader->skipZero(4);
|
|
return object;
|
|
}
|
|
|
|
std::shared_ptr<CGObjectInstance> CMapLoaderH3M::readMine(const int3 & mapPosition)
|
|
{
|
|
auto object = std::make_shared<CGMine>(map->cb);
|
|
setOwnerAndValidate(mapPosition, object.get(), reader->readPlayer32());
|
|
return object;
|
|
}
|
|
|
|
std::shared_ptr<CGObjectInstance> CMapLoaderH3M::readAbandonedMine(const int3 & mapPosition)
|
|
{
|
|
auto object = std::make_shared<CGMine>(map->cb);
|
|
object->setOwner(PlayerColor::NEUTRAL);
|
|
reader->readBitmaskResources(object->abandonedMineResources, false);
|
|
|
|
if(features.levelHOTA5)
|
|
{
|
|
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->warn("Map '%s': Abandoned Mine %s: not implemented guards of %d-%d %s", mapName, mapPosition.toString(), guardsMin, guardsMax, guardsUnit.toEntity(LIBRARY)->getJsonKey());
|
|
}
|
|
}
|
|
return object;
|
|
}
|
|
|
|
std::shared_ptr<CGObjectInstance> CMapLoaderH3M::readDwelling(const int3 & position)
|
|
{
|
|
auto object = std::make_shared<CGDwelling>(map->cb);
|
|
setOwnerAndValidate(position, object.get(), reader->readPlayer32());
|
|
return object;
|
|
}
|
|
|
|
std::shared_ptr<CGObjectInstance> CMapLoaderH3M::readDwellingRandom(const int3 & mapPosition, std::shared_ptr<const ObjectTemplate> objectTemplate)
|
|
{
|
|
auto object = std::make_shared<CGDwelling>(map->cb);
|
|
|
|
setOwnerAndValidate(mapPosition, object.get(), reader->readPlayer32());
|
|
|
|
object->randomizationInfo = CGDwellingRandomizationInfo();
|
|
|
|
bool hasFactionInfo = objectTemplate->id == Obj::RANDOM_DWELLING || objectTemplate->id == Obj::RANDOM_DWELLING_LVL;
|
|
bool hasLevelInfo = objectTemplate->id == Obj::RANDOM_DWELLING || objectTemplate->id == Obj::RANDOM_DWELLING_FACTION;
|
|
|
|
if (hasFactionInfo)
|
|
{
|
|
object->randomizationInfo->identifier = reader->readUInt32();
|
|
|
|
if(object->randomizationInfo->identifier == 0)
|
|
reader->readBitmaskFactions(object->randomizationInfo->allowedFactions, false);
|
|
}
|
|
else
|
|
object->randomizationInfo->allowedFactions.insert(FactionID(objectTemplate->subid));
|
|
|
|
if(hasLevelInfo)
|
|
{
|
|
object->randomizationInfo->minLevel = std::max(reader->readUInt8(), static_cast<ui8>(0)) + 1;
|
|
object->randomizationInfo->maxLevel = std::min(reader->readUInt8(), static_cast<ui8>(6)) + 1;
|
|
}
|
|
else
|
|
{
|
|
object->randomizationInfo->minLevel = objectTemplate->subid;
|
|
object->randomizationInfo->maxLevel = objectTemplate->subid;
|
|
}
|
|
|
|
return object;
|
|
}
|
|
|
|
std::shared_ptr<CGObjectInstance> CMapLoaderH3M::readShrine(const int3 & position, std::shared_ptr<const ObjectTemplate> objectTemplate)
|
|
{
|
|
auto object = readGeneric(position, objectTemplate);
|
|
auto rewardable = std::dynamic_pointer_cast<CRewardableObject>(object);
|
|
|
|
SpellID spell = reader->readSpell32();
|
|
|
|
if (rewardable)
|
|
{
|
|
if(spell != SpellID::NONE)
|
|
{
|
|
JsonNode variable;
|
|
variable.String() = LIBRARY->spells()->getById(spell)->getJsonKey();
|
|
variable.setModScope(ModScope::scopeGame()); // list may include spells from all mods
|
|
rewardable->configuration.presetVariable("spell", "gainedSpell", variable);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
logGlobal->warn("Failed to set selected spell to a Shrine!. Object is not rewardable!");
|
|
}
|
|
return object;
|
|
}
|
|
|
|
std::shared_ptr<CGObjectInstance> CMapLoaderH3M::readHeroPlaceholder(const int3 & mapPosition)
|
|
{
|
|
auto object = std::make_shared<CGHeroPlaceholder>(map->cb);
|
|
|
|
setOwnerAndValidate(mapPosition, object.get(), reader->readPlayer());
|
|
|
|
HeroTypeID htid = reader->readHero(); //hero type id
|
|
|
|
if(htid.getNum() == -1)
|
|
{
|
|
object->powerRank = reader->readUInt8();
|
|
logGlobal->debug("Map '%s': Hero placeholder: by power at %s, owned by %s", mapName, mapPosition.toString(), object->getOwner().toString());
|
|
}
|
|
else
|
|
{
|
|
object->heroType = htid;
|
|
logGlobal->debug("Map '%s': Hero placeholder: %s at %s, owned by %s", mapName, LIBRARY->heroh->getById(htid)->getJsonKey(), mapPosition.toString(), object->getOwner().toString());
|
|
}
|
|
|
|
if(features.levelHOTA5)
|
|
{
|
|
bool customizedStatingUnits = reader->readBool();
|
|
|
|
if (customizedStatingUnits)
|
|
logGlobal->warn("Map '%s': Hero placeholder: not implemented option to customize starting units", mapName);
|
|
|
|
for (int i = 0; i < 7; ++i)
|
|
{
|
|
int32_t unitAmount = reader->readInt32();
|
|
CreatureID unitToGive = reader->readCreature32();
|
|
|
|
if (unitToGive.hasValue())
|
|
logGlobal->warn("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.toEntity(LIBRARY)->getJsonKey(), 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)
|
|
{
|
|
// NOTE: this might actually be 2 bytes for artifact ID + 2 bytes for spell scroll
|
|
ArtifactID startingArtifact = reader->readArtifact32();
|
|
logGlobal->warn("Map '%s': Hero placeholder: not implemented option to give hero artifact %d", mapName, startingArtifact.toEntity(LIBRARY)->getJsonKey());
|
|
}
|
|
}
|
|
|
|
return object;
|
|
}
|
|
|
|
std::shared_ptr<CGObjectInstance> CMapLoaderH3M::readGrail(const int3 & mapPosition)
|
|
{
|
|
map->grailPos = mapPosition;
|
|
map->grailRadius = reader->readInt32();
|
|
return nullptr;
|
|
}
|
|
|
|
std::shared_ptr<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;
|
|
}
|
|
|
|
std::shared_ptr<CGObjectInstance> CMapLoaderH3M::readGeneric(const int3 & mapPosition, std::shared_ptr<const ObjectTemplate> objectTemplate)
|
|
{
|
|
if(LIBRARY->objtypeh->knownSubObjects(objectTemplate->id).count(objectTemplate->subid))
|
|
return LIBRARY->objtypeh->getHandlerFor(objectTemplate->id, objectTemplate->subid)->create(map->cb, objectTemplate);
|
|
|
|
logGlobal->warn("Map '%s': Unrecognized object %d:%d ('%s') at %s found!", mapName, objectTemplate->id.toEnum(), objectTemplate->subid, objectTemplate->animationFile.getOriginalName(), mapPosition.toString());
|
|
return std::make_shared<CGObjectInstance>(map->cb);
|
|
}
|
|
|
|
std::shared_ptr<CGObjectInstance> CMapLoaderH3M::readQuestGuard(const int3 & mapPosition)
|
|
{
|
|
auto guard = std::make_shared<CGQuestGuard>(map->cb);
|
|
readQuest(guard.get(), mapPosition);
|
|
return guard;
|
|
}
|
|
|
|
std::shared_ptr<CGObjectInstance> CMapLoaderH3M::readShipyard(const int3 & mapPosition, std::shared_ptr<const ObjectTemplate> objectTemplate)
|
|
{
|
|
auto object = readGeneric(mapPosition, objectTemplate);
|
|
setOwnerAndValidate(mapPosition, object.get(), reader->readPlayer32());
|
|
return object;
|
|
}
|
|
|
|
std::shared_ptr<CGObjectInstance> CMapLoaderH3M::readLighthouse(const int3 & mapPosition, std::shared_ptr<const ObjectTemplate> objectTemplate)
|
|
{
|
|
auto object = readGeneric(mapPosition, objectTemplate);
|
|
setOwnerAndValidate(mapPosition, object.get(), reader->readPlayer32());
|
|
return object;
|
|
}
|
|
|
|
std::shared_ptr<CGObjectInstance> CMapLoaderH3M::readBank(const int3 & mapPosition, std::shared_ptr<const ObjectTemplate> objectTemplate)
|
|
{
|
|
if(features.levelHOTA3)
|
|
{
|
|
//TODO: HotA
|
|
// index of guards preset. -1 = random, 0-4 = index of possible guards settings
|
|
int32_t guardsPresetIndex = reader->readInt32();
|
|
|
|
// presence of upgraded stack: -1 = random, 0 = never, 1 = always
|
|
int8_t upgradedStackPresence = reader->readInt8Checked(-1, 1);
|
|
|
|
assert(vstd::iswithin(guardsPresetIndex, -1, 4));
|
|
assert(vstd::iswithin(upgradedStackPresence, -1, 1));
|
|
|
|
// list of possible artifacts in reward
|
|
// - if list is empty, artifacts are either not present in reward or random
|
|
// - if non-empty, then list always have same number of elements as number of artifacts in bank
|
|
// - ArtifactID::NONE indictates random artifact, other values indicate artifact that should be used as reward
|
|
std::vector<ArtifactID> artifacts;
|
|
int artNumber = reader->readUInt32();
|
|
for(int yy = 0; yy < artNumber; ++yy)
|
|
{
|
|
artifacts.push_back(reader->readArtifact32());
|
|
}
|
|
|
|
if(guardsPresetIndex != -1 || upgradedStackPresence != -1 || !artifacts.empty())
|
|
logGlobal->warn(
|
|
"Map '%s': creature bank at %s settings %d %d %d are not implemented!",
|
|
mapName,
|
|
mapPosition.toString(),
|
|
guardsPresetIndex,
|
|
int(upgradedStackPresence),
|
|
artifacts.size()
|
|
);
|
|
}
|
|
|
|
return readGeneric(mapPosition, objectTemplate);
|
|
}
|
|
|
|
std::shared_ptr<CGObjectInstance> CMapLoaderH3M::readRewardWithArtifact(const int3 & mapPosition, std::shared_ptr<const ObjectTemplate> objectTemplate)
|
|
{
|
|
if(features.levelHOTA5)
|
|
{
|
|
// TREASURE_CHEST - rewards index, if 3 - then 2nd value is artifact
|
|
// CORPSE - rewards index, if 1 - then 2nd value is artifact
|
|
// SHIPWRECK_SURVIVOR - if content = 0 then 2nd value is granted artifact
|
|
// SEA_CHEST - rewards index, if 2 - then 2nd value is artifact
|
|
// FLOTSAM - rewards index (0-3) + -1
|
|
// TREE_OF_KNOWLEDGE - rewards index (0-2) + -1
|
|
// PYRAMID - if content = 0 then 2nd value is granted spell
|
|
// WARRIORS_TOMB - if content = 0 then 2nd value is granted artifact
|
|
// HOTA_JETSAM - rewards index (0-3) + -1
|
|
// HOTA_VIAL_OF_MANA - rewards index (0-3) + -1
|
|
|
|
int32_t content = reader->readInt32();
|
|
ArtifactID artifact;
|
|
if (content != -1)
|
|
{
|
|
artifact = reader->readArtifact32(); // NOTE: might be 2 byte artifact + 2 bytes scroll spell
|
|
logGlobal->warn("Map '%s': Object (%d) %s settings %d %d are not implemented!", mapName, objectTemplate->id, mapPosition.toString(), content, artifact);
|
|
}
|
|
else
|
|
reader->skipUnused(4); // garbage data, usually -1, but sometimes uninitialized
|
|
}
|
|
return readGeneric(mapPosition, objectTemplate);
|
|
}
|
|
|
|
std::shared_ptr<CGObjectInstance> CMapLoaderH3M::readBlackMarket(const int3 & mapPosition, std::shared_ptr<const ObjectTemplate> 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->warn("Map '%s': Black Market at %s: option to sell artifact %s is not implemented", mapName, mapPosition.toString(), artifact.toEntity(LIBRARY)->getJsonKey());
|
|
else
|
|
logGlobal->warn("Map '%s': Black Market at %s: option to sell scroll %s is not implemented", mapName, mapPosition.toString(), spellID.toEntity(LIBRARY)->getJsonKey());
|
|
}
|
|
}
|
|
}
|
|
return readGeneric(mapPosition, objectTemplate);
|
|
}
|
|
|
|
std::shared_ptr<CGObjectInstance> CMapLoaderH3M::readUniversity(const int3 & mapPosition, std::shared_ptr<const ObjectTemplate> objectTemplate)
|
|
{
|
|
if(features.levelHOTA5)
|
|
{
|
|
int32_t customized = reader->readInt32();
|
|
|
|
std::set<SecondarySkill> allowedSkills;
|
|
reader->readBitmaskSkills(allowedSkills, false);
|
|
|
|
// NOTE: check how this interacts with hota Seafaring Academy that is guaranteed to give Navigation
|
|
assert(customized == -1 || customized == 0);
|
|
if (customized != -1)
|
|
logGlobal->warn("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);
|
|
}
|
|
|
|
std::shared_ptr<CGObjectInstance> CMapLoaderH3M::readRewardWithArtifactAndResources(const int3 & mapPosition, std::shared_ptr<const ObjectTemplate> objectTemplate)
|
|
{
|
|
if(features.levelHOTA5)
|
|
{
|
|
// Sea Barrel / 0 -> aID = -1, aA = resource amount, rA = resource type, aB = garbage, rB = 0
|
|
// Sea Barrel / 1 -> aID = -1, aA = 1, rA = 1, aB = garbage, rB = 0
|
|
// Ancient Lamp / 0 -> aID = -1, aA = amount to recruit, rA = 0, aB = 1, rB = 0
|
|
// Grave / 0 -> aID = artifact to give, aA = resource amount, rA = resource type, aB = 1, rB = garbage
|
|
// CAMPFIRE 12 / 0 -> aID = -1, aA = gold amount, rA = gold type, aB = resource amount, rB = resource type
|
|
// WAGON 105 / 0 -> aID = -1 or artifact, aA = resource amount, rA = resource type, aB = 1, rB = garbage?
|
|
// WAGON 105 / 1 -> empty / garbage?
|
|
// LEAN_TO / 39 / 0 -> aID = -1, aA = resource amount, rA = resource type, aB = garbage, rB = 0
|
|
|
|
int32_t content = reader->readInt32();
|
|
int32_t artifact = reader->readInt32();
|
|
int32_t amountA = reader->readInt32();
|
|
int8_t resourceA = reader->readInt8();
|
|
int32_t amountB = reader->readInt32();
|
|
int8_t resourceB = reader->readInt8();
|
|
|
|
if (content != -1)
|
|
logGlobal->warn("Map '%s': Object (%d) %s settings %d %d %d %d %d %d are not implemented!", mapName, objectTemplate->id, mapPosition.toString(), content, artifact, amountA, static_cast<int>(resourceA), amountB, static_cast<int>(resourceB));
|
|
}
|
|
return readGeneric(mapPosition, objectTemplate);
|
|
}
|
|
|
|
std::shared_ptr<CGObjectInstance> CMapLoaderH3M::readObject(MapObjectID id, MapObjectSubID subid, std::shared_ptr<const ObjectTemplate> objectTemplate, const int3 & mapPosition, const ObjectInstanceID & objectInstanceID)
|
|
{
|
|
switch(id.toEnum())
|
|
{
|
|
case Obj::EVENT:
|
|
return readEvent(mapPosition, objectInstanceID);
|
|
|
|
case Obj::HERO:
|
|
case Obj::RANDOM_HERO:
|
|
case Obj::PRISON:
|
|
return readHero(mapPosition, objectInstanceID);
|
|
|
|
case Obj::MONSTER:
|
|
case Obj::RANDOM_MONSTER:
|
|
case Obj::RANDOM_MONSTER_L1:
|
|
case Obj::RANDOM_MONSTER_L2:
|
|
case Obj::RANDOM_MONSTER_L3:
|
|
case Obj::RANDOM_MONSTER_L4:
|
|
case Obj::RANDOM_MONSTER_L5:
|
|
case Obj::RANDOM_MONSTER_L6:
|
|
case Obj::RANDOM_MONSTER_L7:
|
|
return readMonster(mapPosition, objectInstanceID);
|
|
|
|
case Obj::OCEAN_BOTTLE:
|
|
case Obj::SIGN:
|
|
return readSign(mapPosition);
|
|
|
|
case Obj::SEER_HUT:
|
|
return readSeerHut(mapPosition, objectInstanceID);
|
|
|
|
case Obj::WITCH_HUT:
|
|
return readWitchHut(mapPosition, objectTemplate);
|
|
case Obj::SCHOLAR:
|
|
return readScholar(mapPosition, objectTemplate);
|
|
|
|
case Obj::GARRISON:
|
|
case Obj::GARRISON2:
|
|
return readGarrison(mapPosition, objectInstanceID);
|
|
|
|
case Obj::ARTIFACT:
|
|
case Obj::RANDOM_ART:
|
|
case Obj::RANDOM_TREASURE_ART:
|
|
case Obj::RANDOM_MINOR_ART:
|
|
case Obj::RANDOM_MAJOR_ART:
|
|
case Obj::RANDOM_RELIC_ART:
|
|
return readArtifact(mapPosition, objectTemplate, objectInstanceID);
|
|
case Obj::SPELL_SCROLL:
|
|
return readScroll(mapPosition, objectTemplate, objectInstanceID);
|
|
|
|
case Obj::RANDOM_RESOURCE:
|
|
case Obj::RESOURCE:
|
|
return readResource(mapPosition, objectTemplate, objectInstanceID);
|
|
case Obj::RANDOM_TOWN:
|
|
case Obj::TOWN:
|
|
return readTown(mapPosition, objectTemplate, objectInstanceID);
|
|
|
|
case Obj::MINE:
|
|
case Obj::ABANDONED_MINE:
|
|
if (subid < 7)
|
|
return readMine(mapPosition);
|
|
else
|
|
return readAbandonedMine(mapPosition);
|
|
|
|
case Obj::CREATURE_GENERATOR1:
|
|
case Obj::CREATURE_GENERATOR2:
|
|
case Obj::CREATURE_GENERATOR3:
|
|
case Obj::CREATURE_GENERATOR4:
|
|
return readDwelling(mapPosition);
|
|
|
|
case Obj::SHRINE_OF_MAGIC_INCANTATION:
|
|
case Obj::SHRINE_OF_MAGIC_GESTURE:
|
|
case Obj::SHRINE_OF_MAGIC_THOUGHT:
|
|
return readShrine(mapPosition, objectTemplate);
|
|
|
|
case Obj::PANDORAS_BOX:
|
|
return readPandora(mapPosition, objectInstanceID);
|
|
|
|
case Obj::GRAIL:
|
|
if (subid < 1000)
|
|
return readGrail(mapPosition);
|
|
else
|
|
return readHotaBattleLocation(mapPosition);
|
|
|
|
case Obj::RANDOM_DWELLING:
|
|
case Obj::RANDOM_DWELLING_LVL:
|
|
case Obj::RANDOM_DWELLING_FACTION:
|
|
return readDwellingRandom(mapPosition, objectTemplate);
|
|
|
|
case Obj::QUEST_GUARD:
|
|
return readQuestGuard(mapPosition);
|
|
|
|
case Obj::SHIPYARD:
|
|
return readShipyard(mapPosition, objectTemplate);
|
|
|
|
case Obj::HERO_PLACEHOLDER:
|
|
return readHeroPlaceholder(mapPosition);
|
|
|
|
case Obj::LIGHTHOUSE:
|
|
return readLighthouse(mapPosition, objectTemplate);
|
|
|
|
case Obj::CREATURE_BANK:
|
|
case Obj::DERELICT_SHIP:
|
|
case Obj::DRAGON_UTOPIA:
|
|
case Obj::CRYPT:
|
|
case Obj::SHIPWRECK:
|
|
return readBank(mapPosition, objectTemplate);
|
|
|
|
case Obj::TREASURE_CHEST:
|
|
case Obj::CORPSE:
|
|
case Obj::SHIPWRECK_SURVIVOR:
|
|
case Obj::SEA_CHEST:
|
|
case Obj::FLOTSAM:
|
|
case Obj::TREE_OF_KNOWLEDGE:
|
|
case Obj::PYRAMID:
|
|
case Obj::WARRIORS_TOMB:
|
|
return readRewardWithArtifact(mapPosition, objectTemplate);
|
|
|
|
case Obj::CAMPFIRE:
|
|
case Obj::WAGON:
|
|
case Obj::LEAN_TO:
|
|
return readRewardWithArtifactAndResources(mapPosition, objectTemplate);
|
|
|
|
case Obj::BORDER_GATE:
|
|
if (subid == 1000) // HotA hacks - Quest Gate
|
|
return readQuestGuard(mapPosition);
|
|
if (subid == 1001) // HotA hacks - Grave
|
|
return readRewardWithArtifactAndResources(mapPosition, objectTemplate);
|
|
return readGeneric(mapPosition, objectTemplate);
|
|
|
|
case Obj::HOTA_CUSTOM_OBJECT_1:
|
|
// 0 -> Ancient Lamp
|
|
// 1 -> Sea Barrel
|
|
// 2 -> Jetsam
|
|
// 3 -> Vial of Mana
|
|
if (subid == 0 || subid == 1)
|
|
return readRewardWithArtifactAndResources(mapPosition, objectTemplate);
|
|
else
|
|
return readRewardWithArtifact(mapPosition, objectTemplate);
|
|
|
|
case Obj::HOTA_CUSTOM_OBJECT_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);
|
|
}
|
|
}
|
|
|
|
void CMapLoaderH3M::readObjects()
|
|
{
|
|
uint32_t objectsCount = reader->readUInt32();
|
|
|
|
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();
|
|
|
|
std::shared_ptr<ObjectTemplate> originalTemplate = originalTemplates.at(defIndex);
|
|
std::shared_ptr<ObjectTemplate> remappedTemplate = remappedTemplates.at(defIndex);
|
|
auto originalID = originalTemplate->id;
|
|
auto originalSubID = originalTemplate->subid;
|
|
reader->skipZero(5);
|
|
|
|
ObjectInstanceID newObjectID = map->allocateUniqueInstanceID();
|
|
auto newObject = readObject(originalID, originalSubID, remappedTemplate, mapPosition, newObjectID);
|
|
|
|
if(!newObject)
|
|
continue;
|
|
|
|
newObject->setAnchorPos(mapPosition);
|
|
newObject->ID = remappedTemplate->id;
|
|
newObject->id = newObjectID;
|
|
if(newObject->ID != Obj::HERO && newObject->ID != Obj::HERO_PLACEHOLDER && newObject->ID != Obj::PRISON)
|
|
{
|
|
newObject->subID = remappedTemplate->subid;
|
|
}
|
|
newObject->appearance = remappedTemplate;
|
|
|
|
if (newObject->isVisitable() && !map->isInTheMap(newObject->visitablePos()))
|
|
logGlobal->error("Map '%s': Object at %s - outside of map borders!", mapName, mapPosition.toString());
|
|
|
|
map->generateUniqueInstanceName(newObject.get());
|
|
map->addNewObject(newObject);
|
|
}
|
|
}
|
|
|
|
void CMapLoaderH3M::readCreatureSet(CArmedInstance * out, const ObjectInstanceID & idToBeGiven)
|
|
{
|
|
constexpr int unitsToRead = 7;
|
|
out->id = idToBeGiven;
|
|
|
|
for(int index = 0; index < unitsToRead; ++index)
|
|
{
|
|
CreatureID creatureID = reader->readCreature();
|
|
int count = reader->readUInt16();
|
|
|
|
// Empty slot
|
|
if(creatureID == CreatureID::NONE)
|
|
continue;
|
|
|
|
auto result = std::make_unique<CStackInstance>(map->cb);
|
|
result->setCount(count);
|
|
|
|
if(creatureID < CreatureID::NONE)
|
|
{
|
|
int value = -creatureID.getNum() - 2;
|
|
assert(value >= 0 && value < 14);
|
|
uint8_t level = value / 2;
|
|
uint8_t upgrade = value % 2;
|
|
|
|
//this will happen when random object has random army
|
|
result->randomStack = CStackInstance::RandomStackInfo{level, upgrade};
|
|
}
|
|
else
|
|
{
|
|
result->setType(creatureID);
|
|
}
|
|
|
|
out->putStack(SlotID(index), std::move(result));
|
|
}
|
|
|
|
out->validTypes(true);
|
|
}
|
|
|
|
void CMapLoaderH3M::setOwnerAndValidate(const int3 & mapPosition, CGObjectInstance * object, const PlayerColor & owner)
|
|
{
|
|
assert(owner.isValidPlayer() || owner == PlayerColor::NEUTRAL);
|
|
|
|
if(owner == PlayerColor::NEUTRAL)
|
|
{
|
|
object->setOwner(PlayerColor::NEUTRAL);
|
|
return;
|
|
}
|
|
|
|
if(!owner.isValidPlayer())
|
|
{
|
|
object->setOwner(PlayerColor::NEUTRAL);
|
|
logGlobal->warn("Map '%s': Object at %s - owned by invalid player %d! Will be set to neutral!", mapName, mapPosition.toString(), int(owner.getNum()));
|
|
return;
|
|
}
|
|
|
|
if(!mapHeader->players[owner.getNum()].canAnyonePlay())
|
|
{
|
|
object->setOwner(PlayerColor::NEUTRAL);
|
|
logGlobal->warn("Map '%s': Object at %s - owned by non-existing player %d! Will be set to neutral!", mapName, mapPosition.toString(), int(owner.getNum())
|
|
);
|
|
return;
|
|
}
|
|
|
|
object->setOwner(owner);
|
|
}
|
|
|
|
std::shared_ptr<CGObjectInstance> CMapLoaderH3M::readHero(const int3 & mapPosition, const ObjectInstanceID & objectInstanceID)
|
|
{
|
|
if(features.levelAB)
|
|
{
|
|
unsigned int identifier = reader->readUInt32();
|
|
questIdentifierToId[identifier] = objectInstanceID;
|
|
}
|
|
|
|
PlayerColor owner = reader->readPlayer();
|
|
HeroTypeID heroType = reader->readHero();
|
|
|
|
//If hero of this type has been predefined, use that as a base.
|
|
//Instance data will overwrite the predefined values where appropriate.
|
|
std::shared_ptr<CGHeroInstance> object;
|
|
|
|
if (heroType.hasValue())
|
|
object = map->tryTakeFromHeroPool(heroType);
|
|
|
|
if (!object)
|
|
{
|
|
object = std::make_shared<CGHeroInstance>(map->cb);
|
|
object->subID = heroType.getNum();
|
|
}
|
|
|
|
setOwnerAndValidate(mapPosition, object.get(), owner);
|
|
|
|
for(auto & elem : map->disposedHeroes)
|
|
{
|
|
if(elem.heroId == object->getHeroTypeID())
|
|
{
|
|
object->nameCustomTextId = elem.name;
|
|
object->customPortraitSource = elem.portrait;
|
|
break;
|
|
}
|
|
}
|
|
|
|
bool hasName = reader->readBool();
|
|
if(hasName)
|
|
object->nameCustomTextId = readLocalizedString(TextIdentifier("heroes", object->getHeroTypeID().getNum(), "name"));
|
|
|
|
if(features.levelSOD)
|
|
{
|
|
bool hasCustomExperience = reader->readBool();
|
|
if(hasCustomExperience)
|
|
object->exp = reader->readUInt32();
|
|
}
|
|
else
|
|
{
|
|
object->exp = reader->readUInt32();
|
|
|
|
//0 means "not set" in <=AB maps
|
|
if(!object->exp)
|
|
object->exp = CGHeroInstance::UNINITIALIZED_EXPERIENCE;
|
|
}
|
|
|
|
bool hasPortrait = reader->readBool();
|
|
if(hasPortrait)
|
|
object->customPortraitSource = reader->readHeroPortrait();
|
|
|
|
bool hasSecSkills = reader->readBool();
|
|
if(hasSecSkills)
|
|
{
|
|
if(!object->secSkills.empty())
|
|
{
|
|
if(object->secSkills[0].first != SecondarySkill::NONE)
|
|
logGlobal->debug("Map '%s': Hero %s subID=%d has set secondary skills twice (in map properties and on adventure map instance). Using the latter set...", mapName, object->getNameTextID(), object->subID);
|
|
object->secSkills.clear();
|
|
}
|
|
|
|
uint32_t skillsCount = reader->readUInt32();
|
|
object->secSkills.resize(skillsCount);
|
|
for(int i = 0; i < skillsCount; ++i)
|
|
{
|
|
object->secSkills[i].first = reader->readSkill();
|
|
object->secSkills[i].second = reader->readInt8Checked(1,3);
|
|
}
|
|
}
|
|
|
|
bool hasGarison = reader->readBool();
|
|
if(hasGarison)
|
|
readCreatureSet(object.get(), objectInstanceID);
|
|
|
|
object->formation = static_cast<EArmyFormation>(reader->readInt8Checked(0, 1));
|
|
assert(object->formation == EArmyFormation::LOOSE || object->formation == EArmyFormation::TIGHT);
|
|
|
|
loadArtifactsOfHero(object.get());
|
|
object->patrol.patrolRadius = reader->readUInt8();
|
|
object->patrol.patrolling = (object->patrol.patrolRadius != 0xff);
|
|
|
|
if(features.levelAB)
|
|
{
|
|
bool hasCustomBiography = reader->readBool();
|
|
if(hasCustomBiography)
|
|
object->biographyCustomTextId = readLocalizedString(TextIdentifier("heroes", object->subID, "biography"));
|
|
|
|
object->gender = static_cast<EHeroGender>(reader->readInt8Checked(-1, 1));
|
|
assert(object->gender == EHeroGender::MALE || object->gender == EHeroGender::FEMALE || object->gender == EHeroGender::DEFAULT);
|
|
}
|
|
else
|
|
{
|
|
object->gender = EHeroGender::DEFAULT;
|
|
}
|
|
|
|
// Spells
|
|
if(features.levelSOD)
|
|
{
|
|
bool hasCustomSpells = reader->readBool();
|
|
if(hasCustomSpells)
|
|
{
|
|
if(!object->spells.empty())
|
|
{
|
|
object->spells.clear();
|
|
logGlobal->debug("Hero %s subID=%d has spells set twice (in map properties and on adventure map instance). Using the latter set...", object->getNameTextID(), object->subID);
|
|
}
|
|
|
|
reader->readBitmaskSpells(object->spells, false);
|
|
object->spells.insert(SpellID::PRESET); //placeholder "preset spells"
|
|
}
|
|
}
|
|
else if(features.levelAB)
|
|
{
|
|
//we can read one spell
|
|
SpellID spell = reader->readSpell();
|
|
|
|
// workaround: VCMI uses 'PRESET' spell to indicate that spellbook has been preconfigured on map
|
|
// but H3 uses 'PRESET' spell (-2) to indicate that game should give standard spell to hero
|
|
if(spell == SpellID::NONE)
|
|
object->spells.insert(SpellID::PRESET); //spellbook is preconfigured to be empty
|
|
|
|
if (spell.hasValue())
|
|
object->spells.insert(spell);
|
|
}
|
|
|
|
if(features.levelSOD)
|
|
{
|
|
bool hasCustomPrimSkills = reader->readBool();
|
|
if(hasCustomPrimSkills)
|
|
{
|
|
auto ps = object->getAllBonuses(Selector::type()(BonusType::PRIMARY_SKILL).And(Selector::sourceType()(BonusSource::HERO_BASE_SKILL)), "");
|
|
if(ps->size())
|
|
{
|
|
logGlobal->debug("Hero %s has set primary skills twice (in map properties and on adventure map instance). Using the latter set...", object->getHeroTypeID().getNum() );
|
|
for(const auto & b : *ps)
|
|
object->removeBonus(b);
|
|
}
|
|
|
|
for(int xx = 0; xx < GameConstants::PRIMARY_SKILLS; ++xx)
|
|
{
|
|
object->pushPrimSkill(static_cast<PrimarySkill>(xx), reader->readUInt8());
|
|
}
|
|
}
|
|
}
|
|
|
|
if (object->subID != MapObjectSubID())
|
|
logGlobal->debug("Map '%s': Hero on map: %s at %s, owned by %s", mapName, object->getHeroType()->getJsonKey(), mapPosition.toString(), object->getOwner().toString());
|
|
else
|
|
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;
|
|
}
|
|
|
|
std::shared_ptr<CGObjectInstance> CMapLoaderH3M::readSeerHut(const int3 & position, const ObjectInstanceID & idToBeGiven)
|
|
{
|
|
auto hut = std::make_shared<CGSeerHut>(map->cb);
|
|
|
|
uint32_t questsCount = 1;
|
|
|
|
if(features.levelHOTA3)
|
|
questsCount = reader->readUInt32();
|
|
|
|
//TODO: HotA
|
|
if(questsCount > 1)
|
|
logGlobal->warn("Map '%s': Seer Hut at %s - %d quests are not implemented!", mapName, position.toString(), questsCount);
|
|
|
|
for(size_t i = 0; i < questsCount; ++i)
|
|
readSeerHutQuest(hut.get(), position, idToBeGiven);
|
|
|
|
if(features.levelHOTA3)
|
|
{
|
|
uint32_t repeateableQuestsCount = reader->readUInt32();
|
|
hut->getQuest().repeatedQuest = repeateableQuestsCount != 0;
|
|
|
|
if(repeateableQuestsCount != 0)
|
|
logGlobal->warn("Map '%s': Seer Hut at %s - %d repeatable quests are not implemented!", mapName, position.toString(), repeateableQuestsCount);
|
|
|
|
for(size_t i = 0; i < repeateableQuestsCount; ++i)
|
|
readSeerHutQuest(hut.get(), position, idToBeGiven);
|
|
}
|
|
|
|
reader->skipZero(2);
|
|
|
|
return hut;
|
|
}
|
|
|
|
enum class ESeerHutRewardType : uint8_t
|
|
{
|
|
NOTHING = 0,
|
|
EXPERIENCE = 1,
|
|
MANA_POINTS = 2,
|
|
MORALE = 3,
|
|
LUCK = 4,
|
|
RESOURCES = 5,
|
|
PRIMARY_SKILL = 6,
|
|
SECONDARY_SKILL = 7,
|
|
ARTIFACT = 8,
|
|
SPELL = 9,
|
|
CREATURE = 10,
|
|
};
|
|
|
|
void CMapLoaderH3M::readSeerHutQuest(CGSeerHut * hut, const int3 & position, const ObjectInstanceID & idToBeGiven)
|
|
{
|
|
EQuestMission missionType = EQuestMission::NONE;
|
|
if(features.levelAB)
|
|
{
|
|
missionType = readQuest(hut, position);
|
|
}
|
|
else
|
|
{
|
|
//RoE
|
|
auto artID = reader->readArtifact();
|
|
if(artID != ArtifactID::NONE)
|
|
{
|
|
//not none quest
|
|
hut->getQuest().mission.artifacts.push_back(artID);
|
|
missionType = EQuestMission::ARTIFACT;
|
|
}
|
|
hut->getQuest().lastDay = -1; //no timeout
|
|
hut->getQuest().isCustomFirst = false;
|
|
hut->getQuest().isCustomNext = false;
|
|
hut->getQuest().isCustomComplete = false;
|
|
}
|
|
|
|
if(missionType != EQuestMission::NONE)
|
|
{
|
|
auto rewardType = static_cast<ESeerHutRewardType>(reader->readInt8Checked(0, 10));
|
|
Rewardable::VisitInfo vinfo;
|
|
auto & reward = vinfo.reward;
|
|
switch(rewardType)
|
|
{
|
|
case ESeerHutRewardType::NOTHING:
|
|
{
|
|
// no-op
|
|
break;
|
|
}
|
|
case ESeerHutRewardType::EXPERIENCE:
|
|
{
|
|
reward.heroExperience = reader->readUInt32();
|
|
break;
|
|
}
|
|
case ESeerHutRewardType::MANA_POINTS:
|
|
{
|
|
reward.manaDiff = reader->readUInt32();
|
|
break;
|
|
}
|
|
case ESeerHutRewardType::MORALE:
|
|
{
|
|
reward.heroBonuses.push_back(std::make_shared<Bonus>(BonusDuration::ONE_BATTLE, BonusType::MORALE, BonusSource::OBJECT_INSTANCE, reader->readInt8Checked(-3, 3), BonusSourceID(idToBeGiven)));
|
|
break;
|
|
}
|
|
case ESeerHutRewardType::LUCK:
|
|
{
|
|
reward.heroBonuses.push_back(std::make_shared<Bonus>(BonusDuration::ONE_BATTLE, BonusType::LUCK, BonusSource::OBJECT_INSTANCE, reader->readInt8Checked(-3, 3), BonusSourceID(idToBeGiven)));
|
|
break;
|
|
}
|
|
case ESeerHutRewardType::RESOURCES:
|
|
{
|
|
auto rId = reader->readGameResID();
|
|
auto rVal = reader->readUInt32();
|
|
|
|
reward.resources[rId] = rVal;
|
|
break;
|
|
}
|
|
case ESeerHutRewardType::PRIMARY_SKILL:
|
|
{
|
|
auto rId = reader->readPrimary();
|
|
auto rVal = reader->readUInt8();
|
|
|
|
reward.primary.at(rId.getNum()) = rVal;
|
|
break;
|
|
}
|
|
case ESeerHutRewardType::SECONDARY_SKILL:
|
|
{
|
|
auto rId = reader->readSkill();
|
|
auto rVal = reader->readInt8Checked(1,3);
|
|
|
|
reward.secondary[rId] = rVal;
|
|
break;
|
|
}
|
|
case ESeerHutRewardType::ARTIFACT:
|
|
{
|
|
ArtifactID grantedArtifact = reader->readArtifact();
|
|
if (features.levelHOTA5)
|
|
{
|
|
SpellID scrollSpell = reader->readSpell16();
|
|
if (grantedArtifact == ArtifactID::SPELL_SCROLL)
|
|
reward.grantedScrolls.push_back(scrollSpell);
|
|
}
|
|
else
|
|
reward.grantedArtifacts.push_back(grantedArtifact);
|
|
break;
|
|
}
|
|
case ESeerHutRewardType::SPELL:
|
|
{
|
|
reward.spells.push_back(reader->readSpell());
|
|
break;
|
|
}
|
|
case ESeerHutRewardType::CREATURE:
|
|
{
|
|
auto rId = reader->readCreature();
|
|
auto rVal = reader->readUInt16();
|
|
|
|
reward.creatures.emplace_back(rId, rVal);
|
|
break;
|
|
}
|
|
default:
|
|
{
|
|
assert(0);
|
|
}
|
|
}
|
|
|
|
vinfo.visitType = Rewardable::EEventType::EVENT_FIRST_VISIT;
|
|
hut->configuration.info.push_back(vinfo);
|
|
}
|
|
else
|
|
{
|
|
// missionType==255
|
|
reader->skipZero(1);
|
|
}
|
|
}
|
|
|
|
EQuestMission CMapLoaderH3M::readQuest(IQuestObject * guard, const int3 & position)
|
|
{
|
|
auto missionId = static_cast<EQuestMission>(reader->readInt8Checked(0, 10));
|
|
|
|
switch(missionId)
|
|
{
|
|
case EQuestMission::NONE:
|
|
return missionId;
|
|
case EQuestMission::PRIMARY_SKILL:
|
|
{
|
|
for(int x = 0; x < 4; ++x)
|
|
{
|
|
guard->getQuest().mission.primary[x] = reader->readUInt8();
|
|
}
|
|
break;
|
|
}
|
|
case EQuestMission::LEVEL:
|
|
{
|
|
guard->getQuest().mission.heroLevel = reader->readUInt32();
|
|
break;
|
|
}
|
|
case EQuestMission::KILL_HERO:
|
|
case EQuestMission::KILL_CREATURE:
|
|
{
|
|
// NOTE: assert might fail on multi-quest seers
|
|
//assert(questsToResolve.count(guard) == 0);
|
|
questsToResolve[guard] = reader->readUInt32();
|
|
break;
|
|
}
|
|
case EQuestMission::ARTIFACT:
|
|
{
|
|
size_t artNumber = reader->readUInt8();
|
|
for(size_t yy = 0; yy < artNumber; ++yy)
|
|
{
|
|
ArtifactID requiredArtifact = reader->readArtifact();
|
|
|
|
if (features.levelHOTA5)
|
|
{
|
|
SpellID scrollSpell = reader->readSpell16();
|
|
if (requiredArtifact == ArtifactID::SPELL_SCROLL)
|
|
guard->getQuest().mission.scrolls.push_back(scrollSpell);
|
|
}
|
|
else
|
|
guard->getQuest().mission.artifacts.push_back(requiredArtifact);
|
|
|
|
map->allowedArtifact.erase(requiredArtifact); //these are unavailable for random generation
|
|
}
|
|
break;
|
|
}
|
|
case EQuestMission::ARMY:
|
|
{
|
|
size_t typeNumber = reader->readUInt8();
|
|
guard->getQuest().mission.creatures.resize(typeNumber);
|
|
for(size_t hh = 0; hh < typeNumber; ++hh)
|
|
{
|
|
guard->getQuest().mission.creatures[hh].setType(reader->readCreature().toCreature());
|
|
guard->getQuest().mission.creatures[hh].setCount(reader->readUInt16());
|
|
}
|
|
break;
|
|
}
|
|
case EQuestMission::RESOURCES:
|
|
{
|
|
for(int x = 0; x < 7; ++x)
|
|
guard->getQuest().mission.resources[x] = reader->readUInt32();
|
|
|
|
break;
|
|
}
|
|
case EQuestMission::HERO:
|
|
{
|
|
guard->getQuest().mission.heroes.push_back(reader->readHero());
|
|
break;
|
|
}
|
|
case EQuestMission::PLAYER:
|
|
{
|
|
guard->getQuest().mission.players.push_back(reader->readPlayer());
|
|
break;
|
|
}
|
|
case EQuestMission::HOTA_MULTI:
|
|
{
|
|
uint32_t missionSubID = reader->readUInt32();
|
|
assert(missionSubID < 3);
|
|
|
|
if(missionSubID == 0)
|
|
{
|
|
missionId = EQuestMission::HOTA_HERO_CLASS;
|
|
std::set<HeroClassID> heroClasses;
|
|
reader->readBitmaskHeroClassesSized(heroClasses, false);
|
|
for(auto & hc : heroClasses)
|
|
guard->getQuest().mission.heroClasses.push_back(hc);
|
|
break;
|
|
}
|
|
if(missionSubID == 1)
|
|
{
|
|
missionId = EQuestMission::HOTA_REACH_DATE;
|
|
guard->getQuest().mission.daysPassed = reader->readUInt32() + 1;
|
|
break;
|
|
}
|
|
if(missionSubID == 2)
|
|
{
|
|
missionId = EQuestMission::HOTA_GAME_DIFFICULTY;
|
|
int32_t difficultyMask = reader->readUInt32();
|
|
assert(difficultyMask > 0 && difficultyMask < 32);
|
|
logGlobal->warn("Map '%s': Seer Hut at %s: Difficulty-specific quest (%d) is not implemented!", mapName, position.toString(), difficultyMask);
|
|
break;
|
|
}
|
|
break;
|
|
}
|
|
default:
|
|
{
|
|
assert(0);
|
|
}
|
|
}
|
|
|
|
guard->getQuest().lastDay = reader->readInt32();
|
|
guard->getQuest().firstVisitText.appendTextID(readLocalizedString(TextIdentifier("quest", position.x, position.y, position.z, "firstVisit")));
|
|
guard->getQuest().nextVisitText.appendTextID(readLocalizedString(TextIdentifier("quest", position.x, position.y, position.z, "nextVisit")));
|
|
guard->getQuest().completedText.appendTextID(readLocalizedString(TextIdentifier("quest", position.x, position.y, position.z, "completed")));
|
|
guard->getQuest().isCustomFirst = !guard->getQuest().firstVisitText.empty();
|
|
guard->getQuest().isCustomNext = !guard->getQuest().nextVisitText.empty();
|
|
guard->getQuest().isCustomComplete = !guard->getQuest().completedText.empty();
|
|
return missionId;
|
|
}
|
|
|
|
std::shared_ptr<CGObjectInstance> CMapLoaderH3M::readTown(const int3 & position, std::shared_ptr<const ObjectTemplate> objectTemplate, const ObjectInstanceID & idToBeGiven)
|
|
{
|
|
auto object = std::make_shared<CGTownInstance>(map->cb);
|
|
if(features.levelAB)
|
|
object->identifier = reader->readUInt32();
|
|
|
|
setOwnerAndValidate(position, object.get(), reader->readPlayer());
|
|
|
|
std::optional<FactionID> faction;
|
|
if (objectTemplate->id == Obj::TOWN)
|
|
faction = FactionID(objectTemplate->subid);
|
|
|
|
bool hasName = reader->readBool();
|
|
if(hasName)
|
|
object->setNameTextId(readLocalizedString(TextIdentifier("town", position.x, position.y, position.z, "name")));
|
|
|
|
bool hasGarrison = reader->readBool();
|
|
if(hasGarrison)
|
|
readCreatureSet(object.get(), idToBeGiven);
|
|
|
|
object->formation = static_cast<EArmyFormation>(reader->readInt8Checked(0, 1));
|
|
assert(object->formation == EArmyFormation::LOOSE || object->formation == EArmyFormation::TIGHT);
|
|
|
|
bool hasCustomBuildings = reader->readBool();
|
|
if(hasCustomBuildings)
|
|
{
|
|
std::set<BuildingID> builtBuildings;
|
|
reader->readBitmaskBuildings(builtBuildings, faction);
|
|
for(const auto & building : builtBuildings)
|
|
object->addBuilding(building);
|
|
reader->readBitmaskBuildings(object->forbiddenBuildings, faction);
|
|
}
|
|
// Standard buildings
|
|
else
|
|
{
|
|
bool hasFort = reader->readBool();
|
|
if(hasFort)
|
|
object->addBuilding(BuildingID::FORT);
|
|
|
|
//means that set of standard building should be included
|
|
object->addBuilding(BuildingID::DEFAULT);
|
|
}
|
|
|
|
if(features.levelAB)
|
|
{
|
|
std::set<SpellID> spellsMask;
|
|
|
|
reader->readBitmaskSpells(spellsMask, false);
|
|
std::copy(spellsMask.begin(), spellsMask.end(), std::back_inserter(object->obligatorySpells));
|
|
}
|
|
|
|
{
|
|
std::set<SpellID> spellsMask = LIBRARY->spellh->getDefaultAllowed(); // by default - include spells from mods
|
|
|
|
reader->readBitmaskSpells(spellsMask, true);
|
|
std::copy(spellsMask.begin(), spellsMask.end(), std::back_inserter(object->possibleSpells));
|
|
}
|
|
|
|
if(features.levelHOTA1)
|
|
object->spellResearchAllowed = reader->readBool();
|
|
|
|
if(features.levelHOTA5)
|
|
{
|
|
// Most likely customization of special buildings per faction -> 4x11 table
|
|
// presumably:
|
|
// 0 -> default / allowed
|
|
// 1 -> built?
|
|
// 2 -> banned?
|
|
uint32_t specialBuildingsSize = reader->readUInt32();
|
|
|
|
for (int i = 0; i < specialBuildingsSize; ++i)
|
|
{
|
|
int factionID = i / 4;
|
|
int buildingID = i % 4;
|
|
|
|
int8_t specialBuildingBuilt = reader->readInt8Checked(0, 2);
|
|
if (specialBuildingBuilt != 0)
|
|
logGlobal->warn("Map '%s': Town at %s: Constructing / banning town-specific special building %d in faction %d on start is not implemented!", mapName, position.toString(), buildingID, factionID);
|
|
}
|
|
}
|
|
|
|
// Read castle events
|
|
uint32_t eventsCount = reader->readUInt32();
|
|
|
|
for(int eventID = 0; eventID < eventsCount; ++eventID)
|
|
{
|
|
CCastleEvent event;
|
|
event.creatures.resize(7);
|
|
|
|
readEventCommon(event, TextIdentifier("town", position.x, position.y, position.z, "event", eventID, "description"));
|
|
|
|
if(features.levelHOTA5)
|
|
{
|
|
int32_t creatureGrowth8 = reader->readInt32();
|
|
|
|
// always 44
|
|
int32_t hotaAmount = reader->readInt32();
|
|
|
|
// contains bitmask on which town-specific buildings to build
|
|
// 4 bits / town, for each of special building in town (special 1 - special 4)
|
|
int32_t hotaSpecialA = reader->readInt32();
|
|
int16_t hotaSpecialB = reader->readInt16();
|
|
|
|
if (hotaSpecialA != 0 || hotaSpecialB != 0 || hotaAmount != 44)
|
|
logGlobal->warn("Map '%s': Town at %s: Constructing town-specific special buildings in event is not implemented!", mapName, position.toString());
|
|
|
|
event.creatures.push_back(creatureGrowth8);
|
|
}
|
|
|
|
if(features.levelHOTA7)
|
|
{
|
|
[[maybe_unused]] bool neutralAffected = reader->readBool();
|
|
}
|
|
|
|
// New buildings
|
|
reader->readBitmaskBuildings(event.buildings, faction);
|
|
|
|
for(int i = 0; i < 7; ++i)
|
|
event.creatures[i] = reader->readUInt16();
|
|
|
|
reader->skipZero(4);
|
|
object->events.push_back(event);
|
|
}
|
|
|
|
if(features.levelSOD)
|
|
{
|
|
object->alignmentToPlayer = PlayerColor::NEUTRAL; // "same as owner or random"
|
|
|
|
uint8_t alignment = reader->readUInt8();
|
|
|
|
if(alignment != 255)
|
|
{
|
|
if(alignment < PlayerColor::PLAYER_LIMIT.getNum())
|
|
{
|
|
if (mapHeader->players[alignment].canAnyonePlay())
|
|
object->alignmentToPlayer = PlayerColor(alignment);
|
|
else
|
|
logGlobal->warn("Map '%s': Alignment of town at %s is invalid! Player %d is not present on map!", mapName, position.toString(), static_cast<int>(alignment));
|
|
}
|
|
else
|
|
{
|
|
// TODO: HOTA support
|
|
uint8_t invertedAlignment = alignment - PlayerColor::PLAYER_LIMIT.getNum();
|
|
|
|
if(invertedAlignment < PlayerColor::PLAYER_LIMIT.getNum())
|
|
{
|
|
logGlobal->warn("Map '%s': Alignment of town at %s 'not as player %d' is not implemented!", mapName, position.toString(), alignment - PlayerColor::PLAYER_LIMIT.getNum());
|
|
}
|
|
else
|
|
{
|
|
logGlobal->warn("Map '%s': Alignment of town at %s is corrupted!!", mapName, position.toString());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
reader->skipZero(3);
|
|
|
|
return object;
|
|
}
|
|
|
|
void CMapLoaderH3M::readEventCommon(CMapEvent & event, const TextIdentifier & messageID)
|
|
{
|
|
event.name = readBasicString();
|
|
event.message.appendTextID(readLocalizedString(messageID));
|
|
|
|
reader->readResources(event.resources);
|
|
|
|
reader->readBitmaskPlayers(event.players, false);
|
|
if(features.levelSOD)
|
|
event.humanAffected = reader->readBool();
|
|
else
|
|
event.humanAffected = true;
|
|
|
|
event.computerAffected = reader->readBool();
|
|
event.firstOccurrence = reader->readUInt16();
|
|
event.nextOccurrence = reader->readUInt16();
|
|
|
|
reader->skipZero(16);
|
|
|
|
if (features.levelHOTA7)
|
|
{
|
|
int32_t allowedDifficultiesMask = reader->readInt32();
|
|
assert(allowedDifficultiesMask > 0 && allowedDifficultiesMask < 32);
|
|
if (allowedDifficultiesMask != 31)
|
|
logGlobal->warn("Map '%s': Map event: availability by difficulty (mode %d) is not implemented!", mapName, allowedDifficultiesMask);
|
|
}
|
|
}
|
|
|
|
void CMapLoaderH3M::readEvents()
|
|
{
|
|
uint32_t eventsCount = reader->readUInt32();
|
|
for(int eventID = 0; eventID < eventsCount; ++eventID)
|
|
{
|
|
CMapEvent event;
|
|
readEventCommon(event, TextIdentifier("event", eventID, "description"));
|
|
|
|
// garbage bytes that were present in HOTA5 & HOTA6
|
|
if (features.levelHOTA5 && !features.levelHOTA7)
|
|
reader->skipUnused(14);
|
|
|
|
map->events.push_back(event);
|
|
}
|
|
}
|
|
|
|
void CMapLoaderH3M::readMessageAndGuards(MetaString & message, CArmedInstance * guards, const int3 & position, const ObjectInstanceID & idToBeGiven)
|
|
{
|
|
bool hasMessage = reader->readBool();
|
|
if(hasMessage)
|
|
{
|
|
message.appendTextID(readLocalizedString(TextIdentifier("guards", position.x, position.y, position.z, "message")));
|
|
bool hasGuards = reader->readBool();
|
|
if(hasGuards)
|
|
readCreatureSet(guards, idToBeGiven);
|
|
|
|
reader->skipZero(4);
|
|
}
|
|
}
|
|
|
|
std::string CMapLoaderH3M::readBasicString()
|
|
{
|
|
return TextOperations::toUnicode(reader->readBaseString(), fileEncoding);
|
|
}
|
|
|
|
std::string CMapLoaderH3M::readLocalizedString(const TextIdentifier & stringIdentifier)
|
|
{
|
|
std::string mapString = TextOperations::toUnicode(reader->readBaseString(), fileEncoding);
|
|
TextIdentifier fullIdentifier("map", mapName, stringIdentifier.get());
|
|
|
|
if(mapString.empty())
|
|
return "";
|
|
|
|
return mapRegisterLocalizedString(modName, *mapHeader, fullIdentifier, mapString);
|
|
}
|
|
|
|
void CMapLoaderH3M::afterRead()
|
|
{
|
|
//convert main town positions for all players to actual object position, in H3M it is position of active tile
|
|
|
|
for(auto & p : map->players)
|
|
{
|
|
int3 posOfMainTown = p.posOfMainTown;
|
|
if(posOfMainTown.isValid() && map->isInTheMap(posOfMainTown))
|
|
{
|
|
const TerrainTile & t = map->getTile(posOfMainTown);
|
|
|
|
const CGObjectInstance * mainTown = nullptr;
|
|
|
|
for(ObjectInstanceID objID : t.visitableObjects)
|
|
{
|
|
const CGObjectInstance * object = map->getObject(objID);
|
|
|
|
if(object->ID == Obj::TOWN || object->ID == Obj::RANDOM_TOWN)
|
|
{
|
|
mainTown = object;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if(mainTown == nullptr)
|
|
continue;
|
|
|
|
p.posOfMainTown = posOfMainTown + mainTown->getVisitableOffset();
|
|
}
|
|
}
|
|
|
|
for (auto & quest : questsToResolve)
|
|
quest.first->getQuest().killTarget = questIdentifierToId.at(quest.second);
|
|
}
|
|
|
|
VCMI_LIB_NAMESPACE_END
|