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);
void Prepare();
uint Render(int width, int height);
}
internal interface IPaperdollFrameView
{
bool TryGetVisibleSize(out int width, out int height);
void SetTextureHandle(uint textureHandle);
void ClearTextureHandle();
}
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,
IPrivateEntityViewportResourcePreparation
{
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;
///
/// Pre-world resource phase: rebuild/redress the private clone and advance
/// its mesh/composite readiness BEFORE world draws can consume the
/// bounded per-frame composite-upload budget. In a dense scene the late
/// presentation phase never wins that budget, which is why the doll was
/// visible only while portal space quiesced the world (#443).
///
public void PrepareResources()
{
if (_dirty)
{
if (_factory.TryBuild(out WorldEntity? doll))
{
// Redress every accepted live-player refresh, including an
// appearance-equivalent ObjDesc after portal/relogin. The
// private clone belongs to the current presentation
// generation; retaining the old object merely because its
// pixels compare equal can leave it attached to retired
// mesh/composite readiness that never completes again.
// PrivateEntityViewportRenderer promotes replacements in two
// phases, so the last completed target remains visible until
// this fresh clone is completely drawable.
_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 frame.
}
}
_renderer.Prepare();
}
public void Render()
{
if (!_view.TryGetVisibleSize(out int width, out int height))
return;
// Zero is a transient not-ready result, not a request to erase a
// previously completed paperdoll. Session reset clears explicitly in
// ResetSession; ordinary mesh/composite upload latency keeps the last
// good image instead of intermittently blanking the viewport.
uint textureHandle = _renderer.Render(width, height);
if (textureHandle != 0u)
_view.SetTextureHandle(textureHandle);
}
///
/// Clears the private object only at the owning character-session
/// boundary, matching gmPaperDollUI's private-object lifetime.
///
public void ResetSession()
{
_renderer.SetDoll(null);
_view.ClearTextureHandle();
_doll = null;
_dirty = 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);
public void ClearTextureHandle() =>
_viewport.TextureSlot = GpuTextureSlot.Unassigned;
}
/// 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;
}
}