feat(zone): read zone files of every version the client reads

Zone files older than version 30 were refused. The client reads them
(1.10.64):
- LuzFile::ReadLUZFile reads a version below 20 as 20;
- LuzReader::ReadScenes reads a scene of a version 20-29 file as a bare
  u32 SceneTable ID, with no file name;
- ZoneLoader::ReadZoneFile takes those scenes in SceneTable ID order,
  looks each one up in the CDClient SceneTable and, when there is a
  row, makes it a scene with the next index (0, 1, ...; -1 past 255) as
  its ID and the row's sceneName as its file; a scene with no row is
  left out.

ZoneFile now reads these files (FileFormatVersion::Oldest = 20 for
anything older), keeps the SceneTable ID on ZoneScene::sceneTableID,
and ZoneFile::ResolveSceneTable does the lookup the client does; the
world looks the names up in the CDClient SceneTable.

The only such file on disk (version 20) now reads.

Check in game: nothing to check on live worlds (all are version 36+).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-29 05:10:07 -05:00
parent 00b62edfa0
commit d4fc595e2d
4 changed files with 75 additions and 10 deletions

View File

@@ -2,8 +2,8 @@
#include <algorithm>
#include <cctype>
#include <cstdint>
#include <istream>
#include <stdexcept>
#include <string>
#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<uint32_t>(fileFormatVersion)) + " is older than " + std::to_string(static_cast<uint32_t>(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<uint8_t>(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<std::optional<std::string>(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<ZoneScene> 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);

View File

@@ -1,6 +1,8 @@
#pragma once
#include <cstdint>
#include <functional>
#include <optional>
#include <iosfwd>
#include <map>
#include <string>
@@ -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<std::optional<std::string>(uint32_t)>& sceneName);
private:
void ReadScene(std::istream& file);
void ReadZoneBoundaries(std::istream& file);

View File

@@ -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<std::string> {
auto query = CDClientDatabase::CreatePreppedStmt("SELECT sceneName FROM SceneTable WHERE sceneID = ?;");
query.bind(1, static_cast<int32_t>(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<LWOMAPID>(m_WorldID) != m_ZoneID.GetMapID()) LOG("WorldID: %i doesn't match MapID %i! Is this intended?", m_WorldID, m_ZoneID.GetMapID());

View File

@@ -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<uint32_t>(20).Put<uint32_t>(53).Put<uint8_t>(1).Put<uint32_t>(53).Put<uint8_t>(0).Text(".raw");
w.Put<uint32_t>(12).Put<uint32_t>(53).Put<uint8_t>(4); // version, world, scene count
w.Put<uint32_t>(9).Put<uint32_t>(3).Put<uint32_t>(7).Put<uint32_t>(3); // SceneTable IDs
w.Put<uint8_t>(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<char>::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<std::string> {
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