using DatReaderWriter;
using DatReaderWriter.DBObjs;
using DatReaderWriter.Enums;
namespace AcDream.Core.Meshing;
///
/// Resolve a base GfxObj id to its retail "close-detail" mesh by walking
/// the DIDDegrade table to Degrades[0].
///
///
/// Why this exists (Issue #47). Many AC GfxObjs — most notably
/// humanoid body parts — store the LOW-detail mesh as the GfxObj that
/// the Setup or AnimPartChange references. The high-detail mesh used
/// for close/player rendering is reached indirectly: the base GfxObj's
/// HasDIDDegrade flag is set, DIDDegrade points at a
/// , and at
/// Degrades[0] is the close-detail variant. Drawing the base
/// GfxObj id directly produces the LOD-3 mesh — visibly bulky and
/// detail-less — which is exactly what acdream and ACViewer were both
/// rendering for humanoid body parts before this fix.
///
///
///
/// Concrete example. The Aluvian Male upper-arm GfxObj
/// 0x01000055 is a 14-vertex / 17-poly low-detail stub. Its
/// degrade table 0x110006D0 points at 0x01001795, the
/// 32-vertex / 60-poly close-detail mesh that carries the bicep /
/// deltoid / shoulder geometry retail draws on the player. Same story
/// for the lower arm 0x01000056 → 0x0100178F and matching
/// heritage variants (0x010004BF → 0x010017A8,
/// 0x010004BD → 0x010017A7, 0x010004B7 → 0x0100179A).
///
///
///
/// Retail flow (named-retail decomp).
///
/// -
/// acclient!CPhysicsPart::LoadGfxObjArray at 0x0050DCF0
/// loads the base GfxObj solely to discover DIDDegrade; if
/// a exists, retail loads each entry
/// in Degrades into the part's render array.
///
/// -
/// acclient!CPhysicsPart::UpdateViewerDistance at
/// 0x0050E030 picks deg_level per part by distance.
/// For close / player rendering deg_level == 0.
///
/// -
/// acclient!CPhysicsPart::Draw at 0x0050D7A0
/// draws gfxobj[deg_level].
///
///
///
///
///
/// We don't yet have distance-based LOD plumbing, so this resolver
/// always returns slot 0 (the close-detail mesh). That's correct for
/// player + nearby NPC rendering; far-distance LOD is a future concern.
///
///
public static class GfxObjDegradeResolver
{
///
/// DatCollection-backed convenience overload. Production callers use
/// this; tests use the callback overload below for easy fakes.
///
public static bool TryResolveCloseGfxObj(
DatCollection dats,
uint gfxObjId,
out uint resolvedId,
out GfxObj? resolvedGfxObj)
=> TryResolveCloseGfxObj(
id => dats.Get(id),
id => dats.Get(id),
gfxObjId,
out resolvedId,
out resolvedGfxObj);
///
/// Loader-callback overload. Returns the close-detail GfxObj id and
/// loaded object when a degrade table is present, otherwise the
/// base id and base GfxObj.
///
///
/// Lookup for a GfxObj by id. May return null when not found.
///
///
/// Lookup for a GfxObjDegradeInfo by id. May return null.
///
/// Base GfxObj id (post-AnimPartChange).
///
/// The id to actually render. Same as
/// when no degrade table exists; Degrades[0].Id when it does.
///
///
/// The loaded GfxObj for , cached so
/// callers don't have to re-read.
///
///
/// true if a usable GfxObj was resolved (either base or
/// degrade slot 0 loaded). false only when the base GfxObj
/// itself was missing — caller should drop this part.
///
public static bool TryResolveCloseGfxObj(
Func getGfxObj,
Func getDegradeInfo,
uint gfxObjId,
out uint resolvedId,
out GfxObj? resolvedGfxObj)
{
var gfxObj = getGfxObj(gfxObjId);
if (gfxObj is null)
{
resolvedId = gfxObjId;
resolvedGfxObj = null;
return false;
}
// Default: base mesh stays selected unless the degrade table
// resolves cleanly. Every fallback below leaves these set.
resolvedId = gfxObjId;
resolvedGfxObj = gfxObj;
if (!gfxObj.Flags.HasFlag(GfxObjFlags.HasDIDDegrade) || gfxObj.DIDDegrade == 0)
return true;
var degradeInfo = getDegradeInfo(gfxObj.DIDDegrade);
if (degradeInfo is null || degradeInfo.Degrades.Count == 0)
return true;
uint closeId = (uint)degradeInfo.Degrades[0].Id;
if (closeId == 0)
return true;
var closeGfxObj = getGfxObj(closeId);
if (closeGfxObj is null)
return true;
resolvedId = closeId;
resolvedGfxObj = closeGfxObj;
return true;
}
}