Browse Source

Fix level conversion when migrating saves between Diablo and Hellfire

pull/7103/head
staphen 2 years ago committed by Anders Jenbo
parent
commit
eb5e32aee5
  1. 405
      Source/loadsave.cpp

405
Source/loadsave.cpp

@ -231,6 +231,16 @@ public:
}
};
struct MonsterConversionData {
int8_t monsterLevel;
uint16_t experience;
uint8_t toHitSpecial;
};
struct LevelConversionData {
MonsterConversionData monsterConversionData[MaxMonsters];
};
void LoadItemData(LoadHelper &file, Item &item)
{
item._iSeed = file.NextLE<uint32_t>();
@ -577,7 +587,7 @@ void LoadPlayer(LoadHelper &file, Player &player)
bool gbSkipSync = false;
void LoadMonster(LoadHelper *file, Monster &monster)
void LoadMonster(LoadHelper *file, Monster &monster, MonsterConversionData *monsterConversionData = nullptr)
{
monster.levelType = file->NextLE<int32_t>();
monster.mode = static_cast<MonsterMode>(file->NextLE<int32_t>());
@ -639,9 +649,15 @@ void LoadMonster(LoadHelper *file, Monster &monster)
monster.corpseId = file->NextLE<int8_t>();
monster.whoHit = file->NextLE<int8_t>();
file->Skip(1); // Skip level - now calculated on the fly
file->Skip(1); // Alignment
file->Skip(2); // Skip exp - now calculated from monstdat when the monster dies
if (monsterConversionData != nullptr)
monsterConversionData->monsterLevel = file->NextLE<int8_t>();
else
file->Skip(1); // Skip level - now calculated on the fly
file->Skip(1); // Alignment
if (monsterConversionData != nullptr)
monsterConversionData->experience = file->NextLE<uint16_t>();
else
file->Skip(2); // Skip exp - now calculated from monstdat when the monster dies
if (monster.isPlayerMinion()) // Don't skip for golems
monster.toHit = file->NextLE<uint8_t>();
@ -649,7 +665,10 @@ void LoadMonster(LoadHelper *file, Monster &monster)
file->Skip(1); // Skip hit as it's already initialized
monster.minDamage = file->NextLE<uint8_t>();
monster.maxDamage = file->NextLE<uint8_t>();
file->Skip(1); // Skip toHitSpecial as it's already initialized
if (monsterConversionData != nullptr)
monsterConversionData->toHitSpecial = file->NextLE<uint8_t>();
else
file->Skip(1); // Skip toHitSpecial as it's already initialized
monster.minDamageSpecial = file->NextLE<uint8_t>();
monster.maxDamageSpecial = file->NextLE<uint8_t>();
monster.armorClass = file->NextLE<uint8_t>();
@ -1374,7 +1393,7 @@ void SavePlayer(SaveHelper &file, const Player &player)
// Omit pointer pReserved
}
void SaveMonster(SaveHelper *file, Monster &monster)
void SaveMonster(SaveHelper *file, Monster &monster, MonsterConversionData *monsterConversionData = nullptr)
{
file->WriteLE<int32_t>(monster.levelType);
file->WriteLE<int32_t>(static_cast<int>(monster.mode));
@ -1446,14 +1465,23 @@ void SaveMonster(SaveHelper *file, Monster &monster)
file->WriteLE<int8_t>(monster.corpseId);
file->WriteLE<int8_t>(monster.whoHit);
file->WriteLE<int8_t>(static_cast<int8_t>(monster.level(sgGameInitInfo.nDifficulty)));
if (monsterConversionData != nullptr)
file->WriteLE<int8_t>(monsterConversionData->monsterLevel);
else
file->WriteLE<int8_t>(static_cast<int8_t>(monster.level(sgGameInitInfo.nDifficulty)));
file->Skip(1); // Alignment
file->WriteLE<uint16_t>(static_cast<uint16_t>(std::min<unsigned>(std::numeric_limits<uint16_t>::max(), monster.exp(sgGameInitInfo.nDifficulty))));
if (monsterConversionData != nullptr)
file->WriteLE<uint16_t>(monsterConversionData->experience);
else
file->WriteLE<uint16_t>(static_cast<uint16_t>(std::min<unsigned>(std::numeric_limits<uint16_t>::max(), monster.exp(sgGameInitInfo.nDifficulty))));
file->WriteLE<uint8_t>(static_cast<uint8_t>(std::min<uint16_t>(monster.toHit, std::numeric_limits<uint8_t>::max()))); // For backwards compatibility
file->WriteLE<uint8_t>(monster.minDamage);
file->WriteLE<uint8_t>(monster.maxDamage);
file->WriteLE<uint8_t>(static_cast<uint8_t>(std::min<uint16_t>(monster.toHitSpecial(sgGameInitInfo.nDifficulty), std::numeric_limits<uint8_t>::max()))); // For backwards compatibility
if (monsterConversionData != nullptr)
file->WriteLE<uint8_t>(monsterConversionData->toHitSpecial);
else
file->WriteLE<uint8_t>(static_cast<uint8_t>(std::min<uint16_t>(monster.toHitSpecial(sgGameInitInfo.nDifficulty), std::numeric_limits<uint8_t>::max()))); // For backwards compatibility
file->WriteLE<uint8_t>(monster.minDamageSpecial);
file->WriteLE<uint8_t>(monster.maxDamageSpecial);
file->WriteLE<uint8_t>(monster.armorClass);
@ -1786,6 +1814,187 @@ void LoadLevelSeeds()
}
}
void SaveLevel(SaveWriter &saveWriter, LevelConversionData *levelConversionData)
{
Player &myPlayer = *MyPlayer;
DoUnVision(myPlayer.position.tile, myPlayer._pLightRad); // fix for vision staying on the level
if (leveltype == DTYPE_TOWN)
DungeonSeeds[0] = AdvanceRndSeed();
char szName[MaxMpqPathSize];
GetTempLevelNames(szName);
SaveHelper file(saveWriter, szName, 256 * 1024);
if (leveltype != DTYPE_TOWN) {
for (int j = 0; j < MAXDUNY; j++) {
for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert)
file.WriteLE<int8_t>(dCorpse[i][j]);
}
}
file.WriteBE(static_cast<int32_t>(ActiveMonsterCount));
file.WriteBE<int32_t>(ActiveItemCount);
file.WriteBE<int32_t>(ActiveObjectCount);
if (leveltype != DTYPE_TOWN) {
for (unsigned monsterId : ActiveMonsters)
file.WriteBE<uint32_t>(monsterId);
for (size_t i = 0; i < ActiveMonsterCount; i++) {
MonsterConversionData *monsterConversionData = nullptr;
if (levelConversionData != nullptr)
monsterConversionData = &levelConversionData->monsterConversionData[ActiveMonsters[i]];
SaveMonster(&file, Monsters[ActiveMonsters[i]], monsterConversionData);
}
for (int objectId : ActiveObjects)
file.WriteLE<int8_t>(objectId);
for (int objectId : AvailableObjects)
file.WriteLE<int8_t>(objectId);
for (int i = 0; i < ActiveObjectCount; i++) {
SaveObject(file, Objects[ActiveObjects[i]]);
}
}
auto itemIndexes = SaveDroppedItems(file);
for (int j = 0; j < MAXDUNY; j++) {
for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert)
file.WriteLE<uint8_t>(static_cast<uint8_t>(dFlags[i][j] & DungeonFlag::SavedFlags));
}
SaveDroppedItemLocations(file, itemIndexes);
if (leveltype != DTYPE_TOWN) {
for (int j = 0; j < MAXDUNY; j++) {
for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert)
file.WriteBE<int32_t>(dMonster[i][j]);
}
for (int j = 0; j < MAXDUNY; j++) {
for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert)
file.WriteLE<int8_t>(dObject[i][j]);
}
for (int j = 0; j < MAXDUNY; j++) {
for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert)
file.WriteLE<uint8_t>(dLight[i][j]);
}
for (int j = 0; j < MAXDUNY; j++) {
for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert)
file.WriteLE<uint8_t>(dPreLight[i][j]);
}
for (int j = 0; j < DMAXY; j++) {
for (int i = 0; i < DMAXX; i++) // NOLINT(modernize-loop-convert)
file.WriteLE<uint8_t>(AutomapView[i][j]);
}
}
if (!setlevel)
myPlayer._pLvlVisited[currlevel] = true;
else
myPlayer._pSLvlVisited[setlvlnum] = true;
}
void LoadLevel(LevelConversionData *levelConversionData)
{
char szName[MaxMpqPathSize];
std::optional<SaveReader> archive = OpenSaveArchive(gSaveNumber);
GetTempLevelNames(szName);
if (!archive || !archive->HasFile(szName))
GetPermLevelNames(szName);
LoadHelper file(std::move(archive), szName);
if (!file.IsValid())
app_fatal(_("Unable to open save file archive"));
if (leveltype != DTYPE_TOWN) {
for (int j = 0; j < MAXDUNY; j++) {
for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert)
dCorpse[i][j] = file.NextLE<int8_t>();
}
MoveLightsToCorpses();
}
ActiveMonsterCount = file.NextBE<int32_t>();
auto savedItemCount = file.NextBE<uint32_t>();
ActiveObjectCount = file.NextBE<int32_t>();
if (leveltype != DTYPE_TOWN) {
for (unsigned &monsterId : ActiveMonsters)
monsterId = file.NextBE<uint32_t>();
for (size_t i = 0; i < ActiveMonsterCount; i++) {
Monster &monster = Monsters[ActiveMonsters[i]];
MonsterConversionData *monsterConversionData = nullptr;
if (levelConversionData != nullptr)
monsterConversionData = &levelConversionData->monsterConversionData[ActiveMonsters[i]];
LoadMonster(&file, monster, monsterConversionData);
if (monster.isUnique() && monster.lightId != NO_LIGHT)
Lights[monster.lightId].isInvalid = false;
}
if (!gbSkipSync) {
for (size_t i = 0; i < ActiveMonsterCount; i++)
SyncMonsterAnim(Monsters[ActiveMonsters[i]]);
}
for (int &objectId : ActiveObjects)
objectId = file.NextLE<int8_t>();
for (int &objectId : AvailableObjects)
objectId = file.NextLE<int8_t>();
for (int i = 0; i < ActiveObjectCount; i++)
LoadObject(file, Objects[ActiveObjects[i]]);
if (!gbSkipSync) {
for (int i = 0; i < ActiveObjectCount; i++)
SyncObjectAnim(Objects[ActiveObjects[i]]);
}
}
LoadDroppedItems(file, savedItemCount);
for (int j = 0; j < MAXDUNY; j++) {
for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert)
dFlags[i][j] = static_cast<DungeonFlag>(file.NextLE<uint8_t>()) & DungeonFlag::LoadedFlags;
}
// skip dItem indexes, this gets populated in LoadDroppedItems
file.Skip<uint8_t>(MAXDUNX * MAXDUNY);
if (leveltype != DTYPE_TOWN) {
for (int j = 0; j < MAXDUNY; j++) {
for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert)
dMonster[i][j] = file.NextBE<int32_t>();
}
for (int j = 0; j < MAXDUNY; j++) {
for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert)
dObject[i][j] = file.NextLE<int8_t>();
}
file.Skip<uint8_t>(MAXDUNY * MAXDUNX); // dLight
for (int j = 0; j < MAXDUNY; j++) {
for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert)
dPreLight[i][j] = file.NextLE<uint8_t>();
}
for (int j = 0; j < DMAXY; j++) {
for (int i = 0; i < DMAXX; i++) { // NOLINT(modernize-loop-convert)
const auto automapView = static_cast<MapExplorationType>(file.NextLE<uint8_t>());
AutomapView[i][j] = automapView == MAP_EXP_OLD ? MAP_EXP_SELF : automapView;
}
}
// No need to load dLight, we can recreate it accurately from LightList
memcpy(dLight, dPreLight, sizeof(dLight)); // resets the light on entering a level to get rid of incorrect light
ChangeLightXY(Players[MyPlayerId].lightId, Players[MyPlayerId].position.tile); // forces player light refresh
} else {
memset(dLight, 0, sizeof(dLight));
}
if (!gbSkipSync) {
AutomapZoomReset();
ResyncQuests();
RedoMissileFlags();
UpdateLighting = true;
}
for (Player &player : Players) {
if (player.plractive && player.isOnActiveLevel())
Lights[player.lightId].hasChanged = true;
}
}
const int DiabloItemSaveSize = 368;
const int HellfireItemSaveSize = 372;
@ -1809,8 +2018,9 @@ void ConvertLevels(SaveWriter &saveWriter)
leveltype = GetLevelType(currlevel);
LoadLevel();
SaveLevel(saveWriter);
LevelConversionData levelConversionData;
LoadLevel(&levelConversionData);
SaveLevel(saveWriter, &levelConversionData);
}
setlevel = true; // Convert quest levels
@ -1828,8 +2038,9 @@ void ConvertLevels(SaveWriter &saveWriter)
if (!LevelFileExists(saveWriter))
continue;
LoadLevel();
SaveLevel(saveWriter);
LevelConversionData levelConversionData;
LoadLevel(&levelConversionData);
SaveLevel(saveWriter, &levelConversionData);
}
gbSkipSync = false;
@ -2579,176 +2790,12 @@ void SaveGame()
void SaveLevel(SaveWriter &saveWriter)
{
Player &myPlayer = *MyPlayer;
DoUnVision(myPlayer.position.tile, myPlayer._pLightRad); // fix for vision staying on the level
if (leveltype == DTYPE_TOWN)
DungeonSeeds[0] = AdvanceRndSeed();
char szName[MaxMpqPathSize];
GetTempLevelNames(szName);
SaveHelper file(saveWriter, szName, 256 * 1024);
if (leveltype != DTYPE_TOWN) {
for (int j = 0; j < MAXDUNY; j++) {
for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert)
file.WriteLE<int8_t>(dCorpse[i][j]);
}
}
file.WriteBE(static_cast<int32_t>(ActiveMonsterCount));
file.WriteBE<int32_t>(ActiveItemCount);
file.WriteBE<int32_t>(ActiveObjectCount);
if (leveltype != DTYPE_TOWN) {
for (unsigned monsterId : ActiveMonsters)
file.WriteBE<uint32_t>(monsterId);
for (size_t i = 0; i < ActiveMonsterCount; i++)
SaveMonster(&file, Monsters[ActiveMonsters[i]]);
for (int objectId : ActiveObjects)
file.WriteLE<int8_t>(objectId);
for (int objectId : AvailableObjects)
file.WriteLE<int8_t>(objectId);
for (int i = 0; i < ActiveObjectCount; i++) {
SaveObject(file, Objects[ActiveObjects[i]]);
}
}
auto itemIndexes = SaveDroppedItems(file);
for (int j = 0; j < MAXDUNY; j++) {
for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert)
file.WriteLE<uint8_t>(static_cast<uint8_t>(dFlags[i][j] & DungeonFlag::SavedFlags));
}
SaveDroppedItemLocations(file, itemIndexes);
if (leveltype != DTYPE_TOWN) {
for (int j = 0; j < MAXDUNY; j++) {
for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert)
file.WriteBE<int32_t>(dMonster[i][j]);
}
for (int j = 0; j < MAXDUNY; j++) {
for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert)
file.WriteLE<int8_t>(dObject[i][j]);
}
for (int j = 0; j < MAXDUNY; j++) {
for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert)
file.WriteLE<uint8_t>(dLight[i][j]);
}
for (int j = 0; j < MAXDUNY; j++) {
for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert)
file.WriteLE<uint8_t>(dPreLight[i][j]);
}
for (int j = 0; j < DMAXY; j++) {
for (int i = 0; i < DMAXX; i++) // NOLINT(modernize-loop-convert)
file.WriteLE<uint8_t>(AutomapView[i][j]);
}
}
if (!setlevel)
myPlayer._pLvlVisited[currlevel] = true;
else
myPlayer._pSLvlVisited[setlvlnum] = true;
SaveLevel(saveWriter, nullptr);
}
void LoadLevel()
{
char szName[MaxMpqPathSize];
std::optional<SaveReader> archive = OpenSaveArchive(gSaveNumber);
GetTempLevelNames(szName);
if (!archive || !archive->HasFile(szName))
GetPermLevelNames(szName);
LoadHelper file(std::move(archive), szName);
if (!file.IsValid())
app_fatal(_("Unable to open save file archive"));
if (leveltype != DTYPE_TOWN) {
for (int j = 0; j < MAXDUNY; j++) {
for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert)
dCorpse[i][j] = file.NextLE<int8_t>();
}
MoveLightsToCorpses();
}
ActiveMonsterCount = file.NextBE<int32_t>();
auto savedItemCount = file.NextBE<uint32_t>();
ActiveObjectCount = file.NextBE<int32_t>();
if (leveltype != DTYPE_TOWN) {
for (unsigned &monsterId : ActiveMonsters)
monsterId = file.NextBE<uint32_t>();
for (size_t i = 0; i < ActiveMonsterCount; i++) {
Monster &monster = Monsters[ActiveMonsters[i]];
LoadMonster(&file, monster);
if (monster.isUnique() && monster.lightId != NO_LIGHT)
Lights[monster.lightId].isInvalid = false;
}
if (!gbSkipSync) {
for (size_t i = 0; i < ActiveMonsterCount; i++)
SyncMonsterAnim(Monsters[ActiveMonsters[i]]);
}
for (int &objectId : ActiveObjects)
objectId = file.NextLE<int8_t>();
for (int &objectId : AvailableObjects)
objectId = file.NextLE<int8_t>();
for (int i = 0; i < ActiveObjectCount; i++)
LoadObject(file, Objects[ActiveObjects[i]]);
if (!gbSkipSync) {
for (int i = 0; i < ActiveObjectCount; i++)
SyncObjectAnim(Objects[ActiveObjects[i]]);
}
}
LoadDroppedItems(file, savedItemCount);
for (int j = 0; j < MAXDUNY; j++) {
for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert)
dFlags[i][j] = static_cast<DungeonFlag>(file.NextLE<uint8_t>()) & DungeonFlag::LoadedFlags;
}
// skip dItem indexes, this gets populated in LoadDroppedItems
file.Skip<uint8_t>(MAXDUNX * MAXDUNY);
if (leveltype != DTYPE_TOWN) {
for (int j = 0; j < MAXDUNY; j++) {
for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert)
dMonster[i][j] = file.NextBE<int32_t>();
}
for (int j = 0; j < MAXDUNY; j++) {
for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert)
dObject[i][j] = file.NextLE<int8_t>();
}
file.Skip<uint8_t>(MAXDUNY * MAXDUNX); // dLight
for (int j = 0; j < MAXDUNY; j++) {
for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert)
dPreLight[i][j] = file.NextLE<uint8_t>();
}
for (int j = 0; j < DMAXY; j++) {
for (int i = 0; i < DMAXX; i++) { // NOLINT(modernize-loop-convert)
const auto automapView = static_cast<MapExplorationType>(file.NextLE<uint8_t>());
AutomapView[i][j] = automapView == MAP_EXP_OLD ? MAP_EXP_SELF : automapView;
}
}
// No need to load dLight, we can recreate it accurately from LightList
memcpy(dLight, dPreLight, sizeof(dLight)); // resets the light on entering a level to get rid of incorrect light
ChangeLightXY(Players[MyPlayerId].lightId, Players[MyPlayerId].position.tile); // forces player light refresh
} else {
memset(dLight, 0, sizeof(dLight));
}
if (!gbSkipSync) {
AutomapZoomReset();
ResyncQuests();
RedoMissileFlags();
UpdateLighting = true;
}
for (Player &player : Players) {
if (player.plractive && player.isOnActiveLevel())
Lights[player.lightId].hasChanged = true;
}
LoadLevel(nullptr);
}
} // namespace devilution

Loading…
Cancel
Save