using System.Numerics; using AcDream.App.Interaction; using AcDream.App.Rendering; using AcDream.App.Rendering.Selection; using AcDream.App.Streaming; using AcDream.App.World; using AcDream.Core.Combat; using AcDream.Core.Items; using AcDream.Core.Net; using AcDream.Core.Net.Messages; using AcDream.Core.Physics; using AcDream.Core.Selection; using AcDream.Core.World; using DatReaderWriter.DBObjs; namespace AcDream.App.Tests.Interaction; public sealed class WorldSelectionQueryTests { private const uint Player = 0x5000_0001u; private const uint Target = 0x7000_0001u; private sealed class Resources : ILiveEntityResourceLifecycle { public void Register(WorldEntity entity) { } public void Unregister(WorldEntity entity) { } } private sealed class GeometrySource(RetailSelectionMesh mesh) : IRetailSelectionGeometrySource { public RetailSelectionMesh? Resolve(uint gfxObjId) => mesh; } private sealed class Harness { public readonly ClientObjectTable Objects = new(); public readonly LiveEntityRuntime Runtime; public readonly RetailSelectionScene Scene; public readonly WorldSelectionQuery Query; public PlayerInteractionPose? PlayerPose = new(0x0101_0001u, Vector3.Zero); public Harness() { var spatial = new GpuWorldState(); spatial.AddLandblock(new LoadedLandblock( 0x0101_FFFFu, new LandBlock(), Array.Empty())); Runtime = new LiveEntityRuntime(spatial, new Resources()); Scene = new RetailSelectionScene(new GeometrySource(Mesh())); Query = new WorldSelectionQuery( Runtime, Objects, Scene, () => Player, Camera, () => new Vector2(400f, 300f), () => PlayerPose, (_, _) => (0.5f, 2f), _ => (new Vector3(1f, 0f, 0f), 2f)); Add(Player, Vector3.Zero, ItemType.Creature, SelectedObjectHealthPolicy.BfPlayer); } public WorldEntity Add( uint guid, Vector3 position, ItemType type, uint publicFlags = 0u, uint? useability = null, uint objectDescriptionFlags = 0u, ushort instance = 1, float scale = 1f, Quaternion? rotation = null) { WorldSession.EntitySpawn spawn = Spawn(guid, instance) with { ItemType = (uint)type, Useability = useability, ObjectDescriptionFlags = objectDescriptionFlags, }; Runtime.RegisterLiveEntity(spawn); WorldEntity entity = Runtime.MaterializeLiveEntity( guid, 0x0101_0001u, id => Entity(id, guid, position, scale, rotation ?? Quaternion.Identity))!; Objects.AddOrUpdate(new ClientObject { ObjectId = guid, Name = $"Object {guid:X8}", Type = type, PublicWeenieBitfield = publicFlags, }); return entity; } public void Publish(WorldEntity entity) { SelectionCameraSnapshot camera = Camera(); Scene.BeginFrame(); Scene.SetViewFrustum(FrustumPlanes.FromViewProjection( camera.View * camera.Projection)); Scene.AddVisiblePart( entity, 0, 0x0100_0001u, Matrix4x4.CreateTranslation(entity.Position)); Scene.CompleteFrame(); } } [Fact] public void PickRejectsPublishedPartAfterGuidWasReused() { var h = new Harness(); WorldEntity oldTarget = h.Add(Target, new Vector3(0f, 0f, -5f), ItemType.Misc); h.Publish(oldTarget); Assert.Equal(Target, h.Query.PickAtCursor(includeSelf: false)); Assert.True(h.Runtime.UnregisterLiveEntity( new DeleteObject.Parsed(Target, 1), isLocalPlayer: false)); WorldEntity replacement = h.Add( Target, new Vector3(0f, 0f, -5f), ItemType.Misc, instance: 1); Assert.NotEqual(oldTarget.Id, replacement.Id); Assert.Null(h.Query.PickAtCursor(includeSelf: false)); } [Fact] public void ClosestTargetIncludesOnlyVisibleLivingHostileMonsters() { var h = new Harness(); h.Add( 0x7000_0010u, new Vector3(8f, 0f, 0f), ItemType.Creature, SelectedObjectHealthPolicy.BfAttackable); h.Add( 0x7000_0011u, new Vector3(2f, 0f, 0f), ItemType.Creature, publicFlags: 0u); h.Add( 0x7000_0012u, new Vector3(1f, 0f, 0f), ItemType.Misc, SelectedObjectHealthPolicy.BfAttackable); ClosestCombatTarget? closest = h.Query.FindClosestHostileMonster(); Assert.Equal(0x7000_0010u, closest?.ServerGuid); Assert.Equal(64f, closest?.DistanceSquared); } [Theory] [InlineData(0.59f, true)] [InlineData(0.61f, false)] [InlineData(7.49f, false)] [InlineData(7.50f, true)] public void ApproachUsesRetailGroundItemRadiusAndAceChargeBoundary( float distance, bool expectedBoundary) { var h = new Harness(); h.Add(Target, new Vector3(distance, 0f, 0f), ItemType.Misc); Assert.True(h.Query.TryGetApproach(Target, out InteractionApproach approach)); if (distance < 1f) Assert.Equal(expectedBoundary, approach.IsCloseRange); else Assert.Equal(expectedBoundary, approach.CanCharge); Assert.Equal(0.6f, approach.UseRadius); Assert.Equal(0.5f, approach.TargetRadius); Assert.Equal(2f, approach.TargetHeight); } [Fact] public void PickupAndUseabilityPreserveIndependentRetailGates() { var h = new Harness(); const uint stuckComponent = 0x7000_0020u; const uint looseComponent = 0x7000_0021u; h.Add( stuckComponent, Vector3.UnitX, ItemType.SpellComponents, useability: 1u, objectDescriptionFlags: 0x0004u); h.Add( looseComponent, Vector3.UnitX * 2f, ItemType.SpellComponents, useability: 1u); Assert.False(h.Query.IsUseable(stuckComponent)); Assert.False(h.Query.IsPickupable(stuckComponent)); Assert.False(h.Query.IsUseable(looseComponent)); Assert.True(h.Query.IsPickupable(looseComponent)); } [Fact] public void SelectionSphereAppliesSetupOffsetScaleAndRotation() { var h = new Harness(); h.Add( Target, new Vector3(10f, 20f, 3f), ItemType.Misc, scale: 2f, rotation: Quaternion.CreateFromAxisAngle(Vector3.UnitZ, MathF.PI / 2f)); Assert.True(h.Query.TryGetSelectionSphere(Target, out Vector3 center, out float radius)); Assert.True(Vector3.Distance(new Vector3(10f, 22f, 3f), center) < 0.0001f); Assert.Equal(4f, radius); } private static SelectionCameraSnapshot Camera() { Matrix4x4 view = Matrix4x4.CreateLookAt( Vector3.Zero, -Vector3.UnitZ, Vector3.UnitY); Matrix4x4 projection = Matrix4x4.CreatePerspectiveFieldOfView( MathF.PI / 3f, 4f / 3f, 0.1f, 100f); return new SelectionCameraSnapshot(view, projection, new Vector2(800f, 600f)); } private static RetailSelectionMesh Mesh() => new( Vector3.Zero, 2f, [new RetailSelectionPolygon( [ new(-1f, -1f, 0f), new( 1f, -1f, 0f), new( 1f, 1f, 0f), new(-1f, 1f, 0f), ], SingleSided: false)]); private static WorldEntity Entity( uint id, uint guid, Vector3 position, float scale, Quaternion rotation) => new() { Id = id, ServerGuid = guid, SourceGfxObjOrSetupId = 0x0200_0001u, Position = position, Rotation = rotation, Scale = scale, MeshRefs = [], }; private static WorldSession.EntitySpawn Spawn(uint guid, ushort instance) { var position = new CreateObject.ServerPosition( 0x0101_0001u, 10f, 10f, 5f, 1f, 0f, 0f, 0f); var physics = new PhysicsSpawnData( RawState: (uint)PhysicsStateFlags.ReportCollisions, Position: position, Movement: null, AnimationFrame: null, SetupTableId: 0x0200_0001u, MotionTableId: 0x0900_0001u, SoundTableId: null, PhysicsScriptTableId: null, Parent: null, Children: null, Scale: null, Friction: null, Elasticity: null, Translucency: null, Velocity: null, Acceleration: null, AngularVelocity: null, DefaultScriptType: null, DefaultScriptIntensity: null, Timestamps: new PhysicsTimestamps(1, 1, 1, 1, 0, 1, 0, 1, instance)); return new WorldSession.EntitySpawn( guid, position, 0x0200_0001u, [], [], [], null, null, "fixture", null, null, 0x0900_0001u, PhysicsState: (uint)PhysicsStateFlags.ReportCollisions, InstanceSequence: instance, MovementSequence: 1, ServerControlSequence: 1, PositionSequence: 1, Physics: physics); } }