fix: the zone keeps its paths after the merge, and the tests follow the merged readers

- Zone::LoadZoneIntoMemory takes the paths the zone file reader read (lost in the merge of the raw terrain loading)
- the terrain reader is main's unchanged: files from before version 32 keep their colour and texture maps as the file
  has them, and the dashboard's terrain tests expect that (bounds by the span between the first and last point too)
- the model component test counts the model block only: the item block in front of it is the ItemComponent's
- scene file tests: a file from before chunks keeps only its header, editor settings are a count of colours, and the
  tests that log have a logger
- the settings catalog lists export_terrain_to_obj

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-11-10 12:14:23 -06:00
parent ba90898b75
commit 8a1d278265
7 changed files with 38 additions and 38 deletions

View File

@@ -165,6 +165,7 @@ namespace {
c.Add(Format(Text(SHARED, "dump_folder", "Crash dump folder", "Where crash logs go. Empty turns them off.", "", true), eFormat::PATH));
c.Add(Bool(WORLD, "generate_dump", "Crash dumps from world servers", "Write a dump when a world server crashes (needs the crash dump folder).", false, true));
c.Add(Bool(WORLD, "save_lxfmls", "Save model files", "Save players' models (LXFML) to disk before they are split, for debugging.", false));
c.Add(Bool(WORLD, "export_terrain_to_obj", "Export terrain meshes", "Save each zone's terrain as terrain_<zone>.obj in the server folder when the zone loads, for debugging.", false));
c.Add(Bool(WORLD, "ghosting_scenes", "Scene ghosting", "Players get the objects of the scenes their game keeps loaded (the scene under them, the scenes connected to it and the global scene), as the client streams scenes, instead of the ones within 100 to 150 units. Needs the zone's terrain scene map; zones without one keep distance ghosting.", true, true));
c.Add(Bool(WORLD, "save_property_location", "Log back in on properties", "Logging out on a property and back in returns to that property. Off (as live): to the last world before it.", false));
c.Add(Bool(WORLD, "bbb_consume_bricks", "Brick building uses bricks", "Saving a brick by brick model uses up the bricks in it, as live did. Off: the bricks go back to the backpack.", false));

View File

@@ -157,23 +157,14 @@ NiPoint3 Chunk::GridToWorldPos(uint32_t i, uint32_t j) const {
}
chunk.colorMap.resize(colorMapPixelCount);
stream.read(reinterpret_cast<char*>(chunk.colorMap.data()), static_cast<std::streamsize>(colorMapPixelCount));
} else if (chunk.width > 0) {
// width x width BGRA pixels, of which the client keeps the (width - 1) x (width - 1) before the last row and
// column, as RGBA (RAWReadColorandLightMaps)
} else {
const size_t legacyColorBytes = static_cast<size_t>(chunk.width) * chunk.width * 4;
if (legacyColorBytes > kMaxBlobBytes) {
LOG("Chunk legacy colorMap size %zu exceeds maximum %zu bytes", legacyColorBytes, kMaxBlobBytes);
return false;
}
std::vector<uint8_t> pixels(legacyColorBytes);
stream.read(reinterpret_cast<char*>(pixels.data()), static_cast<std::streamsize>(legacyColorBytes));
chunk.colorMap.reserve(static_cast<size_t>(chunk.colorMapResolution) * chunk.colorMapResolution * 4);
for (uint32_t y = 0; y < chunk.colorMapResolution; ++y) {
for (uint32_t x = 0; x < chunk.colorMapResolution; ++x) {
const auto* bgra = &pixels[(static_cast<size_t>(y) * chunk.width + x) * 4];
chunk.colorMap.insert(chunk.colorMap.end(), { bgra[2], bgra[1], bgra[0], bgra[3] });
}
}
chunk.colorMap.resize(legacyColorBytes);
stream.read(reinterpret_cast<char*>(chunk.colorMap.data()), static_cast<std::streamsize>(legacyColorBytes));
}
if (stream.fail()) {
@@ -211,10 +202,6 @@ NiPoint3 Chunk::GridToWorldPos(uint32_t i, uint32_t j) const {
}
chunk.textureMap.resize(textureMapPixelCount);
stream.read(reinterpret_cast<char*>(chunk.textureMap.data()), static_cast<std::streamsize>(textureMapPixelCount));
// Before version 32 the pixels are BGRA; the client keeps them as RGBA (0x0103aaf0)
if (version < 32) {
for (size_t i = 0; i + 3 < chunk.textureMap.size(); i += 4) std::swap(chunk.textureMap[i], chunk.textureMap[i + 2]);
}
if (stream.fail()) {
return false;

View File

@@ -93,6 +93,10 @@ void Zone::LoadZoneIntoMemory() {
m_Boundaries = std::move(zoneFile.zoneBoundaries);
m_PathDataLength = zoneFile.pathDataLength;
m_PathChunkVersion = zoneFile.pathChunkVersion;
m_Paths = std::move(zoneFile.paths);
// The terrain gives the scene at a position. Like the client, a missing or unreadable .raw is logged
// and the zone loads without it.
const auto rawPath = m_ZoneFilePath.substr(0, m_ZoneFilePath.rfind('/') + 1) + m_ZoneRawPath;

View File

@@ -451,10 +451,11 @@ TEST(LevelFileTests, ReadsFilesWithoutChunks) {
ASSERT_EQ(level.objects.size(), 1u);
EXPECT_EQ(level.objects[0].position.x, 3.0f);
EXPECT_EQ(level.objects[0].settings.find(u"respawnname")->second->GetValueAsString(), "NS_GF");
EXPECT_EQ(level.chunkHeaders.count(LevelFile::SceneObjectData), 1u);
// A file from before chunks has only its header, kept as the file info
EXPECT_EQ(level.chunkHeaders.count(LevelFile::FileInfo), 1u);
}
// An object's render techniques (from version 7) are skipped; the fields a file has no room for get the client's defaults
// An object's render techniques (from version 7) are kept; the fields a file has no room for get the client's defaults
TEST(LevelFileTests, ReadsObjectsAsTheClient) {
ZoneBytes w;
w.Put<uint16_t>(30).Put<uint16_t>(0);
@@ -477,15 +478,15 @@ TEST(LevelFileTests, ReadsObjectsAsTheClient) {
EXPECT_EQ(level.objects[1].position.x, 5.0f);
}
// Editor settings are skipped by their size; a file older than version 3 has no objects for the client
TEST(LevelFileTests, SkipsEditorSettingsBySize) {
// Editor settings are a count of colours; a file older than version 3 has no objects for the client
TEST(LevelFileTests, ReadsEditorSettings) {
ZoneBytes w;
w.Put<uint16_t>(37).Put<uint16_t>(0).Put<uint8_t>(0).Put<uint32_t>(4); // version, type, important, revision
w.data.append(48 + 8 + 12 + 12 + 12 + 16, '\0'); // lighting
w.Put<uint32_t>(0); // skydome
for (int i = 0; i < 5; i++) w.Put<uint32_t>(0);
w.Put<uint32_t>(7);
w.data.append(7, '\x55'); // editor settings
w.Put<uint32_t>(4 + 12).Put<uint32_t>(1); // editor settings: their size, one colour
w.Put(0.25f).Put(0.5f).Put(0.75f);
w.Put<uint32_t>(1);
w.Object(37, 4945, 3, "a=0:b");

View File

@@ -29,8 +29,8 @@ protected:
}
};
// Item block (1 + 64 + 32 + 1), model block (1 + 1 + 32 + 96 + 128)
constexpr uint32_t ModelBlockBits = 98 + 258;
// Model block (1 + 1 + 32 + 96 + 128); the item block in front of it is the ItemComponent's
constexpr uint32_t ModelBlockBits = 258;
// Behavior info: flag, count, paused, and on the initial update the editing info flag
constexpr uint32_t BehaviorBits = 1 + 32 + 1 + 1;

View File

@@ -5,6 +5,8 @@
#include <cstring>
#include <sstream>
#include "GameDependencies.h"
namespace {
template<typename T>
void Write(std::ostream& stream, const T& value) {
@@ -220,7 +222,13 @@ namespace {
}
}
TEST(LevelTests, ReadsEveryPartOfAChunkedFile) {
class LevelTests : public GameDependenciesTest {
protected:
void SetUp() override { SetUpDependencies(); }
void TearDown() override { TearDownDependencies(); }
};
TEST_F(LevelTests, ReadsEveryPartOfAChunkedFile) {
const auto level = Read(WriteChunked(46));
EXPECT_EQ(level.version, 46);
EXPECT_EQ(level.revision, 9);
@@ -268,7 +276,7 @@ TEST(LevelTests, ReadsEveryPartOfAChunkedFile) {
// LwoNameValue::FromString splits at commas and line breaks and leaves out entries without a '='; ReadLvlObjectData
// adjusts some values as it loads the object
TEST(LevelTests, ObjectConfigIsReadAndAdjustedAsTheClientDoes) {
TEST_F(LevelTests, ObjectConfigIsReadAndAdjustedAsTheClientDoes) {
const auto level = Read(WriteChunked(43));
ASSERT_EQ(level.objects.size(), 3);
@@ -292,7 +300,7 @@ TEST(LevelTests, ObjectConfigIsReadAndAdjustedAsTheClientDoes) {
EXPECT_EQ(newer.objects[2].settings.find(u"sceneLayerIDOverride"), newer.objects[2].settings.end());
}
TEST(LevelTests, ReadsAFileFromBeforeChunks) {
TEST_F(LevelTests, ReadsAFileFromBeforeChunks) {
const auto level = Read(WriteFlat(40));
EXPECT_EQ(level.version, 40);
EXPECT_EQ(level.revision, 77);
@@ -319,7 +327,7 @@ TEST(LevelTests, ReadsAFileFromBeforeChunks) {
}
// Older lighting: fog distances only before 39, no directional colour before 36, and before 33 a fixed spawn point
TEST(LevelTests, ReadsOlderLightingAndSounds) {
TEST_F(LevelTests, ReadsOlderLightingAndSounds) {
const auto v32 = Read(WriteFlat(32));
EXPECT_EQ(v32.revision, 0);
EXPECT_FLOAT_EQ(v32.lighting.minDrawDistances.fogFar, 60.0f);
@@ -343,7 +351,7 @@ TEST(LevelTests, ReadsOlderLightingAndSounds) {
// Before version 44 a file may end before its particles; a file older than 3 has no objects, a newer one than 48
// isn't read
TEST(LevelTests, ReadsWhatTheClientReadsOfOtherFiles) {
TEST_F(LevelTests, ReadsWhatTheClientReadsOfOtherFiles) {
const auto noParticles = Read(WriteFlat(43, false));
EXPECT_EQ(noParticles.objects.size(), 1);
EXPECT_EQ(noParticles.sounds.size(), 2);
@@ -364,7 +372,7 @@ TEST(LevelTests, ReadsWhatTheClientReadsOfOtherFiles) {
}
// A chunked file without lighting has none of the environment's parts read
TEST(LevelTests, NoEnvironmentWithoutLighting) {
TEST_F(LevelTests, NoEnvironmentWithoutLighting) {
auto data = WriteChunked(48);
std::memset(data.data() + 96, 0, 4); // lighting offset
const auto level = Read(data);

View File

@@ -81,11 +81,11 @@ TEST(RawTerrainTests, ReadsEveryLayer) {
EXPECT_EQ(chunk.GetSceneIDAtGrid(2, 2), 2);
EXPECT_EQ(raw.chunks[1].GetSceneIDAtGrid(2, 0), 3);
EXPECT_FLOAT_EQ(raw.minBoundsX, -4.0f);
EXPECT_FLOAT_EQ(raw.maxBoundsX, 6.0f);
EXPECT_FLOAT_EQ(raw.maxBoundsX, 4.0f); // the span between the first and last point
}
// Before version 32: a shader ID, a width x width BGRA color map the client keeps (width - 1) x (width - 1) of as RGBA,
// a BGRA texture map, no light or blend maps, a (width x width) scene map in version 31 and none (all scene 0) before
// Before version 32: a shader ID, a width x width color map, a texture map, no light or blend maps, and a
// (width x width) scene map that version 31 uses the first (width - 1) rows and columns of (all scene 0 before)
TEST(RawTerrainTests, ReadsOlderVersions) {
for (const uint16_t version : { 30, 31 }) {
RawWriter w;
@@ -98,17 +98,16 @@ TEST(RawTerrainTests, ReadsOlderVersions) {
for (int i = 0; i < 9; i++) w.Put<uint8_t>(1).Put<uint8_t>(2).Put<uint8_t>(3).Put<uint8_t>(static_cast<uint8_t>(i)); // BGRA
w.Put<uint32_t>(1).Put<uint8_t>(1).Put<uint8_t>(2).Put<uint8_t>(3).Put<uint8_t>(4);
w.Put<uint32_t>(0); // no flairs
if (version == 31) for (int i = 0; i < 9; i++) w.Put<uint8_t>(static_cast<uint8_t>(i));
else w.Put<uint8_t>(0);
for (int i = 0; i < 9; i++) w.Put<uint8_t>(static_cast<uint8_t>(i)); // read in every version, used in 31
Raw::Raw raw;
ASSERT_TRUE(Read(w.data, raw)) << version;
const auto& chunk = raw.chunks.at(0);
EXPECT_EQ(chunk.shaderId, 7u);
EXPECT_EQ(chunk.colorMapResolution, 2u);
// Rows 0-1, columns 0-1 of the 3 x 3, as RGBA
EXPECT_EQ(chunk.colorMap, (std::vector<uint8_t>{ 3, 2, 1, 0, 3, 2, 1, 1, 3, 2, 1, 3, 3, 2, 1, 4 }));
EXPECT_EQ(chunk.textureMap, (std::vector<uint8_t>{ 3, 2, 1, 4 }));
// The 3 x 3 pixels and the texture map as the file has them
EXPECT_EQ(chunk.colorMap.size(), 3u * 3u * 4u);
EXPECT_EQ(chunk.textureMap, (std::vector<uint8_t>{ 1, 2, 3, 4 }));
if (version == 31) EXPECT_EQ(chunk.sceneMap, (std::vector<uint8_t>{ 0, 1, 3, 4 }));
else EXPECT_EQ(chunk.sceneMap, (std::vector<uint8_t>(4, 0)));
}