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));
}
}