Files
SnowcloakClient/MareSynchronos/Services/CharaData/CharaDataNearbyManager.cs
2025-08-22 02:19:48 +01:00

297 lines
12 KiB
C#

using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using MareSynchronos.API.Data;
using MareSynchronos.Interop;
using MareSynchronos.MareConfiguration;
using MareSynchronos.Services.CharaData.Models;
using MareSynchronos.Services.Mediator;
using MareSynchronos.Services.ServerConfiguration;
using Microsoft.Extensions.Logging;
using System.Numerics;
namespace MareSynchronos.Services;
public sealed class CharaDataNearbyManager : DisposableMediatorSubscriberBase
{
public record NearbyCharaDataEntry
{
public float Direction { get; init; }
public float Distance { get; init; }
}
private readonly DalamudUtilService _dalamudUtilService;
private readonly Dictionary<PoseEntryExtended, NearbyCharaDataEntry> _nearbyData = [];
private readonly Dictionary<PoseEntryExtended, Guid> _poseVfx = [];
private readonly ServerConfigurationManager _serverConfigurationManager;
private readonly CharaDataConfigService _charaDataConfigService;
private readonly Dictionary<UserData, List<CharaDataMetaInfoExtendedDto>> _metaInfoCache = [];
private readonly VfxSpawnManager _vfxSpawnManager;
private Task? _filterEntriesRunningTask;
private (Guid VfxId, PoseEntryExtended Pose)? _hoveredVfx = null;
private DateTime _lastExecutionTime = DateTime.UtcNow;
private SemaphoreSlim _sharedDataUpdateSemaphore = new(1, 1);
public CharaDataNearbyManager(ILogger<CharaDataNearbyManager> logger, MareMediator mediator,
DalamudUtilService dalamudUtilService, VfxSpawnManager vfxSpawnManager,
ServerConfigurationManager serverConfigurationManager,
CharaDataConfigService charaDataConfigService) : base(logger, mediator)
{
mediator.Subscribe<FrameworkUpdateMessage>(this, (_) => HandleFrameworkUpdate());
mediator.Subscribe<CutsceneFrameworkUpdateMessage>(this, (_) => HandleFrameworkUpdate());
_dalamudUtilService = dalamudUtilService;
_vfxSpawnManager = vfxSpawnManager;
_serverConfigurationManager = serverConfigurationManager;
_charaDataConfigService = charaDataConfigService;
mediator.Subscribe<GposeStartMessage>(this, (_) => ClearAllVfx());
}
public bool ComputeNearbyData { get; set; } = false;
public IDictionary<PoseEntryExtended, NearbyCharaDataEntry> NearbyData => _nearbyData;
public string UserNoteFilter { get; set; } = string.Empty;
public void UpdateSharedData(Dictionary<string, CharaDataMetaInfoExtendedDto?> newData)
{
_sharedDataUpdateSemaphore.Wait();
try
{
_metaInfoCache.Clear();
foreach (var kvp in newData)
{
if (kvp.Value == null) continue;
if (!_metaInfoCache.TryGetValue(kvp.Value.Uploader, out var list))
{
_metaInfoCache[kvp.Value.Uploader] = list = [];
}
list.Add(kvp.Value);
}
}
finally
{
_sharedDataUpdateSemaphore.Release();
}
}
internal void SetHoveredVfx(PoseEntryExtended? hoveredPose)
{
if (hoveredPose == null && _hoveredVfx == null)
return;
if (hoveredPose == null)
{
_vfxSpawnManager.DespawnObject(_hoveredVfx!.Value.VfxId);
_hoveredVfx = null;
return;
}
if (_hoveredVfx == null)
{
var vfxGuid = _vfxSpawnManager.SpawnObject(hoveredPose.Position, hoveredPose.Rotation, Vector3.One * 4, 1, 0.2f, 0.2f, 1f);
if (vfxGuid != null)
_hoveredVfx = (vfxGuid.Value, hoveredPose);
return;
}
if (hoveredPose != _hoveredVfx!.Value.Pose)
{
_vfxSpawnManager.DespawnObject(_hoveredVfx.Value.VfxId);
var vfxGuid = _vfxSpawnManager.SpawnObject(hoveredPose.Position, hoveredPose.Rotation, Vector3.One * 4, 1, 0.2f, 0.2f, 1f);
if (vfxGuid != null)
_hoveredVfx = (vfxGuid.Value, hoveredPose);
}
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
ClearAllVfx();
}
private static float CalculateYawDegrees(Vector3 directionXZ)
{
// Calculate yaw angle in radians using Atan2 (X, Z)
float yawRadians = (float)Math.Atan2(-directionXZ.X, directionXZ.Z);
float yawDegrees = yawRadians * (180f / (float)Math.PI);
// Normalize to [0, 360)
if (yawDegrees < 0)
yawDegrees += 360f;
return yawDegrees;
}
private static float GetAngleToTarget(Vector3 cameraPosition, float cameraYawDegrees, Vector3 targetPosition)
{
// Step 4: Calculate the direction vector from camera to target
Vector3 directionToTarget = targetPosition - cameraPosition;
// Step 5: Project the directionToTarget onto the XZ plane (ignore Y)
Vector3 directionToTargetXZ = new Vector3(directionToTarget.X, 0, directionToTarget.Z);
// Handle the case where the target is directly above or below the camera
if (directionToTargetXZ.LengthSquared() < 1e-10f)
{
return 0; // Default direction
}
directionToTargetXZ = Vector3.Normalize(directionToTargetXZ);
// Step 6: Calculate the target's yaw angle
float targetYawDegrees = CalculateYawDegrees(directionToTargetXZ);
// Step 7: Calculate relative angle
float relativeAngle = targetYawDegrees - cameraYawDegrees;
if (relativeAngle < 0)
relativeAngle += 360f;
// Step 8: Map relative angle to ArrowDirection
return relativeAngle;
}
private static float GetCameraYaw(Vector3 cameraPosition, Vector3 lookAtVector)
{
// Step 1: Calculate the direction vector from camera to LookAtPoint
Vector3 directionFacing = lookAtVector - cameraPosition;
// Step 2: Project the directionFacing onto the XZ plane (ignore Y)
Vector3 directionFacingXZ = new Vector3(directionFacing.X, 0, directionFacing.Z);
// Handle the case where the LookAtPoint is directly above or below the camera
if (directionFacingXZ.LengthSquared() < 1e-10f)
{
// Default to facing forward along the Z-axis if LookAtPoint is directly above or below
directionFacingXZ = new Vector3(0, 0, 1);
}
else
{
directionFacingXZ = Vector3.Normalize(directionFacingXZ);
}
// Step 3: Calculate the camera's yaw angle based on directionFacingXZ
return (CalculateYawDegrees(directionFacingXZ));
}
private void ClearAllVfx()
{
foreach (var vfx in _poseVfx)
{
_vfxSpawnManager.DespawnObject(vfx.Value);
}
_poseVfx.Clear();
}
private async Task FilterEntriesAsync(Vector3 cameraPos, Vector3 cameraLookAt)
{
var previousPoses = _nearbyData.Keys.ToList();
_nearbyData.Clear();
var ownLocation = await _dalamudUtilService.RunOnFrameworkThread(() => _dalamudUtilService.GetMapData()).ConfigureAwait(false);
var player = await _dalamudUtilService.RunOnFrameworkThread(() => _dalamudUtilService.GetPlayerCharacter()).ConfigureAwait(false);
var currentServer = player.CurrentWorld;
var playerPos = player.Position;
var cameraYaw = GetCameraYaw(cameraPos, cameraLookAt);
bool ignoreHousingLimits = _charaDataConfigService.Current.NearbyIgnoreHousingLimitations;
bool onlyCurrentServer = _charaDataConfigService.Current.NearbyOwnServerOnly;
bool showOwnData = _charaDataConfigService.Current.NearbyShowOwnData;
// initial filter on name
foreach (var data in _metaInfoCache.Where(d => (string.IsNullOrWhiteSpace(UserNoteFilter)
|| ((d.Key.Alias ?? string.Empty).Contains(UserNoteFilter, StringComparison.OrdinalIgnoreCase)
|| d.Key.UID.Contains(UserNoteFilter, StringComparison.OrdinalIgnoreCase)
|| (_serverConfigurationManager.GetNoteForUid(UserNoteFilter) ?? string.Empty).Contains(UserNoteFilter, StringComparison.OrdinalIgnoreCase))))
.ToDictionary(k => k.Key, k => k.Value))
{
// filter all poses based on territory, that always must be correct
foreach (var pose in data.Value.Where(v => v.HasPoses && v.HasWorldData && (showOwnData || !v.IsOwnData))
.SelectMany(k => k.PoseExtended)
.Where(p => p.HasPoseData
&& p.HasWorldData
&& p.WorldData!.Value.LocationInfo.TerritoryId == ownLocation.TerritoryId)
.ToList())
{
var poseLocation = pose.WorldData!.Value.LocationInfo;
bool isInHousing = poseLocation.WardId != 0;
var distance = Vector3.Distance(playerPos, pose.Position);
if (distance > _charaDataConfigService.Current.NearbyDistanceFilter) continue;
bool addEntry = (!isInHousing && poseLocation.MapId == ownLocation.MapId
&& (!onlyCurrentServer || poseLocation.ServerId == currentServer.RowId))
|| (isInHousing
&& (((ignoreHousingLimits && !onlyCurrentServer)
|| (ignoreHousingLimits && onlyCurrentServer) && poseLocation.ServerId == currentServer.RowId)
|| poseLocation.ServerId == currentServer.RowId)
&& ((poseLocation.HouseId == 0 && poseLocation.DivisionId == ownLocation.DivisionId
&& (ignoreHousingLimits || poseLocation.WardId == ownLocation.WardId))
|| (poseLocation.HouseId > 0
&& (ignoreHousingLimits || (poseLocation.HouseId == ownLocation.HouseId && poseLocation.WardId == ownLocation.WardId && poseLocation.DivisionId == ownLocation.DivisionId && poseLocation.RoomId == ownLocation.RoomId)))
));
if (addEntry)
_nearbyData[pose] = new() { Direction = GetAngleToTarget(cameraPos, cameraYaw, pose.Position), Distance = distance };
}
}
if (_charaDataConfigService.Current.NearbyDrawWisps && !_dalamudUtilService.IsInGpose && !_dalamudUtilService.IsInCombatOrPerforming)
await _dalamudUtilService.RunOnFrameworkThread(() => ManageWispsNearby(previousPoses)).ConfigureAwait(false);
}
private unsafe void HandleFrameworkUpdate()
{
if (_lastExecutionTime.AddSeconds(0.5) > DateTime.UtcNow) return;
_lastExecutionTime = DateTime.UtcNow;
if (!ComputeNearbyData && !_charaDataConfigService.Current.NearbyShowAlways)
{
if (_nearbyData.Any())
_nearbyData.Clear();
if (_poseVfx.Any())
ClearAllVfx();
return;
}
if (!_charaDataConfigService.Current.NearbyDrawWisps || _dalamudUtilService.IsInGpose || _dalamudUtilService.IsInCombatOrPerforming)
ClearAllVfx();
var camera = CameraManager.Instance()->CurrentCamera;
Vector3 cameraPos = new(camera->Position.X, camera->Position.Y, camera->Position.Z);
Vector3 lookAt = new(camera->LookAtVector.X, camera->LookAtVector.Y, camera->LookAtVector.Z);
if (_filterEntriesRunningTask?.IsCompleted ?? true && _dalamudUtilService.IsLoggedIn)
_filterEntriesRunningTask = FilterEntriesAsync(cameraPos, lookAt);
}
private void ManageWispsNearby(List<PoseEntryExtended> previousPoses)
{
foreach (var data in _nearbyData.Keys)
{
if (_poseVfx.TryGetValue(data, out var _)) continue;
Guid? vfxGuid;
if (data.MetaInfo.IsOwnData)
{
vfxGuid = _vfxSpawnManager.SpawnObject(data.Position, data.Rotation, Vector3.One * 2, 0.8f, 0.5f, 0.0f, 0.7f);
}
else
{
vfxGuid = _vfxSpawnManager.SpawnObject(data.Position, data.Rotation, Vector3.One * 2);
}
if (vfxGuid != null)
{
_poseVfx[data] = vfxGuid.Value;
}
}
foreach (var data in previousPoses.Except(_nearbyData.Keys))
{
if (_poseVfx.Remove(data, out var guid))
{
_vfxSpawnManager.DespawnObject(guid);
}
}
}
}