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