using AcDream.Core.Meshing; using DatReaderWriter.DBObjs; using DatReaderWriter.Enums; using DatReaderWriter.Types; namespace AcDream.Core.Tests.Meshing; /// /// Unit tests for . The resolver is /// the Issue #47 fix: route a base GfxObj id to its retail close-detail /// mesh via the DIDDegrade table's slot 0. Tests use the callback /// overload so we can stand up tiny in-memory fixtures without dragging /// in a real DatCollection. /// public class GfxObjDegradeResolverTests { /// /// When the base GfxObj has no degrade table (HasDIDDegrade flag /// clear), the resolver returns the base id unchanged. /// [Fact] public void NoDegradeTable_ReturnsBaseMesh() { const uint baseId = 0x01001212u; var baseGfx = new GfxObj { Flags = 0, DIDDegrade = 0 }; var gfxObjs = new Dictionary { [baseId] = baseGfx }; bool ok = GfxObjDegradeResolver.TryResolveCloseGfxObj( id => gfxObjs.GetValueOrDefault(id), _ => null, baseId, out uint resolvedId, out var resolvedGfx); Assert.True(ok); Assert.Equal(baseId, resolvedId); Assert.Same(baseGfx, resolvedGfx); } /// /// When the base GfxObj has a populated DIDDegrade table, the /// resolver returns Degrades[0].Id and its loaded GfxObj — the /// close-detail mesh retail draws for nearby objects. /// [Fact] public void ValidDegradeTable_ReturnsSlotZero() { const uint baseId = 0x01000055u; // low-detail Aluvian Male upper arm const uint degradeInfoId = 0x110006D0u; const uint closeId = 0x01001795u; // retail close-detail variant var baseGfx = new GfxObj { Flags = GfxObjFlags.HasDIDDegrade, DIDDegrade = degradeInfoId, }; var closeGfx = new GfxObj { Flags = 0 }; var degradeInfo = new GfxObjDegradeInfo { Degrades = { new GfxObjInfo { Id = closeId } }, }; var gfxObjs = new Dictionary { [baseId] = baseGfx, [closeId] = closeGfx, }; var degradeInfos = new Dictionary { [degradeInfoId] = degradeInfo, }; bool ok = GfxObjDegradeResolver.TryResolveCloseGfxObj( id => gfxObjs.GetValueOrDefault(id), id => degradeInfos.GetValueOrDefault(id), baseId, out uint resolvedId, out var resolvedGfx); Assert.True(ok); Assert.Equal(closeId, resolvedId); Assert.Same(closeGfx, resolvedGfx); } /// /// If the degrade table references a GfxObj that isn't present in /// the dat (corrupt / partial dat), the resolver falls back to the /// base mesh rather than returning null. Better to render the /// low-detail variant than nothing at all. /// [Fact] public void MissingSlotZeroMesh_FallsBackToBase() { const uint baseId = 0x01000055u; const uint degradeInfoId = 0x110006D0u; const uint missingCloseId = 0xDEADBEEFu; var baseGfx = new GfxObj { Flags = GfxObjFlags.HasDIDDegrade, DIDDegrade = degradeInfoId, }; var degradeInfo = new GfxObjDegradeInfo { Degrades = { new GfxObjInfo { Id = missingCloseId } }, }; var gfxObjs = new Dictionary { [baseId] = baseGfx }; var degradeInfos = new Dictionary { [degradeInfoId] = degradeInfo, }; bool ok = GfxObjDegradeResolver.TryResolveCloseGfxObj( id => gfxObjs.GetValueOrDefault(id), id => degradeInfos.GetValueOrDefault(id), baseId, out uint resolvedId, out var resolvedGfx); Assert.True(ok); Assert.Equal(baseId, resolvedId); Assert.Same(baseGfx, resolvedGfx); } /// /// Empty Degrades list (table present but no entries) falls back /// to base. Mirrors retail's "no LOD entries → just draw the base" /// behavior. /// [Fact] public void EmptyDegradesList_FallsBackToBase() { const uint baseId = 0x01000055u; const uint degradeInfoId = 0x110006D0u; var baseGfx = new GfxObj { Flags = GfxObjFlags.HasDIDDegrade, DIDDegrade = degradeInfoId, }; var degradeInfo = new GfxObjDegradeInfo(); // empty Degrades var gfxObjs = new Dictionary { [baseId] = baseGfx }; var degradeInfos = new Dictionary { [degradeInfoId] = degradeInfo, }; bool ok = GfxObjDegradeResolver.TryResolveCloseGfxObj( id => gfxObjs.GetValueOrDefault(id), id => degradeInfos.GetValueOrDefault(id), baseId, out uint resolvedId, out var resolvedGfx); Assert.True(ok); Assert.Equal(baseId, resolvedId); Assert.Same(baseGfx, resolvedGfx); } /// /// When the base GfxObj itself is missing from the dat, the /// resolver returns false so the caller can drop the part rather /// than trying to render a null mesh. /// [Fact] public void MissingBaseGfxObj_ReturnsFalse() { const uint baseId = 0xDEADBEEFu; bool ok = GfxObjDegradeResolver.TryResolveCloseGfxObj( _ => null, _ => null, baseId, out uint resolvedId, out var resolvedGfx); Assert.False(ok); Assert.Equal(baseId, resolvedId); Assert.Null(resolvedGfx); } // ── #136: editor-only placement marker detection ────────────────────────── /// /// The #136 dungeon "cone": its degrade table's slot 0 is visible ONLY at distance 0 /// (MaxDist=0) and the table degrades to GfxObj id 0 (= nothing) at real distance. /// Retail's distance degrade never draws it in the live client; we must skip it. /// [Fact] public void IsRuntimeHiddenMarker_EditorMarkerDegradingToNothing_True() { const uint markerGfx = 0x010028CAu; const uint degradeId = 0x11000118u; var gfx = new GfxObj { Flags = GfxObjFlags.HasDIDDegrade, DIDDegrade = degradeId }; var info = new GfxObjDegradeInfo { Degrades = { new GfxObjInfo { Id = markerGfx, MaxDist = 0f }, new GfxObjInfo { Id = 0u, MaxDist = float.MaxValue }, }, }; var gfxObjs = new Dictionary { [markerGfx] = gfx }; var infos = new Dictionary { [degradeId] = info }; Assert.True(GfxObjDegradeResolver.IsRuntimeHiddenMarker( id => gfxObjs.GetValueOrDefault(id), id => infos.GetValueOrDefault(id), markerGfx)); } /// A real LOD object — slot 0 visible out to a real distance (MaxDist>0) — /// is NOT a marker, even though it degrades further. [Fact] public void IsRuntimeHiddenMarker_NormalLodObject_False() { const uint baseId = 0x01000055u; const uint degradeId = 0x110006D0u; var gfx = new GfxObj { Flags = GfxObjFlags.HasDIDDegrade, DIDDegrade = degradeId }; var info = new GfxObjDegradeInfo { Degrades = { new GfxObjInfo { Id = 0x01001795u, MaxDist = 25f }, new GfxObjInfo { Id = 0u, MaxDist = float.MaxValue }, }, }; var gfxObjs = new Dictionary { [baseId] = gfx }; var infos = new Dictionary { [degradeId] = info }; Assert.False(GfxObjDegradeResolver.IsRuntimeHiddenMarker( id => gfxObjs.GetValueOrDefault(id), id => infos.GetValueOrDefault(id), baseId)); } /// No degrade table at all → not a marker. [Fact] public void IsRuntimeHiddenMarker_NoDegradeTable_False() { const uint baseId = 0x01001212u; var gfx = new GfxObj { Flags = 0, DIDDegrade = 0 }; var gfxObjs = new Dictionary { [baseId] = gfx }; Assert.False(GfxObjDegradeResolver.IsRuntimeHiddenMarker( id => gfxObjs.GetValueOrDefault(id), _ => null, baseId)); } /// slot 0 is editor-only (MaxDist=0) but degrades to a REAL mesh (no id-0 /// entry) — a genuine close-only LOD, not an invisible marker. Do NOT skip. [Fact] public void IsRuntimeHiddenMarker_EditorSlotButDegradesToRealMesh_False() { const uint baseId = 0x01002000u; const uint degradeId = 0x11002000u; var gfx = new GfxObj { Flags = GfxObjFlags.HasDIDDegrade, DIDDegrade = degradeId }; var info = new GfxObjDegradeInfo { Degrades = { new GfxObjInfo { Id = baseId, MaxDist = 0f }, new GfxObjInfo { Id = 0x01002001u, MaxDist = float.MaxValue }, }, }; var gfxObjs = new Dictionary { [baseId] = gfx }; var infos = new Dictionary { [degradeId] = info }; Assert.False(GfxObjDegradeResolver.IsRuntimeHiddenMarker( id => gfxObjs.GetValueOrDefault(id), id => infos.GetValueOrDefault(id), baseId)); } }