fix(interaction): bind selection to live incarnations
Carry local WorldEntity identity through render hits, lighting pulses, and deferred movement actions so GUID reuse cannot target a replacement. Reset all session-owned selection and ItemHolder state and prevent combat auto-target during teardown.
This commit is contained in:
parent
c271383714
commit
047a4c83b5
19 changed files with 374 additions and 42 deletions
|
|
@ -10,28 +10,28 @@ public sealed class RetailSelectionLightingPulseTests
|
|||
double now = 10d;
|
||||
var pulse = new RetailSelectionLightingPulse(() => now);
|
||||
|
||||
pulse.Start(0x5000_1234u);
|
||||
AssertLighting(pulse, 0x5000_1234u, RetailSelectionLighting.High);
|
||||
pulse.Start(0x5000_1234u, 1234u);
|
||||
AssertLighting(pulse, 0x5000_1234u, 1234u, RetailSelectionLighting.High);
|
||||
|
||||
now = 10.199;
|
||||
pulse.Tick();
|
||||
AssertLighting(pulse, 0x5000_1234u, RetailSelectionLighting.High);
|
||||
AssertLighting(pulse, 0x5000_1234u, 1234u, RetailSelectionLighting.High);
|
||||
|
||||
now = 10.2;
|
||||
pulse.Tick();
|
||||
AssertLighting(pulse, 0x5000_1234u, RetailSelectionLighting.Low);
|
||||
AssertLighting(pulse, 0x5000_1234u, 1234u, RetailSelectionLighting.Low);
|
||||
|
||||
now = 10.4;
|
||||
pulse.Tick();
|
||||
AssertLighting(pulse, 0x5000_1234u, RetailSelectionLighting.High);
|
||||
AssertLighting(pulse, 0x5000_1234u, 1234u, RetailSelectionLighting.High);
|
||||
|
||||
now = 10.6;
|
||||
pulse.Tick();
|
||||
AssertLighting(pulse, 0x5000_1234u, RetailSelectionLighting.Low);
|
||||
AssertLighting(pulse, 0x5000_1234u, 1234u, RetailSelectionLighting.Low);
|
||||
|
||||
now = 10.8;
|
||||
pulse.Tick();
|
||||
Assert.False(pulse.TryGet(0x5000_1234u, out _));
|
||||
Assert.False(pulse.TryGet(0x5000_1234u, 1234u, out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -39,12 +39,12 @@ public sealed class RetailSelectionLightingPulseTests
|
|||
{
|
||||
double now = 1d;
|
||||
var pulse = new RetailSelectionLightingPulse(() => now);
|
||||
pulse.Start(0x5000_1234u);
|
||||
pulse.Start(0x5000_1234u, 1234u);
|
||||
|
||||
now = 20d;
|
||||
pulse.Tick();
|
||||
|
||||
AssertLighting(pulse, 0x5000_1234u, RetailSelectionLighting.Low);
|
||||
AssertLighting(pulse, 0x5000_1234u, 1234u, RetailSelectionLighting.Low);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -52,24 +52,35 @@ public sealed class RetailSelectionLightingPulseTests
|
|||
{
|
||||
double now = 4d;
|
||||
var pulse = new RetailSelectionLightingPulse(() => now);
|
||||
pulse.Start(0x5000_0001u);
|
||||
pulse.Start(0x5000_0001u, 1u);
|
||||
|
||||
now = 4.2;
|
||||
pulse.Tick();
|
||||
AssertLighting(pulse, 0x5000_0001u, RetailSelectionLighting.Low);
|
||||
AssertLighting(pulse, 0x5000_0001u, 1u, RetailSelectionLighting.Low);
|
||||
|
||||
pulse.Start(0x5000_0002u);
|
||||
pulse.Start(0x5000_0002u, 2u);
|
||||
|
||||
Assert.False(pulse.TryGet(0x5000_0001u, out _));
|
||||
AssertLighting(pulse, 0x5000_0002u, RetailSelectionLighting.High);
|
||||
Assert.False(pulse.TryGet(0x5000_0001u, 1u, out _));
|
||||
AssertLighting(pulse, 0x5000_0002u, 2u, RetailSelectionLighting.High);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GuidReuseDoesNotTransferPulseToReplacementIncarnation()
|
||||
{
|
||||
var pulse = new RetailSelectionLightingPulse(() => 1d);
|
||||
pulse.Start(0x5000_0001u, 10u);
|
||||
|
||||
Assert.False(pulse.TryGet(0x5000_0001u, 11u, out _));
|
||||
AssertLighting(pulse, 0x5000_0001u, 10u, RetailSelectionLighting.High);
|
||||
}
|
||||
|
||||
private static void AssertLighting(
|
||||
RetailSelectionLightingPulse pulse,
|
||||
uint serverGuid,
|
||||
uint localEntityId,
|
||||
RetailSelectionLighting expected)
|
||||
{
|
||||
Assert.True(pulse.TryGet(serverGuid, out var actual));
|
||||
Assert.True(pulse.TryGet(serverGuid, localEntityId, out var actual));
|
||||
Assert.Equal(expected, actual);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
114
tests/AcDream.App.Tests/Rendering/RetailSelectionSceneTests.cs
Normal file
114
tests/AcDream.App.Tests/Rendering/RetailSelectionSceneTests.cs
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Rendering.Selection;
|
||||
using AcDream.Core.Selection;
|
||||
using AcDream.Core.World;
|
||||
|
||||
namespace AcDream.App.Tests.Rendering;
|
||||
|
||||
public sealed class RetailSelectionSceneTests
|
||||
{
|
||||
private sealed class GeometrySource(RetailSelectionMesh mesh)
|
||||
: IRetailSelectionGeometrySource
|
||||
{
|
||||
public RetailSelectionMesh? Resolve(uint gfxObjId)
|
||||
=> gfxObjId == 0x0100_0001u ? mesh : null;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PublishedHitCarriesLogicalEntityIdentity()
|
||||
{
|
||||
var scene = CreateScene();
|
||||
var entity = Entity(localId: 44u, serverGuid: 0x5000_0044u);
|
||||
Publish(scene, entity);
|
||||
|
||||
RetailSelectionHit? hit = PickCenter(scene);
|
||||
|
||||
Assert.NotNull(hit);
|
||||
Assert.Equal(entity.ServerGuid, hit.Value.ServerGuid);
|
||||
Assert.Equal(entity.Id, hit.Value.LocalEntityId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResetClearsPublishedFrameAndLightingPulse()
|
||||
{
|
||||
double now = 1d;
|
||||
var pulse = new RetailSelectionLightingPulse(() => now);
|
||||
var scene = CreateScene(pulse);
|
||||
var entity = Entity(localId: 45u, serverGuid: 0x5000_0045u);
|
||||
Publish(scene, entity);
|
||||
scene.BeginLightingPulse(entity.ServerGuid, entity.Id);
|
||||
|
||||
scene.Reset();
|
||||
|
||||
Assert.Null(PickCenter(scene));
|
||||
Assert.False(scene.TryGetLighting(entity.ServerGuid, entity.Id, out _));
|
||||
}
|
||||
|
||||
private static RetailSelectionScene CreateScene(
|
||||
RetailSelectionLightingPulse? pulse = null)
|
||||
{
|
||||
var mesh = new RetailSelectionMesh(
|
||||
Vector3.Zero,
|
||||
2f,
|
||||
[new RetailSelectionPolygon(
|
||||
[
|
||||
new(-1f, -1f, 0f),
|
||||
new( 1f, -1f, 0f),
|
||||
new( 1f, 1f, 0f),
|
||||
new(-1f, 1f, 0f),
|
||||
],
|
||||
SingleSided: false)]);
|
||||
return new RetailSelectionScene(new GeometrySource(mesh), pulse);
|
||||
}
|
||||
|
||||
private static WorldEntity Entity(uint localId, uint serverGuid)
|
||||
=> new()
|
||||
{
|
||||
Id = localId,
|
||||
ServerGuid = serverGuid,
|
||||
SourceGfxObjOrSetupId = 0x0200_0001u,
|
||||
Position = Vector3.Zero,
|
||||
Rotation = Quaternion.Identity,
|
||||
MeshRefs = [],
|
||||
};
|
||||
|
||||
private static void Publish(RetailSelectionScene scene, WorldEntity entity)
|
||||
{
|
||||
scene.BeginFrame();
|
||||
(Matrix4x4 view, Matrix4x4 projection) = Camera();
|
||||
scene.SetViewFrustum(FrustumPlanes.FromViewProjection(view * projection));
|
||||
scene.AddVisiblePart(
|
||||
entity,
|
||||
partIndex: 0,
|
||||
gfxObjId: 0x0100_0001u,
|
||||
Matrix4x4.CreateTranslation(0f, 0f, -5f));
|
||||
scene.CompleteFrame();
|
||||
}
|
||||
|
||||
private static RetailSelectionHit? PickCenter(RetailSelectionScene scene)
|
||||
{
|
||||
(Matrix4x4 view, Matrix4x4 projection) = Camera();
|
||||
return scene.Pick(
|
||||
400f,
|
||||
300f,
|
||||
new Vector2(800f, 600f),
|
||||
view,
|
||||
projection,
|
||||
skipServerGuid: 0u);
|
||||
}
|
||||
|
||||
private static (Matrix4x4 View, Matrix4x4 Projection) Camera()
|
||||
{
|
||||
Matrix4x4 view = Matrix4x4.CreateLookAt(
|
||||
Vector3.Zero,
|
||||
-Vector3.UnitZ,
|
||||
Vector3.UnitY);
|
||||
Matrix4x4 projection = Matrix4x4.CreatePerspectiveFieldOfView(
|
||||
MathF.PI / 3f,
|
||||
4f / 3f,
|
||||
0.1f,
|
||||
100f);
|
||||
return (view, projection);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue