diff --git a/include/BasicTypes.hpp b/include/BasicTypes.hpp index 9969f6c..a2805d0 100644 --- a/include/BasicTypes.hpp +++ b/include/BasicTypes.hpp @@ -157,7 +157,7 @@ class NiVersion { // Check if file has a Fallout 76 version range bool IsFO76() const { return file == V20_2_0_7 && stream == 155; } // Check if file has a Starfield version range - bool IsSF() const { return file == V20_2_0_7 && stream >= 172 && stream <= 173; } + bool IsSF() const { return file == V20_2_0_7 && stream >= 172 && stream <= 175; } // Return an Oblivion file version static NiVersion getOB() { return NiVersion(NiFileVersion::V20_0_0_5, 11, 11); } @@ -358,12 +358,13 @@ class NiStreamReversible { } void SyncUDEC3(Vector3& vec) { - uint32_t data; + uint32_t data = 0; if (mode == Mode::Writing) { - data = (((uint32_t)((vec.z+1.0)*511.5)) & 1023) << 20; - data &= (((uint32_t)((vec.y+1.0)*511.5)) & 1023) << 10; - data &= (((uint32_t)((vec.x+1.0)*511.5)) & 1023); + data = (static_cast(std::round((vec.x + 1.0) * 511.5)) & 1023); + data |= (static_cast(std::round((vec.y + 1.0) * 511.5)) & 1023) << 10; + data |= (static_cast(std::round((vec.z + 1.0) * 511.5)) & 1023) << 20; + data |= static_cast(1) << 30; } Sync(data); @@ -372,7 +373,27 @@ class NiStreamReversible { vec.x = (float)(((data & 1023) / 511.5) - 1.0); vec.y = (float)((((data >> 10) & 1023) / 511.5) - 1.0); vec.z = (float)((((data >> 20) & 1023) / 511.5) - 1.0); - } + } + } + + void SyncUDEC3(Vector3& vec, uint8_t& w) { + uint32_t data = 0; + + if (mode == Mode::Writing) { + data = (static_cast(std::round((vec.x + 1.0) * 511.5)) & 1023); + data |= (static_cast(std::round((vec.y + 1.0) * 511.5)) & 1023) << 10; + data |= (static_cast(std::round((vec.z + 1.0) * 511.5)) & 1023) << 20; + data |= static_cast(w & 3) << 30; + } + + Sync(data); + + if (mode == Mode::Reading) { + vec.x = (float)(((data & 1023) / 511.5) - 1.0); + vec.y = (float)((((data >> 10) & 1023) / 511.5) - 1.0); + vec.z = (float)((((data >> 20) & 1023) / 511.5) - 1.0); + w = static_cast((data >> 30) & 3); + } } diff --git a/include/Geometry.hpp b/include/Geometry.hpp index 2cf185d..e7e94ed 100644 --- a/include/Geometry.hpp +++ b/include/Geometry.hpp @@ -591,6 +591,7 @@ class BSGeometryMeshData : public NiCloneableStreamable tangentWs; // 2-bit W component of each tangent (bitangent sign) uint32_t nTotalWeights = 0; std::vector> skinWeights; @@ -612,10 +613,12 @@ struct BSGeometryMesh { uint32_t numVerts = 0; uint32_t flags = 0; // Often 64 - // in official files, this is 41 characters: hex characters from sha1 of the mesh data split into 2 parts - // with a path separator. The game does not seem to check the digest, so the same name can be used for - // replacement, or probably a human-readable one - NiString meshName; + // When internalGeom is false (default), meshName holds the external .mesh path + // (41 hex chars from sha1, or a human-readable name). When true, mesh data is + // serialized inline in the NIF and meshName is unused. + bool internalGeom = false; + + NiString meshName; BSGeometryMeshData meshData; void Sync(NiStreamReversible& stream); @@ -651,6 +654,16 @@ class BSGeometry : public NiCloneableStreamable { uint8_t MeshCount() { return (uint8_t) meshes.size(); } + // Flag 0x200 (512) on BSGeometry controls whether mesh data is embedded inline + // in the NIF (internal) or stored as separate .mesh files (external). + bool HasInternalGeomData() const { return (flags & 0x200) != 0; } + void SetInternalGeomData(bool internal) { + if (internal) + flags |= 0x200; + else + flags &= ~uint32_t(0x200); + } + // SelectMesh provides a way to choose which mesh from the BSGeometryMesh list data accesessors will use. // If this is not called, functions to retrieve vertices, triangles, etc will default to the first mesh. // Returns a pointer to the mesh data selected. diff --git a/include/NifUtil.hpp b/include/NifUtil.hpp index 3541a26..e02b705 100644 --- a/include/NifUtil.hpp +++ b/include/NifUtil.hpp @@ -21,7 +21,7 @@ void ApplyMapToTriangles(std::vector& tris, const std::vector& map, std::vector* deletedTris = nullptr) { const size_t mapsz = map.size(); - int di = 0; + size_t di = 0; for (IndexType2 si = 0; si < static_cast(tris.size()); ++si) { const Triangle& stri = tris[si]; // Triangle's indices are unsigned, but IndexType might be signed. @@ -195,6 +195,9 @@ inline bool is_relative_path(std::string_view path) noexcept { // Helper to trim whitespace characters including newlines from the start and end of a string void trim_whitespace(std::string& str); +std::unique_ptr GetBinaryInputFileStream(const std::filesystem::path& path); +std::unique_ptr GetBinaryOutputFileStream(const std::filesystem::path& path); + // Convenience wrapper for std::find template auto find(Container& cont, Value&& val) { diff --git a/src/Geometry.cpp b/src/Geometry.cpp index b91eee3..91714a0 100644 --- a/src/Geometry.cpp +++ b/src/Geometry.cpp @@ -12,6 +12,7 @@ See the included GPLv3 LICENSE file #include "NifUtil.hpp" #include +#include using namespace nifly; @@ -1599,6 +1600,24 @@ void BSGeometryMeshData::Sync(NiStreamReversible& stream) { SetTangents(true); SetVertexColors(true); + // When writing, update counts from actual data sizes + if (stream.GetMode() == NiStreamReversible::Mode::Writing) { + nTriIndices = static_cast(tris.size()) * 3; + nVertices = static_cast(vertices.size()); + numVertices = static_cast(std::min(nVertices, static_cast(0xFFFF))); + nUV1 = uvSets.size() > 0 ? static_cast(uvSets[0].size()) : 0; + nUV2 = uvSets.size() > 1 ? static_cast(uvSets[1].size()) : 0; + nColors = static_cast(vColors.size()); + nNormals = static_cast(normals.size()); + nTangents = static_cast(tangents.size()); + nTotalWeights = 0; + for (auto& vw : skinWeights) + nTotalWeights += static_cast(vw.size()); + nLODS = static_cast(lods.size()); + nMeshlets = static_cast(meshletList.size()); + nCullData = static_cast(cullDataList.size()); + } + stream.Sync(version); if (version > 2) return; @@ -1615,9 +1634,10 @@ void BSGeometryMeshData::Sync(NiStreamReversible& stream) { stream.Sync(nWeightsPerVert); stream.Sync(nVertices); - // maybe not a good idea to do the below, in case some meshes have over 65k verts, however since - // triangles still use 16 bit indices, the total count must still fit under that limit ... - numVertices = (uint16_t) nVertices; + if (stream.GetMode() == NiStreamReversible::Mode::Reading) + numVertices = static_cast(nVertices); + else + numVertices = static_cast(std::min(nVertices, static_cast(0xFFFF))); vertices.resize(nVertices); for (uint32_t v = 0; v < nVertices; v++) { if (stream.GetMode() == NiStreamReversible::Mode::Reading) { @@ -1636,13 +1656,11 @@ void BSGeometryMeshData::Sync(NiStreamReversible& stream) { } else { auto pack = [&](float component, float posScale) { - uint16_t factor; + int16_t val; if (component < 0) - factor = 32768; + val = static_cast(std::round((component / (scale * posScale)) * 32768.0f)); else - factor = 32767; - - uint16_t val = (uint16_t) ((component / (scale * posScale)) * factor); + val = static_cast(std::round((component / (scale * posScale)) * 32767.0f)); stream.Sync(val); }; @@ -1683,9 +1701,9 @@ void BSGeometryMeshData::Sync(NiStreamReversible& stream) { stream.Sync(nTangents); tangents.resize(nTangents); + tangentWs.resize(nTangents, 1); for (uint32_t t = 0; t < nTangents; t++) { - stream.SyncUDEC3(tangents[t]); - // need to calculate tangent basis and bitangents on read? + stream.SyncUDEC3(tangents[t], tangentWs[t]); } /* @@ -1737,7 +1755,15 @@ void BSGeometryMesh::Sync(NiStreamReversible& stream) { stream.Sync(triSize); stream.Sync(numVerts); stream.Sync(flags); - meshName.Sync(stream, 4); + + if (internalGeom) { + // Mesh data is embedded inline in the NIF (flag 0x200 on BSGeometry) + meshData.Sync(stream); + } + else { + // External .mesh file path reference + meshName.Sync(stream, 4); + } } void BSGeometry::Sync(NiStreamReversible& stream) { @@ -1753,6 +1779,8 @@ void BSGeometry::Sync(NiStreamReversible& stream) { if (stream.GetMode() == NiStreamReversible::Mode::Reading) meshes.clear(); + bool internal = HasInternalGeomData(); + size_t meshCount = meshes.size(); for (uint32_t i = 0; i < 4; i++) { uint8_t testByte = i < meshCount; @@ -1762,6 +1790,7 @@ void BSGeometry::Sync(NiStreamReversible& stream) { BSGeometryMesh mesh{}; meshes.push_back(mesh); } + meshes[i].internalGeom = internal; meshes[i].Sync(stream); } } diff --git a/src/NifFile.cpp b/src/NifFile.cpp index d374f9b..59ecb0c 100644 --- a/src/NifFile.cpp +++ b/src/NifFile.cpp @@ -896,7 +896,7 @@ NiGeometryData* NifFile::GetGeometryData(NiShape* shape) const { std::vector> NifFile::GetExternalGeometryPathRefs(NiShape* shape) const { std::vector> meshPaths; auto bsgeo = dynamic_cast(shape); - if (bsgeo) { + if (bsgeo && !bsgeo->HasInternalGeomData()) { for (uint8_t i = 0; i < bsgeo->MeshCount(); i++) { auto mesh = bsgeo->SelectMesh(i); meshPaths.push_back(mesh->meshName.get()); @@ -922,9 +922,9 @@ bool NifFile::SaveExternalShapeData(NiShape* shape, std::ostream& outfile, uint8 auto bsgeo = dynamic_cast(shape); if (bsgeo && (shapeIndex < bsgeo->MeshCount())) { NiOStream meshStream(&outfile, nullptr); - NiStreamReversible s(nullptr, &meshStream,NiStreamReversible::Mode::Reading); + NiStreamReversible s(nullptr, &meshStream, NiStreamReversible::Mode::Writing); auto mesh = bsgeo->SelectMesh(shapeIndex); - mesh->Sync(s); + mesh->meshData.Sync(s); bsgeo->ReleaseMesh(); } return true; @@ -2078,6 +2078,18 @@ void NifFile::FinalizeData() { } } + auto bsgeo = dynamic_cast(shape); + if (bsgeo) { + for (uint8_t i = 0; i < bsgeo->MeshCount(); i++) { + auto mesh = bsgeo->SelectMesh(i); + if (mesh && !mesh->meshData.vertices.empty()) { + mesh->triSize = static_cast(mesh->meshData.tris.size()) * 3; + mesh->numVerts = static_cast(mesh->meshData.vertices.size()); + } + bsgeo->ReleaseMesh(); + } + } + if (hdr.GetVersion().IsOB()) { // Move tangents and bitangents from shape back to binary extra data if (shape->HasTangents()) { diff --git a/src/NifUtil.cpp b/src/NifUtil.cpp index 543cf51..850f7dd 100644 --- a/src/NifUtil.cpp +++ b/src/NifUtil.cpp @@ -8,6 +8,8 @@ See the included GPLv3 LICENSE file #include "NifUtil.hpp" +#include + namespace nifly { void trim_whitespace(std::string& str) { @@ -33,4 +35,22 @@ void trim_whitespace(std::string& str) { str = str.substr(i, j - i + 1); } +std::unique_ptr GetBinaryInputFileStream(const std::filesystem::path& path) { + if (std::filesystem::exists(path)) { + auto fileStream = std::make_unique(path, std::ios::in | std::ios::binary); + if (fileStream && !fileStream->fail()) + return fileStream; + } + + return nullptr; +} + +std::unique_ptr GetBinaryOutputFileStream(const std::filesystem::path& path) { + auto fileStream = std::make_unique(path, std::ios::out | std::ios::binary); + if (fileStream && !fileStream->fail()) + return fileStream; + + return nullptr; +} + } // namespace nifly \ No newline at end of file diff --git a/tests/TestNifFile.cpp b/tests/TestNifFile.cpp index e0906ef..c5b5783 100644 --- a/tests/TestNifFile.cpp +++ b/tests/TestNifFile.cpp @@ -6,18 +6,21 @@ #include #include +#include using namespace nifly; const std::string nifSuffix = ".nif"; +const std::string meshSuffix = ".mesh"; + const std::string folderInput = "input"; const std::string folderOutput = "output"; const std::string folderExpected = "expected"; -std::tuple GetNifFileTuple(const char* fileName) { - std::string fileInput = folderInput + "/" + fileName + nifSuffix; - std::string fileOutput = folderOutput + "/" + + fileName + nifSuffix; - std::string fileExpected = folderExpected + "/" + + fileName + nifSuffix; +std::tuple GetFileTuple(const char* fileName, const std::string& suffix) { + std::string fileInput = folderInput + "/" + fileName + suffix; + std::string fileOutput = folderOutput + "/" + fileName + suffix; + std::string fileExpected = folderExpected + "/" + fileName + suffix; return std::make_tuple(fileInput, fileOutput, fileExpected); } @@ -30,7 +33,7 @@ TEST_CASE("Load not existing file", "[NifFile]") { TEST_CASE("Load and save static file (SE)", "[NifFile]") { constexpr auto fileName = "TestNifFile_Static_SE"; - const auto [fileInput, fileOutput, fileExpected] = GetNifFileTuple(fileName); + const auto [fileInput, fileOutput, fileExpected] = GetFileTuple(fileName, nifSuffix); NifFile nif; REQUIRE(nif.Load(fileInput) == 0); @@ -67,7 +70,7 @@ TEST_CASE("Trim texture paths", "[NifFile]") { TEST_CASE("Load and save static file (FO4)", "[NifFile]") { constexpr auto fileName = "TestNifFile_Static_FO4"; - const auto [fileInput, fileOutput, fileExpected] = GetNifFileTuple(fileName); + const auto [fileInput, fileOutput, fileExpected] = GetFileTuple(fileName, nifSuffix); NifFile nif; REQUIRE(nif.Load(fileInput) == 0); @@ -78,7 +81,7 @@ TEST_CASE("Load and save static file (FO4)", "[NifFile]") { TEST_CASE("Load and save static file (FO4, Version 132)", "[NifFile]") { constexpr auto fileName = "TestNifFile_Static_FO4_132"; - const auto [fileInput, fileOutput, fileExpected] = GetNifFileTuple(fileName); + const auto [fileInput, fileOutput, fileExpected] = GetFileTuple(fileName, nifSuffix); NifFile nif; REQUIRE(nif.Load(fileInput) == 0); @@ -89,7 +92,7 @@ TEST_CASE("Load and save static file (FO4, Version 132)", "[NifFile]") { TEST_CASE("Load and save static file (FO4, Version 139)", "[NifFile]") { constexpr auto fileName = "TestNifFile_Static_FO4_139"; - const auto [fileInput, fileOutput, fileExpected] = GetNifFileTuple(fileName); + const auto [fileInput, fileOutput, fileExpected] = GetFileTuple(fileName, nifSuffix); NifFile nif; REQUIRE(nif.Load(fileInput) == 0); @@ -100,7 +103,7 @@ TEST_CASE("Load and save static file (FO4, Version 139)", "[NifFile]") { TEST_CASE("Load and save skinned file (OB)", "[NifFile]") { constexpr auto fileName = "TestNifFile_Skinned_OB"; - const auto [fileInput, fileOutput, fileExpected] = GetNifFileTuple(fileName); + const auto [fileInput, fileOutput, fileExpected] = GetFileTuple(fileName, nifSuffix); NifFile nif; REQUIRE(nif.Load(fileInput) == 0); @@ -111,7 +114,7 @@ TEST_CASE("Load and save skinned file (OB)", "[NifFile]") { TEST_CASE("Load and save skinned file (SE)", "[NifFile]") { constexpr auto fileName = "TestNifFile_Skinned_SE"; - const auto [fileInput, fileOutput, fileExpected] = GetNifFileTuple(fileName); + const auto [fileInput, fileOutput, fileExpected] = GetFileTuple(fileName, nifSuffix); NifFile nif; REQUIRE(nif.Load(fileInput) == 0); @@ -122,7 +125,7 @@ TEST_CASE("Load and save skinned file (SE)", "[NifFile]") { TEST_CASE("Load and save skinned, dynamic file (SE)", "[NifFile]") { constexpr auto fileName = "TestNifFile_Skinned_Dynamic_SE"; - const auto[fileInput, fileOutput, fileExpected] = GetNifFileTuple(fileName); + const auto[fileInput, fileOutput, fileExpected] = GetFileTuple(fileName, nifSuffix); NifFile nif; REQUIRE(nif.Load(fileInput) == 0); @@ -133,7 +136,7 @@ TEST_CASE("Load and save skinned, dynamic file (SE)", "[NifFile]") { TEST_CASE("Load and save file without weights in NiSkinData", "[NifFile]") { constexpr auto fileName = "TestNifFile_Skinned_NoNiSkinDataWeights"; - const auto [fileInput, fileOutput, fileExpected] = GetNifFileTuple(fileName); + const auto [fileInput, fileOutput, fileExpected] = GetFileTuple(fileName, nifSuffix); NifFile nif; REQUIRE(nif.Load(fileInput) == 0); @@ -144,7 +147,7 @@ TEST_CASE("Load and save file without weights in NiSkinData", "[NifFile]") { TEST_CASE("Load and save skinned file (FO4)", "[NifFile]") { constexpr auto fileName = "TestNifFile_Skinned_FO4"; - const auto [fileInput, fileOutput, fileExpected] = GetNifFileTuple(fileName); + const auto [fileInput, fileOutput, fileExpected] = GetFileTuple(fileName, nifSuffix); NifFile nif; REQUIRE(nif.Load(fileInput) == 0); @@ -155,7 +158,7 @@ TEST_CASE("Load and save skinned file (FO4)", "[NifFile]") { TEST_CASE("Load and save furniture file (SE)", "[NifFile]") { constexpr auto fileName = "TestNifFile_Furniture_Col_SE"; - const auto [fileInput, fileOutput, fileExpected] = GetNifFileTuple(fileName); + const auto [fileInput, fileOutput, fileExpected] = GetFileTuple(fileName, nifSuffix); NifFile nif; REQUIRE(nif.Load(fileInput) == 0); @@ -166,7 +169,7 @@ TEST_CASE("Load and save furniture file (SE)", "[NifFile]") { TEST_CASE("Load and save file with loose blocks (SE)", "[NifFile]") { constexpr auto fileName = "TestNifFile_LooseBlocks_SE"; - const auto [fileInput, fileOutput, fileExpected] = GetNifFileTuple(fileName); + const auto [fileInput, fileOutput, fileExpected] = GetFileTuple(fileName, nifSuffix); NifFile nif; REQUIRE(nif.Load(fileInput) == 0); @@ -177,7 +180,7 @@ TEST_CASE("Load and save file with loose blocks (SE)", "[NifFile]") { TEST_CASE("Load and save file with multi bound node (SE)", "[NifFile]") { constexpr auto fileName = "TestNifFile_MultiBound_SE"; - const auto[fileInput, fileOutput, fileExpected] = GetNifFileTuple(fileName); + const auto[fileInput, fileOutput, fileExpected] = GetFileTuple(fileName, nifSuffix); NifFile nif; REQUIRE(nif.Load(fileInput) == 0); @@ -188,7 +191,7 @@ TEST_CASE("Load and save file with multi bound node (SE)", "[NifFile]") { TEST_CASE("Load and save animated file (LE)", "[NifFile]") { constexpr auto fileName = "TestNifFile_Animated_LE"; - const auto [fileInput, fileOutput, fileExpected] = GetNifFileTuple(fileName); + const auto [fileInput, fileOutput, fileExpected] = GetFileTuple(fileName, nifSuffix); NifFile nif; REQUIRE(nif.Load(fileInput) == 0); @@ -199,7 +202,7 @@ TEST_CASE("Load and save animated file (LE)", "[NifFile]") { TEST_CASE("Load and save file with deep scene graph (SE)", "[NifFile]") { constexpr auto fileName = "TestNifFile_DeepGraph_SE"; - const auto [fileInput, fileOutput, fileExpected] = GetNifFileTuple(fileName); + const auto [fileInput, fileOutput, fileExpected] = GetFileTuple(fileName, nifSuffix); NifFile nif; REQUIRE(nif.Load(fileInput) == 0); @@ -210,7 +213,7 @@ TEST_CASE("Load and save file with deep scene graph (SE)", "[NifFile]") { TEST_CASE("Load, optimize (LE to SE) and save file", "[NifFile]") { constexpr auto fileName = "TestNifFile_Optimize_LE_to_SE"; - const auto [fileInput, fileOutput, fileExpected] = GetNifFileTuple(fileName); + const auto [fileInput, fileOutput, fileExpected] = GetFileTuple(fileName, nifSuffix); OptOptions options; options.targetVersion = NiVersion::getSSE(); @@ -225,7 +228,7 @@ TEST_CASE("Load, optimize (LE to SE) and save file", "[NifFile]") { TEST_CASE("Load, optimize (LE to SE, dynamic) and save file", "[NifFile]") { constexpr auto fileName = "TestNifFile_Optimize_Dynamic_LE_to_SE"; - const auto[fileInput, fileOutput, fileExpected] = GetNifFileTuple(fileName); + const auto[fileInput, fileOutput, fileExpected] = GetFileTuple(fileName, nifSuffix); OptOptions options; options.targetVersion = NiVersion::getSSE(); @@ -241,7 +244,7 @@ TEST_CASE("Load, optimize (LE to SE, dynamic) and save file", "[NifFile]") { TEST_CASE("Load, optimize (SE to LE) and save file", "[NifFile]") { constexpr auto fileName = "TestNifFile_Optimize_SE_to_LE"; - const auto [fileInput, fileOutput, fileExpected] = GetNifFileTuple(fileName); + const auto [fileInput, fileOutput, fileExpected] = GetFileTuple(fileName, nifSuffix); OptOptions options; options.targetVersion = NiVersion::getSK(); @@ -256,7 +259,7 @@ TEST_CASE("Load, optimize (SE to LE) and save file", "[NifFile]") { TEST_CASE("Load and save file with ordered node (SE)", "[NifFile]") { constexpr auto fileName = "TestNifFile_OrderedNode_SE"; - const auto [fileInput, fileOutput, fileExpected] = GetNifFileTuple(fileName); + const auto [fileInput, fileOutput, fileExpected] = GetFileTuple(fileName, nifSuffix); OptOptions options; options.targetVersion = NiVersion::getSK(); @@ -270,7 +273,7 @@ TEST_CASE("Load and save file with ordered node (SE)", "[NifFile]") { TEST_CASE("Load, optimize (SE to LE, dynamic) and save file", "[NifFile]") { constexpr auto fileName = "TestNifFile_Optimize_Dynamic_SE_to_LE"; - const auto[fileInput, fileOutput, fileExpected] = GetNifFileTuple(fileName); + const auto[fileInput, fileOutput, fileExpected] = GetFileTuple(fileName, nifSuffix); OptOptions options; options.targetVersion = NiVersion::getSK(); @@ -286,7 +289,7 @@ TEST_CASE("Load, optimize (SE to LE, dynamic) and save file", "[NifFile]") { TEST_CASE("Load and save file with non-zero index root node", "[NifFile]") { constexpr auto fileName = "TestNifFile_RootNonZero"; - const auto [fileInput, fileOutput, fileExpected] = GetNifFileTuple(fileName); + const auto [fileInput, fileOutput, fileExpected] = GetFileTuple(fileName, nifSuffix); NifFile nif; REQUIRE(nif.Load(fileInput) == 0); @@ -297,7 +300,7 @@ TEST_CASE("Load and save file with non-zero index root node", "[NifFile]") { TEST_CASE("Load and save file (FO76)", "[NifFile]") { constexpr auto fileName = "TestNifFile_FO76"; - const auto [fileInput, fileOutput, fileExpected] = GetNifFileTuple(fileName); + const auto [fileInput, fileOutput, fileExpected] = GetFileTuple(fileName, nifSuffix); NifFile nif; REQUIRE(nif.Load(fileInput) == 0); @@ -308,18 +311,118 @@ TEST_CASE("Load and save file (FO76)", "[NifFile]") { TEST_CASE("Load and save file (SF)", "[NifFile]") { constexpr auto fileName = "TestNifFile_SF"; - const auto [fileInput, fileOutput, fileExpected] = GetNifFileTuple(fileName); + const auto [fileInput, fileOutput, fileExpected] = GetFileTuple(fileName, nifSuffix); + + NifFile nif; + REQUIRE(nif.Load(fileInput) == 0); + + auto shapes = nif.GetShapes(); + REQUIRE(!shapes.empty()); + + for (auto& s : shapes) { + auto meshPaths = nif.GetExternalGeometryPathRefs(s); + REQUIRE(!meshPaths.empty()); + + uint8_t meshIndex = 0; + for (auto meshPath : meshPaths) { + std::string meshPathStr = meshPath.get(); + REQUIRE(!meshPathStr.empty()); + + const auto [meshFileInput, meshFileOutput, meshFileExpected] = GetFileTuple(meshPathStr.c_str(), meshSuffix); + const std::filesystem::path meshInputPath = std::filesystem::u8path(meshFileInput); + auto meshStream = GetBinaryInputFileStream(meshInputPath); + REQUIRE(meshStream); + + // Load external mesh data into the shape (virtual BSGeometryMeshData block). + REQUIRE(nif.LoadExternalShapeData(s, *meshStream, meshIndex)); + meshStream.reset(); + + const std::filesystem::path meshOutputPath = std::filesystem::u8path(meshFileOutput); + auto meshOutputStream = GetBinaryOutputFileStream(meshOutputPath); + REQUIRE(meshOutputStream); + + // Save the virtual BSGeometryMeshData block of the shape to an external .mesh file. + REQUIRE(nif.SaveExternalShapeData(s, *meshOutputStream, meshIndex)); + meshOutputStream.reset(); + + const std::filesystem::path meshExpectedPath = std::filesystem::u8path(meshFileExpected); + REQUIRE(CompareBinaryFiles(meshOutputPath, meshExpectedPath)); + + meshIndex++; + } + + REQUIRE(meshIndex == meshPaths.size()); + } + + REQUIRE(nif.Save(fileOutput) == 0); + + REQUIRE(CompareBinaryFiles(fileOutput, fileExpected)); +} + +TEST_CASE("Load external and save as internal mesh data (SF)", "[NifFile]") { + constexpr auto fileName = "TestNifFile_ToInternalMesh_SF"; + const auto [fileInput, fileOutput, fileExpected] = GetFileTuple(fileName, nifSuffix); + // Load NIF with external mesh references NifFile nif; REQUIRE(nif.Load(fileInput) == 0); + + auto shapes = nif.GetShapes(); + REQUIRE(!shapes.empty()); + + for (auto& s : shapes) { + auto meshPaths = nif.GetExternalGeometryPathRefs(s); + REQUIRE(!meshPaths.empty()); + + uint8_t meshIndex = 0; + for (auto meshPath : meshPaths) { + std::string meshPathStr = meshPath.get(); + REQUIRE(!meshPathStr.empty()); + + const auto [meshFileInput, meshFileOutput, meshFileExpected] = GetFileTuple(meshPathStr.c_str(), meshSuffix); + const std::filesystem::path meshInputPath = std::filesystem::u8path(meshFileInput); + auto meshStream = GetBinaryInputFileStream(meshInputPath); + REQUIRE(meshStream); + + REQUIRE(nif.LoadExternalShapeData(s, *meshStream, meshIndex)); + meshStream.reset(); + meshIndex++; + } + + // Switch from external to internal mesh data + auto bsgeo = dynamic_cast(s); + REQUIRE(bsgeo); + bsgeo->SetInternalGeomData(true); + } + + // Save NIF with embedded mesh data REQUIRE(nif.Save(fileOutput) == 0); + // Reload and verify the internal data round-trips + NifFile nif2; + REQUIRE(nif2.Load(fileOutput) == 0); + + auto shapes2 = nif2.GetShapes(); + REQUIRE(shapes2.size() == shapes.size()); + + for (auto& s2 : shapes2) { + auto bsgeo2 = dynamic_cast(s2); + REQUIRE(bsgeo2); + REQUIRE(bsgeo2->HasInternalGeomData()); + + // External mesh paths should be empty for internal data + auto meshPaths2 = nif2.GetExternalGeometryPathRefs(s2); + REQUIRE(meshPaths2.empty()); + } + + // Save again and verify binary stability + REQUIRE(nif2.Save(fileOutput) == 0); REQUIRE(CompareBinaryFiles(fileOutput, fileExpected)); } TEST_CASE("FixBSXFlags (remove external emittance)", "[NifFile]") { constexpr auto fileName = "TestNifFile_FixBSXFlags_RemoveExtEmit"; - const auto [fileInput, fileOutput, fileExpected] = GetNifFileTuple(fileName); + const auto [fileInput, fileOutput, fileExpected] = GetFileTuple(fileName, nifSuffix); NifFile nif; REQUIRE(nif.Load(fileInput) == 0); @@ -331,7 +434,7 @@ TEST_CASE("FixBSXFlags (remove external emittance)", "[NifFile]") { TEST_CASE("FixBSXFlags (add external emittance)", "[NifFile]") { constexpr auto fileName = "TestNifFile_FixBSXFlags_AddExtEmit"; - const auto [fileInput, fileOutput, fileExpected] = GetNifFileTuple(fileName); + const auto [fileInput, fileOutput, fileExpected] = GetFileTuple(fileName, nifSuffix); NifFile nif; REQUIRE(nif.Load(fileInput) == 0); @@ -343,7 +446,7 @@ TEST_CASE("FixBSXFlags (add external emittance)", "[NifFile]") { TEST_CASE("FixShaderFlags (remove environment mapping)", "[NifFile]") { constexpr auto fileName = "TestNifFile_FixShaderFlags_RemoveEnvMap"; - const auto [fileInput, fileOutput, fileExpected] = GetNifFileTuple(fileName); + const auto [fileInput, fileOutput, fileExpected] = GetFileTuple(fileName, nifSuffix); NifFile nif; REQUIRE(nif.Load(fileInput) == 0); @@ -355,7 +458,7 @@ TEST_CASE("FixShaderFlags (remove environment mapping)", "[NifFile]") { TEST_CASE("FixShaderFlags (add environment mapping)", "[NifFile]") { constexpr auto fileName = "TestNifFile_FixShaderFlags_AddEnvMap"; - const auto [fileInput, fileOutput, fileExpected] = GetNifFileTuple(fileName); + const auto [fileInput, fileOutput, fileExpected] = GetFileTuple(fileName, nifSuffix); NifFile nif; REQUIRE(nif.Load(fileInput) == 0); diff --git a/tests/TestNifFile.hpp b/tests/TestNifFile.hpp index 69c158f..9da9d91 100644 --- a/tests/TestNifFile.hpp +++ b/tests/TestNifFile.hpp @@ -1,3 +1,3 @@ #include -std::tuple GetNifFileTuple(const char* fileName); +std::tuple GetFileTuple(const char* fileName, const std::string& suffix); diff --git a/tests/expected/TestNifFile_SF.mesh b/tests/expected/TestNifFile_SF.mesh new file mode 100644 index 0000000..201b9ab Binary files /dev/null and b/tests/expected/TestNifFile_SF.mesh differ diff --git a/tests/expected/TestNifFile_SF.nif b/tests/expected/TestNifFile_SF.nif index c7fc20d..ca522d2 100644 Binary files a/tests/expected/TestNifFile_SF.nif and b/tests/expected/TestNifFile_SF.nif differ diff --git a/tests/expected/TestNifFile_ToInternalMesh_SF.nif b/tests/expected/TestNifFile_ToInternalMesh_SF.nif new file mode 100644 index 0000000..dc364a3 Binary files /dev/null and b/tests/expected/TestNifFile_ToInternalMesh_SF.nif differ diff --git a/tests/input/TestNifFile_SF.mesh b/tests/input/TestNifFile_SF.mesh new file mode 100644 index 0000000..201b9ab Binary files /dev/null and b/tests/input/TestNifFile_SF.mesh differ diff --git a/tests/input/TestNifFile_SF.nif b/tests/input/TestNifFile_SF.nif index c7fc20d..ca522d2 100644 Binary files a/tests/input/TestNifFile_SF.nif and b/tests/input/TestNifFile_SF.nif differ diff --git a/tests/input/TestNifFile_ToInternalMesh_SF.mesh b/tests/input/TestNifFile_ToInternalMesh_SF.mesh new file mode 100644 index 0000000..201b9ab Binary files /dev/null and b/tests/input/TestNifFile_ToInternalMesh_SF.mesh differ diff --git a/tests/input/TestNifFile_ToInternalMesh_SF.nif b/tests/input/TestNifFile_ToInternalMesh_SF.nif new file mode 100644 index 0000000..1dd6390 Binary files /dev/null and b/tests/input/TestNifFile_ToInternalMesh_SF.nif differ