From e0e8e681417900c4df9b4ebbfaebb77cc564e64b Mon Sep 17 00:00:00 2001 From: staphen Date: Mon, 22 Apr 2024 22:55:40 -0400 Subject: [PATCH] Fix level conversion when migrating saves between Diablo and Hellfire --- Source/loadsave.cpp | 405 ++++++++++++++++++++++++-------------------- 1 file changed, 226 insertions(+), 179 deletions(-) diff --git a/Source/loadsave.cpp b/Source/loadsave.cpp index 7a1dd95389d..9182395ee88 100644 --- a/Source/loadsave.cpp +++ b/Source/loadsave.cpp @@ -231,6 +231,16 @@ class SaveHelper { } }; +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(); @@ -579,7 +589,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(); monster.mode = static_cast(file->NextLE()); @@ -641,9 +651,15 @@ void LoadMonster(LoadHelper *file, Monster &monster) monster.corpseId = file->NextLE(); monster.whoHit = file->NextLE(); - 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(); + else + file->Skip(1); // Skip level - now calculated on the fly + file->Skip(1); // Alignment + if (monsterConversionData != nullptr) + monsterConversionData->experience = file->NextLE(); + 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(); @@ -651,7 +667,10 @@ void LoadMonster(LoadHelper *file, Monster &monster) file->Skip(1); // Skip hit as it's already initialized monster.minDamage = file->NextLE(); monster.maxDamage = file->NextLE(); - file->Skip(1); // Skip toHitSpecial as it's already initialized + if (monsterConversionData != nullptr) + monsterConversionData->toHitSpecial = file->NextLE(); + else + file->Skip(1); // Skip toHitSpecial as it's already initialized monster.minDamageSpecial = file->NextLE(); monster.maxDamageSpecial = file->NextLE(); monster.armorClass = file->NextLE(); @@ -1376,7 +1395,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(monster.levelType); file->WriteLE(static_cast(monster.mode)); @@ -1448,14 +1467,23 @@ void SaveMonster(SaveHelper *file, Monster &monster) file->WriteLE(monster.corpseId); file->WriteLE(monster.whoHit); - file->WriteLE(static_cast(monster.level(sgGameInitInfo.nDifficulty))); + if (monsterConversionData != nullptr) + file->WriteLE(monsterConversionData->monsterLevel); + else + file->WriteLE(static_cast(monster.level(sgGameInitInfo.nDifficulty))); file->Skip(1); // Alignment - file->WriteLE(static_cast(std::min(std::numeric_limits::max(), monster.exp(sgGameInitInfo.nDifficulty)))); + if (monsterConversionData != nullptr) + file->WriteLE(monsterConversionData->experience); + else + file->WriteLE(static_cast(std::min(std::numeric_limits::max(), monster.exp(sgGameInitInfo.nDifficulty)))); file->WriteLE(static_cast(std::min(monster.toHit, std::numeric_limits::max()))); // For backwards compatibility file->WriteLE(monster.minDamage); file->WriteLE(monster.maxDamage); - file->WriteLE(static_cast(std::min(monster.toHitSpecial(sgGameInitInfo.nDifficulty), std::numeric_limits::max()))); // For backwards compatibility + if (monsterConversionData != nullptr) + file->WriteLE(monsterConversionData->toHitSpecial); + else + file->WriteLE(static_cast(std::min(monster.toHitSpecial(sgGameInitInfo.nDifficulty), std::numeric_limits::max()))); // For backwards compatibility file->WriteLE(monster.minDamageSpecial); file->WriteLE(monster.maxDamageSpecial); file->WriteLE(monster.armorClass); @@ -1761,6 +1789,187 @@ void LoadAdditionalMissiles() } } +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) + glSeedTbl[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(dCorpse[i][j]); + } + } + + file.WriteBE(ActiveMonsterCount); + file.WriteBE(ActiveItemCount); + file.WriteBE(ActiveObjectCount); + + if (leveltype != DTYPE_TOWN) { + for (int monsterId : ActiveMonsters) + file.WriteBE(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(objectId); + for (int objectId : AvailableObjects) + file.WriteLE(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(static_cast(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(dMonster[i][j]); + } + for (int j = 0; j < MAXDUNY; j++) { + for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert) + file.WriteLE(dObject[i][j]); + } + for (int j = 0; j < MAXDUNY; j++) { + for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert) + file.WriteLE(dLight[i][j]); + } + for (int j = 0; j < MAXDUNY; j++) { + for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert) + file.WriteLE(dPreLight[i][j]); + } + for (int j = 0; j < DMAXY; j++) { + for (int i = 0; i < DMAXX; i++) // NOLINT(modernize-loop-convert) + file.WriteLE(AutomapView[i][j]); + } + } + + if (!setlevel) + myPlayer._pLvlVisited[currlevel] = true; + else + myPlayer._pSLvlVisited[setlvlnum] = true; +} + +void LoadLevel(LevelConversionData *levelConversionData) +{ + char szName[MaxMpqPathSize]; + std::optional 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(); + } + MoveLightsToCorpses(); + } + + ActiveMonsterCount = file.NextBE(); + auto savedItemCount = file.NextBE(); + ActiveObjectCount = file.NextBE(); + + if (leveltype != DTYPE_TOWN) { + for (int &monsterId : ActiveMonsters) + monsterId = file.NextBE(); + 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(); + for (int &objectId : AvailableObjects) + objectId = file.NextLE(); + 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(file.NextLE()) & DungeonFlag::LoadedFlags; + } + + // skip dItem indexes, this gets populated in LoadDroppedItems + file.Skip(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(); + } + for (int j = 0; j < MAXDUNY; j++) { + for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert) + dObject[i][j] = file.NextLE(); + } + file.Skip(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(); + } + for (int j = 0; j < DMAXY; j++) { + for (int i = 0; i < DMAXX; i++) { // NOLINT(modernize-loop-convert) + const auto automapView = static_cast(file.NextLE()); + 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; @@ -1784,8 +1993,9 @@ void ConvertLevels(SaveWriter &saveWriter) leveltype = GetLevelType(currlevel); - LoadLevel(); - SaveLevel(saveWriter); + LevelConversionData levelConversionData; + LoadLevel(&levelConversionData); + SaveLevel(saveWriter, &levelConversionData); } setlevel = true; // Convert quest levels @@ -1803,8 +2013,9 @@ void ConvertLevels(SaveWriter &saveWriter) if (!LevelFileExists(saveWriter)) continue; - LoadLevel(); - SaveLevel(saveWriter); + LevelConversionData levelConversionData; + LoadLevel(&levelConversionData); + SaveLevel(saveWriter, &levelConversionData); } gbSkipSync = false; @@ -2551,176 +2762,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) - glSeedTbl[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(dCorpse[i][j]); - } - } - - file.WriteBE(ActiveMonsterCount); - file.WriteBE(ActiveItemCount); - file.WriteBE(ActiveObjectCount); - - if (leveltype != DTYPE_TOWN) { - for (int monsterId : ActiveMonsters) - file.WriteBE(monsterId); - for (size_t i = 0; i < ActiveMonsterCount; i++) - SaveMonster(&file, Monsters[ActiveMonsters[i]]); - for (int objectId : ActiveObjects) - file.WriteLE(objectId); - for (int objectId : AvailableObjects) - file.WriteLE(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(static_cast(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(dMonster[i][j]); - } - for (int j = 0; j < MAXDUNY; j++) { - for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert) - file.WriteLE(dObject[i][j]); - } - for (int j = 0; j < MAXDUNY; j++) { - for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert) - file.WriteLE(dLight[i][j]); - } - for (int j = 0; j < MAXDUNY; j++) { - for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert) - file.WriteLE(dPreLight[i][j]); - } - for (int j = 0; j < DMAXY; j++) { - for (int i = 0; i < DMAXX; i++) // NOLINT(modernize-loop-convert) - file.WriteLE(AutomapView[i][j]); - } - } - - if (!setlevel) - myPlayer._pLvlVisited[currlevel] = true; - else - myPlayer._pSLvlVisited[setlvlnum] = true; + SaveLevel(saveWriter, nullptr); } void LoadLevel() { - char szName[MaxMpqPathSize]; - std::optional 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(); - } - MoveLightsToCorpses(); - } - - ActiveMonsterCount = file.NextBE(); - auto savedItemCount = file.NextBE(); - ActiveObjectCount = file.NextBE(); - - if (leveltype != DTYPE_TOWN) { - for (int &monsterId : ActiveMonsters) - monsterId = file.NextBE(); - 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(); - for (int &objectId : AvailableObjects) - objectId = file.NextLE(); - 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(file.NextLE()) & DungeonFlag::LoadedFlags; - } - - // skip dItem indexes, this gets populated in LoadDroppedItems - file.Skip(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(); - } - for (int j = 0; j < MAXDUNY; j++) { - for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert) - dObject[i][j] = file.NextLE(); - } - file.Skip(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(); - } - for (int j = 0; j < DMAXY; j++) { - for (int i = 0; i < DMAXX; i++) { // NOLINT(modernize-loop-convert) - const auto automapView = static_cast(file.NextLE()); - 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