using AcDream.Core.Combat; using AcDream.Core.Items; using AcDream.Core.Net; using AcDream.Core.Net.Messages; using AcDream.Core.Physics; using AcDream.Core.Spells; using AcDream.Runtime.Entities; using AcDream.Runtime.Gameplay; namespace AcDream.Runtime.Tests.Gameplay; public sealed class RuntimeHostileTargetQueryTests { private const uint Player = 0x50000001u; [Fact] public void FindClosest_UsesAbsoluteDerethCoordinatesAcrossLandblocks() { using GameRuntime runtime = Create(); runtime.PlayerIdentity.ServerGuid = Player; Add( runtime, Player, landblock: 0x01010001u, x: 191f, y: 100f, PlayerObject(Player)); Add( runtime, 0x50000010u, landblock: 0x02010001u, x: 1f, y: 100f, Hostile(0x50000010u)); Add( runtime, 0x50000011u, landblock: 0x01010001u, x: 180f, y: 100f, Hostile(0x50000011u)); Assert.Equal( 0x50000010u, RuntimeHostileTargetQuery.FindClosest(runtime)); Assert.True( RuntimeHostileTargetQuery.IsHostile( runtime, 0x50000010u)); } [Fact] public void Query_RejectsHiddenNoDrawDeadPlayersPetsNpcsAndNonCreatures() { using GameRuntime runtime = Create(); runtime.PlayerIdentity.ServerGuid = Player; Add(runtime, Player, 0x01010001u, 10f, 10f, PlayerObject(Player)); Add( runtime, 0x50000010u, 0x01010001u, 11f, 10f, Hostile(0x50000010u), PhysicsStateFlags.Hidden); Add( runtime, 0x50000011u, 0x01010001u, 12f, 10f, Hostile(0x50000011u), PhysicsStateFlags.NoDraw); Add( runtime, 0x50000012u, 0x01010001u, 13f, 10f, Hostile(0x50000012u)); runtime.ActionOwner.Combat.OnUpdateHealth(0x50000012u, 0f); Add( runtime, 0x50000013u, 0x01010001u, 14f, 10f, PlayerObject(0x50000013u)); Add( runtime, 0x50000014u, 0x01010001u, 15f, 10f, Hostile(0x50000014u, petOwner: Player)); Add( runtime, 0x50000015u, 0x01010001u, 16f, 10f, new ClientObject { ObjectId = 0x50000015u, Type = ItemType.Misc, PublicWeenieBitfield = SelectedObjectHealthPolicy.BfAttackable, }); Add( runtime, 0x50000016u, 0x01010001u, 17f, 10f, new ClientObject { ObjectId = 0x50000016u, Type = ItemType.Creature, }); Add( runtime, 0x50000017u, 0x01010001u, 18f, 10f, Hostile(0x50000017u)); Assert.Equal( 0x50000017u, RuntimeHostileTargetQuery.FindClosest(runtime)); Assert.False(RuntimeHostileTargetQuery.IsHostile(runtime, 0u)); Assert.False( RuntimeHostileTargetQuery.IsHostile( runtime, 0x50000010u)); Assert.False( RuntimeHostileTargetQuery.IsHostile( runtime, 0x50000012u)); Assert.False( RuntimeHostileTargetQuery.IsHostile( runtime, 0x50000014u)); Assert.True( RuntimeHostileTargetQuery.IsHostile( runtime, 0x50000017u)); } [Fact] public void FindClosest_ReturnsNullWithoutPlayerPositionOrHostile() { using GameRuntime runtime = Create(); runtime.PlayerIdentity.ServerGuid = Player; runtime.InventoryOwner.Objects.AddOrUpdate(PlayerObject(Player)); Assert.Null(RuntimeHostileTargetQuery.FindClosest(runtime)); Add(runtime, Player, 0x01010001u, 10f, 10f, PlayerObject(Player)); Add( runtime, 0x50000010u, 0x01010001u, 11f, 10f, new ClientObject { ObjectId = 0x50000010u, Type = ItemType.Creature, }); Assert.Null(RuntimeHostileTargetQuery.FindClosest(runtime)); } private static GameRuntime Create() { var operations = new Operations(); return new GameRuntime(new GameRuntimeDependencies( operations, operations, operations, operations)); } private static ClientObject PlayerObject(uint id) => new() { ObjectId = id, Type = ItemType.Creature, PublicWeenieBitfield = SelectedObjectHealthPolicy.BfPlayer, }; private static ClientObject Hostile( uint id, uint petOwner = 0u) => new() { ObjectId = id, Type = ItemType.Creature, PublicWeenieBitfield = SelectedObjectHealthPolicy.BfAttackable, PetOwnerId = petOwner, }; private static void Add( GameRuntime runtime, uint guid, uint landblock, float x, float y, ClientObject item, PhysicsStateFlags state = 0) { RuntimeEntityRecord record = runtime.EntityObjects .RegisterEntity( Spawn(guid, landblock, x, y, state)) .Canonical!; Assert.True(runtime.EntityObjects.ApplyAcceptedSpawn( record, record.CreateIntegrationVersion, record.Snapshot, replaceGeneration: false)); runtime.InventoryOwner.Objects.AddOrUpdate(item); } private static WorldSession.EntitySpawn Spawn( uint guid, uint landblock, float x, float y, PhysicsStateFlags state) { var position = new CreateObject.ServerPosition( landblock, x, y, 5f, 1f, 0f, 0f, 0f); var timestamps = new PhysicsTimestamps( Position: 1, Movement: 1, State: 1, Vector: 1, Teleport: 0, ServerControlledMove: 1, ForcePosition: 0, ObjDesc: 1, Instance: 1); var physics = new PhysicsSpawnData( RawState: (uint)state, Position: position, Movement: null, AnimationFrame: null, SetupTableId: 0x02000001u, MotionTableId: null, 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: timestamps); return new WorldSession.EntitySpawn( guid, position, 0x02000001u, [], [], [], null, null, guid.ToString("X8"), null, null, null, PhysicsState: physics.RawState, InstanceSequence: 1, MovementSequence: 1, ServerControlSequence: 1, PositionSequence: 1, Physics: physics); } private sealed class Operations : IRuntimeCombatAttackOperations, IRuntimeCombatTargetOperations, IRuntimeCombatModeOperations, IRuntimeSpellCastOperations { public bool CanStartAttack() => false; public void PrepareAttackRequest() { } public bool SendAttack(AttackHeight height, float power) => false; public void SendCancelAttack() { } public bool IsDualWield => false; public bool PlayerReadyForAttack => false; public bool AutoRepeatAttack => false; public bool AutoTarget => false; public uint? SelectClosestTarget() => null; public bool IsInWorld => false; public IReadOnlyList GetOrderedEquipment() => []; public void NotifyExplicitCombatModeRequest() { } public void SendChangeCombatMode(CombatMode mode) { } public uint LocalPlayerId => 0u; public bool CanSend => false; public bool HasRequiredComponents(uint spellId) => false; public bool IsTargetCompatible( uint targetId, SpellMetadata spell, bool showMessage) => false; public void StopCompletely() { } public void SendUntargeted(uint spellId) { } public void SendTargeted(uint targetId, uint spellId) { } public void DisplayMessage(string message) { } public void IncrementBusy() { } } }