using System.Numerics;
using AcDream.App.World;
using AcDream.Core.Items;
using AcDream.Core.Net;
using AcDream.Core.Physics.Motion;
using AcDream.Core.Ui;
using AcDream.Core.World;
namespace AcDream.App.UI.Layout;
///
/// Joins acdream's existing two-table world model into the immutable projected frame
/// consumed by . This is the App-layer equivalent of retail's
/// gmRadarUI::DrawObjects object-maintenance walk: Core still owns every
/// AC-specific classification and math decision, while the retained widget remains a
/// backend-only renderer.
///
public sealed class RadarSnapshotProvider
{
private static readonly Vector2 ProductionCenter = new(60f, 60f);
private readonly ClientObjectTable _objects;
private readonly ILiveEntityRadarSource _liveEntities;
private readonly Func> _spawns;
private readonly Func _playerGuid;
private readonly Func _playerYawRadians;
private readonly Func _playerCellId;
private readonly Func _selectedGuid;
private readonly Func _coordinatesOnRadar;
private readonly Func _uiLocked;
private readonly Func? _relationshipFor;
private readonly Func? _spatialQuery;
private readonly List> _candidateScratch = new();
public RadarSnapshotProvider(
ClientObjectTable objects,
ILiveEntityRadarSource liveEntities,
Func> spawns,
Func playerGuid,
Func playerYawRadians,
Func playerCellId,
Func selectedGuid,
Func coordinatesOnRadar,
Func uiLocked,
Func? relationshipFor = null,
Func? spatialQuery = null)
{
_objects = objects ?? throw new ArgumentNullException(nameof(objects));
_liveEntities = liveEntities ?? throw new ArgumentNullException(nameof(liveEntities));
_spawns = spawns ?? throw new ArgumentNullException(nameof(spawns));
_playerGuid = playerGuid ?? throw new ArgumentNullException(nameof(playerGuid));
_playerYawRadians = playerYawRadians ?? throw new ArgumentNullException(nameof(playerYawRadians));
_playerCellId = playerCellId ?? throw new ArgumentNullException(nameof(playerCellId));
_selectedGuid = selectedGuid ?? throw new ArgumentNullException(nameof(selectedGuid));
_coordinatesOnRadar = coordinatesOnRadar ?? throw new ArgumentNullException(nameof(coordinatesOnRadar));
_uiLocked = uiLocked ?? throw new ArgumentNullException(nameof(uiLocked));
_relationshipFor = relationshipFor;
_spatialQuery = spatialQuery;
}
public UiRadarSnapshot BuildSnapshot()
{
// The retained UI mounts before LiveEntityRuntime during GameWindow
// startup. Resolve these canonical projections per snapshot so the
// provider observes that runtime once it is installed, rather than
// retaining the bootstrap empty-map sentinels forever.
IReadOnlyDictionary spawns = _spawns();
bool uiLocked = _uiLocked();
uint playerGuid = _playerGuid();
if (playerGuid == 0u
|| !_liveEntities.TryGetMaterialized(
playerGuid,
out WorldEntity playerEntity))
return UiRadarSnapshot.Empty with { UiLocked = uiLocked };
float heading = MoveToMath.HeadingFromYaw(_playerYawRadians());
uint playerCellId = _playerCellId();
bool isOutside = RadarCoordinates.TryFromCell(playerCellId, out var playerCoordinates);
string? coordinates = _coordinatesOnRadar() && isOutside
? playerCoordinates.CombinedText
: null;
uint playerPwd = spawns.TryGetValue(playerGuid, out var playerSpawn)
? playerSpawn.ObjectDescriptionFlags ?? 0u
: 0u;
var playerTraits = RadarObjectTraits.FromPublicWeenieDescription(
(uint)(_objects.Get(playerGuid)?.Type ?? ItemType.None), playerPwd);
float range = RetailRadar.GetRangeMeters(isOutside);
_candidateScratch.Clear();
// Retained UI mounts before the production GpuWorldState. Resolve the
// spatial owner per snapshot just like the live dictionaries above;
// retaining a method group here binds forever to the empty bootstrap
// state and produces a working compass with no object blips.
if (_spatialQuery?.Invoke() is { } spatialQuery)
{
// Retail radar is at most 75 metres while a landblock is 192
// metres wide. The current block plus its eight neighbours is a
// complete broadphase (the cheap spatial pre-filter).
spatialQuery.CopyLiveEntitiesNearLandblock(playerCellId, 1, _candidateScratch);
}
else
{
_liveEntities.CopyVisibleTo(_candidateScratch);
}
var blips = new List(Math.Min(_candidateScratch.Count, 64));
foreach (var pair in _candidateScratch)
{
uint guid = pair.Key;
if (guid == playerGuid)
continue;
var entity = pair.Value;
if (!_liveEntities.TryGetVisible(guid, out WorldEntity? visibleEntity)
|| !ReferenceEquals(entity, visibleEntity))
{
continue;
}
var clientObject = _objects.Get(guid);
spawns.TryGetValue(guid, out var spawn);
byte? rawBehavior = clientObject?.RadarBehavior ?? spawn.RadarBehavior;
if (rawBehavior is null
|| !RetailRadar.IsShowable((RadarBehavior)rawBehavior.Value, hasPhysicsObject: true))
{
continue;
}
uint itemType = (uint)(clientObject?.Type ?? ItemType.None);
if (itemType == 0u)
itemType = spawn.ItemType ?? 0u;
uint pwd = spawn.ObjectDescriptionFlags ?? 0u;
byte colorOverride = clientObject?.RadarBlipColor ?? spawn.RadarBlipColor ?? 0;
var traits = RadarObjectTraits.FromPublicWeenieDescription(
itemType, pwd, colorOverride);
var relationship = _relationshipFor?.Invoke(guid) ?? default;
relationship = relationship with
{
PlayerIsPlayerKiller = playerTraits.IsPlayerKiller,
PlayerIsPkLite = playerTraits.IsPkLite,
};
var shape = RetailRadar.GetBlipShape(traits, relationship);
if (shape == RadarBlipShape.Undef)
continue;
Vector3 playerSpace = MoveToMath.GlobalToLocalVec(
playerEntity.Rotation,
entity.Position - playerEntity.Position);
if (!RetailRadar.TryProject(
playerSpace,
ProductionCenter,
RadarController.RadarPixelRadius,
range,
out var projection))
{
continue;
}
var color = RadarBlipColors.For(traits, relationship)
.DimRgb(projection.RgbMultiplier);
string? name = clientObject?.Name;
if (string.IsNullOrEmpty(name))
name = spawn.Name ?? $"0x{guid:X8}";
blips.Add(new UiRadarBlip(
ObjectId: guid,
Name: name,
PixelX: projection.Pixel.X,
PixelY: projection.Pixel.Y,
Color: new Vector4(color.Red, color.Green, color.Blue, color.Alpha),
Shape: shape,
Selected: _selectedGuid() == guid));
}
return new UiRadarSnapshot(
PlayerHeadingDegrees: heading,
Blips: blips,
CoordinatesText: coordinates,
BlankBlips: false,
UiLocked: uiLocked);
}
}