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