1
0
mirror of https://github.com/vcmi/vcmi.git synced 2024-12-14 10:12:59 +02:00
vcmi/lib/mapping/MapFormatH3M.cpp
Ivan Savenko 20d5b33ea6 Remove marketModes as member
marketModes are now generated in runtime and are not a member of
IMarket. Was not a bad change, but towns load buildings before town type
is randomized, leading to case where market modes are not actually known
when building is added to town (like random towns with market built)

Since altar requires CArtifactSet for work, IMarket will now always
contain it, but it will only be accessible if market supports altar
mode.
2024-08-27 14:07:00 +00:00

2401 lines
70 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 "CMap.h"
#include "MapReaderH3M.h"
#include "MapFormat.h"
#include "../ArtifactUtils.h"
#include "../CCreatureHandler.h"
#include "../texts/CGeneralTextHandler.h"
#include "../CHeroHandler.h"
#include "../CSkillHandler.h"
#include "../CStopWatch.h"
#include "../GameSettings.h"
#include "../RiverHandler.h"
#include "../RoadHandler.h"
#include "../TerrainHandler.h"
#include "../VCMI_Lib.h"
#include "../constants/StringConstants.h"
#include "../filesystem/CBinaryReader.h"
#include "../filesystem/Filesystem.h"
#include "../mapObjectConstructors/AObjectTypeHandler.h"
#include "../mapObjectConstructors/CObjectClassesHandler.h"
#include "../mapObjects/CGCreature.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 <boost/crc.hpp>
VCMI_LIB_NAMESPACE_BEGIN
static std::string convertMapName(std::string input)
{
boost::algorithm::to_lower(input);
boost::algorithm::trim(input);
boost::algorithm::erase_all(input, ".");
size_t slashPos = input.find_last_of('/');
if(slashPos != std::string::npos)
return input.substr(slashPos + 1);
return input;
}
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(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(IGameCallback * 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()
{
//TODO: get rid of double input process
si64 temp_size = inputStream->getSize();
inputStream->seek(0);
auto * temp_buffer = new ui8[temp_size];
inputStream->read(temp_buffer, temp_size);
// Compute checksum
boost::crc_32_type result;
result.process_bytes(temp_buffer, temp_size);
map->checksum = result.checksum();
delete[] temp_buffer;
inputStream->seek(0);
readHeader();
readDisposedHeroes();
readMapOptions();
readAllowedArtifacts();
readAllowedSpellsAbilities();
readRumors();
readPredefinedHeroes();
readTerrain();
readObjectTemplates();
readObjects();
readEvents();
map->calculateGuardingGreaturePositions();
afterRead();
//map->banWaterContent(); //Not sure if force this for custom scenarios
}
static MapIdentifiersH3M generateMapping(EMapFormat format)
{
auto features = MapFormatFeaturesH3M::find(format, 0);
MapIdentifiersH3M identifierMapper;
if(features.levelROE)
identifierMapper.loadMapping(VLC->settings()->getValue(EGameSettings::MAP_FORMAT_RESTORATION_OF_ERATHIA));
if(features.levelAB)
identifierMapper.loadMapping(VLC->settings()->getValue(EGameSettings::MAP_FORMAT_ARMAGEDDONS_BLADE));
if(features.levelSOD)
identifierMapper.loadMapping(VLC->settings()->getValue(EGameSettings::MAP_FORMAT_SHADOW_OF_DEATH));
if(features.levelWOG)
identifierMapper.loadMapping(VLC->settings()->getValue(EGameSettings::MAP_FORMAT_IN_THE_WAKE_OF_GODS));
if(features.levelHOTA0)
identifierMapper.loadMapping(VLC->settings()->getValue(EGameSettings::MAP_FORMAT_HORN_OF_THE_ABYSS));
return identifierMapper;
}
static std::map<EMapFormat, MapIdentifiersH3M> generateMappings()
{
std::map<EMapFormat, MapIdentifiersH3M> result;
auto addMapping = [&result](EMapFormat format)
{
try
{
result[format] = generateMapping(format);
}
catch(const std::runtime_error &)
{
// unsupported map format - skip
}
};
addMapping(EMapFormat::ROE);
addMapping(EMapFormat::AB);
addMapping(EMapFormat::SOD);
addMapping(EMapFormat::HOTA);
addMapping(EMapFormat::WOG);
return result;
}
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(hotaVersion > 0)
{
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(hotaVersion > 1)
{
[[maybe_unused]] uint8_t unknown = reader->readUInt32();
assert(unknown == 12);
}
}
else
{
features = MapFormatFeaturesH3M::find(mapHeader->version, 0);
reader->setFormatLevel(features);
}
// optimization - load mappings only once to avoid slow parsing of map headers for map list
static const std::map<EMapFormat, MapIdentifiersH3M> identifierMappers = generateMappings();
const MapIdentifiersH3M & identifierMapper = identifierMappers.at(mapHeader->version);
reader->setIdentifierRemapper(identifierMapper);
// include basic mod
if(mapHeader->version == EMapFormat::WOG)
mapHeader->mods["wake-of-gods"];
// 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, VLC->heroh->maxSupportedLevel()));
else
mapHeader->levelLimit = 0;
readPlayerInfo();
readVictoryLossConditions();
readTeamInfo();
readAllowedHeroes();
}
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); //TODO: check meaning?
std::set<FactionID> allowedFactions;
reader->readBitmaskFactions(allowedFactions, false);
const bool isFactionRandom = playerInfo.isFactionRandom = reader->readBool();
const bool allFactionsAllowed = isFactionRandom && allowedFactions.size() == features.factionsCount;
if(!allFactionsAllowed)
playerInfo.allowedFactions = allowedFactions;
playerInfo.hasMainTown = reader->readBool();
if(playerInfo.hasMainTown)
{
if(features.levelAB)
{
playerInfo.generateHeroAtMainTown = reader->readBool();
reader->skipUnused(1); //TODO: check meaning?
}
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 = VLC->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();
map->disposedHeroes.resize(disp);
for(size_t g = 0; g < disp; ++g)
{
map->disposedHeroes[g].heroId = reader->readHero();
map->disposedHeroes[g].portrait = reader->readHeroPortrait();
map->disposedHeroes[g].name = readLocalizedString(TextIdentifier("header", "heroes", map->disposedHeroes[g].heroId.getNum()));
reader->readBitmaskPlayers(map->disposedHeroes[g].players, false);
}
}
}
void CMapLoaderH3M::readMapOptions()
{
//omitting NULLS
reader->skipZero(31);
if(features.levelHOTA0)
{
//TODO: HotA
bool allowSpecialMonths = reader->readBool();
if(!allowSpecialMonths)
logGlobal->warn("Map '%s': Option 'allow special months' is not implemented!", mapName);
reader->skipZero(3);
}
if(features.levelHOTA1)
{
// Unknown, may be another "sized bitmap", e.g
// 4 bytes - size of bitmap (16)
// 2 bytes - bitmap data (16 bits / 2 bytes)
[[maybe_unused]] uint8_t unknownConstant = reader->readUInt8();
assert(unknownConstant == 16);
reader->skipZero(5);
}
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);
}
}
void CMapLoaderH3M::readAllowedArtifacts()
{
map->allowedArtifact = VLC->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 : VLC->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 = VLC->spellh->getDefaultAllowed();
map->allowedAbilities = VLC->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 * hero = new CGHeroInstance(map->cb);
hero->ID = Obj::HERO;
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);
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->predefinedHeroes.emplace_back(hero);
logGlobal->debug("Map '%s': Hero predefined in map: %s", mapName, VLC->heroh->getById(hero->getHeroType())->getJsonKey());
}
}
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->getHeroType().getNum(), hero->pos.toString());
hero->artifactsInBackpack.clear();
while(!hero->artifactsWorn.empty())
hero->eraseArtSlot(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();
if(artifactID == ArtifactID::NONE)
return false;
const Artifact * art = artifactID.toEntity(VLC);
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
auto * artifact = ArtifactUtils::createArtifact(map, artifactID);
if(artifact->canBePutAt(hero, ArtifactPosition(slot)))
{
artifact->putAt(*hero, ArtifactPosition(slot));
}
else
{
logGlobal->warn("Map '%s': Artifact '%s' can't be put at the slot %d", mapName, artifact->artType->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.terType = VLC->terrainTypeHandler->getById(reader->readTerrain());
tile.terView = reader->readUInt8();
tile.riverType = VLC->riverTypeHandler->getById(reader->readRiver());
tile.riverDir = reader->readUInt8();
tile.roadType = VLC->roadTypeHandler->getById(reader->readRoad());
tile.roadDir = reader->readUInt8();
tile.extTileFlags = reader->readUInt8();
tile.blocked = !tile.terType->isPassable();
tile.visitable = false;
assert(tile.terType->getId() != ETerrainId::NONE);
}
}
}
map->calculateWaterContent();
}
void CMapLoaderH3M::readObjectTemplates()
{
uint32_t defAmount = reader->readUInt32();
templates.reserve(defAmount);
// Read custom defs
for(int defID = 0; defID < defAmount; ++defID)
{
auto tmpl = reader->readObjectTemplate();
templates.push_back(tmpl);
if (!CResourceHandler::get()->existsResource(tmpl->animationFile.addPrefix("SPRITES/")))
logMod->warn("Template animation %s of type (%d %d) is missing!", tmpl->animationFile.getOriginalName(), tmpl->id, tmpl->subid );
}
}
CGObjectInstance * CMapLoaderH3M::readEvent(const int3 & mapPosition, const ObjectInstanceID & idToBeGiven)
{
auto * object = new CGEvent(map->cb);
readBoxContent(object, 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;
return object;
}
CGObjectInstance * CMapLoaderH3M::readPandora(const int3 & mapPosition, const ObjectInstanceID & idToBeGiven)
{
auto * object = new CGPandoraBox(map->cb);
readBoxContent(object, mapPosition, idToBeGiven);
return object;
}
void CMapLoaderH3M::readBoxContent(CGPandoraBox * object, const int3 & mapPosition, const ObjectInstanceID & idToBeGiven)
{
readMessageAndGuards(object->message, object, mapPosition);
Rewardable::VisitInfo vinfo;
auto & reward = vinfo.reward;
reward.heroExperience = reader->readUInt32();
reward.manaDiff = reader->readInt32();
if(auto val = reader->readInt8Checked(-3, 3))
reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::MORALE, BonusSource::OBJECT_INSTANCE, val, BonusSourceID(idToBeGiven));
if(auto val = reader->readInt8Checked(-3, 3))
reward.bonuses.emplace_back(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)
reward.artifacts.push_back(reader->readArtifact());
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);
}
CGObjectInstance * CMapLoaderH3M::readMonster(const int3 & mapPosition, const ObjectInstanceID & objectInstanceID)
{
auto * object = new CGCreature(map->cb);
if(features.levelAB)
{
object->identifier = reader->readUInt32();
map->questIdentifierToId[object->identifier] = objectInstanceID;
}
auto * hlp = new CStackInstance();
hlp->count = reader->readUInt16();
//type will be set during initialization
object->putStack(SlotID(0), hlp);
object->character = reader->readInt8Checked(0, 4);
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
);
}
return object;
}
CGObjectInstance * CMapLoaderH3M::readSign(const int3 & mapPosition)
{
auto * object = new CGSignBottle(map->cb);
object->message.appendTextID(readLocalizedString(TextIdentifier("sign", mapPosition.x, mapPosition.y, mapPosition.z, "message")));
reader->skipZero(4);
return object;
}
CGObjectInstance * CMapLoaderH3M::readWitchHut(const int3 & position, std::shared_ptr<const ObjectTemplate> objectTemplate)
{
auto * object = readGeneric(position, objectTemplate);
auto * rewardable = dynamic_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 = VLC->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() = VLC->skills()->getById(*allowedAbilities.begin())->getJsonKey();
}
else
{
JsonVector anyOfList;
for (auto const & skill : allowedAbilities)
{
JsonNode entry;
entry.String() = VLC->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;
}
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 = dynamic_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() = VLC->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() = VLC->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;
}
CGObjectInstance * CMapLoaderH3M::readGarrison(const int3 & mapPosition)
{
auto * object = new CGGarrison(map->cb);
setOwnerAndValidate(mapPosition, object, reader->readPlayer32());
readCreatureSet(object, 7);
if(features.levelAB)
object->removableUnits = reader->readBool();
else
object->removableUnits = true;
reader->skipZero(8);
return object;
}
CGObjectInstance * CMapLoaderH3M::readArtifact(const int3 & mapPosition, std::shared_ptr<const ObjectTemplate> objectTemplate)
{
ArtifactID artID = ArtifactID::NONE; //random, set later
SpellID spellID = SpellID::NONE;
auto * object = new CGArtifact(map->cb);
readMessageAndGuards(object->message, object, mapPosition);
if(objectTemplate->id == Obj::SPELL_SCROLL)
{
spellID = reader->readSpell32();
artID = ArtifactID::SPELL_SCROLL;
}
else if(objectTemplate->id == Obj::ARTIFACT)
{
//specific artifact
artID = ArtifactID(objectTemplate->subid);
}
object->storedArtifact = ArtifactUtils::createArtifact(map, artID, spellID.getNum());
return object;
}
CGObjectInstance * CMapLoaderH3M::readResource(const int3 & mapPosition, std::shared_ptr<const ObjectTemplate> objectTemplate)
{
auto * object = new CGResource(map->cb);
readMessageAndGuards(object->message, object, mapPosition);
object->amount = reader->readUInt32();
if(GameResID(objectTemplate->subid) == GameResID(EGameResID::GOLD))
{
// Gold is multiplied by 100.
object->amount *= CGResource::GOLD_AMOUNT_MULTIPLIER;
}
reader->skipZero(4);
return object;
}
CGObjectInstance * CMapLoaderH3M::readMine(const int3 & mapPosition, std::shared_ptr<const ObjectTemplate> objectTemplate)
{
auto * object = new CGMine(map->cb);
if(objectTemplate->subid < 7)
{
setOwnerAndValidate(mapPosition, object, reader->readPlayer32());
}
else
{
object->setOwner(PlayerColor::NEUTRAL);
reader->readBitmaskResources(object->abandonedMineResources, false);
}
return object;
}
CGObjectInstance * CMapLoaderH3M::readDwelling(const int3 & position)
{
auto * object = new CGDwelling(map->cb);
setOwnerAndValidate(position, object, reader->readPlayer32());
return object;
}
CGObjectInstance * CMapLoaderH3M::readDwellingRandom(const int3 & mapPosition, std::shared_ptr<const ObjectTemplate> objectTemplate)
{
auto * object = new CGDwelling(map->cb);
setOwnerAndValidate(mapPosition, object, 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;
}
CGObjectInstance * CMapLoaderH3M::readShrine(const int3 & position, std::shared_ptr<const ObjectTemplate> objectTemplate)
{
auto * object = readGeneric(position, objectTemplate);
auto * rewardable = dynamic_cast<CRewardableObject*>(object);
SpellID spell = reader->readSpell32();
if (rewardable)
{
if(spell != SpellID::NONE)
{
JsonNode variable;
variable.String() = VLC->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;
}
CGObjectInstance * CMapLoaderH3M::readHeroPlaceholder(const int3 & mapPosition)
{
auto * object = new CGHeroPlaceholder(map->cb);
setOwnerAndValidate(mapPosition, object, 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, VLC->heroh->getById(htid)->getJsonKey(), mapPosition.toString(), object->getOwner().toString());
}
return object;
}
CGObjectInstance * CMapLoaderH3M::readGrail(const int3 & mapPosition, std::shared_ptr<const ObjectTemplate> objectTemplate)
{
if (objectTemplate->subid < 1000)
{
map->grailPos = mapPosition;
map->grailRadius = reader->readInt32();
}
else
{
// Battle location for arena mode in HotA
logGlobal->warn("Map '%s': Arena mode is not supported!", mapName);
}
return nullptr;
}
CGObjectInstance * CMapLoaderH3M::readGeneric(const int3 & mapPosition, std::shared_ptr<const ObjectTemplate> objectTemplate)
{
if(VLC->objtypeh->knownSubObjects(objectTemplate->id).count(objectTemplate->subid))
return VLC->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 new CGObjectInstance(map->cb);
}
CGObjectInstance * CMapLoaderH3M::readPyramid(const int3 & mapPosition, std::shared_ptr<const ObjectTemplate> objectTemplate)
{
if(objectTemplate->subid == 0)
return new CBank(map->cb);
return new CGObjectInstance(map->cb);
}
CGObjectInstance * CMapLoaderH3M::readQuestGuard(const int3 & mapPosition)
{
auto * guard = new CGQuestGuard(map->cb);
readQuest(guard, mapPosition);
return guard;
}
CGObjectInstance * CMapLoaderH3M::readShipyard(const int3 & mapPosition, std::shared_ptr<const ObjectTemplate> objectTemplate)
{
auto * object = readGeneric(mapPosition, objectTemplate);
setOwnerAndValidate(mapPosition, object, reader->readPlayer32());
return object;
}
CGObjectInstance * CMapLoaderH3M::readLighthouse(const int3 & mapPosition)
{
auto * object = new CGLighthouse(map->cb);
setOwnerAndValidate(mapPosition, object, reader->readPlayer32());
return object;
}
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);
}
CGObjectInstance * CMapLoaderH3M::readObject(std::shared_ptr<const ObjectTemplate> objectTemplate, const int3 & mapPosition, const ObjectInstanceID & objectInstanceID)
{
switch(objectTemplate->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);
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:
case Obj::SPELL_SCROLL:
return readArtifact(mapPosition, objectTemplate);
case Obj::RANDOM_RESOURCE:
case Obj::RESOURCE:
return readResource(mapPosition, objectTemplate);
case Obj::RANDOM_TOWN:
case Obj::TOWN:
return readTown(mapPosition, objectTemplate);
case Obj::MINE:
case Obj::ABANDONED_MINE:
return readMine(mapPosition, objectTemplate);
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:
return readGrail(mapPosition, objectTemplate);
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::PYRAMID:
return readPyramid(mapPosition, objectTemplate);
case Obj::LIGHTHOUSE:
return readLighthouse(mapPosition);
case Obj::CREATURE_BANK:
case Obj::DERELICT_SHIP:
case Obj::DRAGON_UTOPIA:
case Obj::CRYPT:
case Obj::SHIPWRECK:
return readBank(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();
uint32_t defIndex = reader->readUInt32();
ObjectInstanceID objectInstanceID = ObjectInstanceID(static_cast<si32>(map->objects.size()));
std::shared_ptr<const ObjectTemplate> objectTemplate = templates.at(defIndex);
reader->skipZero(5);
CGObjectInstance * newObject = readObject(objectTemplate, mapPosition, objectInstanceID);
if(!newObject)
continue;
newObject->pos = mapPosition;
newObject->ID = objectTemplate->id;
newObject->id = objectInstanceID;
if(newObject->ID != Obj::HERO && newObject->ID != Obj::HERO_PLACEHOLDER && newObject->ID != Obj::PRISON)
{
newObject->subID = objectTemplate->subid;
}
newObject->appearance = objectTemplate;
assert(objectInstanceID == ObjectInstanceID((si32)map->objects.size()));
if (newObject->isVisitable() && !map->isInTheMap(newObject->visitablePos()))
logGlobal->error("Map '%s': Object at %s - outside of map borders!", mapName, mapPosition.toString());
{
//TODO: define valid typeName and subtypeName for H3M maps
//boost::format fmt("%s_%d");
//fmt % nobj->typeName % nobj->id.getNum();
boost::format fmt("obj_%d");
fmt % newObject->id.getNum();
newObject->instanceName = fmt.str();
}
map->addNewObject(newObject);
}
std::sort(
map->heroesOnMap.begin(),
map->heroesOnMap.end(),
[](const ConstTransitivePtr<CGHeroInstance> & a, const ConstTransitivePtr<CGHeroInstance> & b)
{
return a->subID < b->subID;
}
);
}
void CMapLoaderH3M::readCreatureSet(CCreatureSet * out, int number)
{
for(int index = 0; index < number; ++index)
{
CreatureID creatureID = reader->readCreature();
int count = reader->readUInt16();
// Empty slot
if(creatureID == CreatureID::NONE)
continue;
auto * result = new CStackInstance();
result->count = 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), 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);
}
CGObjectInstance * CMapLoaderH3M::readHero(const int3 & mapPosition, const ObjectInstanceID & objectInstanceID)
{
auto * object = new CGHeroInstance(map->cb);
if(features.levelAB)
{
unsigned int identifier = reader->readUInt32();
map->questIdentifierToId[identifier] = objectInstanceID;
}
PlayerColor owner = reader->readPlayer();
object->subID = reader->readHero().getNum();
//If hero of this type has been predefined, use that as a base.
//Instance data will overwrite the predefined values where appropriate.
for(auto & elem : map->predefinedHeroes)
{
if(elem->subID == object->subID)
{
logGlobal->debug("Hero %d will be taken from the predefined heroes list.", object->subID);
delete object;
object = elem;
break;
}
}
setOwnerAndValidate(mapPosition, object, owner);
for(auto & elem : map->disposedHeroes)
{
if(elem.heroId == object->getHeroType())
{
object->nameCustomTextId = elem.name;
object->customPortraitSource = elem.portrait;
break;
}
}
bool hasName = reader->readBool();
if(hasName)
object->nameCustomTextId = readLocalizedString(TextIdentifier("heroes", object->getHeroType().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, 7);
object->formation = static_cast<EArmyFormation>(reader->readInt8Checked(0, 1));
assert(object->formation == EArmyFormation::LOOSE || object->formation == EArmyFormation::TIGHT);
loadArtifactsOfHero(object);
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)), nullptr);
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->getHeroType().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, VLC->heroh->getById(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);
return object;
}
CGObjectInstance * CMapLoaderH3M::readSeerHut(const int3 & position, const ObjectInstanceID & idToBeGiven)
{
auto * hut = new 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, position, idToBeGiven);
if(features.levelHOTA3)
{
uint32_t repeateableQuestsCount = reader->readUInt32();
hut->quest->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, 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->quest->mission.artifacts.push_back(artID);
missionType = EQuestMission::ARTIFACT;
}
hut->quest->lastDay = -1; //no timeout
hut->quest->isCustomFirst = false;
hut->quest->isCustomNext = false;
hut->quest->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.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::MORALE, BonusSource::OBJECT_INSTANCE, reader->readInt8Checked(-3, 3), BonusSourceID(idToBeGiven));
break;
}
case ESeerHutRewardType::LUCK:
{
reward.bonuses.emplace_back(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:
{
reward.artifacts.push_back(reader->readArtifact());
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->quest->mission.primary[x] = reader->readUInt8();
}
break;
}
case EQuestMission::LEVEL:
{
guard->quest->mission.heroLevel = reader->readUInt32();
break;
}
case EQuestMission::KILL_HERO:
case EQuestMission::KILL_CREATURE:
{
guard->quest->killTarget = ObjectInstanceID(reader->readUInt32());
break;
}
case EQuestMission::ARTIFACT:
{
size_t artNumber = reader->readUInt8();
for(size_t yy = 0; yy < artNumber; ++yy)
{
auto artid = reader->readArtifact();
guard->quest->mission.artifacts.push_back(artid);
map->allowedArtifact.erase(artid); //these are unavailable for random generation
}
break;
}
case EQuestMission::ARMY:
{
size_t typeNumber = reader->readUInt8();
guard->quest->mission.creatures.resize(typeNumber);
for(size_t hh = 0; hh < typeNumber; ++hh)
{
guard->quest->mission.creatures[hh].type = reader->readCreature().toCreature();
guard->quest->mission.creatures[hh].count = reader->readUInt16();
}
break;
}
case EQuestMission::RESOURCES:
{
for(int x = 0; x < 7; ++x)
guard->quest->mission.resources[x] = reader->readUInt32();
break;
}
case EQuestMission::HERO:
{
guard->quest->mission.heroes.push_back(reader->readHero());
break;
}
case EQuestMission::PLAYER:
{
guard->quest->mission.players.push_back(reader->readPlayer());
break;
}
case EQuestMission::HOTA_MULTI:
{
uint32_t missionSubID = reader->readUInt32();
if(missionSubID == 0)
{
missionId = EQuestMission::HOTA_HERO_CLASS;
std::set<HeroClassID> heroClasses;
reader->readBitmaskHeroClassesSized(heroClasses, false);
for(auto & hc : heroClasses)
guard->quest->mission.heroClasses.push_back(hc);
break;
}
if(missionSubID == 1)
{
missionId = EQuestMission::HOTA_REACH_DATE;
guard->quest->mission.daysPassed = reader->readUInt32() + 1;
break;
}
break;
}
default:
{
assert(0);
}
}
guard->quest->lastDay = reader->readInt32();
guard->quest->firstVisitText.appendTextID(readLocalizedString(TextIdentifier("quest", position.x, position.y, position.z, "firstVisit")));
guard->quest->nextVisitText.appendTextID(readLocalizedString(TextIdentifier("quest", position.x, position.y, position.z, "nextVisit")));
guard->quest->completedText.appendTextID(readLocalizedString(TextIdentifier("quest", position.x, position.y, position.z, "completed")));
guard->quest->isCustomFirst = !guard->quest->firstVisitText.empty();
guard->quest->isCustomNext = !guard->quest->nextVisitText.empty();
guard->quest->isCustomComplete = !guard->quest->completedText.empty();
return missionId;
}
CGObjectInstance * CMapLoaderH3M::readTown(const int3 & position, std::shared_ptr<const ObjectTemplate> objectTemplate)
{
auto * object = new CGTownInstance(map->cb);
if(features.levelAB)
object->identifier = reader->readUInt32();
setOwnerAndValidate(position, object, 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, 7);
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 = VLC->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)
{
// TODO: HOTA support
[[maybe_unused]] bool spellResearchAvailable = reader->readBool();
}
// Read castle events
uint32_t eventsCount = reader->readUInt32();
for(int eventID = 0; eventID < eventsCount; ++eventID)
{
CCastleEvent event;
event.name = readBasicString();
event.message.appendTextID(readLocalizedString(TextIdentifier("town", position.x, position.y, position.z, "event", eventID, "description")));
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->readUInt8();
reader->skipZero(17);
// New buildings
reader->readBitmaskBuildings(event.buildings, faction);
event.creatures.resize(7);
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("%s - Alignment of town at %s is invalid! Player %d is not present on map!", mapName, position.toString(), int(alignment));
}
else
{
// TODO: HOTA support
uint8_t invertedAlignment = alignment - PlayerColor::PLAYER_LIMIT.getNum();
if(invertedAlignment < PlayerColor::PLAYER_LIMIT.getNum())
{
logGlobal->warn("%s - Alignment of town at %s 'not as player %d' is not implemented!", mapName, position.toString(), alignment - PlayerColor::PLAYER_LIMIT.getNum());
}
else
{
logGlobal->warn("%s - Alignment of town at %s is corrupted!!", mapName, position.toString());
}
}
}
}
reader->skipZero(3);
return object;
}
void CMapLoaderH3M::readEvents()
{
uint32_t eventsCount = reader->readUInt32();
for(int eventID = 0; eventID < eventsCount; ++eventID)
{
CMapEvent event;
event.name = readBasicString();
event.message.appendTextID(readLocalizedString(TextIdentifier("event", eventID, "description")));
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->readUInt8();
reader->skipZero(17);
map->events.push_back(event);
}
}
void CMapLoaderH3M::readMessageAndGuards(MetaString & message, CCreatureSet * guards, const int3 & position)
{
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, 7);
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.valid() && map->isInTheMap(posOfMainTown))
{
const TerrainTile & t = map->getTile(posOfMainTown);
const CGObjectInstance * mainTown = nullptr;
for(auto * obj : t.visitableObjects)
{
if(obj->ID == Obj::TOWN || obj->ID == Obj::RANDOM_TOWN)
{
mainTown = obj;
break;
}
}
if(mainTown == nullptr)
continue;
p.posOfMainTown = posOfMainTown + mainTown->getVisitableOffset();
}
}
map->resolveQuestIdentifiers();
}
VCMI_LIB_NAMESPACE_END