mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-11-14 07:47: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:
@@ -6,104 +6,98 @@
|
||||
|
||||
#include "BinaryIO.h"
|
||||
#include "GeneralUtils.h"
|
||||
#include "Logger.h"
|
||||
|
||||
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 LevelFile::Read(std::istream& file) {
|
||||
const uint32_t CHNK_HEADER = ('C' + ('H' << 8) + ('N' << 16) + ('K' << 24));
|
||||
|
||||
uint32_t magic = 0;
|
||||
BinaryIO::BinaryRead(file, magic);
|
||||
if (magic == CHNK_HEADER) {
|
||||
// SceneLoader::ReadLvlFile / DoLvlChunk: the file info chunk starts the file and gives where the environment,
|
||||
// object and particle chunks start (0 for none); only the objects are read here
|
||||
// SceneLoader::LoadLvlFile / LvlChunkFile::Read: the file info chunk starts the file and gives where the
|
||||
// environment, object and particle chunks start (0 for none)
|
||||
ChunkHeader fileInfo = ReadChunkHeader(file, 0);
|
||||
file.seekg(fileInfo.startPosition);
|
||||
ReadFileInfoChunk(file, fileInfo);
|
||||
chunkHeaders.insert({ fileInfo.id, fileInfo });
|
||||
for (const auto start : { fileInfo.fileInfo.enviromentChunkStart, fileInfo.fileInfo.objectChunkStart, fileInfo.fileInfo.particleChunkStart }) {
|
||||
if (start == 0) continue;
|
||||
const auto header = ReadChunkHeader(file, start);
|
||||
version = fileInfo.fileInfo.version;
|
||||
revision = fileInfo.fileInfo.revision;
|
||||
|
||||
if (fileInfo.fileInfo.enviromentChunkStart != 0) {
|
||||
const auto header = ReadChunkHeader(file, fileInfo.fileInfo.enviromentChunkStart);
|
||||
chunkHeaders.insert({ header.id, header });
|
||||
if (start != fileInfo.fileInfo.objectChunkStart) continue;
|
||||
// The objects are laid out by the file's version, from its file info chunk
|
||||
// EnvironmentChunk::ReadLvlChunk2000: where the lighting, skydome and editor settings start
|
||||
file.seekg(header.startPosition);
|
||||
ReadSceneObjectDataChunk(file, fileInfo.fileInfo.version);
|
||||
BinaryIO::BinaryRead(file, enviroment.lightingStart);
|
||||
BinaryIO::BinaryRead(file, enviroment.skydomeStart);
|
||||
BinaryIO::BinaryRead(file, enviroment.editorSettingsStart);
|
||||
// The client reads all three only when there is lighting
|
||||
if (enviroment.lightingStart != 0) {
|
||||
file.seekg(enviroment.lightingStart);
|
||||
ReadLightingInfo(file);
|
||||
file.seekg(enviroment.skydomeStart);
|
||||
ReadSkydomeInfo(file);
|
||||
file.seekg(enviroment.editorSettingsStart);
|
||||
ReadEditorSettings(file);
|
||||
}
|
||||
}
|
||||
|
||||
if (fileInfo.fileInfo.objectChunkStart != 0) {
|
||||
const auto header = ReadChunkHeader(file, fileInfo.fileInfo.objectChunkStart);
|
||||
chunkHeaders.insert({ header.id, header });
|
||||
file.seekg(header.startPosition);
|
||||
ReadSceneObjectDataChunk(file);
|
||||
}
|
||||
|
||||
if (fileInfo.fileInfo.particleChunkStart != 0) {
|
||||
const auto header = ReadChunkHeader(file, fileInfo.fileInfo.particleChunkStart);
|
||||
chunkHeaders.insert({ header.id, header });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Scenes from before chunks (SceneLoader::ReadLvlSections): a header, then lighting, skydome and editor settings
|
||||
// the world skips, then the objects
|
||||
// Scenes from before chunks: a header (SceneLoader::ReadLvlHeader), then each part in turn
|
||||
file.seekg(0);
|
||||
ChunkHeader header;
|
||||
header.id = ChunkTypeID::FileInfo;
|
||||
BinaryIO::BinaryRead(file, header.chunkVersion);
|
||||
BinaryIO::BinaryRead(file, header.chunkType);
|
||||
// Only from version 32 (SceneLoader::ReadLvlHeader)
|
||||
uint8_t important = 0;
|
||||
if (header.chunkVersion >= 32) BinaryIO::BinaryRead(file, important);
|
||||
if (header.chunkVersion > 36) {
|
||||
BinaryIO::BinaryRead(file, header.fileInfo.revision);
|
||||
}
|
||||
// Lighting (SceneLoader::ReadLightingInfo)
|
||||
if (header.chunkVersion >= 45) file.ignore(4);
|
||||
file.ignore(4 * (4 * 3));
|
||||
version = header.chunkVersion;
|
||||
if (version >= 32) BinaryIO::BinaryRead(file, noEditorSettings);
|
||||
if (version > 36) BinaryIO::BinaryRead(file, revision);
|
||||
header.fileInfo.version = version;
|
||||
header.fileInfo.revision = revision;
|
||||
chunkHeaders.insert({ header.id, header });
|
||||
|
||||
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);
|
||||
// A newer file is refused ("newer than the client understands")
|
||||
if (version > 48) {
|
||||
LOG("Scene file version %u is newer than the client reads; nothing read from it", version);
|
||||
return;
|
||||
}
|
||||
|
||||
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; five more strings from version 34 (SceneLoader::ReadSkydomeInfo)
|
||||
uint32_t count = 0;
|
||||
BinaryIO::BinaryRead(file, count);
|
||||
file.ignore(count);
|
||||
|
||||
if (header.chunkVersion >= 34) {
|
||||
for (uint32_t i = 0; i < 5; ++i) {
|
||||
uint32_t count = 0;
|
||||
BinaryIO::BinaryRead(file, count);
|
||||
file.ignore(count);
|
||||
}
|
||||
}
|
||||
// editor settings: their size and then that many bytes (SceneLoader::ReadEditorSettings)
|
||||
if (!important && header.chunkVersion >= 37) {
|
||||
uint32_t size = 0;
|
||||
BinaryIO::BinaryRead(file, size);
|
||||
file.ignore(size);
|
||||
}
|
||||
|
||||
// The client has no objects for a file older than version 3 ("Level file is unsupported")
|
||||
header.fileInfo.version = header.chunkVersion;
|
||||
if (header.chunkVersion >= 3) {
|
||||
header.id = ChunkTypeID::SceneObjectData;
|
||||
ReadSceneObjectDataChunk(file, header.fileInfo.version);
|
||||
}
|
||||
chunkHeaders.insert(std::make_pair(header.id, header));
|
||||
ReadLightingInfo(file);
|
||||
ReadSkydomeInfo(file);
|
||||
ReadEditorSettings(file);
|
||||
ReadSceneObjectDataChunk(file);
|
||||
}
|
||||
|
||||
LevelFile::ChunkHeader LevelFile::ReadChunkHeader(std::istream& file, uint32_t start) {
|
||||
@@ -128,88 +122,127 @@ void LevelFile::ReadFileInfoChunk(std::istream& file, ChunkHeader& header) {
|
||||
BinaryIO::BinaryRead(file, header.fileInfo.particleChunkStart);
|
||||
}
|
||||
|
||||
void LevelFile::ApplyClientConfigFixups(SceneObject& obj, uint32_t version) {
|
||||
// What ReadLvlObjectData (1.10.64, 0x0103ba20) changes in an object's config as it loads it
|
||||
auto& settings = obj.settings;
|
||||
const auto typeOf = [&settings](const std::u16string& key) {
|
||||
const auto it = settings.find(key);
|
||||
return it == settings.end() || !it->second ? LDF_TYPE_UNKNOWN : it->second->GetValueType();
|
||||
};
|
||||
const auto valueOf = [&settings]<typename T>(const std::u16string& key) {
|
||||
return static_cast<const LDFData<T>*>(settings.find(key)->second.get())->GetValue();
|
||||
};
|
||||
void LevelFile::ReadLightingInfo(std::istream& file) {
|
||||
// SceneLoader::ReadLightingInfo (0x0102f8f0)
|
||||
hasLighting = true;
|
||||
if (version >= 45) BinaryIO::BinaryRead(file, lighting.blendTime);
|
||||
BinaryIO::BinaryRead(file, lighting.ambient);
|
||||
BinaryIO::BinaryRead(file, lighting.specular);
|
||||
BinaryIO::BinaryRead(file, lighting.upperHemi);
|
||||
BinaryIO::BinaryRead(file, lighting.position);
|
||||
|
||||
// Before version 47 a respawn time was in milliseconds: a float over 100, or any u32, becomes seconds (a float)
|
||||
if (version < 47) {
|
||||
if (typeOf(u"respawn") == LDF_TYPE_FLOAT) {
|
||||
const auto respawn = valueOf.operator()<float>(u"respawn");
|
||||
if (respawn > 100.0f) settings.Insert<float>(u"respawn", respawn * 0.001f);
|
||||
} else if (typeOf(u"respawn") == LDF_TYPE_U32) {
|
||||
settings.Insert<float>(u"respawn", static_cast<float>(valueOf.operator()<uint32_t>(u"respawn") * 0.001));
|
||||
if (version >= 31) {
|
||||
if (version < 39) {
|
||||
BinaryIO::BinaryRead(file, lighting.maxDrawDistances.fogNear);
|
||||
BinaryIO::BinaryRead(file, lighting.maxDrawDistances.fogFar);
|
||||
lighting.minDrawDistances.fogNear = lighting.maxDrawDistances.fogNear;
|
||||
lighting.minDrawDistances.fogFar = lighting.maxDrawDistances.fogFar;
|
||||
} else {
|
||||
for (auto* distances : { &lighting.minDrawDistances, &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 (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);
|
||||
lighting.cullSettings.push_back(setting);
|
||||
}
|
||||
}
|
||||
}
|
||||
BinaryIO::BinaryRead(file, lighting.fogColor);
|
||||
}
|
||||
|
||||
// A model spawner (LOT 176 spawning template 14): its blueprint is its subkey unless it has one, its behaviors are
|
||||
// off unless it says otherwise, and it never wraps its render
|
||||
if (obj.lot == 176 && typeOf(u"spawntemplate") == LDF_TYPE_S32 && valueOf.operator()<int32_t>(u"spawntemplate") == 14) {
|
||||
if (typeOf(u"blueprintid") == LDF_TYPE_UNKNOWN) {
|
||||
const LWOOBJID subkey = typeOf(u"subkey") == LDF_TYPE_OBJID ? valueOf.operator()<LWOOBJID>(u"subkey") : LWOOBJID_EMPTY;
|
||||
settings.Insert<LWOOBJID>(u"blueprintid", subkey);
|
||||
}
|
||||
if (typeOf(u"DisableModelBehaviors") == LDF_TYPE_UNKNOWN) settings.Insert<bool>(u"DisableModelBehaviors", true);
|
||||
settings.Insert<bool>(u"preventRenderWrapping", true);
|
||||
}
|
||||
if (version >= 36) BinaryIO::BinaryRead(file, lighting.directional);
|
||||
|
||||
// Before version 44 an object that overrides its scene goes to layer 0 of the scene it names
|
||||
if (version < 44 && typeOf(u"sceneIDOverrideEnabled") != LDF_TYPE_UNKNOWN) settings.Insert<uint32_t>(u"sceneLayerIDOverride", 0);
|
||||
if (version < 42) {
|
||||
lighting.hasSpawnPoint = true;
|
||||
BinaryIO::BinaryRead(file, lighting.filePosition);
|
||||
if (version < 33) {
|
||||
lighting.spawnPosition = NiPoint3(347.0f, 284.0f, -325.0f);
|
||||
lighting.spawnRotation = NiQuaternion(0.97082251f, 0.0f, 0.23979919f, 0.0f);
|
||||
} else {
|
||||
lighting.spawnPosition = lighting.filePosition;
|
||||
BinaryIO::BinaryRead(file, lighting.spawnRotation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void LevelFile::ReadSceneObjectDataChunk(std::istream& file, uint32_t version) {
|
||||
void LevelFile::ReadSkydomeInfo(std::istream& file) {
|
||||
// SceneLoader::ReadSkydomeInfo (0x0102f550)
|
||||
BinaryIO::ReadString<uint32_t>(file, skydome.filename, BinaryIO::ReadType::String);
|
||||
if (version >= 34) {
|
||||
for (auto& filename : skydome.extraFilenames) BinaryIO::ReadString<uint32_t>(file, filename, BinaryIO::ReadType::String);
|
||||
}
|
||||
}
|
||||
|
||||
void LevelFile::ReadEditorSettings(std::istream& file) {
|
||||
// SceneLoader::ReadEditorSettings (0x0101b090): their size, then a count of colours; the client keeps the first 32
|
||||
if (version < 37 || 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) editorColors.push_back(color);
|
||||
}
|
||||
}
|
||||
|
||||
void LevelFile::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 (version < 3) return;
|
||||
|
||||
uint32_t objectsCount = 0;
|
||||
BinaryIO::BinaryRead(file, objectsCount);
|
||||
|
||||
for (uint32_t i = 0; i < objectsCount; ++i) {
|
||||
std::u16string ldfString;
|
||||
// SceneLoader::ReadLvlObjectData (0x0103ba20)
|
||||
SceneObject obj;
|
||||
BinaryIO::BinaryRead(file, obj.id);
|
||||
BinaryIO::BinaryRead(file, obj.lot);
|
||||
|
||||
if (version >= 38) {
|
||||
int32_t tmp = 1;
|
||||
BinaryIO::BinaryRead(file, tmp);
|
||||
if (tmp > -1 && tmp < 11) obj.nodeType = tmp;
|
||||
int32_t nodeType = 1;
|
||||
BinaryIO::BinaryRead(file, nodeType);
|
||||
if (nodeType > -1 && nodeType < 11) obj.nodeType = nodeType;
|
||||
}
|
||||
|
||||
if (version >= 32) {
|
||||
BinaryIO::BinaryRead(file, obj.glomId);
|
||||
}
|
||||
if (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);
|
||||
// From version 7 the object's render techniques: a count, and when there are any a 64 byte header and 133 bytes
|
||||
// each (64 byte name, u32, u8, 16 floats) (ReadLvlObjectData)
|
||||
if (version > 6) {
|
||||
BinaryIO::BinaryRead(file, obj.renderTechniqueCount);
|
||||
if (obj.renderTechniqueCount != 0) {
|
||||
std::string techniques(64 + static_cast<size_t>(obj.renderTechniqueCount) * (64 + 4 + 1 + 64), '\0');
|
||||
file.read(techniques.data(), static_cast<std::streamsize>(techniques.size()));
|
||||
const auto config = ReadWideString32(file);
|
||||
|
||||
if (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.");
|
||||
|
||||
// LDF_FROM_STRING (0x010fa220): entries are split at every comma and line break, empty ones and ones without a
|
||||
// '=' are left out
|
||||
const auto config = GeneralUtils::UTF16ToWTF8(ldfString);
|
||||
size_t start = 0;
|
||||
while (start <= config.size()) {
|
||||
const auto end = std::min(config.find_first_of(",\n", start), config.size());
|
||||
const auto entry = config.substr(start, end - start);
|
||||
if (entry.find('=') != std::string::npos) obj.settings.ParseInsert(entry);
|
||||
start = end + 1;
|
||||
}
|
||||
ApplyClientConfigFixups(obj, version);
|
||||
for (const auto& token : GeneralUtils::SplitString(config, '\n')) obj.settings.ParseInsert(token);
|
||||
|
||||
objects.push_back(std::move(obj));
|
||||
}
|
||||
|
||||
@@ -1,31 +1,89 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <iosfwd>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "dCommonVars.h"
|
||||
#include "NiColor.h"
|
||||
#include "NiPoint3.h"
|
||||
#include "NiQuaternion.h"
|
||||
#include "LDFFormat.h"
|
||||
|
||||
// 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;
|
||||
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 renderTechniqueCount{};
|
||||
std::string renderTechniquesName; // 64 bytes in the file, only there when there are render techniques
|
||||
std::vector<LevelRenderTechnique> renderTechniques;
|
||||
LwoNameValue settings;
|
||||
};
|
||||
|
||||
// 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) as the world server loads it (Level): its chunk headers and every object in it, with the object's
|
||||
* settings parsed. Only reads; which objects the world spawns (feature gating, client-only objects, spawners) stays in
|
||||
* dZoneManager, so the dashboard reads scenes with the same code.
|
||||
* A scene file (.lvl), read as the client reads it (SceneLoader::LoadLvlFile): its chunk headers, environment, every
|
||||
* object in it with the object's settings parsed. Only reads; which objects the world
|
||||
* spawns (feature gating, client-only objects, spawners) stays in dZoneManager, so the dashboard reads scenes with the
|
||||
* same code.
|
||||
*/
|
||||
struct LevelFile {
|
||||
enum ChunkTypeID : uint16_t {
|
||||
@@ -52,7 +110,26 @@ struct LevelFile {
|
||||
FileInfoChunk fileInfo;
|
||||
};
|
||||
|
||||
// The environment chunk: where its parts start in the file (0 for none)
|
||||
struct EnviromentChunk {
|
||||
uint32_t lightingStart{};
|
||||
uint32_t skydomeStart{};
|
||||
uint32_t editorSettingsStart{};
|
||||
};
|
||||
|
||||
std::map<uint32_t, ChunkHeader> chunkHeaders;
|
||||
|
||||
// The file's version: from the file info chunk, or the header of a file from before chunks
|
||||
uint32_t version{};
|
||||
uint32_t revision{};
|
||||
// Before chunks, from version 32: set when the file has no editor settings
|
||||
uint8_t noEditorSettings{};
|
||||
EnviromentChunk enviroment;
|
||||
bool hasLighting = false;
|
||||
LevelLighting lighting;
|
||||
LevelSkydome skydome;
|
||||
// Version 37 on: the editor's saved colours (the client keeps at most 32)
|
||||
std::vector<NiColor> editorColors;
|
||||
std::vector<SceneObject> objects;
|
||||
|
||||
/**
|
||||
@@ -64,6 +141,8 @@ struct LevelFile {
|
||||
private:
|
||||
static ChunkHeader ReadChunkHeader(std::istream& file, uint32_t start);
|
||||
void ReadFileInfoChunk(std::istream& file, ChunkHeader& header);
|
||||
void ReadSceneObjectDataChunk(std::istream& file, uint32_t version);
|
||||
static void ApplyClientConfigFixups(SceneObject& obj, uint32_t version);
|
||||
void ReadLightingInfo(std::istream& file);
|
||||
void ReadSkydomeInfo(std::istream& file);
|
||||
void ReadEditorSettings(std::istream& file);
|
||||
void ReadSceneObjectDataChunk(std::istream& file);
|
||||
};
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include "PhysicsComponent.h"
|
||||
#include "dConfig.h"
|
||||
#include <ranges>
|
||||
#include <stdexcept>
|
||||
|
||||
Level::Level(Zone* parentZone, const std::string& filepath, int32_t sceneID) {
|
||||
m_ParentZone = parentZone;
|
||||
@@ -35,7 +36,12 @@ Level::Level(Zone* parentZone, const std::string& filepath, int32_t sceneID) {
|
||||
|
||||
// The file itself is read by LevelFile (dCommon), which the dashboard uses too
|
||||
LevelFile levelFile;
|
||||
levelFile.Read(stream);
|
||||
try {
|
||||
levelFile.Read(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(), levelFile.objects.size());
|
||||
}
|
||||
for (const auto& [id, chunkHeader] : levelFile.chunkHeaders) {
|
||||
Header header;
|
||||
static_cast<LevelFile::ChunkHeader&>(header) = chunkHeader;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
SET(DZONEMANAGER_TESTS
|
||||
"LevelTests.cpp"
|
||||
"RawTests.cpp"
|
||||
"ZoneFileVersionTests.cpp"
|
||||
"ZonePathTests.cpp"
|
||||
|
||||
323
tests/dGameTests/dZoneManagerTests/LevelTests.cpp
Normal file
323
tests/dGameTests/dZoneManagerTests/LevelTests.cpp
Normal file
@@ -0,0 +1,323 @@
|
||||
#include "LevelFile.h"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <cstring>
|
||||
#include <sstream>
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// Reads a scene file as the world and the dashboard do
|
||||
LevelFile Read(const std::string& data) {
|
||||
std::istringstream stream(data);
|
||||
LevelFile level;
|
||||
level.Read(stream);
|
||||
return level;
|
||||
}
|
||||
}
|
||||
|
||||
TEST(LevelTests, ReadsEveryPartOfAChunkedFile) {
|
||||
const auto level = Read(WriteChunked(46));
|
||||
EXPECT_EQ(level.version, 46);
|
||||
EXPECT_EQ(level.revision, 9);
|
||||
ASSERT_EQ(level.chunkHeaders.size(), 4);
|
||||
EXPECT_EQ(level.chunkHeaders.at(1000).fileInfo.revision, 9);
|
||||
|
||||
ASSERT_TRUE(level.hasLighting);
|
||||
EXPECT_FLOAT_EQ(level.lighting.blendTime, 2.5f);
|
||||
EXPECT_FLOAT_EQ(level.lighting.ambient.m_Blue, 0.3f);
|
||||
EXPECT_FLOAT_EQ(level.lighting.upperHemi.m_Red, 0.7f);
|
||||
EXPECT_FLOAT_EQ(level.lighting.position.z, 30.0f);
|
||||
EXPECT_FLOAT_EQ(level.lighting.minDrawDistances.dynamicObjectDistance, 6.0f);
|
||||
EXPECT_FLOAT_EQ(level.lighting.maxDrawDistances.fogNear, 7.0f);
|
||||
ASSERT_EQ(level.lighting.cullSettings.size(), 1);
|
||||
EXPECT_EQ(level.lighting.cullSettings[0].group, 3);
|
||||
EXPECT_FLOAT_EQ(level.lighting.cullSettings[0].maxDistance, 200.0f);
|
||||
EXPECT_FLOAT_EQ(level.lighting.fogColor.m_Green, 0.5f);
|
||||
EXPECT_FLOAT_EQ(level.lighting.directional.m_Red, 1.0f);
|
||||
EXPECT_FALSE(level.lighting.hasSpawnPoint);
|
||||
|
||||
EXPECT_EQ(level.skydome.filename, "mesh/env/sky.nif");
|
||||
EXPECT_EQ(level.skydome.extraFilenames[4], "e.nif");
|
||||
ASSERT_EQ(level.editorColors.size(), 2);
|
||||
EXPECT_FLOAT_EQ(level.editorColors[1].m_Green, 1.0f);
|
||||
|
||||
ASSERT_EQ(level.objects.size(), 3);
|
||||
const auto& technique = level.objects[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(LevelTests, ReadsAFileFromBeforeChunks) {
|
||||
const auto level = Read(WriteFlat(40));
|
||||
EXPECT_EQ(level.version, 40);
|
||||
EXPECT_EQ(level.revision, 77);
|
||||
ASSERT_EQ(level.chunkHeaders.size(), 1);
|
||||
EXPECT_EQ(level.chunkHeaders.at(1000).fileInfo.revision, 77);
|
||||
EXPECT_FLOAT_EQ(level.lighting.blendTime, 10.0f); // the client's default before version 45
|
||||
ASSERT_EQ(level.lighting.cullSettings.size(), 1);
|
||||
ASSERT_TRUE(level.lighting.hasSpawnPoint);
|
||||
EXPECT_FLOAT_EQ(level.lighting.spawnPosition.y, 6.0f);
|
||||
EXPECT_FLOAT_EQ(level.lighting.spawnRotation.x, 1.0f);
|
||||
EXPECT_EQ(level.editorColors.size(), 2);
|
||||
|
||||
ASSERT_EQ(level.objects.size(), 1);
|
||||
}
|
||||
|
||||
// Older lighting: fog distances only before 39, no directional colour before 36, and before 33 a fixed spawn point
|
||||
TEST(LevelTests, ReadsOlderLighting) {
|
||||
const auto v32 = Read(WriteFlat(32));
|
||||
EXPECT_EQ(v32.revision, 0);
|
||||
EXPECT_FLOAT_EQ(v32.lighting.minDrawDistances.fogFar, 60.0f);
|
||||
EXPECT_FLOAT_EQ(v32.lighting.maxDrawDistances.fogNear, 50.0f);
|
||||
EXPECT_FLOAT_EQ(v32.lighting.maxDrawDistances.staticObjectDistance, -1.0f);
|
||||
EXPECT_FLOAT_EQ(v32.lighting.directional.m_Red, 0.0f);
|
||||
EXPECT_FLOAT_EQ(v32.lighting.filePosition.x, 5.0f);
|
||||
EXPECT_FLOAT_EQ(v32.lighting.spawnPosition.x, 347.0f);
|
||||
EXPECT_TRUE(v32.skydome.extraFilenames[0].empty());
|
||||
EXPECT_TRUE(v32.editorColors.empty());
|
||||
ASSERT_EQ(v32.objects.size(), 1);
|
||||
|
||||
const auto v30 = Read(WriteFlat(30));
|
||||
EXPECT_FLOAT_EQ(v30.lighting.fogColor.m_Red, 0.75f); // the client's default
|
||||
EXPECT_FLOAT_EQ(v30.lighting.spawnRotation.y, 0.23979919f);
|
||||
EXPECT_EQ(v30.objects.size(), 1);
|
||||
}
|
||||
|
||||
// A file older than 3 has no objects, a newer one than 48 isn't read
|
||||
TEST(LevelTests, ReadsWhatTheClientReadsOfOtherFiles) {
|
||||
const auto old = Read(WriteFlat(2));
|
||||
EXPECT_TRUE(old.hasLighting);
|
||||
EXPECT_TRUE(old.objects.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.version, 49);
|
||||
EXPECT_FALSE(refused.hasLighting);
|
||||
}
|
||||
|
||||
// A chunked file without lighting has none of the environment's parts read
|
||||
TEST(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.skydome.filename.empty());
|
||||
EXPECT_TRUE(level.editorColors.empty());
|
||||
EXPECT_EQ(level.objects.size(), 3);
|
||||
}
|
||||
Reference in New Issue
Block a user