mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-11-06 11:57:13 +00:00
fix: read a scene file's chunks, environment and objects as the client does
The world read only a scene's objects and skipped the rest with file.ignore. Level now reads a .lvl the way SceneLoader::LoadLvlFile does (1.10.64) and keeps what it reads. Chunked files: the file info chunk gives where the environment, object and particle chunks start (the chunks were walked in order before); the environment chunk gives where the lighting, skydome and editor settings start, and the client reads all three only when there is lighting. Files from before chunks: the no-editor-settings byte only from version 32, the revision from 37, a file newer than 48 is not read, a file older than 3 has no objects; then lighting, skydome, editor settings and objects in turn. Kept, by version: - lighting: blend time (45 on, default 10), ambient, specular, upper hemisphere, position, min and max draw distances (31 on; before 39 fog near and far only), cull settings (40 on), fog colour (31 on), directional colour (36 on), and before 42 the zone's spawn point (before 33 the fixed one the client uses); - the skydome and its five other files (34 on; they were skipped from 33); - the editor's colours (37 on, the first 32); - each object's node type (38 on), glom ID (32 on) and render techniques (7 on: a 64 byte name, then per technique a 64 byte name, u32, u8 and 16 floats); before, a count of techniques was read and the techniques themselves were not, so an object with any misread the rest of the file. Object config is split at line breaks as before. A file that ends early keeps the objects read before its end. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
SET(DZONEMANAGER_TESTS
|
||||
"LevelTests.cpp"
|
||||
"RawTests.cpp"
|
||||
"ZoneSceneTests.cpp")
|
||||
|
||||
|
||||
328
tests/dGameTests/dZoneManagerTests/LevelTests.cpp
Normal file
328
tests/dGameTests/dZoneManagerTests/LevelTests.cpp
Normal file
@@ -0,0 +1,328 @@
|
||||
#include "Level.h"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <cstring>
|
||||
#include <sstream>
|
||||
|
||||
#include "GameDependencies.h"
|
||||
|
||||
namespace {
|
||||
template<typename T>
|
||||
void Write(std::ostream& stream, const T& value) {
|
||||
stream.write(reinterpret_cast<const char*>(&value), sizeof(T));
|
||||
}
|
||||
|
||||
void WriteFloats(std::ostream& stream, std::initializer_list<float> values) {
|
||||
for (const float value : values) Write(stream, value);
|
||||
}
|
||||
|
||||
// A u32 length, then that many bytes
|
||||
void WriteString32(std::ostream& stream, const std::string& value) {
|
||||
Write<uint32_t>(stream, static_cast<uint32_t>(value.size()));
|
||||
stream.write(value.data(), value.size());
|
||||
}
|
||||
|
||||
// A u32 length, then that many UTF-16 characters (ASCII only)
|
||||
void WriteWString32(std::ostream& stream, const std::string& value) {
|
||||
Write<uint32_t>(stream, static_cast<uint32_t>(value.size()));
|
||||
for (const char c : value) Write<uint16_t>(stream, static_cast<uint16_t>(c));
|
||||
}
|
||||
|
||||
void WriteName64(std::ostream& stream, const std::string& name) {
|
||||
std::string bytes(64, '\0');
|
||||
bytes.replace(0, name.size(), name);
|
||||
stream.write(bytes.data(), bytes.size());
|
||||
}
|
||||
|
||||
// The light environment as the given version has it
|
||||
void WriteLighting(std::ostream& stream, uint32_t version) {
|
||||
if (version >= 45) Write(stream, 2.5f); // blend time
|
||||
WriteFloats(stream, { 0.1f, 0.2f, 0.3f }); // ambient
|
||||
WriteFloats(stream, { 0.4f, 0.5f, 0.6f }); // specular
|
||||
WriteFloats(stream, { 0.7f, 0.8f, 0.9f }); // upper hemisphere
|
||||
WriteFloats(stream, { 10.0f, 20.0f, 30.0f }); // position
|
||||
if (version >= 31) {
|
||||
if (version < 39) {
|
||||
WriteFloats(stream, { 50.0f, 60.0f }); // fog near and far
|
||||
} else {
|
||||
WriteFloats(stream, { 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f }); // min draw distances
|
||||
WriteFloats(stream, { 7.0f, 8.0f, 9.0f, 10.0f, 11.0f, 12.0f }); // max draw distances
|
||||
if (version >= 40) {
|
||||
Write<uint32_t>(stream, 1);
|
||||
Write<uint32_t>(stream, 3); // group
|
||||
WriteFloats(stream, { 100.0f, 200.0f });
|
||||
}
|
||||
}
|
||||
WriteFloats(stream, { 0.25f, 0.5f, 0.75f }); // fog colour
|
||||
}
|
||||
if (version >= 36) WriteFloats(stream, { 1.0f, 0.5f, 0.0f }); // directional
|
||||
if (version < 42) {
|
||||
WriteFloats(stream, { 5.0f, 6.0f, 7.0f }); // spawn position
|
||||
if (version >= 33) WriteFloats(stream, { 0.0f, 1.0f, 0.0f, 0.0f }); // spawn rotation, w first
|
||||
}
|
||||
}
|
||||
|
||||
void WriteSkydome(std::ostream& stream, uint32_t version) {
|
||||
WriteString32(stream, "mesh/env/sky.nif");
|
||||
if (version >= 34) {
|
||||
for (const auto* name : { "a.nif", "b.nif", "c.nif", "d.nif", "e.nif" }) WriteString32(stream, name);
|
||||
}
|
||||
}
|
||||
|
||||
void WriteEditorSettings(std::ostream& stream) {
|
||||
Write<uint32_t>(stream, 4 + 12 * 2); // size
|
||||
Write<uint32_t>(stream, 2);
|
||||
WriteFloats(stream, { 1.0f, 0.0f, 0.0f });
|
||||
WriteFloats(stream, { 0.0f, 1.0f, 0.0f });
|
||||
}
|
||||
|
||||
void WriteObject(std::ostream& stream, uint32_t version, LWOOBJID id, LOT lot, const std::string& config, uint32_t techniques = 0) {
|
||||
Write(stream, id);
|
||||
Write(stream, lot);
|
||||
if (version >= 38) Write<uint32_t>(stream, 12); // node type out of range: the client uses 1
|
||||
if (version >= 32) Write<uint32_t>(stream, 4); // glom ID
|
||||
WriteFloats(stream, { 1.0f, 2.0f, 3.0f }); // position
|
||||
WriteFloats(stream, { 1.0f, 0.0f, 0.0f, 0.0f }); // rotation, w first
|
||||
Write(stream, 2.0f); // scale
|
||||
WriteWString32(stream, config);
|
||||
if (version >= 7) {
|
||||
Write(stream, techniques);
|
||||
if (techniques != 0) {
|
||||
WriteName64(stream, "techniques");
|
||||
for (uint32_t i = 0; i < techniques; ++i) {
|
||||
WriteName64(stream, "technique" + std::to_string(i));
|
||||
Write<uint32_t>(stream, 4);
|
||||
Write<uint8_t>(stream, 1);
|
||||
for (uint32_t j = 0; j < 16; ++j) Write(stream, static_cast<float>(j));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void WriteParticle(std::ostream& stream, uint32_t version, const std::string& names, const std::string& config) {
|
||||
if (version >= 43) Write<int16_t>(stream, -2); // priority
|
||||
WriteFloats(stream, { 4.0f, 5.0f, 6.0f }); // position
|
||||
WriteFloats(stream, { 1.0f, 0.0f, 0.0f, 0.0f }); // rotation, w first
|
||||
WriteWString32(stream, names);
|
||||
if (version < 46) Write<uint16_t>(stream, 9);
|
||||
else WriteWString32(stream, config);
|
||||
}
|
||||
|
||||
void WriteChunkHeader(std::ostream& stream, uint32_t id, uint32_t size, uint32_t dataStart) {
|
||||
stream.write("CHNK", 4);
|
||||
Write(stream, id);
|
||||
Write<uint16_t>(stream, 1);
|
||||
Write<uint16_t>(stream, 1);
|
||||
Write(stream, size);
|
||||
Write(stream, dataStart);
|
||||
}
|
||||
|
||||
// Pads to the next 16 bytes with 0xcd, as the client's files are
|
||||
void Pad(std::ostringstream& stream) {
|
||||
while (stream.tellp() % 16 != 0) Write<uint8_t>(stream, 0xcd);
|
||||
}
|
||||
|
||||
// A scene file from before chunks
|
||||
std::string WriteFlat(uint32_t version, bool withParticles = true) {
|
||||
std::ostringstream stream;
|
||||
Write<uint16_t>(stream, static_cast<uint16_t>(version));
|
||||
Write<uint16_t>(stream, 0);
|
||||
if (version >= 32) Write<uint8_t>(stream, 0); // no editor settings flag
|
||||
if (version > 36) Write<uint32_t>(stream, 77); // revision
|
||||
WriteLighting(stream, version);
|
||||
WriteSkydome(stream, version);
|
||||
if (version >= 37) WriteEditorSettings(stream);
|
||||
if (version >= 3) {
|
||||
Write<uint32_t>(stream, 1);
|
||||
WriteObject(stream, version, 1000, 2000, "respawn=5:20000\ncustom=0:a");
|
||||
}
|
||||
if (version >= 5) {
|
||||
Write<uint32_t>(stream, 2);
|
||||
WriteFloats(stream, { 8.0f, 9.0f, 10.0f });
|
||||
if (version < 35) {
|
||||
std::u16string name(260, u'\0');
|
||||
name.replace(0, 5, u"birds");
|
||||
stream.write(reinterpret_cast<const char*>(name.data()), name.size() * sizeof(char16_t));
|
||||
} else {
|
||||
WriteWString32(stream, "soundName=0:birds");
|
||||
}
|
||||
WriteFloats(stream, { 11.0f, 12.0f, 13.0f });
|
||||
if (version < 35) stream.write(std::string(520, '\0').data(), 520);
|
||||
else WriteWString32(stream, "");
|
||||
if (withParticles) {
|
||||
Write<uint32_t>(stream, 1);
|
||||
WriteParticle(stream, version, "fx.psb", "");
|
||||
}
|
||||
}
|
||||
return stream.str();
|
||||
}
|
||||
|
||||
// A chunked scene file: file info, environment, objects and particles chunks, each padded as the client's are
|
||||
std::string WriteChunked(uint32_t version) {
|
||||
std::ostringstream stream;
|
||||
WriteChunkHeader(stream, 1000, 64, 32);
|
||||
Pad(stream);
|
||||
Write(stream, version);
|
||||
Write<uint32_t>(stream, 9); // revision
|
||||
Write<uint32_t>(stream, 64); // environment
|
||||
Write<uint32_t>(stream, 0); // objects, set below
|
||||
Write<uint32_t>(stream, 0); // particles, set below
|
||||
Pad(stream);
|
||||
|
||||
// Environment: its header, the three offsets, then each part
|
||||
WriteChunkHeader(stream, 2000, 0, 96);
|
||||
Pad(stream);
|
||||
const auto offsets = stream.tellp();
|
||||
WriteFloats(stream, { 0.0f, 0.0f, 0.0f });
|
||||
Pad(stream);
|
||||
const uint32_t lighting = static_cast<uint32_t>(stream.tellp());
|
||||
WriteLighting(stream, version);
|
||||
Pad(stream);
|
||||
const uint32_t skydome = static_cast<uint32_t>(stream.tellp());
|
||||
WriteSkydome(stream, version);
|
||||
Pad(stream);
|
||||
const uint32_t editor = static_cast<uint32_t>(stream.tellp());
|
||||
WriteEditorSettings(stream);
|
||||
Pad(stream);
|
||||
|
||||
const uint32_t objects = static_cast<uint32_t>(stream.tellp());
|
||||
WriteChunkHeader(stream, 2001, 0, objects + 32);
|
||||
Pad(stream);
|
||||
Write<uint32_t>(stream, 3);
|
||||
WriteObject(stream, version, 1, 176, "spawntemplate=1:14,subkey=9:55\nrespawn=3:250");
|
||||
WriteObject(stream, version, 2, 1, "");
|
||||
WriteObject(stream, version, 3, 4000, "sceneIDOverrideEnabled=7:1,novalue", 2);
|
||||
Pad(stream);
|
||||
|
||||
const uint32_t particles = static_cast<uint32_t>(stream.tellp());
|
||||
WriteChunkHeader(stream, 2002, 0, particles + 32);
|
||||
Pad(stream);
|
||||
Write<uint32_t>(stream, 3);
|
||||
WriteParticle(stream, version, "fx1.psb;fx2.psb", "fxconfig=0:1");
|
||||
WriteParticle(stream, version, "fx3.psb", "");
|
||||
WriteParticle(stream, version, "", "");
|
||||
|
||||
auto data = stream.str();
|
||||
const auto patch = [&data](size_t at, uint32_t value) { std::memcpy(data.data() + at, &value, sizeof(value)); };
|
||||
patch(32 + 12, objects);
|
||||
patch(32 + 16, particles);
|
||||
patch(static_cast<size_t>(offsets), lighting);
|
||||
patch(static_cast<size_t>(offsets) + 4, skydome);
|
||||
patch(static_cast<size_t>(offsets) + 8, editor);
|
||||
return data;
|
||||
}
|
||||
|
||||
Level Read(const std::string& data) {
|
||||
std::istringstream stream(data);
|
||||
return Level(stream);
|
||||
}
|
||||
}
|
||||
|
||||
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.GetVersion(), 46);
|
||||
EXPECT_EQ(level.GetRevision(), 9);
|
||||
ASSERT_EQ(level.m_ChunkHeaders.size(), 4);
|
||||
EXPECT_EQ(level.m_ChunkHeaders.at(1000).fileInfo.revision, 9);
|
||||
|
||||
ASSERT_TRUE(level.HasLighting());
|
||||
EXPECT_FLOAT_EQ(level.GetLighting().blendTime, 2.5f);
|
||||
EXPECT_FLOAT_EQ(level.GetLighting().ambient.m_Blue, 0.3f);
|
||||
EXPECT_FLOAT_EQ(level.GetLighting().upperHemi.m_Red, 0.7f);
|
||||
EXPECT_FLOAT_EQ(level.GetLighting().position.z, 30.0f);
|
||||
EXPECT_FLOAT_EQ(level.GetLighting().minDrawDistances.dynamicObjectDistance, 6.0f);
|
||||
EXPECT_FLOAT_EQ(level.GetLighting().maxDrawDistances.fogNear, 7.0f);
|
||||
ASSERT_EQ(level.GetLighting().cullSettings.size(), 1);
|
||||
EXPECT_EQ(level.GetLighting().cullSettings[0].group, 3);
|
||||
EXPECT_FLOAT_EQ(level.GetLighting().cullSettings[0].maxDistance, 200.0f);
|
||||
EXPECT_FLOAT_EQ(level.GetLighting().fogColor.m_Green, 0.5f);
|
||||
EXPECT_FLOAT_EQ(level.GetLighting().directional.m_Red, 1.0f);
|
||||
EXPECT_FALSE(level.GetLighting().hasSpawnPoint);
|
||||
|
||||
EXPECT_EQ(level.GetSkydome().filename, "mesh/env/sky.nif");
|
||||
EXPECT_EQ(level.GetSkydome().extraFilenames[4], "e.nif");
|
||||
ASSERT_EQ(level.GetEditorColors().size(), 2);
|
||||
EXPECT_FLOAT_EQ(level.GetEditorColors()[1].m_Green, 1.0f);
|
||||
|
||||
ASSERT_EQ(level.GetObjects().size(), 3);
|
||||
const auto& technique = level.GetObjects()[2];
|
||||
EXPECT_EQ(technique.nodeType, 1);
|
||||
EXPECT_EQ(technique.glomId, 4);
|
||||
EXPECT_FLOAT_EQ(technique.scale, 2.0f);
|
||||
EXPECT_EQ(technique.renderTechniquesName, "techniques");
|
||||
ASSERT_EQ(technique.renderTechniques.size(), 2);
|
||||
EXPECT_EQ(technique.renderTechniques[1].name, "technique1");
|
||||
EXPECT_EQ(technique.renderTechniques[1].valueCount, 4);
|
||||
EXPECT_EQ(technique.renderTechniques[1].isColor, 1);
|
||||
EXPECT_FLOAT_EQ(technique.renderTechniques[1].values[15], 15.0f);
|
||||
}
|
||||
|
||||
TEST_F(LevelTests, ReadsAFileFromBeforeChunks) {
|
||||
const auto level = Read(WriteFlat(40));
|
||||
EXPECT_EQ(level.GetVersion(), 40);
|
||||
EXPECT_EQ(level.GetRevision(), 77);
|
||||
ASSERT_EQ(level.m_ChunkHeaders.size(), 1);
|
||||
EXPECT_EQ(level.m_ChunkHeaders.at(1000).fileInfo.revision, 77);
|
||||
EXPECT_FLOAT_EQ(level.GetLighting().blendTime, 10.0f); // the client's default before version 45
|
||||
ASSERT_EQ(level.GetLighting().cullSettings.size(), 1);
|
||||
ASSERT_TRUE(level.GetLighting().hasSpawnPoint);
|
||||
EXPECT_FLOAT_EQ(level.GetLighting().spawnPosition.y, 6.0f);
|
||||
EXPECT_FLOAT_EQ(level.GetLighting().spawnRotation.x, 1.0f);
|
||||
EXPECT_EQ(level.GetEditorColors().size(), 2);
|
||||
|
||||
ASSERT_EQ(level.GetObjects().size(), 1);
|
||||
}
|
||||
|
||||
// Older lighting: fog distances only before 39, no directional colour before 36, and before 33 a fixed spawn point
|
||||
TEST_F(LevelTests, ReadsOlderLighting) {
|
||||
const auto v32 = Read(WriteFlat(32));
|
||||
EXPECT_EQ(v32.GetRevision(), 0);
|
||||
EXPECT_FLOAT_EQ(v32.GetLighting().minDrawDistances.fogFar, 60.0f);
|
||||
EXPECT_FLOAT_EQ(v32.GetLighting().maxDrawDistances.fogNear, 50.0f);
|
||||
EXPECT_FLOAT_EQ(v32.GetLighting().maxDrawDistances.staticObjectDistance, -1.0f);
|
||||
EXPECT_FLOAT_EQ(v32.GetLighting().directional.m_Red, 0.0f);
|
||||
EXPECT_FLOAT_EQ(v32.GetLighting().filePosition.x, 5.0f);
|
||||
EXPECT_FLOAT_EQ(v32.GetLighting().spawnPosition.x, 347.0f);
|
||||
EXPECT_TRUE(v32.GetSkydome().extraFilenames[0].empty());
|
||||
EXPECT_TRUE(v32.GetEditorColors().empty());
|
||||
ASSERT_EQ(v32.GetObjects().size(), 1);
|
||||
|
||||
const auto v30 = Read(WriteFlat(30));
|
||||
EXPECT_FLOAT_EQ(v30.GetLighting().fogColor.m_Red, 0.75f); // the client's default
|
||||
EXPECT_FLOAT_EQ(v30.GetLighting().spawnRotation.y, 0.23979919f);
|
||||
EXPECT_EQ(v30.GetObjects().size(), 1);
|
||||
}
|
||||
|
||||
// A file older than 3 has no objects, a newer one than 48 isn't read
|
||||
TEST_F(LevelTests, ReadsWhatTheClientReadsOfOtherFiles) {
|
||||
const auto old = Read(WriteFlat(2));
|
||||
EXPECT_TRUE(old.HasLighting());
|
||||
EXPECT_TRUE(old.GetObjects().empty());
|
||||
|
||||
std::ostringstream newer;
|
||||
Write<uint16_t>(newer, 49);
|
||||
Write<uint16_t>(newer, 0);
|
||||
Write<uint8_t>(newer, 0);
|
||||
Write<uint32_t>(newer, 1);
|
||||
const auto refused = Read(newer.str());
|
||||
EXPECT_EQ(refused.GetVersion(), 49);
|
||||
EXPECT_FALSE(refused.HasLighting());
|
||||
}
|
||||
|
||||
// A chunked file without lighting has none of the environment's parts read
|
||||
TEST_F(LevelTests, NoEnvironmentWithoutLighting) {
|
||||
auto data = WriteChunked(48);
|
||||
std::memset(data.data() + 96, 0, 4); // lighting offset
|
||||
const auto level = Read(data);
|
||||
EXPECT_FALSE(level.HasLighting());
|
||||
EXPECT_TRUE(level.GetSkydome().filename.empty());
|
||||
EXPECT_TRUE(level.GetEditorColors().empty());
|
||||
EXPECT_EQ(level.GetObjects().size(), 3);
|
||||
}
|
||||
Reference in New Issue
Block a user