using System.Numerics; using AcDream.App.Rendering.Selection; using AcDream.App.UI.Layout; using AcDream.App.World; using AcDream.Core.Combat; using AcDream.Core.Items; using AcDream.Core.Net; using AcDream.Core.Physics; using AcDream.Core.Selection; using AcDream.Core.World; namespace AcDream.App.Interaction; internal readonly record struct SelectionCameraSnapshot( Matrix4x4 View, Matrix4x4 Projection, Vector2 Viewport); internal readonly record struct PlayerInteractionPose(uint CellId, Vector3 Position); internal readonly record struct WorldInteractionTarget( uint ServerGuid, uint LocalEntityId, WorldEntity Entity); internal readonly record struct ClosestCombatTarget(uint ServerGuid, float DistanceSquared); internal readonly record struct InteractionApproach( WorldInteractionTarget Target, PlayerInteractionPose Player, float UseRadius, bool IsCloseRange, bool CanCharge, float TargetRadius, float TargetHeight); internal interface IWorldSelectionQuery { uint? PickAtCursor(bool includeSelf); uint? PickAt(float mouseX, float mouseY, bool includeSelf); void BeginLightingPulse(uint serverGuid); bool TryCaptureIdentity(uint serverGuid, out uint localEntityId); bool IsCurrent(uint serverGuid, uint localEntityId); string Describe(uint serverGuid); bool IsCreature(uint serverGuid); bool IsHostileMonster(uint serverGuid); ClosestCombatTarget? FindClosestHostileMonster(); bool IsUseable(uint serverGuid); bool IsPickupable(uint serverGuid); bool TryGetApproach(uint serverGuid, out InteractionApproach approach); Vector3? GetCombatCameraTargetPoint(uint serverGuid); } internal interface IRetainedUiSelectionQuery { bool ShouldShowHealth(uint serverGuid); VividTargetInfo? ResolveVividTargetInfo(uint serverGuid); bool IsWithinExternalContainerUseRange(uint serverGuid); } internal interface ISelectionViewPlaneSource { AcDream.App.Rendering.ICamera ApplyViewPlane( AcDream.App.Rendering.ICamera camera); } /// /// Read-only owner for retail world selection and interaction classification. /// It consumes the canonical live-entity runtime but never mutates selection, /// movement, inventory, or network state. /// internal sealed class WorldSelectionQuery : IWorldSelectionQuery, IRetainedUiSelectionQuery { private const uint LargeUseObjectFlags = 0x1000u | 0x4000u | 0x40000u | 0x2000u; private const uint StuckObjectFlag = 0x0004u; private const float DefaultUseRadius = 0.6f; private const float CreatureUseRadius = 3f; private const float LargeObjectUseRadius = 2f; private const float AceCanChargeDistance = 7.5f; private const uint SmallItemMask = (uint)(ItemType.MeleeWeapon | ItemType.Armor | ItemType.Clothing | ItemType.Jewelry | ItemType.Food | ItemType.Money | ItemType.Misc | ItemType.MissileWeapon | ItemType.Container | ItemType.Gem | ItemType.SpellComponents | ItemType.Writable | ItemType.Key | ItemType.Caster); private readonly LiveEntityRuntime _liveEntities; private readonly ClientObjectTable _objects; private readonly RetailSelectionScene _selectionScene; private readonly Func _playerGuid; private readonly Func _camera; private readonly Func _cursor; private readonly Func _playerPose; private readonly Func _setupCylinder; private readonly Func _selectionSphere; public WorldSelectionQuery( LiveEntityRuntime liveEntities, ClientObjectTable objects, RetailSelectionScene selectionScene, Func playerGuid, Func camera, Func cursor, Func playerPose, Func setupCylinder, Func selectionSphere) { _liveEntities = liveEntities ?? throw new ArgumentNullException(nameof(liveEntities)); _objects = objects ?? throw new ArgumentNullException(nameof(objects)); _selectionScene = selectionScene ?? throw new ArgumentNullException(nameof(selectionScene)); _playerGuid = playerGuid ?? throw new ArgumentNullException(nameof(playerGuid)); _camera = camera ?? throw new ArgumentNullException(nameof(camera)); _cursor = cursor ?? throw new ArgumentNullException(nameof(cursor)); _playerPose = playerPose ?? throw new ArgumentNullException(nameof(playerPose)); _setupCylinder = setupCylinder ?? throw new ArgumentNullException(nameof(setupCylinder)); _selectionSphere = selectionSphere ?? throw new ArgumentNullException(nameof(selectionSphere)); } public uint? PickAtCursor(bool includeSelf) { Vector2 cursor = _cursor(); return PickAt(cursor.X, cursor.Y, includeSelf); } public uint? PickAt(float mouseX, float mouseY, bool includeSelf) { SelectionCameraSnapshot camera = _camera(); RetailSelectionHit? hit = _selectionScene.Pick( mouseX, mouseY, camera.Viewport, camera.View, camera.Projection, includeSelf ? 0u : _playerGuid()); return hit is { } found && _liveEntities.TryGetInteractionEligibleRecord( found.ServerGuid, found.LocalEntityId, out _) ? found.ServerGuid : null; } public void BeginLightingPulse(uint serverGuid) { if (TryGetInteractionTarget(serverGuid, out WorldInteractionTarget target)) _selectionScene.BeginLightingPulse(target.ServerGuid, target.LocalEntityId); } public bool TryCaptureIdentity(uint serverGuid, out uint localEntityId) { if (TryGetInteractionTarget(serverGuid, out WorldInteractionTarget target)) { localEntityId = target.LocalEntityId; return true; } localEntityId = 0u; return false; } public bool TryGetInteractionTarget( uint serverGuid, out WorldInteractionTarget target) { if (_liveEntities.TryGetInteractionEligibleRecord( serverGuid, out LiveEntityRecord record) && record.WorldEntity is { } entity) { target = new WorldInteractionTarget(serverGuid, entity.Id, entity); return true; } target = default; return false; } public bool IsCurrent(WorldInteractionTarget target) => IsCurrent(target.ServerGuid, target.LocalEntityId); public bool IsCurrent(uint serverGuid, uint localEntityId) => _liveEntities.TryGetInteractionEligibleRecord(serverGuid, localEntityId, out _); public ItemType GetItemType(uint serverGuid) => _objects.Get(serverGuid)?.Type ?? ItemType.None; public string Describe(uint serverGuid) { string? name = _objects.Get(serverGuid)?.Name; return string.IsNullOrWhiteSpace(name) ? $"0x{serverGuid:X8}" : name; } public bool IsCreature(uint serverGuid) { if (serverGuid == _playerGuid() || !TryGetInteractionTarget(serverGuid, out WorldInteractionTarget target)) { return false; } if (_liveEntities.TryGetAnimationRuntime(target.LocalEntityId, out var animation) && animation.CurrentMotion == MotionCommand.Dead) { return false; } return (GetItemType(serverGuid) & ItemType.Creature) != 0; } public bool IsHostileMonster(uint serverGuid) => IsCreature(serverGuid) && CombatTargetPolicy.IsHostileMonster( _playerGuid(), _objects.Get(_playerGuid()), _objects.Get(serverGuid)); public bool ShouldShowHealth(uint serverGuid) => SelectedObjectHealthPolicy.ShouldQueryHealth( _playerGuid(), _objects.Get(_playerGuid()), _objects.Get(serverGuid)); public ClosestCombatTarget? FindClosestHostileMonster() { if (!_liveEntities.TryGetWorldEntity(_playerGuid(), out WorldEntity player)) return null; ClosestCombatTarget? best = null; foreach (LiveEntityRecord record in _liveEntities.VisibleRecords) { uint guid = record.ServerGuid; WorldEntity entity = record.WorldEntity!; if (!IsHostileMonster(guid)) continue; float distanceSquared = Vector3.DistanceSquared(entity.Position, player.Position); if (best is null || distanceSquared < best.Value.DistanceSquared) best = new ClosestCombatTarget(guid, distanceSquared); } return best; } public Vector3? GetCombatCameraTargetPoint(uint serverGuid) => IsHostileMonster(serverGuid) && TryGetInteractionTarget(serverGuid, out WorldInteractionTarget target) ? target.Entity.Position + Vector3.Transform(new Vector3(0f, 0f, 0.5f), target.Entity.Rotation) : null; public VividTargetInfo? ResolveVividTargetInfo(uint serverGuid) { if (!_liveEntities.TryGetWorldEntity(serverGuid, out _) || !TryGetSelectionSphere(serverGuid, out Vector3 center, out float radius)) { return null; } uint pwdBits = _liveEntities.TryGetSnapshot(serverGuid, out var spawn) ? spawn.ObjectDescriptionFlags ?? 0u : 0u; return new VividTargetInfo(center, radius, (uint)GetItemType(serverGuid), pwdBits); } /// /// SmartBox::GetObjectBoundingBox @ 0x00452E20 delegates to /// CPhysicsObj::GetSelectionSphere @ 0x0050EA40 and uses a 0.1-unit /// fallback when Setup authored no sphere. /// public bool TryGetSelectionSphere( uint serverGuid, out Vector3 worldCenter, out float worldRadius) { worldCenter = default; worldRadius = 0f; if (!_liveEntities.TryGetWorldEntity(serverGuid, out WorldEntity entity)) return false; worldCenter = entity.Position; worldRadius = 0.1f; if (!_liveEntities.TryGetSnapshot(serverGuid, out var spawn) || spawn.SetupTableId is not uint setupId || _selectionSphere(setupId) is not { } sphere || sphere.Radius <= 1e-4f) { return true; } float scale = entity.Scale > 0f ? entity.Scale : 1f; Vector3 localCenter = sphere.Origin * scale; worldCenter = entity.Position + Vector3.Transform(localCenter, entity.Rotation); worldRadius = sphere.Radius * scale; return true; } /// ItemUses::IsUseable @ retail 0x004FCCC0 call family. public bool IsUseable(uint serverGuid) { if (_liveEntities.TryGetSnapshot(serverGuid, out var spawn)) return ItemUseability.IsUseable( spawn.Useability ?? ItemUseability.Undef); return false; } /// ItemHolder::DetermineUseResult @ 0x00588460 pickup gate. public bool IsPickupable(uint serverGuid) { if (!_liveEntities.TryGetSnapshot(serverGuid, out var spawn) || ((spawn.ObjectDescriptionFlags ?? 0u) & StuckObjectFlag) != 0u) { return false; } return ((spawn.ItemType ?? 0u) & SmallItemMask) != 0u; } public bool TryGetApproach( uint serverGuid, out InteractionApproach approach) { if (_playerPose() is not { } player || !TryGetInteractionTarget(serverGuid, out WorldInteractionTarget target)) { approach = default; return false; } float useRadius = GetUseRadius(serverGuid); float dx = target.Entity.Position.X - player.Position.X; float dy = target.Entity.Position.Y - player.Position.Y; float distanceSquared = dx * dx + dy * dy; (float radius, float height) = _setupCylinder(serverGuid, target.Entity); approach = new InteractionApproach( target, player, useRadius, distanceSquared <= useRadius * useRadius, distanceSquared >= AceCanChargeDistance * AceCanChargeDistance, radius, height); return true; } public bool IsWithinExternalContainerUseRange(uint serverGuid) { if (_playerPose() is not { } playerPose || !_liveEntities.TryGetWorldEntity(_playerGuid(), out WorldEntity player) || !TryGetInteractionTarget(serverGuid, out WorldInteractionTarget target) || !_liveEntities.TryGetSnapshot(serverGuid, out var spawn) || spawn.UseRadius is not > 0f) { // The server remains authoritative while render projection is absent. return true; } var playerCylinder = _setupCylinder(_playerGuid(), player); var targetCylinder = _setupCylinder(serverGuid, target.Entity); return ObjectRangeMath.ObjectsInRange( playerPose.Position, playerCylinder.Radius, playerCylinder.Height, target.Entity.Position, targetCylinder.Radius, targetCylinder.Height, spawn.UseRadius.Value, useRadii: true, ignoreZDelta: false); } private float GetUseRadius(uint serverGuid) { if ((GetItemType(serverGuid) & ItemType.Creature) != 0) return CreatureUseRadius; return _liveEntities.TryGetSnapshot(serverGuid, out var spawn) && ((spawn.ObjectDescriptionFlags ?? 0u) & LargeUseObjectFlags) != 0u ? LargeObjectUseRadius : DefaultUseRadius; } }