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:
Aaron Kimbrell
2026-10-27 10:34:14 -05:00
committed by GitHub
parent 8b7d1b946b
commit e3370d7042
5 changed files with 698 additions and 163 deletions

View File

@@ -17,6 +17,8 @@
#include "ClientVersion.h"
#include "dConfig.h"
#include <ranges>
#include <algorithm>
#include <stdexcept>
Level::Level(Zone* parentZone, const std::string& filepath) {
m_ParentZone = parentZone;
@@ -28,7 +30,17 @@ Level::Level(Zone* parentZone, const std::string& filepath) {
return;
}
ReadChunks(stream);
try {
ReadChunks(stream);
} catch (const std::runtime_error&) {
// The client reads past the end of such a file; what was read before it is kept
LOG("%s ends early; loading the %zu objects read before its end", filepath.c_str(), m_Objects.size());
}
LoadSceneObjects();
}
Level::Level(std::istream& file) {
ReadChunks(file);
}
void Level::MakeSpawner(const SceneObject& obj) {
@@ -100,123 +112,7 @@ void Level::MakeSpawner(const SceneObject& obj) {
Game::zoneManager->MakeSpawner(spawnInfo);
}
void Level::ReadChunks(std::istream& file) {
const uint32_t CHNK_HEADER = ('C' + ('H' << 8) + ('N' << 16) + ('K' << 24));
while (!file.eof()) {
uint32_t initPos = uint32_t(file.tellg());
uint32_t header = 0;
BinaryIO::BinaryRead(file, header);
if (header == CHNK_HEADER) { //Make sure we're reading a valid CHNK
Header header;
BinaryIO::BinaryRead(file, header.id);
BinaryIO::BinaryRead(file, header.chunkVersion);
BinaryIO::BinaryRead(file, header.chunkType);
BinaryIO::BinaryRead(file, header.size);
BinaryIO::BinaryRead(file, header.startPosition);
uint32_t target = initPos + header.size;
file.seekg(header.startPosition);
//We're currently not loading env or particle data
if (header.id == ChunkTypeID::FileInfo) {
ReadFileInfoChunk(file, header);
} else if (header.id == ChunkTypeID::SceneObjectData) {
ReadSceneObjectDataChunk(file, header);
}
m_ChunkHeaders.insert(std::make_pair(header.id, header));
file.seekg(target);
} else {
if (initPos == std::streamoff(0)) { //Really old chunk version
file.seekg(0);
Header header;
header.id = ChunkTypeID::FileInfo;
BinaryIO::BinaryRead(file, header.chunkVersion);
BinaryIO::BinaryRead(file, header.chunkType);
uint8_t important = 0;
BinaryIO::BinaryRead(file, important);
// file.ignore(1); //probably used
if (header.chunkVersion > 36) {
BinaryIO::BinaryRead(file, header.fileInfo.revision);
}
// HARDCODED 3
if (header.chunkVersion >= 45) file.ignore(4);
file.ignore(4 * (4 * 3));
if (header.chunkVersion >= 31) {
if (header.chunkVersion >= 39) {
file.ignore(12 * 4);
if (header.chunkVersion >= 40) {
uint32_t s = 0;
BinaryIO::BinaryRead(file, s);
for (uint32_t i = 0; i < s; ++i) {
file.ignore(4 * 3); //a uint and two floats
}
}
} else {
file.ignore(8);
}
file.ignore(3 * 4);
}
if (header.chunkVersion >= 36) {
file.ignore(3 * 4);
}
if (header.chunkVersion < 42) {
file.ignore(3 * 4);
if (header.chunkVersion >= 33) {
file.ignore(4 * 4);
}
}
// skydome info
uint32_t count = 0;
BinaryIO::BinaryRead(file, count);
file.ignore(count);
if (header.chunkVersion >= 33) {
for (uint32_t i = 0; i < 5; ++i) {
uint32_t count = 0;
BinaryIO::BinaryRead(file, count);
file.ignore(count);
}
}
// editor settings
if (!important && header.chunkVersion >= 37){
file.ignore(4);
uint32_t count = 0;
BinaryIO::BinaryRead(file, count);
file.ignore(count * 12);
}
header.id = ChunkTypeID::SceneObjectData;
header.fileInfo.version = header.chunkVersion;
ReadSceneObjectDataChunk(file, header);
m_ChunkHeaders.insert(std::make_pair(header.id, header));
} break;
}
}
}
void Level::ReadFileInfoChunk(std::istream& file, Header& header) {
BinaryIO::BinaryRead(file, header.fileInfo.version);
BinaryIO::BinaryRead(file, header.fileInfo.revision);
BinaryIO::BinaryRead(file, header.fileInfo.enviromentChunkStart);
BinaryIO::BinaryRead(file, header.fileInfo.objectChunkStart);
BinaryIO::BinaryRead(file, header.fileInfo.particleChunkStart);
}
void Level::ReadSceneObjectDataChunk(std::istream& file, Header& header) {
uint32_t objectsCount = 0;
BinaryIO::BinaryRead(file, objectsCount);
void Level::LoadSceneObjects() {
CDFeatureGatingTable* featureGatingTable = CDClientManager::GetTable<CDFeatureGatingTable>();
CDFeatureGating gating;
@@ -229,28 +125,7 @@ void Level::ReadSceneObjectDataChunk(std::istream& file, Header& header) {
const auto zoneControlObject = Game::zoneManager->GetZoneControlObject();
DluAssert(zoneControlObject != nullptr);
for (uint32_t i = 0; i < objectsCount; ++i) {
std::u16string ldfString;
SceneObject obj;
BinaryIO::BinaryRead(file, obj.id);
BinaryIO::BinaryRead(file, obj.lot);
if (header.fileInfo.version >= 38) {
int32_t tmp = 1;
BinaryIO::BinaryRead(file, tmp);
if (tmp > -1 && tmp < 11) obj.nodeType = tmp;
}
if (header.fileInfo.version >= 32) {
BinaryIO::BinaryRead(file, obj.glomId);
}
BinaryIO::BinaryRead(file, obj.position);
BinaryIO::BinaryRead(file, obj.rotation);
BinaryIO::BinaryRead(file, obj.scale);
BinaryIO::ReadString<uint32_t>(file, ldfString);
BinaryIO::BinaryRead(file, obj.value3);
for (const auto& obj : m_Objects) {
//This is a little bit of a bodge, but because the alpha client (HF) doesn't store the
//spawn position / rotation like the later versions do, we need to check the LOT for the spawn pos & set it.
if (obj.lot == LOT_MARKER_PLAYER_START) {
@@ -258,11 +133,6 @@ void Level::ReadSceneObjectDataChunk(std::istream& file, Header& header) {
Game::zoneManager->GetZoneMut()->SetSpawnRot(obj.rotation);
}
for (const auto& token : GeneralUtils::SplitString(GeneralUtils::UTF16ToWTF8(ldfString), '\n')) {
obj.settings.ParseInsert(token);
}
// We should never have more than 1 zone control object
bool skipLoadingObject = obj.lot == zoneControlObject->GetLOT();
for (const auto& data : obj.settings | std::views::values) {
@@ -309,3 +179,243 @@ void Level::ReadSceneObjectDataChunk(std::istream& file, Header& header) {
}
}
}
namespace {
constexpr uint32_t CHNK_HEADER = ('C' + ('H' << 8) + ('N' << 16) + ('K' << 24));
// A fixed size, zero padded name
std::string ReadName64(std::istream& file) {
std::string name(64, '\0');
file.read(name.data(), static_cast<std::streamsize>(name.size()));
if (file.fail()) throw std::runtime_error("Failed to read from istream.");
name.resize(std::min(name.find('\0'), name.size()));
return name;
}
// A u32 count of wide characters
std::string ReadWideString32(std::istream& file) {
std::u16string value;
BinaryIO::ReadString<uint32_t>(file, value);
if (file.fail()) throw std::runtime_error("Failed to read from istream.");
return GeneralUtils::UTF16ToWTF8(value);
}
}
void Level::ReadChunks(std::istream& file) {
uint32_t magic = 0;
BinaryIO::BinaryRead(file, magic);
if (magic == CHNK_HEADER) {
// SceneLoader::LoadLvlFile / LvlChunkFile::Read: the file info chunk starts the file and gives where the
// environment, object and particle chunks start (0 for none)
Header fileInfo = ReadChunkHeader(file, 0);
file.seekg(fileInfo.startPosition);
ReadFileInfoChunk(file, fileInfo);
m_ChunkHeaders.insert({ fileInfo.id, fileInfo });
m_Version = fileInfo.fileInfo.version;
m_Revision = fileInfo.fileInfo.revision;
if (fileInfo.fileInfo.enviromentChunkStart != 0) {
const auto header = ReadChunkHeader(file, fileInfo.fileInfo.enviromentChunkStart);
m_ChunkHeaders.insert({ header.id, header });
// EnvironmentChunk::ReadLvlChunk2000: where the lighting, skydome and editor settings start
file.seekg(header.startPosition);
BinaryIO::BinaryRead(file, m_Enviroment.lightingStart);
BinaryIO::BinaryRead(file, m_Enviroment.skydomeStart);
BinaryIO::BinaryRead(file, m_Enviroment.editorSettingsStart);
// The client reads all three only when there is lighting
if (m_Enviroment.lightingStart != 0) {
file.seekg(m_Enviroment.lightingStart);
ReadLightingInfo(file);
file.seekg(m_Enviroment.skydomeStart);
ReadSkydomeInfo(file);
file.seekg(m_Enviroment.editorSettingsStart);
ReadEditorSettings(file);
}
}
if (fileInfo.fileInfo.objectChunkStart != 0) {
const auto header = ReadChunkHeader(file, fileInfo.fileInfo.objectChunkStart);
m_ChunkHeaders.insert({ header.id, header });
file.seekg(header.startPosition);
ReadSceneObjectDataChunk(file);
}
if (fileInfo.fileInfo.particleChunkStart != 0) {
const auto header = ReadChunkHeader(file, fileInfo.fileInfo.particleChunkStart);
m_ChunkHeaders.insert({ header.id, header });
}
return;
}
// Scenes from before chunks: a header (SceneLoader::ReadLvlHeader), then each part in turn
file.seekg(0);
Header header;
header.id = ChunkTypeID::FileInfo;
BinaryIO::BinaryRead(file, header.chunkVersion);
BinaryIO::BinaryRead(file, header.chunkType);
m_Version = header.chunkVersion;
if (m_Version >= 32) BinaryIO::BinaryRead(file, m_NoEditorSettings);
if (m_Version > 36) BinaryIO::BinaryRead(file, m_Revision);
header.fileInfo.version = m_Version;
header.fileInfo.revision = m_Revision;
m_ChunkHeaders.insert({ header.id, header });
// A newer file is refused ("newer than the client understands")
if (m_Version > 48) {
LOG("Scene file version %u is newer than the client reads; nothing read from it", m_Version);
return;
}
ReadLightingInfo(file);
ReadSkydomeInfo(file);
ReadEditorSettings(file);
ReadSceneObjectDataChunk(file);
}
Level::Header Level::ReadChunkHeader(std::istream& file, uint32_t start) {
// No check of the magic, as in the client (0x01063a00)
file.seekg(start);
uint32_t magic = 0;
Header header;
BinaryIO::BinaryRead(file, magic);
BinaryIO::BinaryRead(file, header.id);
BinaryIO::BinaryRead(file, header.chunkVersion);
BinaryIO::BinaryRead(file, header.chunkType);
BinaryIO::BinaryRead(file, header.size);
BinaryIO::BinaryRead(file, header.startPosition);
return header;
}
void Level::ReadFileInfoChunk(std::istream& file, Header& header) {
BinaryIO::BinaryRead(file, header.fileInfo.version);
BinaryIO::BinaryRead(file, header.fileInfo.revision);
BinaryIO::BinaryRead(file, header.fileInfo.enviromentChunkStart);
BinaryIO::BinaryRead(file, header.fileInfo.objectChunkStart);
BinaryIO::BinaryRead(file, header.fileInfo.particleChunkStart);
}
void Level::ReadLightingInfo(std::istream& file) {
// SceneLoader::ReadLightingInfo (0x0102f8f0)
m_HasLighting = true;
if (m_Version >= 45) BinaryIO::BinaryRead(file, m_Lighting.blendTime);
BinaryIO::BinaryRead(file, m_Lighting.ambient);
BinaryIO::BinaryRead(file, m_Lighting.specular);
BinaryIO::BinaryRead(file, m_Lighting.upperHemi);
BinaryIO::BinaryRead(file, m_Lighting.position);
if (m_Version >= 31) {
if (m_Version < 39) {
BinaryIO::BinaryRead(file, m_Lighting.maxDrawDistances.fogNear);
BinaryIO::BinaryRead(file, m_Lighting.maxDrawDistances.fogFar);
m_Lighting.minDrawDistances.fogNear = m_Lighting.maxDrawDistances.fogNear;
m_Lighting.minDrawDistances.fogFar = m_Lighting.maxDrawDistances.fogFar;
} else {
for (auto* distances : { &m_Lighting.minDrawDistances, &m_Lighting.maxDrawDistances }) {
BinaryIO::BinaryRead(file, distances->fogNear);
BinaryIO::BinaryRead(file, distances->fogFar);
BinaryIO::BinaryRead(file, distances->postFogSolid);
BinaryIO::BinaryRead(file, distances->postFogFade);
BinaryIO::BinaryRead(file, distances->staticObjectDistance);
BinaryIO::BinaryRead(file, distances->dynamicObjectDistance);
}
if (m_Version >= 40) {
uint32_t count = 0;
BinaryIO::BinaryRead(file, count);
for (uint32_t i = 0; i < count; ++i) {
SceneCullSetting setting;
BinaryIO::BinaryRead(file, setting.group);
BinaryIO::BinaryRead(file, setting.minDistance);
BinaryIO::BinaryRead(file, setting.maxDistance);
m_Lighting.cullSettings.push_back(setting);
}
}
}
BinaryIO::BinaryRead(file, m_Lighting.fogColor);
}
if (m_Version >= 36) BinaryIO::BinaryRead(file, m_Lighting.directional);
if (m_Version < 42) {
m_Lighting.hasSpawnPoint = true;
BinaryIO::BinaryRead(file, m_Lighting.filePosition);
if (m_Version < 33) {
m_Lighting.spawnPosition = NiPoint3(347.0f, 284.0f, -325.0f);
m_Lighting.spawnRotation = NiQuaternion(0.97082251f, 0.0f, 0.23979919f, 0.0f);
} else {
m_Lighting.spawnPosition = m_Lighting.filePosition;
BinaryIO::BinaryRead(file, m_Lighting.spawnRotation);
}
}
}
void Level::ReadSkydomeInfo(std::istream& file) {
// SceneLoader::ReadSkydomeInfo (0x0102f550)
BinaryIO::ReadString<uint32_t>(file, m_Skydome.filename, BinaryIO::ReadType::String);
if (m_Version >= 34) {
for (auto& filename : m_Skydome.extraFilenames) BinaryIO::ReadString<uint32_t>(file, filename, BinaryIO::ReadType::String);
}
}
void Level::ReadEditorSettings(std::istream& file) {
// SceneLoader::ReadEditorSettings (0x0101b090): their size, then a count of colours; the client keeps the first 32
if (m_Version < 37 || m_NoEditorSettings != 0) return;
uint32_t size = 0;
BinaryIO::BinaryRead(file, size);
uint32_t count = 0;
BinaryIO::BinaryRead(file, count);
for (uint32_t i = 0; i < count; ++i) {
NiColor color;
BinaryIO::BinaryRead(file, color);
if (i < 32) m_EditorColors.push_back(color);
}
}
void Level::ReadSceneObjectDataChunk(std::istream& file) {
// SceneLoader::ReadLvlGameObjects: the client has no objects for a file older than version 3 ("Level file is
// unsupported"), and before chunks reads nothing after them either
if (m_Version < 3) return;
uint32_t objectsCount = 0;
BinaryIO::BinaryRead(file, objectsCount);
for (uint32_t i = 0; i < objectsCount; ++i) {
// SceneLoader::ReadLvlObjectData (0x0103ba20)
SceneObject obj;
BinaryIO::BinaryRead(file, obj.id);
BinaryIO::BinaryRead(file, obj.lot);
if (m_Version >= 38) {
int32_t nodeType = 1;
BinaryIO::BinaryRead(file, nodeType);
if (nodeType > -1 && nodeType < 11) obj.nodeType = nodeType;
}
if (m_Version >= 32) BinaryIO::BinaryRead(file, obj.glomId);
BinaryIO::BinaryRead(file, obj.position);
BinaryIO::BinaryRead(file, obj.rotation);
BinaryIO::BinaryRead(file, obj.scale);
const auto config = ReadWideString32(file);
if (m_Version >= 7) {
uint32_t techniqueCount = 0;
BinaryIO::BinaryRead(file, techniqueCount);
if (techniqueCount != 0) {
obj.renderTechniquesName = ReadName64(file);
obj.renderTechniques.resize(techniqueCount);
for (auto& technique : obj.renderTechniques) {
technique.name = ReadName64(file);
BinaryIO::BinaryRead(file, technique.valueCount);
BinaryIO::BinaryRead(file, technique.isColor);
for (auto& value : technique.values) BinaryIO::BinaryRead(file, value);
}
}
}
if (file.fail()) throw std::runtime_error("Failed to read from istream.");
for (const auto& token : GeneralUtils::SplitString(config, '\n')) obj.settings.ParseInsert(token);
m_Objects.push_back(std::move(obj));
}
}

View File

@@ -2,8 +2,55 @@
#include "dZMCommon.h"
#include <map>
#include <iostream>
#include "NiColor.h"
#include "Zone.h"
// Fog and object draw distances (SceneDrawDistances); -1 is the client's default for a value the file doesn't have
struct SceneDrawDistances {
float fogNear = -1.0f;
float fogFar = -1.0f;
float postFogSolid = -1.0f;
float postFogFade = -1.0f;
float staticObjectDistance = -1.0f;
float dynamicObjectDistance = -1.0f;
};
// The draw distances of a group of objects (version 40 on)
struct SceneCullSetting {
uint32_t group{};
float minDistance{};
float maxDistance{};
};
// A scene's light environment (SceneLoader::ReadLightingInfo); the defaults are the client's for what older files don't have
struct LevelLighting {
float blendTime = 10.0f; // version 45 on
NiColor ambient;
NiColor specular;
NiColor upperHemi;
NiPoint3 position;
// Version 31 on; before 39 only the fog distances, the same in both
SceneDrawDistances minDrawDistances;
SceneDrawDistances maxDrawDistances;
std::vector<SceneCullSetting> cullSettings; // version 40 on
NiColor fogColor{ 0.75f, 0.88f, 0.91f }; // version 31 on
NiColor directional; // version 36 on
// Before version 42 the zone's spawn point is here. Before 33 the file has only a position, which the client
// ignores: it uses a fixed spawn point instead
bool hasSpawnPoint = false;
NiPoint3 filePosition;
NiPoint3 spawnPosition;
NiQuaternion spawnRotation = QuatUtils::IDENTITY;
};
// The skydome's file and five more (version 34 on) the client reads and doesn't use (SceneLoader::ReadSkydomeInfo)
struct LevelSkydome {
std::string filename;
std::array<std::string, 5> extraFilenames;
};
// A scene file (.lvl), read as the client reads it (SceneLoader::LoadLvlFile)
class Level {
public:
enum ChunkTypeID : uint16_t {
@@ -20,34 +67,72 @@ public:
};
struct FileInfoChunk {
uint32_t version;
uint32_t revision;
uint32_t enviromentChunkStart;
uint32_t objectChunkStart;
uint32_t particleChunkStart;
uint32_t version{};
uint32_t revision{};
uint32_t enviromentChunkStart{};
uint32_t objectChunkStart{};
uint32_t particleChunkStart{};
};
struct Header {
uint32_t id;
uint16_t chunkVersion;
ChunkTypeID chunkType;
uint32_t size;
uint32_t startPosition;
uint32_t id{};
uint16_t chunkVersion{};
ChunkTypeID chunkType{};
uint32_t size{};
uint32_t startPosition{};
FileInfoChunk fileInfo;
LWOSCENEID lwoSceneID;
};
// The environment chunk: where its parts start in the file (0 for none)
struct EnviromentChunk {
uint32_t lightingStart{};
uint32_t skydomeStart{};
uint32_t editorSettingsStart{};
};
public:
// Reads the scene file and loads its objects into the world
Level(Zone* parentZone, const std::string& filepath);
// Only reads the scene file. Throws std::runtime_error (BinaryIO) if it ends early; what was read before that
// stays (an object is only added once all of it was read)
explicit Level(std::istream& file);
static void MakeSpawner(const SceneObject& obj);
uint32_t GetVersion() const { return m_Version; }
uint32_t GetRevision() const { return m_Revision; }
const EnviromentChunk& GetEnviroment() const { return m_Enviroment; }
bool HasLighting() const { return m_HasLighting; }
const LevelLighting& GetLighting() const { return m_Lighting; }
const LevelSkydome& GetSkydome() const { return m_Skydome; }
const std::vector<NiColor>& GetEditorColors() const { return m_EditorColors; }
const std::vector<SceneObject>& GetObjects() const { return m_Objects; }
std::map<uint32_t, Header> m_ChunkHeaders;
private:
Zone* m_ParentZone;
Zone* m_ParentZone = nullptr;
// The file's version: from the file info chunk, or the header of a file from before chunks
uint32_t m_Version{};
uint32_t m_Revision{};
// Before chunks, from version 32: set when the file has no editor settings
uint8_t m_NoEditorSettings{};
EnviromentChunk m_Enviroment;
bool m_HasLighting = false;
LevelLighting m_Lighting;
LevelSkydome m_Skydome;
// Version 37 on: the editor's saved colours (the client keeps at most 32)
std::vector<NiColor> m_EditorColors;
std::vector<SceneObject> m_Objects;
//private functions:
void ReadChunks(std::istream& file);
static Header ReadChunkHeader(std::istream& file, uint32_t start);
void ReadFileInfoChunk(std::istream& file, Header& header);
void ReadSceneObjectDataChunk(std::istream& file, Header& header);
void ReadLightingInfo(std::istream& file);
void ReadSkydomeInfo(std::istream& file);
void ReadEditorSettings(std::istream& file);
void ReadSceneObjectDataChunk(std::istream& file);
void LoadSceneObjects();
};

View File

@@ -3,17 +3,28 @@
#include "NiPoint3.h"
#include "NiQuaternion.h"
#include "LDFFormat.h"
#include <array>
#include <string>
#include <vector>
// One of an object's render techniques (SceneLoader::ReadLvlObjectData, version 7 on)
struct LevelRenderTechnique {
std::string name; // 64 bytes in the file
uint32_t valueCount{};
uint8_t isColor{};
std::array<float, 16> values{};
};
struct SceneObject {
LWOOBJID id;
LOT lot;
uint32_t nodeType;
uint32_t glomId;
LWOOBJID id{};
LOT lot{};
uint32_t nodeType = 1; // the client's default when the file has none, or one outside 0-10
uint32_t glomId = 1;
NiPoint3 position;
NiQuaternion rotation = QuatUtils::IDENTITY;
float scale = 1.0f;
uint32_t value3;
std::string renderTechniquesName; // 64 bytes in the file, only there when there are render techniques
std::vector<LevelRenderTechnique> renderTechniques;
LwoNameValue settings;
};

View File

@@ -1,4 +1,5 @@
SET(DZONEMANAGER_TESTS
"LevelTests.cpp"
"RawTests.cpp"
"ZoneSceneTests.cpp")

View 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);
}