Decode C++ namespace syntax in NIF block type names#27
Conversation
|
@MANOLOV02 There's an error in one of the tests here. To fix the issue with the "::" naming, I would prefer having the source generator create a new type of attribute into the declarations of these classes that list the "real" class name and checking that attribute in places where the real name is necessary. So the suggested alternative solution from your description. For example: [NifBlockName("BSSkin::Instance")]
partial class BSSkin_Instance |
Switches the bidirectional table to a source-generator-emitted attribute,
as suggested in the PR review.
NiflySharp.Generator now emits `[global::NiflySharp.NifBlockName("...")]`
on every generated class whose binary name in nif.xml differs from its
C# identifier (i.e. types with `::` such as `BSSkin::Instance`).
NifBlockNameAttribute.GetBinaryName(Type) is the single read path used at
save time (NifFile.Save line 355 and NiHeader.AddBlockInfo /
ReplaceBlockInfo). For the load path, NifFile builds a one-shot
`binaryName -> Type` lookup over the loaded assembly's
`NiflySharp.Blocks` namespace and falls back to `Type.GetType` for the
common case where the binary name already matches the C# class.
This removes the need to hand-maintain a list of namespaced block types:
the source generator picks them up from nif.xml automatically.
62b5cc9 to
ef71a74
Compare
|
Pushed the source-generator + attribute approach as suggested. Summary of the changes:
About the
|
|
@MANOLOV02 You can set KeepEmptyRefs and include it in this PR. But what makes it harder is that it would be better if KeepEmptyRefs is only set for Starfield and only for BSSkin::Instance. |
Starfield standalone skinned meshes carry NPOS pointers in BSSkin_Instance.Bones because the actual NiNode references are resolved at runtime against the external skeleton. Without KeepEmptyRefs the FinalizeData -> CleanInvalidRefs pass strips the placeholders, shrinks the array to zero, and corrupts the round-trip (LoadAndSave_Skinned_SF would fail by 152 bytes). Earlier Bethesda formats (FO4, FO76) do not use this pattern in standalone files, so the flag is gated on stream.Version.IsSF() in BeforeSync. Pattern mirrors NiMultiTargetTransformController.ExtraTargets and the version gating style used in BSTriShape.BeforeSync (IsSSE()).
|
Folded in the KeepEmptyRefs fix per your guidance — scoped to Starfield and to BSSkin_Instance only. Implementation in public new void BeforeSync(NiStreamReversible stream)
{
if (_bones == null)
_bones = new NiBlockPtrArray<NiNode>();
_bones.KeepEmptyRefs = stream.Version.IsSF();
}Followed two existing patterns in the codebase: the No changes needed to the source generator side — the partial only adds Local test run is 34/34 green, including both |
Certain NIF block types are serialized with C++ namespace syntax:
BSSkin::Instance,BSSkin::BoneData,BSConnectPoint::Parents,BSConnectPoint::Children. C# class identifiers can't contain::, so the corresponding NiflySharp classes are named with_(e.g.BSSkin_Instance).In
NifFile.Load, blocks are instantiated viaType.GetType("NiflySharp.Blocks." + blockTypeStr)using the binary string directly. That call returnsnullfor any::-named type, and the block falls through to theNiUnknowncatch-all.Net effect: any FO4 NIF with a skin rig (
BSSkin::Instance/BSSkin::BoneData) or connect points loads those blocks asNiUnknown, andGetBlock<BSSkin_Instance>(...)returnsnull— silent data loss.C++ nifly avoids the problem because each block class declares its binary name explicitly:
nifly/include/Skin.hpp:238—static constexpr const char* BlockName = "BSSkin::Instance";nifly/include/Skin.hpp:224—static constexpr const char* BlockName = "BSSkin::BoneData";The C++ class identifiers are independent of the binary name (
BSSkinInstancethe class vs"BSSkin::Instance"the string), so no mapping is needed there.Fix: introduce a small bidirectional table
NifFile.NamespacedBlockTypesand apply it at the two boundaries:NifFile.Load): decode binary → C# name beforeType.GetType.NiHeader.AddBlockInfo): encode C# → binary before serializing the header block-type string.The list is centralized; new
::-named types only need one entry.Alternative considered: an attribute-based approach mirroring C++ (
[NifBlockName("BSSkin::Instance")]on the class). Rejected for this PR to keep the change minimal and reversible — an attribute scan would also need a fallback for types without the attribute and would touch the source generator. Happy to switch to that approach if you'd prefer.Not in issue #15 (was originally classified as a C# language workaround). It is a real bug that affects any C# consumer loading FO4 NIFs with skin rigs.