mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-11-14 15:57:13 +00:00
LwoNameValue::FromString (0x010fa220) splits a config at every comma and line break and leaves out entries without a '='; the world split only at line breaks. Objects and sounds are now split the client's way. ReadLvlObjectData (0x0103ba20) also adjusts an object's config as it loads it, and now so does the world: - before version 47 a respawn time was in milliseconds: a float over 100, or any u32, becomes seconds (a float); - a model spawner (LOT 176 spawning template 14) gets its blueprint from its subkey unless it has one, behaviors off unless it says otherwise, and no render wrapping; - before version 44 an object that overrides its scene goes to layer 0 of the scene it names. Check in game: every world spawns its objects as before; spawner respawn times are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
357 lines
13 KiB
C++
357 lines
13 KiB
C++
#include "LevelFile.h"
|
|
|
|
#include <algorithm>
|
|
#include <istream>
|
|
#include <stdexcept>
|
|
|
|
#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);
|
|
}
|
|
|
|
// LwoNameValue::FromString (0x010fa220): entries are split at every comma and line break, empty ones and ones
|
|
// without a '=' are left out
|
|
void ParseConfig(LwoNameValue& config, const std::string& text) {
|
|
size_t start = 0;
|
|
while (start <= text.size()) {
|
|
const auto end = std::min(text.find_first_of(",\n", start), text.size());
|
|
const auto entry = text.substr(start, end - start);
|
|
if (entry.find('=') != std::string::npos) config.ParseInsert(entry);
|
|
start = end + 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
void LevelFile::Read(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)
|
|
ChunkHeader fileInfo = ReadChunkHeader(file, 0);
|
|
file.seekg(fileInfo.startPosition);
|
|
ReadFileInfoChunk(file, fileInfo);
|
|
chunkHeaders.insert({ fileInfo.id, fileInfo });
|
|
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 });
|
|
// EnvironmentChunk::ReadLvlChunk2000: where the lighting, skydome and editor settings start
|
|
file.seekg(header.startPosition);
|
|
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 });
|
|
file.seekg(header.startPosition);
|
|
ReadParticles(file);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// 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);
|
|
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 });
|
|
|
|
// 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;
|
|
}
|
|
|
|
ReadLightingInfo(file);
|
|
ReadSkydomeInfo(file);
|
|
ReadEditorSettings(file);
|
|
ReadSceneObjectDataChunk(file);
|
|
ReadSounds(file);
|
|
ReadParticles(file);
|
|
}
|
|
|
|
LevelFile::ChunkHeader LevelFile::ReadChunkHeader(std::istream& file, uint32_t start) {
|
|
// No check of the magic, as in the client (0x01063a00)
|
|
file.seekg(start);
|
|
uint32_t magic = 0;
|
|
ChunkHeader 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 LevelFile::ReadFileInfoChunk(std::istream& file, ChunkHeader& 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 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);
|
|
|
|
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);
|
|
}
|
|
|
|
if (version >= 36) BinaryIO::BinaryRead(file, lighting.directional);
|
|
|
|
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::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) {
|
|
// SceneLoader::ReadLvlObjectData (0x0103ba20)
|
|
SceneObject obj;
|
|
BinaryIO::BinaryRead(file, obj.id);
|
|
BinaryIO::BinaryRead(file, obj.lot);
|
|
|
|
if (version >= 38) {
|
|
int32_t nodeType = 1;
|
|
BinaryIO::BinaryRead(file, nodeType);
|
|
if (nodeType > -1 && nodeType < 11) obj.nodeType = nodeType;
|
|
}
|
|
|
|
if (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 (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.");
|
|
|
|
ParseConfig(obj.settings, config);
|
|
ApplyClientConfigFixups(obj);
|
|
|
|
objects.push_back(std::move(obj));
|
|
}
|
|
}
|
|
|
|
void LevelFile::ApplyClientConfigFixups(SceneObject& obj) const {
|
|
// What ReadLvlObjectData 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();
|
|
};
|
|
|
|
// 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));
|
|
}
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
|
|
void LevelFile::ReadSounds(std::istream& file) {
|
|
// SceneLoader::LoadSounds (0x0103c110), only in files from before chunks
|
|
if (version < 5) return;
|
|
uint32_t count = 0;
|
|
BinaryIO::BinaryRead(file, count);
|
|
for (uint32_t i = 0; i < count; ++i) {
|
|
LevelSound sound;
|
|
BinaryIO::BinaryRead(file, sound.position);
|
|
if (version < 35) {
|
|
// A 260 character name, up to its first null
|
|
std::u16string name(260, u'\0');
|
|
file.read(reinterpret_cast<char*>(name.data()), static_cast<std::streamsize>(name.size() * sizeof(char16_t)));
|
|
name.resize(std::min(name.find(u'\0'), name.size()));
|
|
sound.config.Insert<std::u16string>(u"soundName", name);
|
|
} else {
|
|
ParseConfig(sound.config, ReadWideString32(file));
|
|
}
|
|
if (file.fail()) throw std::runtime_error("Failed to read from istream.");
|
|
sounds.push_back(std::move(sound));
|
|
}
|
|
}
|
|
|
|
void LevelFile::ReadParticles(std::istream& file) {
|
|
// SceneLoader::LoadParticles (0x01048320); before version 44 a file may end before them
|
|
if (version < 5) return;
|
|
if (version < 44 && file.peek() == std::char_traits<char>::eof()) {
|
|
file.clear();
|
|
return;
|
|
}
|
|
|
|
uint32_t count = 0;
|
|
BinaryIO::BinaryRead(file, count);
|
|
std::string config;
|
|
for (uint32_t i = 0; i < count; ++i) {
|
|
LevelParticle particle;
|
|
if (version >= 43) BinaryIO::BinaryRead(file, particle.priority);
|
|
BinaryIO::BinaryRead(file, particle.position);
|
|
BinaryIO::BinaryRead(file, particle.rotation);
|
|
particle.effectNames = ReadWideString32(file);
|
|
if (version < 46) {
|
|
// Before version 46 the effect names end with a wide null
|
|
uint16_t nameTerminator{};
|
|
BinaryIO::BinaryRead(file, nameTerminator);
|
|
} else {
|
|
config = ReadWideString32(file);
|
|
}
|
|
particle.config = config;
|
|
if (file.fail()) throw std::runtime_error("Failed to read from istream.");
|
|
// The client stops at a particle with no effect
|
|
if (particle.effectNames.empty()) break;
|
|
particles.push_back(std::move(particle));
|
|
}
|
|
}
|