309 lines
9.7 KiB
C#
309 lines
9.7 KiB
C#
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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));
|
|
}
|
|
}
|
|
|
|
/// <summary>Retained-UI visibility and texture publication for the doll view.</summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>Narrow visibility adapter for the paperdoll's inventory host.</summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>Canonical live-entity lookup used by the paperdoll factory.</summary>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Builds the static retail paperdoll clone from the canonical live player
|
|
/// projection and applies the DAT-defined held pose.
|
|
/// </summary>
|
|
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<MeshRef>(player.MeshRefs),
|
|
basePalette,
|
|
subPalettes,
|
|
partOverrides);
|
|
_pose.Apply(doll, player.SourceGfxObjOrSetupId);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/// <summary>Applies retail's DAT-defined settled paperdoll stance.</summary>
|
|
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));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Retail <c>gmPaperDollUI</c> resolves its held pose with
|
|
/// <c>DBCache::GetDIDFromEnumStatic(0x10000005, 7)</c>. The master map
|
|
/// therefore resolves key 7 to a sub-map, then key 0x10000005 to the
|
|
/// Animation DID.
|
|
/// </summary>
|
|
private uint ResolvePoseDid()
|
|
{
|
|
uint masterDid = (uint)_dats.Portal.Db.Header.MasterMapId;
|
|
if (masterDid == 0
|
|
|| !_dats.Portal.TryGet<DatReaderWriter.DBObjs.EnumIDMap>(
|
|
masterDid,
|
|
out var master)
|
|
|| !master.ClientEnumToID.TryGetValue(7u, out uint subDid)
|
|
|| !_dats.Portal.TryGet<DatReaderWriter.DBObjs.EnumIDMap>(
|
|
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<DatReaderWriter.DBObjs.Setup>(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<MeshRef>(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;
|
|
}
|
|
}
|