diff --git a/dCommon/ZoneFile.cpp b/dCommon/ZoneFile.cpp index c19147f3d..8f1069d79 100644 --- a/dCommon/ZoneFile.cpp +++ b/dCommon/ZoneFile.cpp @@ -2,8 +2,8 @@ #include #include +#include #include -#include #include #include "BinaryIO.h" @@ -12,10 +12,8 @@ void ZoneFile::ReadHeader(std::istream& file) { BinaryIO::BinaryRead(file, fileFormatVersion); - // Before PrePreAlpha a scene is only its ID, with no file to load it from (LuzReader::ReadScenes) - if (fileFormatVersion < FileFormatVersion::PrePreAlpha) { - throw std::runtime_error("Zone file version " + std::to_string(static_cast(fileFormatVersion)) + " is older than " + std::to_string(static_cast(FileFormatVersion::PrePreAlpha)) + ": its scenes have no files"); - } + // The client reads anything older than 20 as 20 (LuzFile::ReadLUZFile) + if (fileFormatVersion < FileFormatVersion::Oldest) fileFormatVersion = FileFormatVersion::Oldest; if (fileFormatVersion >= FileFormatVersion::Alpha) BinaryIO::BinaryRead(file, mapRevision); @@ -75,6 +73,14 @@ void ZoneFile::Read(std::istream& file) { void ZoneFile::ReadScene(std::istream& file) { ZoneScene scene; + // Before PrePreAlpha a scene is only its SceneTable ID; ResolveSceneTable finds its file (LuzReader::ReadScenes) + if (fileFormatVersion < FileFormatVersion::PrePreAlpha) { + BinaryIO::BinaryRead(file, scene.sceneTableID); + scene.id = scene.sceneTableID; + scenes.push_back(std::move(scene)); + return; + } + BinaryIO::ReadString(file, scene.filename, BinaryIO::ReadType::String); if (fileFormatVersion >= FileFormatVersion::LatePreAlpha) { @@ -98,6 +104,26 @@ void ZoneFile::ReadScene(std::istream& file) { scenes.push_back(std::move(scene)); } +void ZoneFile::ResolveSceneTable(const std::function(uint32_t)>& sceneName) { + if (fileFormatVersion >= FileFormatVersion::PrePreAlpha) return; + // ZoneLoader::ReadZoneFile: the scenes go by their SceneTable ID; each one with a row there gets the next index + // (0, 1, ...; -1 past 255) as its ID and the row's sceneName as its file, the others are left out + std::ranges::stable_sort(scenes, {}, &ZoneScene::sceneTableID); + std::vector resolved; + uint32_t index = 0; + for (auto& scene : scenes) { + if (!resolved.empty() && resolved.back().sceneTableID == scene.sceneTableID) continue; + const auto name = sceneName(scene.sceneTableID); + if (!name) continue; + scene.id = index < 256 ? index : UINT32_MAX; + scene.filename = *name; + scene.name = *name; + resolved.push_back(std::move(scene)); + index++; + } + scenes = std::move(resolved); +} + void ZoneFile::ReadZoneBoundaries(std::istream& file) { uint8_t count = 0; BinaryIO::BinaryRead(file, count); diff --git a/dCommon/ZoneFile.h b/dCommon/ZoneFile.h index d07a74cda..eac96885e 100644 --- a/dCommon/ZoneFile.h +++ b/dCommon/ZoneFile.h @@ -1,6 +1,8 @@ #pragma once #include +#include +#include #include #include #include @@ -27,6 +29,7 @@ struct WaypointCommand { struct ZoneScene { std::string filename; uint32_t id{}; + uint32_t sceneTableID{}; // before PrePreAlpha: the scene's row in the CDClient SceneTable (see ResolveSceneTable) eSceneType sceneType{}; std::string name; NiPoint3 unknown1; @@ -199,6 +202,7 @@ struct Path { struct ZoneFile { enum class FileFormatVersion : uint32_t { //Times are guessed. + Oldest = 20, // the client reads anything older as this PrePreAlpha = 30, PreAlpha = 32, LatePreAlpha = 33, @@ -243,6 +247,13 @@ struct ZoneFile { */ void ReadHeader(std::istream& file); + /** + * Before PrePreAlpha a scene is only its SceneTable ID. As the client does (ZoneLoader::ReadZoneFile), the scenes + * are taken in SceneTable ID order, each one `sceneName` finds a row for gets that row's sceneName as its file and + * name and the next index as its ID, and the others are left out. Nothing to do for newer files. + */ + void ResolveSceneTable(const std::function(uint32_t)>& sceneName); + private: void ReadScene(std::istream& file); void ReadZoneBoundaries(std::istream& file); diff --git a/dZoneManager/Zone.cpp b/dZoneManager/Zone.cpp index 4d51e128b..289c86cd2 100644 --- a/dZoneManager/Zone.cpp +++ b/dZoneManager/Zone.cpp @@ -12,6 +12,7 @@ #include "AssetManager.h" #include "CDClientManager.h" #include "CDZoneTableTable.h" +#include "CDClientDatabase.h" #include "Spawner.h" #include "dZoneManager.h" #include "dpWorld.h" @@ -56,6 +57,14 @@ void Zone::LoadZoneIntoMemory() { // The file itself is read by ZoneFile (dCommon), which the dashboard uses too ZoneFile zoneFile; zoneFile.Read(file); + // The oldest files name their scenes by SceneTable ID + zoneFile.ResolveSceneTable([](uint32_t sceneTableID) -> std::optional { + auto query = CDClientDatabase::CreatePreppedStmt("SELECT sceneName FROM SceneTable WHERE sceneID = ?;"); + query.bind(1, static_cast(sceneTableID)); + auto result = query.execQuery(); + if (result.eof() || result.fieldIsNull("sceneName")) return std::nullopt; + return std::string(result.getStringField("sceneName")); + }); m_FileFormatVersion = zoneFile.fileFormatVersion; m_WorldID = zoneFile.worldID; if (static_cast(m_WorldID) != m_ZoneID.GetMapID()) LOG("WorldID: %i doesn't match MapID %i! Is this intended?", m_WorldID, m_ZoneID.GetMapID()); diff --git a/tests/dCommonTests/ZoneFileTests.cpp b/tests/dCommonTests/ZoneFileTests.cpp index 82c9c0e5d..20e8baf8f 100644 --- a/tests/dCommonTests/ZoneFileTests.cpp +++ b/tests/dCommonTests/ZoneFileTests.cpp @@ -138,14 +138,33 @@ TEST(ZoneFileTests, LateAlphaSceneCountIsAU32) { EXPECT_EQ(zone.zoneRawPath, "zone.raw"); } -// Before version 30 a scene is only an ID, with no file to load -TEST(ZoneFileTests, VersionsBeforePrePreAlphaThrow) { +// Before version 30 a scene is only its SceneTable ID; below 20 the file reads as 20 +TEST(ZoneFileTests, ReadsVersionsBeforePrePreAlpha) { ZoneBytes w; - w.Put(20).Put(53).Put(1).Put(53).Put(0).Text(".raw"); + w.Put(12).Put(53).Put(4); // version, world, scene count + w.Put(9).Put(3).Put(7).Put(3); // SceneTable IDs + w.Put(0).Text("zone.raw"); std::istringstream stream(w.data); ZoneFile zone; - EXPECT_THROW(zone.Read(stream), std::runtime_error); - EXPECT_TRUE(zone.scenes.empty()); + zone.Read(stream); + EXPECT_FALSE(stream.fail()); + EXPECT_EQ(stream.peek(), std::char_traits::eof()); + EXPECT_EQ(zone.fileFormatVersion, ZoneFile::FileFormatVersion::Oldest); + EXPECT_EQ(zone.zoneRawPath, "zone.raw"); + ASSERT_EQ(zone.scenes.size(), 4u); + EXPECT_TRUE(zone.scenes[0].filename.empty()); + + // In SceneTable ID order, once each; 7 has no row + zone.ResolveSceneTable([](uint32_t id) -> std::optional { + if (id == 7) return std::nullopt; + return "scene" + std::to_string(id) + ".lvl"; + }); + ASSERT_EQ(zone.scenes.size(), 2u); + EXPECT_EQ(zone.scenes[0].id, 0u); + EXPECT_EQ(zone.scenes[0].filename, "scene3.lvl"); + EXPECT_EQ(zone.scenes[0].name, "scene3.lvl"); + EXPECT_EQ(zone.scenes[1].id, 1u); + EXPECT_EQ(zone.scenes[1].filename, "scene9.lvl"); } // A PrePreAlpha (30) file ends at its terrain file's name: no zone name, description, transitions or paths