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