1
0
mirror of https://github.com/vcmi/vcmi.git synced 2025-06-17 00:07:41 +02:00

Fix and cleanup event loading, clarify remaining hota keys

This commit is contained in:
Ivan Savenko
2025-02-13 17:53:03 +00:00
parent f9fe8fc312
commit 0337ef90ec
3 changed files with 88 additions and 76 deletions

View File

@ -166,7 +166,7 @@ MapFormatFeaturesH3M MapFormatFeaturesH3M::getFeaturesHOTA(uint32_t hotaVersion)
result.creaturesCount = 186; // + 16 Factory result.creaturesCount = 186; // + 16 Factory
result.artifactsCount = 166; // +pendant of reflection, +sleepkeeper result.artifactsCount = 166; // +pendant of reflection, +sleepkeeper
result.heroesCount = 198; // + 16 Factory, +3 campaign result.heroesCount = 198; // + 16 Factory, +3 campaign
result.heroesPortraitsCount = 208; // + 16 Factory, +10 campaign result.heroesPortraitsCount = 228; // + 16 Factory, +A LOT campaign
result.heroesBytes = 25; result.heroesBytes = 25;
} }

View File

@ -201,7 +201,7 @@ void CMapLoaderH3M::readHeader()
logGlobal->warn("Map '%s': Expected %d factions, but %d found!", mapName, features.factionsCount, townTypesCount); logGlobal->warn("Map '%s': Expected %d factions, but %d found!", mapName, features.factionsCount, townTypesCount);
if (allowedDifficultiesMask != 0) if (allowedDifficultiesMask != 0)
logGlobal->warn("Map '%s': List of allowed difficulties (%d) is not implemented!", mapName, allowedDifficultiesMask); logGlobal->warn("Map '%s': List of allowed difficulties (%d) is not implemented!", mapName, static_cast<int>(allowedDifficultiesMask));
} }
if(features.levelHOTA7) if(features.levelHOTA7)
@ -1195,7 +1195,8 @@ CGObjectInstance * CMapLoaderH3M::readMonster(const int3 & mapPosition, const Ob
//type will be set during initialization //type will be set during initialization
object->putStack(SlotID(0), hlp); object->putStack(SlotID(0), hlp);
object->character = reader->readInt8Checked(0, 4); //TODO: 0-4 is h3 range. 5 is hota extension for exact aggression?
object->character = reader->readInt8Checked(0, 5);
bool hasMessage = reader->readBool(); bool hasMessage = reader->readBool();
if(hasMessage) if(hasMessage)
@ -1232,11 +1233,11 @@ CGObjectInstance * CMapLoaderH3M::readMonster(const int3 & mapPosition, const Ob
if (features.levelHOTA5) if (features.levelHOTA5)
{ {
int8_t unknownA = reader->readInt8(); bool sizeByValue = reader->readBool();//FIXME: double-check this flag effect
int32_t unknownB = reader->readInt32(); int32_t targetValue = reader->readInt32();
if (unknownA != 0 || unknownB != 0) if (sizeByValue || targetValue)
logGlobal->warn( "Map '%s': Wandering monsters %s unknown settings %d %d is not implemented!", mapName, mapPosition.toString(), unknownA, unknownB); 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; return object;
@ -1402,7 +1403,7 @@ CGObjectInstance * CMapLoaderH3M::readArtifact(const int3 & mapPosition, std::sh
assert(pickupMode == 0 || pickupMode == 1 || pickupMode == 2); // DISABLED, RANDOM, CUSTOM assert(pickupMode == 0 || pickupMode == 1 || pickupMode == 2); // DISABLED, RANDOM, CUSTOM
if (pickupMode != 0) if (pickupMode != 0)
logGlobal->debug("Map '%s': Artifact %s: not implemented pickup mode %d (flags: %d)", mapName, mapPosition.toString(), pickupMode, static_cast<int>(pickupFlags)); logGlobal->warn("Map '%s': Artifact %s: not implemented pickup mode %d (flags: %d)", mapName, mapPosition.toString(), pickupMode, static_cast<int>(pickupFlags));
} }
object->storedArtifact = ArtifactUtils::createArtifact(artID, SpellID::NONE); object->storedArtifact = ArtifactUtils::createArtifact(artID, SpellID::NONE);
@ -1465,7 +1466,7 @@ CGObjectInstance * CMapLoaderH3M::readAbandonedMine(const int3 & mapPosition)
{ {
assert(guardsMin <= guardsMax); assert(guardsMin <= guardsMax);
assert(guardsUnit.hasValue()); assert(guardsUnit.hasValue());
logGlobal->debug("Map '%s': Abandoned Mine %s: not implemented guards of %d-%d %s", mapName, mapPosition.toString(), guardsMin, guardsMax, guardsUnit.toEntity(VLC)->getJsonKey()); logGlobal->warn("Map '%s': Abandoned Mine %s: not implemented guards of %d-%d %s", mapName, mapPosition.toString(), guardsMin, guardsMax, guardsUnit.toEntity(VLC)->getJsonKey());
} }
} }
return object; return object;
@ -1561,7 +1562,7 @@ CGObjectInstance * CMapLoaderH3M::readHeroPlaceholder(const int3 & mapPosition)
bool customizedStatingUnits = reader->readBool(); bool customizedStatingUnits = reader->readBool();
if (customizedStatingUnits) if (customizedStatingUnits)
logGlobal->debug("Map '%s': Hero placeholder: not implemented option to customize starting units", mapName); logGlobal->warn("Map '%s': Hero placeholder: not implemented option to customize starting units", mapName);
for (int i = 0; i < 7; ++i) for (int i = 0; i < 7; ++i)
{ {
@ -1569,7 +1570,7 @@ CGObjectInstance * CMapLoaderH3M::readHeroPlaceholder(const int3 & mapPosition)
CreatureID unitToGive = reader->readCreature32(); CreatureID unitToGive = reader->readCreature32();
if (unitToGive.hasValue()) if (unitToGive.hasValue())
logGlobal->debug("Map '%s': Hero placeholder: not implemented option to give %d units of type %d on map start to slot %d is not implemented!", mapName, unitAmount, unitToGive.toEntity(VLC)->getJsonKey(), i); 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(VLC)->getJsonKey(), i);
} }
int32_t artifactsToGive = reader->readInt32(); int32_t artifactsToGive = reader->readInt32();
@ -1580,7 +1581,7 @@ CGObjectInstance * CMapLoaderH3M::readHeroPlaceholder(const int3 & mapPosition)
{ {
// NOTE: this might actually be 2 bytes for artifact ID + 2 bytes for spell scroll // NOTE: this might actually be 2 bytes for artifact ID + 2 bytes for spell scroll
ArtifactID startingArtifact = reader->readArtifact32(); ArtifactID startingArtifact = reader->readArtifact32();
logGlobal->debug("Map '%s': Hero placeholder: not implemented option to give hero artifact %d", mapName, startingArtifact.toEntity(VLC)->getJsonKey()); logGlobal->warn("Map '%s': Hero placeholder: not implemented option to give hero artifact %d", mapName, startingArtifact.toEntity(VLC)->getJsonKey());
} }
} }
@ -1658,7 +1659,7 @@ CGObjectInstance * CMapLoaderH3M::readBank(const int3 & mapPosition, std::shared
if(guardsPresetIndex != -1 || upgradedStackPresence != -1 || !artifacts.empty()) if(guardsPresetIndex != -1 || upgradedStackPresence != -1 || !artifacts.empty())
logGlobal->warn( logGlobal->warn(
"Map '%s: creature bank at %s settings %d %d %d are not implemented!", "Map '%s': creature bank at %s settings %d %d %d are not implemented!",
mapName, mapName,
mapPosition.toString(), mapPosition.toString(),
guardsPresetIndex, guardsPresetIndex,
@ -1690,7 +1691,7 @@ CGObjectInstance * CMapLoaderH3M::readRewardWithArtifact(const int3 & mapPositio
if (content != -1) if (content != -1)
{ {
artifact = reader->readArtifact32(); // NOTE: might be 2 byte artifact + 2 bytes scroll spell 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); logGlobal->warn("Map '%s': Object (%d) %s settings %d %d are not implemented!", mapName, objectTemplate->id, mapPosition.toString(), content, artifact);
} }
else else
reader->skipUnused(4); // garbage data, usually -1, but sometimes uninitialized reader->skipUnused(4); // garbage data, usually -1, but sometimes uninitialized
@ -1710,9 +1711,9 @@ CGObjectInstance * CMapLoaderH3M::readBlackMarket(const int3 & mapPosition, std:
if (artifact.hasValue()) if (artifact.hasValue())
{ {
if (artifact != ArtifactID::SPELL_SCROLL) if (artifact != ArtifactID::SPELL_SCROLL)
logGlobal->debug("Map '%s': Black Market at %s: option to sell artifact %s is not implemented", mapName, mapPosition.toString(), artifact.toEntity(VLC)->getJsonKey()); logGlobal->warn("Map '%s': Black Market at %s: option to sell artifact %s is not implemented", mapName, mapPosition.toString(), artifact.toEntity(VLC)->getJsonKey());
else else
logGlobal->debug("Map '%s': Black Market at %s: option to sell scroll %s is not implemented", mapName, mapPosition.toString(), spellID.toEntity(VLC)->getJsonKey()); logGlobal->warn("Map '%s': Black Market at %s: option to sell scroll %s is not implemented", mapName, mapPosition.toString(), spellID.toEntity(VLC)->getJsonKey());
} }
} }
} }
@ -1731,7 +1732,7 @@ CGObjectInstance * CMapLoaderH3M::readUniversity(const int3 & mapPosition, std::
// NOTE: check how this interacts with hota Seafaring Academy that is guaranteed to give Navigation // NOTE: check how this interacts with hota Seafaring Academy that is guaranteed to give Navigation
assert(customized == -1 || customized == 0); assert(customized == -1 || customized == 0);
if (customized != -1) if (customized != -1)
logGlobal->debug("Map '%s': University at %s: option to give specific skills out of %d is not implemented", mapName, mapPosition.toString(), allowedSkills.size()); 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); return readGeneric(mapPosition, objectTemplate);
} }
@ -1757,7 +1758,7 @@ CGObjectInstance * CMapLoaderH3M::readRewardWithArtifactAndResources(const int3
int8_t resourceB = reader->readInt8(); int8_t resourceB = reader->readInt8();
if (content != -1) if (content != -1)
logGlobal->warn("Map '%s': Campfire (%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)); 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); return readGeneric(mapPosition, objectTemplate);
} }
@ -2571,10 +2572,21 @@ CGObjectInstance * CMapLoaderH3M::readTown(const int3 & position, std::shared_pt
if(features.levelHOTA5) if(features.levelHOTA5)
{ {
// Most likely customization of special buildings per faction -> 4x11 table // Most likely customization of special buildings per faction -> 4x11 table
// TODO: is this customization of what can be built, or what is already built? // presumably:
uint32_t unknownSize = reader->readUInt32(); // 0 -> default / allowed
assert(unknownSize == 44); // 1 -> built?
reader->skipUnused(unknownSize); // 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 // Read castle events
@ -2584,41 +2596,36 @@ CGObjectInstance * CMapLoaderH3M::readTown(const int3 & position, std::shared_pt
{ {
// TODO: a lot of copy-pasted code with map event // TODO: a lot of copy-pasted code with map event
CCastleEvent event; CCastleEvent event;
event.name = readBasicString(); event.creatures.resize(7);
event.message.appendTextID(readLocalizedString(TextIdentifier("town", position.x, position.y, position.z, "event", eventID, "description")));
reader->readResources(event.resources); readEventCommon(event, TextIdentifier("town", position.x, position.y, position.z, "event", eventID, "description"));
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.levelHOTA5) if(features.levelHOTA5)
{ {
[[maybe_unused]] int32_t allowedDifficultiesMask = reader->readInt32(); int32_t creatureGrowth8 = reader->readInt32();
[[maybe_unused]] int32_t hotaLevel7b = reader->readInt32(); // ???
[[maybe_unused]] int32_t hota_amount = reader->readInt32(); // ??? // always 44
[[maybe_unused]] int16_t hota_special = reader->readInt16(); // ??? 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) if(features.levelHOTA7)
{ {
[[maybe_unused]] int32_t hotaAmount = reader->readInt32(); // amount of creatures in 8th dwelling?
[[maybe_unused]] bool neutralAffected = reader->readBool(); [[maybe_unused]] bool neutralAffected = reader->readBool();
} }
// New buildings // New buildings
reader->readBitmaskBuildings(event.buildings, faction); reader->readBitmaskBuildings(event.buildings, faction);
event.creatures.resize(7);
for(int i = 0; i < 7; ++i) for(int i = 0; i < 7; ++i)
event.creatures[i] = reader->readUInt16(); event.creatures[i] = reader->readUInt16();
@ -2639,7 +2646,7 @@ CGObjectInstance * CMapLoaderH3M::readTown(const int3 & position, std::shared_pt
if (mapHeader->players[alignment].canAnyonePlay()) if (mapHeader->players[alignment].canAnyonePlay())
object->alignmentToPlayer = PlayerColor(alignment); object->alignmentToPlayer = PlayerColor(alignment);
else else
logGlobal->warn("%s - Alignment of town at %s is invalid! Player %d is not present on map!", mapName, position.toString(), int(alignment)); logGlobal->warn("Map '%s': Alignment of town at %s is invalid! Player %d is not present on map!", mapName, position.toString(), int(alignment));
} }
else else
{ {
@ -2648,11 +2655,11 @@ CGObjectInstance * CMapLoaderH3M::readTown(const int3 & position, std::shared_pt
if(invertedAlignment < 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()); 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 else
{ {
logGlobal->warn("%s - Alignment of town at %s is corrupted!!", mapName, position.toString()); logGlobal->warn("Map '%s': Alignment of town at %s is corrupted!!", mapName, position.toString());
} }
} }
} }
@ -2662,26 +2669,19 @@ CGObjectInstance * CMapLoaderH3M::readTown(const int3 & position, std::shared_pt
return object; return object;
} }
void CMapLoaderH3M::readEvents() void CMapLoaderH3M::readEventCommon(CMapEvent & event, const TextIdentifier & messageID)
{ {
uint32_t eventsCount = reader->readUInt32();
for(int eventID = 0; eventID < eventsCount; ++eventID)
{
// FIXME: a lot of copy-pasted code with town event
CMapEvent event;
event.name = readBasicString(); event.name = readBasicString();
event.message.appendTextID(readLocalizedString(TextIdentifier("event", eventID, "description"))); event.message.appendTextID(readLocalizedString(messageID));
reader->readResources(event.resources); reader->readResources(event.resources);
reader->readBitmaskPlayers(event.players, false); reader->readBitmaskPlayers(event.players, false);
if(features.levelSOD) if(features.levelSOD)
{
event.humanAffected = reader->readBool(); event.humanAffected = reader->readBool();
}
else else
{
event.humanAffected = true; event.humanAffected = true;
}
event.computerAffected = reader->readBool(); event.computerAffected = reader->readBool();
event.firstOccurrence = reader->readUInt16(); event.firstOccurrence = reader->readUInt16();
event.nextOccurrence = reader->readUInt16(); event.nextOccurrence = reader->readUInt16();
@ -2690,15 +2690,24 @@ void CMapLoaderH3M::readEvents()
if (features.levelHOTA7) if (features.levelHOTA7)
{ {
[[maybe_unused]] int32_t difficulties = reader->readInt32(); 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);
} }
else if (features.levelHOTA5) }
void CMapLoaderH3M::readEvents()
{
uint32_t eventsCount = reader->readUInt32();
for(int eventID = 0; eventID < eventsCount; ++eventID)
{ {
[[maybe_unused]] int32_t difficulties = reader->readInt32(); CMapEvent event;
[[maybe_unused]] int32_t unknownA= reader->readInt32(); readEventCommon(event, TextIdentifier("event", eventID, "description"));
[[maybe_unused]] int32_t unknownB= reader->readInt32();
[[maybe_unused]] int16_t unknownC= reader->readInt16(); // garbage bytes that were present in HOTA5 & HOTA6
} if (features.levelHOTA5 && !features.levelHOTA7)
reader->skipUnused(14);
map->events.push_back(event); map->events.push_back(event);
} }

View File

@ -28,6 +28,7 @@ class CCreatureSet;
class CInputStream; class CInputStream;
class TextIdentifier; class TextIdentifier;
class CGPandoraBox; class CGPandoraBox;
class CMapEvent;
class ObjectInstanceID; class ObjectInstanceID;
class BuildingID; class BuildingID;
@ -230,6 +231,8 @@ private:
void readBoxContent(CGPandoraBox * object, const int3 & position, const ObjectInstanceID & idToBeGiven); void readBoxContent(CGPandoraBox * object, const int3 & position, const ObjectInstanceID & idToBeGiven);
void readBoxHotaContent(CGPandoraBox * object, const int3 & position, const ObjectInstanceID & idToBeGiven); void readBoxHotaContent(CGPandoraBox * object, const int3 & position, const ObjectInstanceID & idToBeGiven);
void readEventCommon(CMapEvent & object, const TextIdentifier & messageID);
/** /**
* Reads a quest for the given quest guard. * Reads a quest for the given quest guard.
* *