Fix Starfield BSGeometry mesh data export#59
Conversation
- Fix SyncUDEC3 write: use |= instead of &= for packing, add W bit (1 << 30) - Add count sync before write in BSGeometryMeshData::Sync - Fix vertex pack to use int16_t + std::round for correct precision - Fix SaveExternalShapeData: use Mode::Writing and meshData.Sync - Add BSGeometry finalization in FinalizeData() to update triSize/numVerts Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
BSGeometry flag 512 (0x200) controls whether mesh data is embedded inline in the NIF or stored as external .mesh files. This adds read/write support for both modes and exposes HasInternalGeomData() and SetInternalGeomData() API for toggling the flag. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Cast ~0x200 to uint32_t to avoid GCC -Werror=sign-conversion. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
FinalizeData was overwriting triSize and numVerts from meshData even when external mesh data hadn't been loaded, zeroing the header values. Only update counts when mesh data is actually populated. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove upper bound on IsSF() stream version check. Starfield patches (MeshesPatch.ba2) ship NIFs with stream version 175 which use the same format as 172/173. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Per ousnius's feedback — keep a bounded range (172–175) so unknown future stream versions don't silently pass validation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Fixes Starfield BSGeometry mesh export by correcting multiple serialization/write-path issues so external .mesh files (and related BSGeometry metadata) are written consistently and with correct packing/counts.
Changes:
- Corrects Starfield packing/serialization primitives (
SyncUDEC3, vertex coordinate packing) and expands Starfield version detection. - Fixes
.meshexternal save path to use writing mode and serializeBSGeometryMeshDatadirectly; avoids returning external path refs when geometry is internal. - Updates BSGeometry mesh metadata finalization and adds support for serializing internal (inline) mesh data vs external mesh name references.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| src/NifFile.cpp | Fix external .mesh save mode/entry point; avoid exposing mesh path refs for internal-geometry BSGeometry; update tri/vert metadata during finalization. |
| src/Geometry.cpp | Sync counts before writing mesh data; fix vertex packing precision; serialize inline mesh data when internal geometry is enabled. |
| include/Geometry.hpp | Introduce internal/external geometry helpers and per-mesh internalGeom switch for correct serialization behavior. |
| include/BasicTypes.hpp | Expand Starfield stream version range; fix SyncUDEC3 bit packing (OR vs AND and set W bit). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // When writing, update counts from actual data sizes | ||
| if (stream.GetMode() == NiStreamReversible::Mode::Writing) { | ||
| nTriIndices = static_cast<uint32_t>(tris.size()) * 3; | ||
| nVertices = static_cast<uint32_t>(vertices.size()); | ||
| numVertices = static_cast<uint16_t>(std::min(nVertices, static_cast<uint32_t>(0xFFFF))); | ||
| nUV1 = uvSets.size() > 0 ? static_cast<uint32_t>(uvSets[0].size()) : 0; | ||
| nUV2 = uvSets.size() > 1 ? static_cast<uint32_t>(uvSets[1].size()) : 0; |
There was a problem hiding this comment.
In BSGeometryMeshData::Sync, the new writing-mode count refresh clamps numVertices to 0xFFFF, but later the function unconditionally does numVertices = (uint16_t)nVertices after syncing nVertices, which overrides the clamp and can wrap if nVertices > 65535. Consider only assigning numVertices from nVertices in reading mode, or keep numVertices consistent with the same clamp when writing.
Set numVertices differently depending on stream mode: when reading, cast nVertices to uint16_t as before; when writing, clamp nVertices to 0xFFFF using std::min before casting. This ensures we don't write a numVertices larger than a 16-bit limit while preserving the original value when reading.
Add support for the 2-bit W component of UDEC3 tangents: introduce SyncUDEC3 to pack/unpack tangent W bits and switch packing to std::round for consistent rounding. Store per-tangent W values in Geometry (tangentWs) and update BSGeometryMeshData::Sync to read/write tangent W components. Add tests that load/save external .mesh data and convert external meshes to internal geometry.
Change variable 'di' from int to size_t to match other size/index types (e.g. map.size() and vector indexing). This fixes signed/unsigned mismatches and avoids related compiler warnings when tracking deleted triangle indices.
ousnius
left a comment
There was a problem hiding this comment.
Added some more fixes and unit tests. This should be fine now.
Summary
SyncUDEC3write: bitwise OR (|=) instead of AND (&=), and set W bit (1 << 30)BSGeometryMeshData::Syncto sync element counts before writing dataint16_twithstd::roundfor correct precisionSaveExternalShapeData: useMode::Writinginstead ofMode::Reading, callmesh->meshData.Syncinstead ofmesh->SyncFinalizeData()to updatetriSizeandnumVertsfrom mesh dataThese fixes enable correct export of external
.meshfiles for Starfield BSGeometry shapes. Companion PR: ousnius/BodySlide-and-Outfit-Studio#570Test plan
.meshfiles are written with correct data.meshfiles in NifSkope — verify vertex positions, normals, and triangles are intact🤖 Generated with Claude Code