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
parent 88d08aa2b0
commit d06cc2923e
5 changed files with 582 additions and 140 deletions

View File

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