256 lines
8.5 KiB
C#
256 lines
8.5 KiB
C#
using System.Numerics;
|
|
using System.Reflection;
|
|
using AcDream.App.Rendering;
|
|
using AcDream.App.Input;
|
|
using AcDream.Core.World;
|
|
|
|
namespace AcDream.App.Tests.Rendering;
|
|
|
|
public sealed class PaperdollFramePresenterTests
|
|
{
|
|
[Fact]
|
|
public void HiddenView_DoesNotBuildOrRender()
|
|
{
|
|
var renderer = new RecordingRenderer();
|
|
var view = new RecordingView { Visible = false };
|
|
var factory = new RecordingFactory();
|
|
var presenter = new PaperdollFramePresenter(renderer, view, factory);
|
|
|
|
presenter.Render();
|
|
|
|
Assert.True(presenter.IsDirty);
|
|
Assert.Equal(0, factory.BuildCount);
|
|
Assert.Equal(0, renderer.RenderCount);
|
|
Assert.Empty(view.TextureHandles);
|
|
}
|
|
|
|
[Fact]
|
|
public void FirstVisibleFrame_BuildsThenRendersAndPublishesTexture()
|
|
{
|
|
var doll = CreateDoll();
|
|
var renderer = new RecordingRenderer { TextureHandle = 91u };
|
|
var view = new RecordingView { Width = 240, Height = 320 };
|
|
var factory = new RecordingFactory { Doll = doll };
|
|
var presenter = new PaperdollFramePresenter(renderer, view, factory);
|
|
|
|
presenter.Render();
|
|
|
|
Assert.False(presenter.IsDirty);
|
|
Assert.Equal(1, factory.BuildCount);
|
|
Assert.Same(doll, renderer.Dolls.Single());
|
|
Assert.Equal([(240, 320)], renderer.RenderSizes);
|
|
Assert.Equal([91u], view.TextureHandles);
|
|
}
|
|
|
|
[Fact]
|
|
public void CleanFrame_ReusesDollUntilMarkedDirty()
|
|
{
|
|
var renderer = new RecordingRenderer();
|
|
var view = new RecordingView();
|
|
var factory = new RecordingFactory
|
|
{
|
|
Doll = CreateDoll(),
|
|
};
|
|
var presenter = new PaperdollFramePresenter(renderer, view, factory);
|
|
|
|
presenter.Render();
|
|
presenter.Render();
|
|
presenter.MarkDirty();
|
|
presenter.Render();
|
|
|
|
Assert.Equal(2, factory.BuildCount);
|
|
Assert.Equal(2, renderer.Dolls.Count);
|
|
Assert.Equal(3, renderer.RenderCount);
|
|
Assert.False(presenter.IsDirty);
|
|
}
|
|
|
|
private static WorldEntity CreateDoll() => new()
|
|
{
|
|
Id = 42u,
|
|
SourceGfxObjOrSetupId = 0x02000001u,
|
|
Position = Vector3.Zero,
|
|
Rotation = Quaternion.Identity,
|
|
MeshRefs = Array.Empty<MeshRef>(),
|
|
};
|
|
|
|
[Fact]
|
|
public void MissingPlayer_ClearsOldDollPublishesEmptyTextureAndRetries()
|
|
{
|
|
var renderer = new RecordingRenderer { TextureHandle = 0u };
|
|
var view = new RecordingView();
|
|
var factory = new RecordingFactory { CanBuild = false };
|
|
var presenter = new PaperdollFramePresenter(renderer, view, factory);
|
|
|
|
presenter.Render();
|
|
presenter.Render();
|
|
|
|
Assert.True(presenter.IsDirty);
|
|
Assert.Equal(2, factory.BuildCount);
|
|
Assert.Equal(2, renderer.Dolls.Count);
|
|
Assert.All(renderer.Dolls, Assert.Null);
|
|
Assert.Equal(2, renderer.RenderCount);
|
|
Assert.Equal([0u, 0u], view.TextureHandles);
|
|
}
|
|
|
|
[Fact]
|
|
public void PresenterAndProductionHelpersHaveNoWindowOrDelegateBackReference()
|
|
{
|
|
Type[] owners =
|
|
[
|
|
typeof(PaperdollFramePresenter),
|
|
typeof(RetailPaperdollFrameView),
|
|
typeof(PaperdollInventoryVisibility),
|
|
typeof(LivePaperdollEntityLookup),
|
|
typeof(RetailPaperdollDollFactory),
|
|
typeof(RetailPaperdollPoseApplicator),
|
|
];
|
|
|
|
foreach (Type owner in owners)
|
|
{
|
|
FieldInfo[] fields = owner.GetFields(
|
|
BindingFlags.Instance | BindingFlags.NonPublic);
|
|
Assert.DoesNotContain(fields, field => field.FieldType == typeof(GameWindow));
|
|
Assert.DoesNotContain(
|
|
fields,
|
|
field => field.FieldType == typeof(AcDream.App.UI.RetailUiRuntime));
|
|
Assert.DoesNotContain(
|
|
fields,
|
|
field => typeof(Delegate).IsAssignableFrom(field.FieldType));
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void RetailFactory_MissingOrEmptyCurrentPlayerRetriesWithoutApplyingPose()
|
|
{
|
|
var entities = new RecordingEntityLookup();
|
|
var identity = new LocalPlayerIdentityState { ServerGuid = 7u };
|
|
var pose = new RecordingPoseApplicator();
|
|
var factory = new RetailPaperdollDollFactory(entities, identity, pose);
|
|
|
|
Assert.False(factory.TryBuild(out WorldEntity? missing));
|
|
Assert.Null(missing);
|
|
|
|
entities.Entities[7u] = CreateDoll();
|
|
Assert.False(factory.TryBuild(out WorldEntity? empty));
|
|
Assert.Null(empty);
|
|
Assert.Empty(pose.Applications);
|
|
}
|
|
|
|
[Fact]
|
|
public void RetailFactory_ResolvesCurrentIdentityAndClonesLiveAppearance()
|
|
{
|
|
var firstMesh = new MeshRef(0x01000001u, Matrix4x4.Identity)
|
|
{
|
|
SurfaceOverrides = new Dictionary<uint, uint> { [2] = 0x08000001u },
|
|
};
|
|
var secondMesh = new MeshRef(
|
|
0x01000002u,
|
|
Matrix4x4.CreateTranslation(1f, 2f, 3f));
|
|
var first = CreatePlayer(0x02000001u, firstMesh);
|
|
var second = CreatePlayer(0x02000002u, secondMesh);
|
|
var entities = new RecordingEntityLookup
|
|
{
|
|
Entities =
|
|
{
|
|
[10u] = first,
|
|
[20u] = second,
|
|
},
|
|
};
|
|
var identity = new LocalPlayerIdentityState { ServerGuid = 10u };
|
|
var pose = new RecordingPoseApplicator();
|
|
var factory = new RetailPaperdollDollFactory(entities, identity, pose);
|
|
|
|
Assert.True(factory.TryBuild(out WorldEntity? firstDoll));
|
|
identity.ServerGuid = 20u;
|
|
Assert.True(factory.TryBuild(out WorldEntity? secondDoll));
|
|
|
|
Assert.NotNull(firstDoll);
|
|
Assert.NotNull(secondDoll);
|
|
Assert.Equal(0x02000001u, firstDoll.SourceGfxObjOrSetupId);
|
|
Assert.Equal(0x02000002u, secondDoll.SourceGfxObjOrSetupId);
|
|
Assert.NotSame(first.MeshRefs, firstDoll.MeshRefs);
|
|
Assert.Equal(firstMesh.GfxObjId, firstDoll.MeshRefs[0].GfxObjId);
|
|
Assert.Same(firstMesh.SurfaceOverrides, firstDoll.MeshRefs[0].SurfaceOverrides);
|
|
Assert.Equal(
|
|
[(firstDoll, 0x02000001u), (secondDoll, 0x02000002u)],
|
|
pose.Applications);
|
|
Assert.Equal([10u, 20u], entities.RequestedGuids);
|
|
}
|
|
|
|
private static WorldEntity CreatePlayer(uint setupId, MeshRef mesh) => new()
|
|
{
|
|
Id = setupId,
|
|
SourceGfxObjOrSetupId = setupId,
|
|
Position = Vector3.Zero,
|
|
Rotation = Quaternion.Identity,
|
|
MeshRefs = new List<MeshRef> { mesh },
|
|
};
|
|
|
|
private sealed class RecordingRenderer : IPaperdollDollRenderer
|
|
{
|
|
public uint TextureHandle { get; init; }
|
|
public List<WorldEntity?> Dolls { get; } = [];
|
|
public List<(int Width, int Height)> RenderSizes { get; } = [];
|
|
public int RenderCount => RenderSizes.Count;
|
|
|
|
public void SetDoll(WorldEntity? doll) => Dolls.Add(doll);
|
|
|
|
public uint Render(int width, int height)
|
|
{
|
|
RenderSizes.Add((width, height));
|
|
return TextureHandle;
|
|
}
|
|
}
|
|
|
|
private sealed class RecordingView : IPaperdollFrameView
|
|
{
|
|
public bool Visible { get; init; } = true;
|
|
public int Width { get; init; } = 100;
|
|
public int Height { get; init; } = 120;
|
|
public List<uint> TextureHandles { get; } = [];
|
|
|
|
public bool TryGetVisibleSize(out int width, out int height)
|
|
{
|
|
width = Width;
|
|
height = Height;
|
|
return Visible;
|
|
}
|
|
|
|
public void SetTextureHandle(uint textureHandle) =>
|
|
TextureHandles.Add(textureHandle);
|
|
}
|
|
|
|
private sealed class RecordingFactory : IPaperdollDollFactory
|
|
{
|
|
public bool CanBuild { get; init; } = true;
|
|
public WorldEntity? Doll { get; init; }
|
|
public int BuildCount { get; private set; }
|
|
|
|
public bool TryBuild(out WorldEntity? doll)
|
|
{
|
|
BuildCount++;
|
|
doll = Doll;
|
|
return CanBuild;
|
|
}
|
|
}
|
|
|
|
private sealed class RecordingEntityLookup : IPaperdollEntityLookup
|
|
{
|
|
public Dictionary<uint, WorldEntity> Entities { get; } = [];
|
|
public List<uint> RequestedGuids { get; } = [];
|
|
|
|
public bool TryGet(uint serverGuid, out WorldEntity player)
|
|
{
|
|
RequestedGuids.Add(serverGuid);
|
|
return Entities.TryGetValue(serverGuid, out player!);
|
|
}
|
|
}
|
|
|
|
private sealed class RecordingPoseApplicator : IPaperdollPoseApplicator
|
|
{
|
|
public List<(WorldEntity Doll, uint SetupId)> Applications { get; } = [];
|
|
|
|
public void Apply(WorldEntity doll, uint setupId) =>
|
|
Applications.Add((doll, setupId));
|
|
}
|
|
}
|