diff --git a/dZoneManager/Level.cpp b/dZoneManager/Level.cpp index 72fbdcdd7..f9f6b679f 100644 --- a/dZoneManager/Level.cpp +++ b/dZoneManager/Level.cpp @@ -17,6 +17,8 @@ #include "ClientVersion.h" #include "dConfig.h" #include +#include +#include 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(); 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(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(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(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(file, m_Skydome.filename, BinaryIO::ReadType::String); + if (m_Version >= 34) { + for (auto& filename : m_Skydome.extraFilenames) BinaryIO::ReadString(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)); + } +} diff --git a/dZoneManager/Level.h b/dZoneManager/Level.h index 22860fe72..56a72485b 100644 --- a/dZoneManager/Level.h +++ b/dZoneManager/Level.h @@ -2,8 +2,55 @@ #include "dZMCommon.h" #include #include +#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 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 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& GetEditorColors() const { return m_EditorColors; } + const std::vector& GetObjects() const { return m_Objects; } + std::map 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 m_EditorColors; + std::vector 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(); }; diff --git a/dZoneManager/dZMCommon.h b/dZoneManager/dZMCommon.h index 53f1d3c4a..b7dc59b26 100644 --- a/dZoneManager/dZMCommon.h +++ b/dZoneManager/dZMCommon.h @@ -3,17 +3,28 @@ #include "NiPoint3.h" #include "NiQuaternion.h" #include "LDFFormat.h" +#include +#include #include +// 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 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 renderTechniques; LwoNameValue settings; }; diff --git a/tests/dGameTests/dZoneManagerTests/CMakeLists.txt b/tests/dGameTests/dZoneManagerTests/CMakeLists.txt index 009ec7ddc..6a816c451 100644 --- a/tests/dGameTests/dZoneManagerTests/CMakeLists.txt +++ b/tests/dGameTests/dZoneManagerTests/CMakeLists.txt @@ -1,4 +1,5 @@ SET(DZONEMANAGER_TESTS + "LevelTests.cpp" "RawTests.cpp" "ZoneSceneTests.cpp") diff --git a/tests/dGameTests/dZoneManagerTests/LevelTests.cpp b/tests/dGameTests/dZoneManagerTests/LevelTests.cpp new file mode 100644 index 000000000..2bb1fb4a3 --- /dev/null +++ b/tests/dGameTests/dZoneManagerTests/LevelTests.cpp @@ -0,0 +1,328 @@ +#include "Level.h" + +#include + +#include +#include + +#include "GameDependencies.h" + +namespace { + template + void Write(std::ostream& stream, const T& value) { + stream.write(reinterpret_cast(&value), sizeof(T)); + } + + void WriteFloats(std::ostream& stream, std::initializer_list 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(stream, static_cast(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(stream, static_cast(value.size())); + for (const char c : value) Write(stream, static_cast(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(stream, 1); + Write(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(stream, 4 + 12 * 2); // size + Write(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(stream, 12); // node type out of range: the client uses 1 + if (version >= 32) Write(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(stream, 4); + Write(stream, 1); + for (uint32_t j = 0; j < 16; ++j) Write(stream, static_cast(j)); + } + } + } + } + + void WriteParticle(std::ostream& stream, uint32_t version, const std::string& names, const std::string& config) { + if (version >= 43) Write(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(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(stream, 1); + Write(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(stream, 0xcd); + } + + // A scene file from before chunks + std::string WriteFlat(uint32_t version, bool withParticles = true) { + std::ostringstream stream; + Write(stream, static_cast(version)); + Write(stream, 0); + if (version >= 32) Write(stream, 0); // no editor settings flag + if (version > 36) Write(stream, 77); // revision + WriteLighting(stream, version); + WriteSkydome(stream, version); + if (version >= 37) WriteEditorSettings(stream); + if (version >= 3) { + Write(stream, 1); + WriteObject(stream, version, 1000, 2000, "respawn=5:20000\ncustom=0:a"); + } + if (version >= 5) { + Write(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(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(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(stream, 9); // revision + Write(stream, 64); // environment + Write(stream, 0); // objects, set below + Write(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(stream.tellp()); + WriteLighting(stream, version); + Pad(stream); + const uint32_t skydome = static_cast(stream.tellp()); + WriteSkydome(stream, version); + Pad(stream); + const uint32_t editor = static_cast(stream.tellp()); + WriteEditorSettings(stream); + Pad(stream); + + const uint32_t objects = static_cast(stream.tellp()); + WriteChunkHeader(stream, 2001, 0, objects + 32); + Pad(stream); + Write(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(stream.tellp()); + WriteChunkHeader(stream, 2002, 0, particles + 32); + Pad(stream); + Write(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(offsets), lighting); + patch(static_cast(offsets) + 4, skydome); + patch(static_cast(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(newer, 49); + Write(newer, 0); + Write(newer, 0); + Write(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); +}