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;
}
}