using System.Numerics; using AcDream.App.Input; using AcDream.App.Rendering.Gpu; 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 render-target /// presentation edge. The renderer remains a borrowed resource disposed by /// the existing window shutdown transaction. /// internal sealed class PaperdollFramePresenter : IPrivateEntityViewportFrame { private readonly IPaperdollDollRenderer _renderer; private readonly IPaperdollFrameView _view; private readonly IPaperdollDollFactory _factory; private WorldEntity? _doll; 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)) { // Same-generation CreateObject refreshes can repeat the exact // player ObjDesc at a portal boundary. Retail redresses its // private inventory object in place; releasing and reacquiring // an identical synthetic owner briefly blanks the viewport and // churns its texture composites. if (!HasEquivalentAppearance(_doll, doll)) { _renderer.SetDoll(doll); _doll = doll; } _dirty = false; } else { // gmPaperDollUI::RedressCreature @ 0x004A3BC0 leaves its // private m_pInventoryObject intact when the SmartBox player // is temporarily unavailable. Keep the successful doll and // retry this dirty redress on the next visible frame. } } _view.SetTextureHandle(_renderer.Render(width, height)); } /// /// Clears the private object only at the owning character-session /// boundary, matching gmPaperDollUI's private-object lifetime. /// public void ResetSession() { _renderer.SetDoll(null); _doll = null; _dirty = true; } private static bool HasEquivalentAppearance( WorldEntity? current, WorldEntity? candidate) { if (current is null || candidate is null) return ReferenceEquals(current, candidate); if (current.SourceGfxObjOrSetupId != candidate.SourceGfxObjOrSetupId || current.Scale != candidate.Scale || current.HiddenPartsMask != candidate.HiddenPartsMask || current.MeshRefs.Count != candidate.MeshRefs.Count || current.PartOverrides.Count != candidate.PartOverrides.Count) { return false; } for (int i = 0; i < current.MeshRefs.Count; i++) { MeshRef left = current.MeshRefs[i]; MeshRef right = candidate.MeshRefs[i]; if (left.GfxObjId != right.GfxObjId || left.PartTransform != right.PartTransform || !DictionaryEquals( left.SurfaceOverrides, right.SurfaceOverrides)) { return false; } } for (int i = 0; i < current.PartOverrides.Count; i++) { if (current.PartOverrides[i] != candidate.PartOverrides[i]) return false; } PaletteOverride? leftPalette = current.PaletteOverride; PaletteOverride? rightPalette = candidate.PaletteOverride; if (leftPalette is null || rightPalette is null) return leftPalette is null && rightPalette is null; if (leftPalette.BasePaletteId != rightPalette.BasePaletteId || leftPalette.SubPalettes.Count != rightPalette.SubPalettes.Count) { return false; } for (int i = 0; i < leftPalette.SubPalettes.Count; i++) { if (leftPalette.SubPalettes[i] != rightPalette.SubPalettes[i]) return false; } return true; } private static bool DictionaryEquals( IReadOnlyDictionary? left, IReadOnlyDictionary? right) { if (left is null || right is null) return left is null && right is null; if (left.Count != right.Count) return false; foreach ((uint key, uint value) in left) { if (!right.TryGetValue(key, out uint rightValue) || rightValue != value) { return false; } } return true; } } /// 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; } /// /// Campaign V slice V6k: the renderer already hands out a /// , so this decodes rather than registers. /// The §7.1 external-texture seam it used to call is deleted with V4g. /// public void SetTextureHandle(uint textureHandle) => _viewport.TextureSlot = UiTextureTableHandle.ToSlot(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) — /// parameterized by the /// paperdoll's own fixed enum key. /// private uint ResolvePoseDid() => RetailHeldPose.ResolvePoseDid(_dats, 0x10000005u); 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 = RetailHeldPose.ComposePartTransform(scale, origin, orientation); MeshRef source = doll.MeshRefs[index]; reposed.Add(new MeshRef(source.GfxObjId, transform) { SurfaceOverrides = source.SurfaceOverrides, }); } doll.MeshRefs = reposed; } }