using System.Numerics; using AcDream.App.Interaction; using AcDream.App.Rendering; using AcDream.App.Rendering.Selection; using AcDream.App.Streaming; using AcDream.App.UI.Layout; 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 const uint Wielder = 0x7000_0100u; private const uint RemoteWeapon = 0x7000_0101u; private const uint OwnWeapon = 0x7000_0102u; 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 record Animation(WorldEntity Entity, uint CurrentMotion) : ILiveEntityAnimationRuntime; 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); /// /// Stands in for EntityEffectPoseRegistry: the composed equipped-child /// root EquippedChildRenderController.PublishChildPose emits per frame. /// public readonly Dictionary ChildRoots = new(); public Harness() { var spatial = new GpuWorldState(); spatial.AddLandblock(new LoadedLandblock( 0x0101_FFFFu, new LandBlock(), Array.Empty())); Runtime = LiveEntityRuntimeFixture.Create(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), localEntityId => ChildRoots.TryGetValue(localEntityId, out Matrix4x4 root) ? root : null); 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; } /// /// Materializes an equipped child exactly as /// EquippedChildRenderController.TryRealize does: an Attached /// projection whose bookkeeping Position/Rotation carry the PARENT's /// composed world root, with the child's own composed root published /// separately for effects and selection. /// public WorldEntity AddAttached( uint guid, Vector3 parentWorldPosition, ItemType type, uint wielderId, Matrix4x4? childRoot = null, float? objScale = null, EquipMask equippedLocation = EquipMask.MeleeWeapon) { WorldSession.EntitySpawn spawn = Spawn(guid, 1) with { ItemType = (uint)type, ObjScale = objScale, }; Runtime.RegisterLiveEntity(spawn); WorldEntity entity = Runtime.MaterializeLiveEntity( guid, 0x0101_0001u, id => Entity( id, guid, parentWorldPosition, 1f, Quaternion.Identity), LiveEntityProjectionKind.Attached)!; Objects.AddOrUpdate(new ClientObject { ObjectId = guid, Name = $"Object {guid:X8}", Type = type, WielderId = wielderId, // CreateObject's PublicWeenieDesc carries CurrentWieldedLocation // alongside Wielder for every equipped child; retail's // pwd._location is what DeterminePositionState reads. CurrentlyEquippedLocation = equippedLocation, }); if (childRoot is { } root) ChildRoots[entity.Id] = root; return entity; } public void Publish(WorldEntity entity) => PublishParts((entity, entity.Position)); public void PublishParts(params (WorldEntity Entity, Vector3 PartWorld)[] parts) { SelectionCameraSnapshot camera = Camera(); Scene.BeginFrame(); Scene.SetViewFrustum(FrustumPlanes.FromViewProjection( camera.View * camera.Projection)); foreach ((WorldEntity entity, Vector3 partWorld) in parts) { Scene.AddVisiblePart( entity, 0, 0x0100_0001u, Matrix4x4.CreateTranslation(partWorld)); } 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)); } [Theory] [InlineData(0)] [InlineData(1)] [InlineData(2)] public void PickRejectsPublishedPartAfterVisibilityLifetimeEnds(int transition) { var h = new Harness(); WorldEntity target = h.Add(Target, new Vector3(0f, 0f, -5f), ItemType.Misc); h.Publish(target); Assert.Equal(Target, h.Query.PickAtCursor(includeSelf: false)); switch (transition) { case 0: Assert.True(h.Runtime.TryApplyState( new SetState.Parsed( Target, (uint)(PhysicsStateFlags.ReportCollisions | PhysicsStateFlags.Hidden), InstanceSequence: 1, StateSequence: 2), out _)); break; case 1: Assert.True(h.Runtime.WithdrawLiveEntityProjection(Target)); break; default: Assert.True(h.Runtime.UnregisterLiveEntity( new DeleteObject.Parsed(Target, 1), isLocalPlayer: false)); break; } 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); h.Add( 0x7000_0013u, new Vector3(3f, 0f, 0f), ItemType.Creature, SelectedObjectHealthPolicy.BfAttackable | SelectedObjectHealthPolicy.BfPlayer); WorldEntity pet = h.Add( 0x7000_0014u, new Vector3(4f, 0f, 0f), ItemType.Creature, SelectedObjectHealthPolicy.BfAttackable); h.Objects.Get(pet.ServerGuid)!.PetOwnerId = Player; WorldEntity dead = h.Add( 0x7000_0015u, new Vector3(5f, 0f, 0f), ItemType.Creature, SelectedObjectHealthPolicy.BfAttackable); h.Runtime.SetAnimationRuntime( dead.ServerGuid, new Animation(dead, MotionCommand.Dead)); WorldEntity hidden = h.Add( 0x7000_0016u, new Vector3(6f, 0f, 0f), ItemType.Creature, SelectedObjectHealthPolicy.BfAttackable); Assert.True(h.Runtime.TryApplyState( new SetState.Parsed( hidden.ServerGuid, (uint)(PhysicsStateFlags.ReportCollisions | PhysicsStateFlags.Hidden), InstanceSequence: 1, StateSequence: 2), out _)); WorldEntity pending = h.Add( 0x7000_0017u, new Vector3(7f, 0f, 0f), ItemType.Creature, SelectedObjectHealthPolicy.BfAttackable); Assert.True(h.Runtime.WithdrawLiveEntityProjection(pending.ServerGuid)); 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 UseabilityUsesRetailLowNoBitForAbsentAndExplicitValues() { var h = new Harness(); const uint absent = 0x7000_0022u; const uint zero = 0x7000_0023u; const uint no = 0x7000_0024u; const uint neverWalk = 0x7000_0025u; h.Add(absent, Vector3.UnitX, ItemType.Gem); h.Add(zero, Vector3.UnitX * 2f, ItemType.Gem, useability: 0u); h.Add(no, Vector3.UnitX * 3f, ItemType.Gem, useability: ItemUseability.No); h.Add( neverWalk, Vector3.UnitX * 4f, ItemType.Gem, useability: ItemUseability.NeverWalk); Assert.True(h.Query.IsUseable(absent)); Assert.True(h.Query.IsUseable(zero)); Assert.False(h.Query.IsUseable(no)); Assert.True(h.Query.IsUseable(neverWalk)); } [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); } [Fact] public void VividTargetMarkerWaitsForFreshProjectionWhenDropMovePrecedesPosition() { var h = new Harness(); WorldEntity entity = h.Add(Target, new Vector3(3f, 4f, 0f), ItemType.Misc); Assert.NotNull(h.Query.ResolveVividTargetInfo(Target)); Assert.True(h.Objects.MoveItem(Target, Player, newSlot: 0)); Assert.True(h.Runtime.WithdrawLiveEntityProjection(Target)); Assert.Null(h.Query.ResolveVividTargetInfo(Target)); // ACE may confirm InventoryPutObjectIn3D before sending the fresh // Position. The retained entity still has the old ground pose here. Assert.True(h.Objects.MoveItem(Target, 0u, newSlot: -1)); Assert.Null(h.Query.ResolveVividTargetInfo(Target)); var newPosition = new Vector3(30f, 40f, 2f); entity.SetPosition(newPosition); Assert.True(h.Runtime.RebucketLiveEntity(Target, 0x0101_0001u)); var marker = h.Query.ResolveVividTargetInfo(Target); Assert.NotNull(marker); Assert.Equal(newPosition + Vector3.UnitX, marker.Value.SelectionSphereCenter); } [Fact] public void VividTargetMarkerUsesFreshPoseWhenPositionPrecedesDropMove() { var h = new Harness(); WorldEntity entity = h.Add(Target, new Vector3(3f, 4f, 0f), ItemType.Misc); Assert.True(h.Objects.MoveItem(Target, Player, newSlot: 0)); Assert.True(h.Runtime.WithdrawLiveEntityProjection(Target)); var newPosition = new Vector3(30f, 40f, 2f); entity.SetPosition(newPosition); Assert.True(h.Runtime.RebucketLiveEntity(Target, 0x0101_0001u)); // The spatial pose is current, but ownership continues to suppress the // marker until ServerSaysMoveItem publishes the world placement. Assert.Null(h.Query.ResolveVividTargetInfo(Target)); Assert.True(h.Objects.MoveItem(Target, 0u, newSlot: -1)); var marker = h.Query.ResolveVividTargetInfo(Target); Assert.NotNull(marker); Assert.Equal(newPosition + Vector3.UnitX, marker.Value.SelectionSphereCenter); } [Fact] public void VividTargetMarkerRejectsRetainedProjectionInsideExternalContainer() { var h = new Harness(); const uint corpse = 0x7000_0040u; h.Add(Target, new Vector3(3f, 4f, 0f), ItemType.Misc); h.Objects.AddOrUpdate(new ClientObject { ObjectId = corpse, Name = "Corpse", Type = ItemType.Container, }); Assert.True(h.Objects.MoveItem(Target, corpse, newSlot: 0)); Assert.False(h.Objects.IsOwnedByObject(Target, Player)); Assert.Null(h.Query.ResolveVividTargetInfo(Target)); } /// /// Render::GfxObjUnderSelectionRay @ 0x0054C740 records each hit under the /// drawn part's own CPhysicsObj id (CPhysicsPart::get_physobj_id @ /// 0x0050D490), and an equipped child is a first-class CPhysicsObj /// (CPhysicsObj::add_child @ 0x0050F870). The closest hit therefore names /// the WEAPON, not its wielder. /// [Fact] public void PickResolvesTheEquippedChildRatherThanItsWielder() { var h = new Harness(); WorldEntity wielder = h.Add( Wielder, new Vector3(0f, 0f, -10f), ItemType.Creature, SelectedObjectHealthPolicy.BfAttackable); WorldEntity weapon = h.AddAttached( RemoteWeapon, wielder.Position, ItemType.MeleeWeapon, wielderId: Wielder, childRoot: Matrix4x4.CreateTranslation(0f, 0f, -5f)); h.PublishParts( (wielder, wielder.Position), (weapon, new Vector3(0f, 0f, -5f))); Assert.Equal(RemoteWeapon, h.Query.PickAtCursor(includeSelf: false)); Assert.True(h.Query.TryCaptureIdentity(RemoteWeapon, out uint localId)); Assert.Equal(weapon.Id, localId); } /// /// Retail's pick keeps only the closest winner; there is no second-choice /// walk to the wielder when the winner is gone. /// [Fact] public void PickRejectsAWithdrawnEquippedChildWithoutFallingBackToTheWielder() { var h = new Harness(); WorldEntity wielder = h.Add( Wielder, new Vector3(0f, 0f, -10f), ItemType.Creature, SelectedObjectHealthPolicy.BfAttackable); WorldEntity weapon = h.AddAttached( RemoteWeapon, wielder.Position, ItemType.MeleeWeapon, wielderId: Wielder, childRoot: Matrix4x4.CreateTranslation(0f, 0f, -5f)); h.PublishParts( (wielder, wielder.Position), (weapon, new Vector3(0f, 0f, -5f))); Assert.Equal(RemoteWeapon, h.Query.PickAtCursor(includeSelf: false)); Assert.True(h.Runtime.WithdrawLiveEntityProjection(RemoteWeapon)); Assert.Null(h.Query.PickAtCursor(includeSelf: false)); Assert.False(h.Query.IsCurrent(RemoteWeapon, weapon.Id)); Assert.False(h.Query.TryGetInteractionTarget(RemoteWeapon, out _)); } [Fact] public void PickRejectsAnEquippedChildWhoseIncarnationWasReplaced() { var h = new Harness(); WorldEntity wielder = h.Add( Wielder, new Vector3(0f, 0f, -10f), ItemType.Creature, SelectedObjectHealthPolicy.BfAttackable); WorldEntity weapon = h.AddAttached( RemoteWeapon, wielder.Position, ItemType.MeleeWeapon, wielderId: Wielder, childRoot: Matrix4x4.CreateTranslation(0f, 0f, -5f)); h.PublishParts((weapon, new Vector3(0f, 0f, -5f))); Assert.Equal(RemoteWeapon, h.Query.PickAtCursor(includeSelf: false)); Assert.True(h.Runtime.UnregisterLiveEntity( new DeleteObject.Parsed(RemoteWeapon, 1), isLocalPlayer: false)); WorldEntity replacement = h.AddAttached( RemoteWeapon, wielder.Position, ItemType.MeleeWeapon, wielderId: Wielder, childRoot: Matrix4x4.CreateTranslation(0f, 0f, -5f)); Assert.NotEqual(weapon.Id, replacement.Id); Assert.Null(h.Query.PickAtCursor(includeSelf: false)); } /// /// SmartBox::GetObjectBoundingBox @ 0x00452E20 pushes the picked object's /// OWN m_position, which CPhysicsObj::UpdateChild @ 0x00512D50 composes /// from Frame::combine(parent part frame, holding frame). The marker must /// therefore track the hand, not the wielder's feet. /// [Fact] public void VividTargetMarkerAnchorsOnTheComposedChildRoot() { var h = new Harness(); var wielderRoot = new Vector3(1f, 2f, 3f); h.Add( Wielder, wielderRoot, ItemType.Creature, SelectedObjectHealthPolicy.BfAttackable); h.AddAttached( RemoteWeapon, wielderRoot, ItemType.MeleeWeapon, wielderId: Wielder, childRoot: Matrix4x4.CreateTranslation(7f, 8f, 9f)); VividTargetInfo? marker = h.Query.ResolveVividTargetInfo(RemoteWeapon); Assert.NotNull(marker); // Setup sphere origin (1,0,0) composed onto the child's own root. Assert.Equal(new Vector3(8f, 8f, 9f), marker!.Value.SelectionSphereCenter); Assert.Equal(2f, marker.Value.SelectionSphereRadius); // The parent-derived bookkeeping pose would have produced (2,2,3). Assert.NotEqual( wielderRoot + Vector3.UnitX, marker.Value.SelectionSphereCenter); } /// /// No parent fallback: a child with no composed root published this tick /// has no live frame of its own and therefore no selection sphere. /// [Fact] public void VividTargetMarkerRefusesAnEquippedChildWithNoComposedRoot() { var h = new Harness(); var wielderRoot = new Vector3(1f, 2f, 3f); h.Add( Wielder, wielderRoot, ItemType.Creature, SelectedObjectHealthPolicy.BfAttackable); WorldEntity weapon = h.AddAttached( RemoteWeapon, wielderRoot, ItemType.MeleeWeapon, wielderId: Wielder, childRoot: Matrix4x4.CreateTranslation(7f, 8f, 9f)); Assert.True(h.ChildRoots.Remove(weapon.Id)); Assert.Null(h.Query.ResolveVividTargetInfo(RemoteWeapon)); Assert.False(h.Query.TryGetSelectionSphere(RemoteWeapon, out _, out _)); } /// /// VividTargetIndicator::SetSelected @ 0x004F5CE0 suppresses only /// self/player-owned/IN_CONTAINER. A remote character's wielded item keeps /// its brackets; the local player's own wielded item does not. /// [Fact] public void VividTargetMarkerSuppressesOnlyThePlayersOwnWieldedChild() { var h = new Harness(); h.Add( Wielder, new Vector3(1f, 2f, 3f), ItemType.Creature, SelectedObjectHealthPolicy.BfAttackable); h.AddAttached( RemoteWeapon, new Vector3(1f, 2f, 3f), ItemType.MeleeWeapon, wielderId: Wielder, childRoot: Matrix4x4.CreateTranslation(7f, 8f, 9f)); h.AddAttached( OwnWeapon, Vector3.Zero, ItemType.MeleeWeapon, wielderId: Player, childRoot: Matrix4x4.CreateTranslation(0f, 0.5f, 1f)); Assert.NotNull(h.Query.ResolveVividTargetInfo(RemoteWeapon)); Assert.Null(h.Query.ResolveVividTargetInfo(OwnWeapon)); Assert.True(h.Query.IsWieldedByPlayer(OwnWeapon)); Assert.False(h.Query.IsWieldedByPlayer(RemoteWeapon)); } /// /// CPhysicsObj::SetLighting @ 0x00511A80 is non-recursive: it lights the /// clicked object's own part array only, so clicking a weapon never /// flashes its wielder. /// [Fact] public void LightingPulseStartsOnTheEquippedChildIdentityOnly() { var h = new Harness(); WorldEntity wielder = h.Add( Wielder, new Vector3(0f, 0f, -10f), ItemType.Creature, SelectedObjectHealthPolicy.BfAttackable); WorldEntity weapon = h.AddAttached( RemoteWeapon, wielder.Position, ItemType.MeleeWeapon, wielderId: Wielder, childRoot: Matrix4x4.CreateTranslation(0f, 0f, -5f)); h.Query.BeginLightingPulse(RemoteWeapon); Assert.True(h.Scene.TryGetLighting(RemoteWeapon, weapon.Id, out _)); Assert.False(h.Scene.TryGetLighting(Wielder, wielder.Id, out _)); } /// /// Regression guard for the Slice 4 scope boundary: picking is widened, /// the interaction/radar/auto-target visible set is NOT. Retail's radar /// has no wielded blips. /// [Fact] public void EquippedChildStaysOutOfTheInteractionAndRadarVisibleSet() { var h = new Harness(); WorldEntity wielder = h.Add( Wielder, new Vector3(0f, 0f, -10f), ItemType.Creature, SelectedObjectHealthPolicy.BfAttackable); WorldEntity weapon = h.AddAttached( RemoteWeapon, wielder.Position, ItemType.MeleeWeapon, wielderId: Wielder, childRoot: Matrix4x4.CreateTranslation(0f, 0f, -5f)); Assert.False(h.Runtime.TryGetInteractionEligibleEntity(RemoteWeapon, out _)); Assert.False(h.Runtime.TryGetInteractionEligibleRecord(RemoteWeapon, out _)); Assert.False(h.Runtime.TryGetInteractionEligibleRecord( RemoteWeapon, weapon.Id, out _)); Assert.DoesNotContain( h.Runtime.VisibleRecords, record => record.ServerGuid == RemoteWeapon); Assert.Contains( h.Runtime.VisibleRecords, record => record.ServerGuid == Wielder); // Picking, and only picking, admits the same child. Assert.True(h.Runtime.TryGetPickEligibleRecord(RemoteWeapon, out _)); Assert.True(h.Runtime.TryGetPickEligibleRecord( RemoteWeapon, weapon.Id, out _)); Assert.False(h.Runtime.TryGetPickEligibleRecord( RemoteWeapon, weapon.Id + 1u, out _)); } /// /// CPartArray::GetSelectionSphere @ 0x00518B80 scales the authored sphere /// by the object's own part-array scale. An attached child carries that /// scale on its spawn record, not on its parent-derived WorldEntity. /// [Fact] public void EquippedChildSelectionSphereUsesItsOwnSpawnScale() { var h = new Harness(); h.Add( Wielder, Vector3.Zero, ItemType.Creature, SelectedObjectHealthPolicy.BfAttackable); h.AddAttached( RemoteWeapon, Vector3.Zero, ItemType.MeleeWeapon, wielderId: Wielder, childRoot: Matrix4x4.CreateTranslation(7f, 8f, 9f), objScale: 2f); Assert.True(h.Query.TryGetSelectionSphere( RemoteWeapon, out Vector3 center, out float radius)); Assert.Equal(new Vector3(9f, 8f, 9f), center); Assert.Equal(4f, radius); } /// /// ACCWeenieObject::DeterminePositionState @ 0x0058BE70: /// BEING_REMOVED, else IN_CONTAINER when pwd._containerID != 0, else /// pwd._location != 0 selects WIELDED over IN_3D_VIEW. Both the /// pickup-legality arm at 0x005872B7 and the IR_PICK_UP-vs- /// IR_PUT_IN_CONTAINER split in /// ACCWeenieObject::UIAttemptPutInContainer @ 0x0058D680 read it. /// [Fact] public void WieldedPositionStateFollowsRetailsContainerThenLocationOrder() { var h = new Harness(); const uint ground = 0x7000_0030u; const uint stowed = 0x7000_0031u; h.Add(ground, Vector3.UnitX, ItemType.MeleeWeapon); h.Add( Wielder, new Vector3(0f, 0f, -10f), ItemType.Creature, SelectedObjectHealthPolicy.BfAttackable); h.AddAttached( RemoteWeapon, new Vector3(0f, 0f, -10f), ItemType.MeleeWeapon, wielderId: Wielder, childRoot: Matrix4x4.CreateTranslation(0f, 0f, -5f)); h.AddAttached( OwnWeapon, Vector3.Zero, ItemType.MeleeWeapon, wielderId: Player, childRoot: Matrix4x4.CreateTranslation(0f, 0.5f, 1f)); h.Objects.AddOrUpdate(new ClientObject { ObjectId = stowed, Name = "Stowed weapon", Type = ItemType.MeleeWeapon, ContainerId = Player, CurrentlyEquippedLocation = EquipMask.MeleeWeapon, }); Assert.True(h.Query.IsWieldedPositionState(RemoteWeapon)); Assert.True(h.Query.IsWieldedPositionState(OwnWeapon)); Assert.False(h.Query.IsWieldedPositionState(ground)); // IN_CONTAINER wins over a stale wielded location. Assert.False(h.Query.IsWieldedPositionState(stowed)); // Unknown weenie: retail's GetWeenieObject miss rejects at 0x005870DC. Assert.False(h.Query.IsWieldedPositionState(0x7000_00FFu)); } /// /// The wielded-item pickup gate lives in /// ItemHolder::AttemptToPlaceInContainer_IsItemLegal @ 0x005870C0, /// NOT in the pick. Retail's pick is render-coupled and consults no /// ownership or position state, so Slice 4's selection behavior stands. /// [Fact] public void PickAndSelectionStayOpenOnARemotesWieldedItemTheGateWillRefuse() { var h = new Harness(); WorldEntity wielder = h.Add( Wielder, new Vector3(0f, 0f, -10f), ItemType.Creature, SelectedObjectHealthPolicy.BfAttackable); WorldEntity weapon = h.AddAttached( RemoteWeapon, wielder.Position, ItemType.MeleeWeapon, wielderId: Wielder, childRoot: Matrix4x4.CreateTranslation(0f, 0f, -5f)); h.PublishParts( (wielder, wielder.Position), (weapon, new Vector3(0f, 0f, -5f))); Assert.Equal(RemoteWeapon, h.Query.PickAtCursor(includeSelf: false)); Assert.True(h.Query.TryCaptureIdentity(RemoteWeapon, out uint localId)); Assert.Equal(weapon.Id, localId); Assert.True(h.Query.IsCurrent(RemoteWeapon, weapon.Id)); Assert.True(h.Query.TryGetInteractionTarget(RemoteWeapon, out _)); Assert.NotNull(h.Query.ResolveVividTargetInfo(RemoteWeapon)); Assert.Equal($"Object {RemoteWeapon:X8}", h.Query.Describe(RemoteWeapon)); // The pick predicates are untouched: the retail gate is the wielded // position state combined with ownership, not pickability. Assert.True(h.Query.IsPickupable(RemoteWeapon)); Assert.True(h.Query.IsWieldedPositionState(RemoteWeapon)); Assert.False(h.Objects.IsOwnedByObject(RemoteWeapon, Player)); } /// /// The world-pickup approach belongs to PositionState.IN_3D_VIEW /// alone (ACCWeenieObject::UIAttemptPutInContainer @ 0x0058D680). /// An attached child's bookkeeping pose is its wielder's root, so an /// approach built from it would walk the player to the wielder. /// [Fact] public void ApproachRefusesAnAttachedChildRatherThanAnchoringOnItsWielder() { var h = new Harness(); const uint ground = 0x7000_0032u; var wielderRoot = new Vector3(20f, 0f, 0f); h.Add(ground, new Vector3(3f, 0f, 0f), ItemType.MeleeWeapon); h.Add( Wielder, wielderRoot, ItemType.Creature, SelectedObjectHealthPolicy.BfAttackable); h.AddAttached( RemoteWeapon, wielderRoot, ItemType.MeleeWeapon, wielderId: Wielder, childRoot: Matrix4x4.CreateTranslation(20f, 0f, 1.2f)); Assert.False(h.Query.TryGetApproach(RemoteWeapon, out _)); // The same call still succeeds for a genuine 3-D-view item. Assert.True(h.Query.TryGetApproach(ground, out InteractionApproach loose)); Assert.Equal(ground, loose.Target.ServerGuid); } 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); } }