using System.Numerics; using AcDream.App.Input; using AcDream.App.UI; using AcDream.App.World; using AcDream.Content; using AcDream.Core.Physics; using AcDream.Core.World; using DatReaderWriter; namespace AcDream.App.Rendering; internal interface IPaperdollDollRenderer { void SetDoll(WorldEntity? doll); uint Render(int width, int height); } internal interface IPaperdollFrameView { bool TryGetVisibleSize(out int width, out int height); void SetTextureHandle(uint textureHandle); } internal interface IPaperdollInventoryVisibility { bool IsVisible { get; } } internal interface IPaperdollDollFactory { bool TryBuild(out WorldEntity? doll); } internal interface IPaperdollEntityLookup { bool TryGet(uint serverGuid, out WorldEntity player); } internal interface IPaperdollPoseApplicator { void Apply(WorldEntity doll, uint setupId); } /// /// Owns paperdoll dirty/rebuild state and the private FBO presentation edge. /// The GL renderer remains a borrowed resource disposed by the existing window /// shutdown transaction. /// internal sealed class PaperdollFramePresenter { private readonly IPaperdollDollRenderer _renderer; private readonly IPaperdollFrameView _view; private readonly IPaperdollDollFactory _factory; private bool _dirty = true; public PaperdollFramePresenter( IPaperdollDollRenderer renderer, IPaperdollFrameView view, IPaperdollDollFactory factory) { _renderer = renderer ?? throw new ArgumentNullException(nameof(renderer)); _view = view ?? throw new ArgumentNullException(nameof(view)); _factory = factory ?? throw new ArgumentNullException(nameof(factory)); } internal bool IsDirty => _dirty; public void MarkDirty() => _dirty = true; public void Render() { if (!_view.TryGetVisibleSize(out int width, out int height)) return; if (_dirty) { if (_factory.TryBuild(out WorldEntity? doll)) { _renderer.SetDoll(doll); _dirty = false; } else { // Retail's private viewport remains present while the live // player projection is still materializing. Keep the dirty // edge armed and clear any doll from the prior session. _renderer.SetDoll(null); } } _view.SetTextureHandle(_renderer.Render(width, height)); } } /// Retained-UI visibility and texture publication for the doll view. internal sealed class RetailPaperdollFrameView : IPaperdollFrameView { private readonly UiViewport _viewport; private readonly IPaperdollInventoryVisibility _inventory; public RetailPaperdollFrameView( UiViewport viewport, IPaperdollInventoryVisibility inventory) { _viewport = viewport ?? throw new ArgumentNullException(nameof(viewport)); _inventory = inventory ?? throw new ArgumentNullException(nameof(inventory)); } public bool TryGetVisibleSize(out int width, out int height) { width = 0; height = 0; if (!_viewport.Visible || !_inventory.IsVisible) { return false; } width = (int)_viewport.Width; height = (int)_viewport.Height; return true; } public void SetTextureHandle(uint textureHandle) => _viewport.TextureHandle = textureHandle; } /// Narrow visibility adapter for the paperdoll's inventory host. internal sealed class PaperdollInventoryVisibility : IPaperdollInventoryVisibility { private readonly UiElement _inventoryFrame; public PaperdollInventoryVisibility(UiElement inventoryFrame) { _inventoryFrame = inventoryFrame ?? throw new ArgumentNullException(nameof(inventoryFrame)); } public bool IsVisible => _inventoryFrame.Visible; } /// Canonical live-entity lookup used by the paperdoll factory. internal sealed class LivePaperdollEntityLookup : IPaperdollEntityLookup { private readonly LiveEntityRuntime _liveEntities; public LivePaperdollEntityLookup(LiveEntityRuntime liveEntities) { _liveEntities = liveEntities ?? throw new ArgumentNullException(nameof(liveEntities)); } public bool TryGet(uint serverGuid, out WorldEntity player) => _liveEntities.TryGetWorldEntity(serverGuid, out player); } /// /// Builds the static retail paperdoll clone from the canonical live player /// projection and applies the DAT-defined held pose. /// internal sealed class RetailPaperdollDollFactory : IPaperdollDollFactory { private readonly IPaperdollEntityLookup _entities; private readonly ILocalPlayerIdentitySource _identity; private readonly IPaperdollPoseApplicator _pose; public RetailPaperdollDollFactory( IPaperdollEntityLookup entities, ILocalPlayerIdentitySource identity, IPaperdollPoseApplicator pose) { _entities = entities ?? throw new ArgumentNullException(nameof(entities)); _identity = identity ?? throw new ArgumentNullException(nameof(identity)); _pose = pose ?? throw new ArgumentNullException(nameof(pose)); } public bool TryBuild(out WorldEntity? doll) { doll = null; if (!_entities.TryGet(_identity.ServerGuid, out WorldEntity player) || player.MeshRefs.Count == 0) { return false; } uint? basePalette = null; List<(uint, byte, byte)>? subPalettes = null; if (player.PaletteOverride is { } palette) { basePalette = palette.BasePaletteId; subPalettes = new List<(uint, byte, byte)>(); foreach (var range in palette.SubPalettes) { subPalettes.Add(( range.SubPaletteId, range.Offset, range.Length)); } } List<(byte, uint)>? partOverrides = null; if (player.PartOverrides.Count > 0) { partOverrides = new List<(byte, uint)>(player.PartOverrides.Count); foreach (var part in player.PartOverrides) partOverrides.Add((part.PartIndex, part.GfxObjId)); } doll = DollEntityBuilder.Build( player.SourceGfxObjOrSetupId, new List(player.MeshRefs), basePalette, subPalettes, partOverrides); _pose.Apply(doll, player.SourceGfxObjOrSetupId); return true; } } /// Applies retail's DAT-defined settled paperdoll stance. internal sealed class RetailPaperdollPoseApplicator : IPaperdollPoseApplicator { private readonly IDatReaderWriter _dats; private readonly IAnimationLoader _animations; private readonly object _datLock; public RetailPaperdollPoseApplicator( IDatReaderWriter dats, IAnimationLoader animations, object datLock) { _dats = dats ?? throw new ArgumentNullException(nameof(dats)); _animations = animations ?? throw new ArgumentNullException(nameof(animations)); _datLock = datLock ?? throw new ArgumentNullException(nameof(datLock)); } /// /// Retail gmPaperDollUI resolves its held pose with /// DBCache::GetDIDFromEnumStatic(0x10000005, 7). The master map /// therefore resolves key 7 to a sub-map, then key 0x10000005 to the /// Animation DID. /// private uint ResolvePoseDid() { uint masterDid = (uint)_dats.Portal.Db.Header.MasterMapId; if (masterDid == 0 || !_dats.Portal.TryGet( masterDid, out var master) || !master.ClientEnumToID.TryGetValue(7u, out uint subDid) || !_dats.Portal.TryGet( subDid, out var sub)) { return 0u; } return sub.ClientEnumToID.TryGetValue(0x10000005u, out uint did) ? did : 0u; } public void Apply(WorldEntity doll, uint setupId) { DatReaderWriter.DBObjs.Animation? animation; DatReaderWriter.DBObjs.Setup? setup; lock (_datLock) { uint poseDid = ResolvePoseDid(); if ((poseDid >> 24) != 0x03u) return; animation = _animations.LoadAnimation(poseDid); setup = _dats.Get(setupId); } if (animation is null || setup is null || animation.PartFrames.Count == 0) return; // RedressCreature @ 0x004A3C22 installs the pose with zero frame rate // and holds the settled final frame. var frame = animation.PartFrames[^1]; var reposed = new List(doll.MeshRefs.Count); for (int index = 0; index < doll.MeshRefs.Count; index++) { Vector3 scale = index < setup.DefaultScale.Count ? setup.DefaultScale[index] : Vector3.One; Vector3 origin = Vector3.Zero; Quaternion orientation = Quaternion.Identity; if (index < frame.Frames.Count) { origin = frame.Frames[index].Origin; orientation = frame.Frames[index].Orientation; } Matrix4x4 transform = Matrix4x4.CreateScale(scale) * Matrix4x4.CreateFromQuaternion(orientation) * Matrix4x4.CreateTranslation(origin); MeshRef source = doll.MeshRefs[index]; reposed.Add(new MeshRef(source.GfxObjId, transform) { SurfaceOverrides = source.SurfaceOverrides, }); } doll.MeshRefs = reposed; } }