various 'improvements'
This commit is contained in:
@@ -1,27 +1,20 @@
|
||||
using LightlessSync;
|
||||
using LightlessObjectKind = LightlessSync.API.Data.Enum.ObjectKind;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Dalamud.Game;
|
||||
using Dalamud.Game.ClientState;
|
||||
using System.Collections.Concurrent;
|
||||
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
using Dalamud.Hooking;
|
||||
using Dalamud.Plugin.Services;
|
||||
using FFXIVClientStructs.Interop;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.Object;
|
||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using DalamudObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind;
|
||||
using FFXIVClientStructs.Interop;
|
||||
using System.Threading;
|
||||
using LightlessObjectKind = LightlessSync.API.Data.Enum.ObjectKind;
|
||||
|
||||
namespace LightlessSync.Services.ActorTracking;
|
||||
|
||||
public sealed unsafe class ActorObjectService : IHostedService, IDisposable
|
||||
public sealed class ActorObjectService : IHostedService, IDisposable
|
||||
{
|
||||
public readonly record struct ActorDescriptor(
|
||||
string Name,
|
||||
@@ -38,25 +31,13 @@ public sealed unsafe class ActorObjectService : IHostedService, IDisposable
|
||||
private readonly IFramework _framework;
|
||||
private readonly IGameInteropProvider _interop;
|
||||
private readonly IObjectTable _objectTable;
|
||||
private readonly IClientState _clientState;
|
||||
private readonly LightlessMediator _mediator;
|
||||
|
||||
private readonly ConcurrentDictionary<nint, ActorDescriptor> _activePlayers = new();
|
||||
private readonly ConcurrentDictionary<string, ActorDescriptor> _actorsByHash = new(StringComparer.Ordinal);
|
||||
private readonly ConcurrentDictionary<string, ConcurrentDictionary<nint, ActorDescriptor>> _actorsByName = new(StringComparer.Ordinal);
|
||||
private ActorDescriptor[] _playerCharacterSnapshot = Array.Empty<ActorDescriptor>();
|
||||
private nint[] _playerAddressSnapshot = Array.Empty<nint>();
|
||||
private readonly HashSet<nint> _renderedPlayers = new();
|
||||
private readonly HashSet<nint> _renderedCompanions = new();
|
||||
private readonly Dictionary<nint, LightlessObjectKind> _ownedObjects = new();
|
||||
private nint[] _renderedPlayerSnapshot = Array.Empty<nint>();
|
||||
private nint[] _renderedCompanionSnapshot = Array.Empty<nint>();
|
||||
private nint[] _ownedObjectSnapshot = Array.Empty<nint>();
|
||||
private IReadOnlyDictionary<nint, LightlessObjectKind> _ownedObjectMapSnapshot = new Dictionary<nint, LightlessObjectKind>();
|
||||
private nint _localPlayerAddress = nint.Zero;
|
||||
private nint _localPetAddress = nint.Zero;
|
||||
private nint _localMinionMountAddress = nint.Zero;
|
||||
private nint _localCompanionAddress = nint.Zero;
|
||||
private readonly OwnedObjectTracker _ownedTracker = new();
|
||||
private ActorSnapshot _snapshot = ActorSnapshot.Empty;
|
||||
|
||||
private Hook<Character.Delegates.OnInitialize>? _onInitializeHook;
|
||||
private Hook<Character.Delegates.Terminate>? _onTerminateHook;
|
||||
@@ -80,16 +61,30 @@ public sealed unsafe class ActorObjectService : IHostedService, IDisposable
|
||||
_framework = framework;
|
||||
_interop = interop;
|
||||
_objectTable = objectTable;
|
||||
_clientState = clientState;
|
||||
_mediator = mediator;
|
||||
}
|
||||
|
||||
public IReadOnlyList<nint> PlayerAddresses => Volatile.Read(ref _playerAddressSnapshot);
|
||||
private ActorSnapshot Snapshot => Volatile.Read(ref _snapshot);
|
||||
|
||||
public IReadOnlyList<nint> PlayerAddresses => Snapshot.PlayerAddresses;
|
||||
|
||||
public IEnumerable<ActorDescriptor> PlayerDescriptors => _activePlayers.Values;
|
||||
public IReadOnlyList<ActorDescriptor> PlayerCharacterDescriptors => Volatile.Read(ref _playerCharacterSnapshot);
|
||||
public IReadOnlyList<ActorDescriptor> PlayerCharacterDescriptors => Snapshot.PlayerDescriptors;
|
||||
|
||||
public bool TryGetActorByHash(string hash, out ActorDescriptor descriptor) => _actorsByHash.TryGetValue(hash, out descriptor);
|
||||
public bool TryGetValidatedActorByHash(string hash, out ActorDescriptor descriptor)
|
||||
{
|
||||
descriptor = default;
|
||||
if (!_actorsByHash.TryGetValue(hash, out var candidate))
|
||||
return false;
|
||||
|
||||
if (!ValidateDescriptorThreadSafe(candidate))
|
||||
return false;
|
||||
|
||||
descriptor = candidate;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryGetPlayerByName(string name, out ActorDescriptor descriptor)
|
||||
{
|
||||
descriptor = default;
|
||||
@@ -100,6 +95,9 @@ public sealed unsafe class ActorObjectService : IHostedService, IDisposable
|
||||
ActorDescriptor? best = null;
|
||||
foreach (var candidate in entries.Values)
|
||||
{
|
||||
if (!ValidateDescriptorThreadSafe(candidate))
|
||||
continue;
|
||||
|
||||
if (best is null || IsBetterNameMatch(candidate, best.Value))
|
||||
{
|
||||
best = candidate;
|
||||
@@ -115,23 +113,54 @@ public sealed unsafe class ActorObjectService : IHostedService, IDisposable
|
||||
return false;
|
||||
}
|
||||
public bool HooksActive => _hooksActive;
|
||||
public IReadOnlyList<nint> RenderedPlayerAddresses => Volatile.Read(ref _renderedPlayerSnapshot);
|
||||
public IReadOnlyList<nint> RenderedCompanionAddresses => Volatile.Read(ref _renderedCompanionSnapshot);
|
||||
public IReadOnlyList<nint> OwnedObjectAddresses => Volatile.Read(ref _ownedObjectSnapshot);
|
||||
public IReadOnlyDictionary<nint, LightlessObjectKind> OwnedObjects => Volatile.Read(ref _ownedObjectMapSnapshot);
|
||||
public nint LocalPlayerAddress => Volatile.Read(ref _localPlayerAddress);
|
||||
public nint LocalPetAddress => Volatile.Read(ref _localPetAddress);
|
||||
public nint LocalMinionOrMountAddress => Volatile.Read(ref _localMinionMountAddress);
|
||||
public nint LocalCompanionAddress => Volatile.Read(ref _localCompanionAddress);
|
||||
public IReadOnlyList<nint> RenderedPlayerAddresses => Snapshot.OwnedObjects.RenderedPlayers;
|
||||
public IReadOnlyList<nint> RenderedCompanionAddresses => Snapshot.OwnedObjects.RenderedCompanions;
|
||||
public IReadOnlyList<nint> OwnedObjectAddresses => Snapshot.OwnedObjects.OwnedAddresses;
|
||||
public IReadOnlyDictionary<nint, LightlessObjectKind> OwnedObjects => Snapshot.OwnedObjects.Map;
|
||||
public nint LocalPlayerAddress => Snapshot.OwnedObjects.LocalPlayer;
|
||||
public nint LocalPetAddress => Snapshot.OwnedObjects.LocalPet;
|
||||
public nint LocalMinionOrMountAddress => Snapshot.OwnedObjects.LocalMinionOrMount;
|
||||
public nint LocalCompanionAddress => Snapshot.OwnedObjects.LocalCompanion;
|
||||
|
||||
public bool TryGetOwnedKind(nint address, out LightlessObjectKind kind)
|
||||
=> OwnedObjects.TryGetValue(address, out kind);
|
||||
|
||||
public bool TryGetOwnedActor(LightlessObjectKind kind, out ActorDescriptor descriptor)
|
||||
{
|
||||
descriptor = default;
|
||||
if (!TryGetOwnedObject(kind, out var address))
|
||||
return false;
|
||||
return TryGetDescriptor(address, out descriptor);
|
||||
}
|
||||
|
||||
public bool TryGetOwnedObjectByIndex(ushort objectIndex, out LightlessObjectKind ownedKind)
|
||||
{
|
||||
ownedKind = default;
|
||||
var ownedSnapshot = OwnedObjects;
|
||||
foreach (var (address, kind) in ownedSnapshot)
|
||||
{
|
||||
if (!TryGetDescriptor(address, out var descriptor))
|
||||
continue;
|
||||
|
||||
if (descriptor.ObjectIndex == objectIndex)
|
||||
{
|
||||
ownedKind = kind;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryGetOwnedObject(LightlessObjectKind kind, out nint address)
|
||||
{
|
||||
var ownedSnapshot = Snapshot.OwnedObjects;
|
||||
address = kind switch
|
||||
{
|
||||
LightlessObjectKind.Player => Volatile.Read(ref _localPlayerAddress),
|
||||
LightlessObjectKind.Pet => Volatile.Read(ref _localPetAddress),
|
||||
LightlessObjectKind.MinionOrMount => Volatile.Read(ref _localMinionMountAddress),
|
||||
LightlessObjectKind.Companion => Volatile.Read(ref _localCompanionAddress),
|
||||
LightlessObjectKind.Player => ownedSnapshot.LocalPlayer,
|
||||
LightlessObjectKind.Pet => ownedSnapshot.LocalPet,
|
||||
LightlessObjectKind.MinionOrMount => ownedSnapshot.LocalMinionOrMount,
|
||||
LightlessObjectKind.Companion => ownedSnapshot.LocalCompanion,
|
||||
_ => nint.Zero
|
||||
};
|
||||
|
||||
@@ -158,7 +187,7 @@ public sealed unsafe class ActorObjectService : IHostedService, IDisposable
|
||||
|
||||
public bool TryGetPlayerAddressByHash(string hash, out nint address)
|
||||
{
|
||||
if (TryGetActorByHash(hash, out var descriptor) && descriptor.Address != nint.Zero)
|
||||
if (TryGetValidatedActorByHash(hash, out var descriptor) && descriptor.Address != nint.Zero)
|
||||
{
|
||||
address = descriptor.Address;
|
||||
return true;
|
||||
@@ -168,6 +197,50 @@ public sealed unsafe class ActorObjectService : IHostedService, IDisposable
|
||||
return false;
|
||||
}
|
||||
|
||||
public async Task WaitForFullyLoadedAsync(nint address, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (address == nint.Zero)
|
||||
throw new ArgumentException("Address cannot be zero.", nameof(address));
|
||||
|
||||
while (true)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var isLoaded = await _framework.RunOnFrameworkThread(() => IsObjectFullyLoaded(address)).ConfigureAwait(false);
|
||||
if (isLoaded)
|
||||
return;
|
||||
|
||||
await Task.Delay(100, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private bool ValidateDescriptorThreadSafe(ActorDescriptor descriptor)
|
||||
{
|
||||
if (_framework.IsInFrameworkUpdateThread)
|
||||
return ValidateDescriptorInternal(descriptor);
|
||||
|
||||
return _framework.RunOnFrameworkThread(() => ValidateDescriptorInternal(descriptor)).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
private bool ValidateDescriptorInternal(ActorDescriptor descriptor)
|
||||
{
|
||||
if (descriptor.Address == nint.Zero)
|
||||
return false;
|
||||
|
||||
if (descriptor.ObjectKind == DalamudObjectKind.Player &&
|
||||
!string.IsNullOrEmpty(descriptor.HashedContentId))
|
||||
{
|
||||
var liveHash = DalamudUtilService.GetHashedCIDFromPlayerPointer(descriptor.Address);
|
||||
if (!string.Equals(liveHash, descriptor.HashedContentId, StringComparison.Ordinal))
|
||||
{
|
||||
UntrackGameObject(descriptor.Address);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void RefreshTrackedActors(bool force = false)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
@@ -185,7 +258,7 @@ public sealed unsafe class ActorObjectService : IHostedService, IDisposable
|
||||
}
|
||||
else
|
||||
{
|
||||
_framework.RunOnFrameworkThread(RefreshTrackedActorsInternal);
|
||||
_ = _framework.RunOnFrameworkThread(RefreshTrackedActorsInternal);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,23 +284,12 @@ public sealed unsafe class ActorObjectService : IHostedService, IDisposable
|
||||
_activePlayers.Clear();
|
||||
_actorsByHash.Clear();
|
||||
_actorsByName.Clear();
|
||||
Volatile.Write(ref _playerCharacterSnapshot, Array.Empty<ActorDescriptor>());
|
||||
Volatile.Write(ref _playerAddressSnapshot, Array.Empty<nint>());
|
||||
Volatile.Write(ref _renderedPlayerSnapshot, Array.Empty<nint>());
|
||||
Volatile.Write(ref _renderedCompanionSnapshot, Array.Empty<nint>());
|
||||
Volatile.Write(ref _ownedObjectSnapshot, Array.Empty<nint>());
|
||||
Volatile.Write(ref _ownedObjectMapSnapshot, new Dictionary<nint, LightlessObjectKind>());
|
||||
Volatile.Write(ref _localPlayerAddress, nint.Zero);
|
||||
Volatile.Write(ref _localPetAddress, nint.Zero);
|
||||
Volatile.Write(ref _localMinionMountAddress, nint.Zero);
|
||||
Volatile.Write(ref _localCompanionAddress, nint.Zero);
|
||||
_renderedPlayers.Clear();
|
||||
_renderedCompanions.Clear();
|
||||
_ownedObjects.Clear();
|
||||
_ownedTracker.Reset();
|
||||
Volatile.Write(ref _snapshot, ActorSnapshot.Empty);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void InitializeHooks()
|
||||
private unsafe void InitializeHooks()
|
||||
{
|
||||
if (_hooksActive)
|
||||
return;
|
||||
@@ -271,7 +333,7 @@ public sealed unsafe class ActorObjectService : IHostedService, IDisposable
|
||||
});
|
||||
}
|
||||
|
||||
private void OnCharacterInitialized(Character* chara)
|
||||
private unsafe void OnCharacterInitialized(Character* chara)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -285,7 +347,7 @@ public sealed unsafe class ActorObjectService : IHostedService, IDisposable
|
||||
QueueFrameworkUpdate(() => TrackGameObject((GameObject*)chara));
|
||||
}
|
||||
|
||||
private void OnCharacterTerminated(Character* chara)
|
||||
private unsafe void OnCharacterTerminated(Character* chara)
|
||||
{
|
||||
var address = (nint)chara;
|
||||
QueueFrameworkUpdate(() => UntrackGameObject(address));
|
||||
@@ -299,7 +361,7 @@ public sealed unsafe class ActorObjectService : IHostedService, IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
private GameObject* OnCharacterDisposed(Character* chara, byte freeMemory)
|
||||
private unsafe GameObject* OnCharacterDisposed(Character* chara, byte freeMemory)
|
||||
{
|
||||
var address = (nint)chara;
|
||||
QueueFrameworkUpdate(() => UntrackGameObject(address));
|
||||
@@ -314,7 +376,7 @@ public sealed unsafe class ActorObjectService : IHostedService, IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
private void TrackGameObject(GameObject* gameObject)
|
||||
private unsafe void TrackGameObject(GameObject* gameObject)
|
||||
{
|
||||
if (gameObject == null)
|
||||
return;
|
||||
@@ -332,14 +394,10 @@ public sealed unsafe class ActorObjectService : IHostedService, IDisposable
|
||||
|
||||
if (_activePlayers.TryGetValue(descriptor.Address, out var existing))
|
||||
{
|
||||
RemoveDescriptorFromIndexes(existing);
|
||||
RemoveDescriptorFromCollections(existing);
|
||||
RemoveDescriptor(existing);
|
||||
}
|
||||
|
||||
_activePlayers[descriptor.Address] = descriptor;
|
||||
IndexDescriptor(descriptor);
|
||||
AddDescriptorToCollections(descriptor);
|
||||
RebuildSnapshots();
|
||||
AddDescriptor(descriptor);
|
||||
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
@@ -355,16 +413,16 @@ public sealed unsafe class ActorObjectService : IHostedService, IDisposable
|
||||
_mediator.Publish(new ActorTrackedMessage(descriptor));
|
||||
}
|
||||
|
||||
private ActorDescriptor? BuildDescriptor(GameObject* gameObject, DalamudObjectKind objectKind)
|
||||
private unsafe ActorDescriptor? BuildDescriptor(GameObject* gameObject, DalamudObjectKind objectKind)
|
||||
{
|
||||
if (gameObject == null)
|
||||
return null;
|
||||
|
||||
var address = (nint)gameObject;
|
||||
string name = string.Empty;
|
||||
ushort objectIndex = (ushort)gameObject->ObjectIndex;
|
||||
ushort objectIndex = gameObject->ObjectIndex;
|
||||
bool isInGpose = objectIndex >= 200;
|
||||
bool isLocal = _clientState.LocalPlayer?.Address == address;
|
||||
bool isLocal = _objectTable.LocalPlayer?.Address == address;
|
||||
string hashedCid = string.Empty;
|
||||
|
||||
if (_objectTable.CreateObjectReference(address) is IPlayerCharacter playerCharacter)
|
||||
@@ -372,7 +430,7 @@ public sealed unsafe class ActorObjectService : IHostedService, IDisposable
|
||||
name = playerCharacter.Name.TextValue ?? string.Empty;
|
||||
objectIndex = playerCharacter.ObjectIndex;
|
||||
isInGpose = objectIndex >= 200;
|
||||
isLocal = playerCharacter.Address == _clientState.LocalPlayer?.Address;
|
||||
isLocal = playerCharacter.Address == _objectTable.LocalPlayer?.Address;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -389,7 +447,7 @@ public sealed unsafe class ActorObjectService : IHostedService, IDisposable
|
||||
return new ActorDescriptor(name, hashedCid, address, objectIndex, isLocal, isInGpose, objectKind, ownedKind, ownerEntityId);
|
||||
}
|
||||
|
||||
private (LightlessObjectKind? OwnedKind, uint OwnerEntityId) DetermineOwnedKind(GameObject* gameObject, DalamudObjectKind objectKind, bool isLocalPlayer)
|
||||
private unsafe (LightlessObjectKind? OwnedKind, uint OwnerEntityId) DetermineOwnedKind(GameObject* gameObject, DalamudObjectKind objectKind, bool isLocalPlayer)
|
||||
{
|
||||
if (gameObject == null)
|
||||
return (null, 0);
|
||||
@@ -406,7 +464,7 @@ public sealed unsafe class ActorObjectService : IHostedService, IDisposable
|
||||
return (LightlessObjectKind.Player, entityId);
|
||||
}
|
||||
|
||||
if (_clientState.LocalPlayer is not { } localPlayer)
|
||||
if (_objectTable.LocalPlayer is not { } localPlayer)
|
||||
return (null, 0);
|
||||
|
||||
var ownerId = gameObject->OwnerId;
|
||||
@@ -453,9 +511,7 @@ public sealed unsafe class ActorObjectService : IHostedService, IDisposable
|
||||
|
||||
if (_activePlayers.TryRemove(address, out var descriptor))
|
||||
{
|
||||
RemoveDescriptorFromIndexes(descriptor);
|
||||
RemoveDescriptorFromCollections(descriptor);
|
||||
RebuildSnapshots();
|
||||
RemoveDescriptor(descriptor);
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Actor untracked: {Name} addr={Address:X} idx={Index} owned={OwnedKind}",
|
||||
@@ -469,7 +525,7 @@ public sealed unsafe class ActorObjectService : IHostedService, IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshTrackedActorsInternal()
|
||||
private unsafe void RefreshTrackedActorsInternal()
|
||||
{
|
||||
var addresses = EnumerateActiveCharacterAddresses();
|
||||
HashSet<nint> seen = new(addresses.Count);
|
||||
@@ -524,7 +580,10 @@ public sealed unsafe class ActorObjectService : IHostedService, IDisposable
|
||||
return candidate.ObjectIndex < current.ObjectIndex;
|
||||
}
|
||||
|
||||
private void OnCompanionInitialized(Companion* companion)
|
||||
private bool TryGetDescriptor(nint address, out ActorDescriptor descriptor)
|
||||
=> _activePlayers.TryGetValue(address, out descriptor);
|
||||
|
||||
private unsafe void OnCompanionInitialized(Companion* companion)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -538,7 +597,7 @@ public sealed unsafe class ActorObjectService : IHostedService, IDisposable
|
||||
QueueFrameworkUpdate(() => TrackGameObject((GameObject*)companion));
|
||||
}
|
||||
|
||||
private void OnCompanionTerminated(Companion* companion)
|
||||
private unsafe void OnCompanionTerminated(Companion* companion)
|
||||
{
|
||||
var address = (nint)companion;
|
||||
QueueFrameworkUpdate(() => UntrackGameObject(address));
|
||||
@@ -559,107 +618,46 @@ public sealed unsafe class ActorObjectService : IHostedService, IDisposable
|
||||
_actorsByHash.TryRemove(descriptor.HashedContentId, out _);
|
||||
}
|
||||
|
||||
if (descriptor.ObjectKind == DalamudObjectKind.Player && !string.IsNullOrEmpty(descriptor.Name))
|
||||
if (descriptor.ObjectKind == DalamudObjectKind.Player
|
||||
&& !string.IsNullOrEmpty(descriptor.Name)
|
||||
&& _actorsByName.TryGetValue(descriptor.Name, out var bucket))
|
||||
{
|
||||
if (_actorsByName.TryGetValue(descriptor.Name, out var bucket))
|
||||
bucket.TryRemove(descriptor.Address, out _);
|
||||
if (bucket.IsEmpty)
|
||||
{
|
||||
bucket.TryRemove(descriptor.Address, out _);
|
||||
if (bucket.IsEmpty)
|
||||
{
|
||||
_actorsByName.TryRemove(descriptor.Name, out _);
|
||||
}
|
||||
_actorsByName.TryRemove(descriptor.Name, out _);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void AddDescriptorToCollections(ActorDescriptor descriptor)
|
||||
private void AddDescriptor(ActorDescriptor descriptor)
|
||||
{
|
||||
if (descriptor.ObjectKind == DalamudObjectKind.Player)
|
||||
{
|
||||
_renderedPlayers.Add(descriptor.Address);
|
||||
if (descriptor.IsLocalPlayer)
|
||||
{
|
||||
Volatile.Write(ref _localPlayerAddress, descriptor.Address);
|
||||
}
|
||||
}
|
||||
else if (descriptor.ObjectKind == DalamudObjectKind.Companion)
|
||||
{
|
||||
_renderedCompanions.Add(descriptor.Address);
|
||||
}
|
||||
|
||||
if (descriptor.OwnedKind is { } ownedKind)
|
||||
{
|
||||
_ownedObjects[descriptor.Address] = ownedKind;
|
||||
switch (ownedKind)
|
||||
{
|
||||
case LightlessObjectKind.Player:
|
||||
Volatile.Write(ref _localPlayerAddress, descriptor.Address);
|
||||
break;
|
||||
case LightlessObjectKind.Pet:
|
||||
Volatile.Write(ref _localPetAddress, descriptor.Address);
|
||||
break;
|
||||
case LightlessObjectKind.MinionOrMount:
|
||||
Volatile.Write(ref _localMinionMountAddress, descriptor.Address);
|
||||
break;
|
||||
case LightlessObjectKind.Companion:
|
||||
Volatile.Write(ref _localCompanionAddress, descriptor.Address);
|
||||
break;
|
||||
}
|
||||
}
|
||||
_activePlayers[descriptor.Address] = descriptor;
|
||||
IndexDescriptor(descriptor);
|
||||
_ownedTracker.OnDescriptorAdded(descriptor);
|
||||
PublishSnapshot();
|
||||
}
|
||||
|
||||
private void RemoveDescriptorFromCollections(ActorDescriptor descriptor)
|
||||
private void RemoveDescriptor(ActorDescriptor descriptor)
|
||||
{
|
||||
if (descriptor.ObjectKind == DalamudObjectKind.Player)
|
||||
{
|
||||
_renderedPlayers.Remove(descriptor.Address);
|
||||
if (descriptor.IsLocalPlayer && Volatile.Read(ref _localPlayerAddress) == descriptor.Address)
|
||||
{
|
||||
Volatile.Write(ref _localPlayerAddress, nint.Zero);
|
||||
}
|
||||
}
|
||||
else if (descriptor.ObjectKind == DalamudObjectKind.Companion)
|
||||
{
|
||||
_renderedCompanions.Remove(descriptor.Address);
|
||||
if (Volatile.Read(ref _localCompanionAddress) == descriptor.Address)
|
||||
{
|
||||
Volatile.Write(ref _localCompanionAddress, nint.Zero);
|
||||
}
|
||||
}
|
||||
|
||||
if (descriptor.OwnedKind is { } ownedKind)
|
||||
{
|
||||
_ownedObjects.Remove(descriptor.Address);
|
||||
switch (ownedKind)
|
||||
{
|
||||
case LightlessObjectKind.Player when Volatile.Read(ref _localPlayerAddress) == descriptor.Address:
|
||||
Volatile.Write(ref _localPlayerAddress, nint.Zero);
|
||||
break;
|
||||
case LightlessObjectKind.Pet when Volatile.Read(ref _localPetAddress) == descriptor.Address:
|
||||
Volatile.Write(ref _localPetAddress, nint.Zero);
|
||||
break;
|
||||
case LightlessObjectKind.MinionOrMount when Volatile.Read(ref _localMinionMountAddress) == descriptor.Address:
|
||||
Volatile.Write(ref _localMinionMountAddress, nint.Zero);
|
||||
break;
|
||||
case LightlessObjectKind.Companion when Volatile.Read(ref _localCompanionAddress) == descriptor.Address:
|
||||
Volatile.Write(ref _localCompanionAddress, nint.Zero);
|
||||
break;
|
||||
}
|
||||
}
|
||||
RemoveDescriptorFromIndexes(descriptor);
|
||||
_ownedTracker.OnDescriptorRemoved(descriptor);
|
||||
PublishSnapshot();
|
||||
}
|
||||
|
||||
private void RebuildSnapshots()
|
||||
private void PublishSnapshot()
|
||||
{
|
||||
var playerDescriptors = _activePlayers.Values
|
||||
.Where(descriptor => descriptor.ObjectKind == DalamudObjectKind.Player)
|
||||
.ToArray();
|
||||
var playerAddresses = new nint[playerDescriptors.Length];
|
||||
for (var i = 0; i < playerDescriptors.Length; i++)
|
||||
playerAddresses[i] = playerDescriptors[i].Address;
|
||||
|
||||
Volatile.Write(ref _playerCharacterSnapshot, playerDescriptors);
|
||||
Volatile.Write(ref _playerAddressSnapshot, playerDescriptors.Select(d => d.Address).ToArray());
|
||||
Volatile.Write(ref _renderedPlayerSnapshot, _renderedPlayers.ToArray());
|
||||
Volatile.Write(ref _renderedCompanionSnapshot, _renderedCompanions.ToArray());
|
||||
Volatile.Write(ref _ownedObjectSnapshot, _ownedObjects.Keys.ToArray());
|
||||
Volatile.Write(ref _ownedObjectMapSnapshot, new Dictionary<nint, LightlessObjectKind>(_ownedObjects));
|
||||
var ownedSnapshot = _ownedTracker.CreateSnapshot();
|
||||
var nextGeneration = Snapshot.Generation + 1;
|
||||
var snapshot = new ActorSnapshot(playerDescriptors, playerAddresses, ownedSnapshot, nextGeneration);
|
||||
Volatile.Write(ref _snapshot, snapshot);
|
||||
}
|
||||
|
||||
private void QueueFrameworkUpdate(Action action)
|
||||
@@ -673,7 +671,7 @@ public sealed unsafe class ActorObjectService : IHostedService, IDisposable
|
||||
return;
|
||||
}
|
||||
|
||||
_framework.RunOnFrameworkThread(action);
|
||||
_ = _framework.RunOnFrameworkThread(action);
|
||||
}
|
||||
|
||||
private void DisposeHooks()
|
||||
@@ -723,7 +721,7 @@ public sealed unsafe class ActorObjectService : IHostedService, IDisposable
|
||||
or DalamudObjectKind.Companion
|
||||
or DalamudObjectKind.MountType;
|
||||
|
||||
private static List<nint> EnumerateActiveCharacterAddresses()
|
||||
private static unsafe List<nint> EnumerateActiveCharacterAddresses()
|
||||
{
|
||||
var results = new List<nint>(64);
|
||||
var manager = GameObjectManager.Instance();
|
||||
@@ -751,4 +749,170 @@ public sealed unsafe class ActorObjectService : IHostedService, IDisposable
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private static unsafe bool IsObjectFullyLoaded(nint address)
|
||||
{
|
||||
if (address == nint.Zero)
|
||||
return false;
|
||||
|
||||
var gameObject = (GameObject*)address;
|
||||
if (gameObject == null)
|
||||
return false;
|
||||
|
||||
var drawObject = gameObject->DrawObject;
|
||||
if (drawObject == null)
|
||||
return false;
|
||||
|
||||
if (gameObject->RenderFlags == 2048)
|
||||
return false;
|
||||
|
||||
var characterBase = (CharacterBase*)drawObject;
|
||||
if (characterBase == null)
|
||||
return false;
|
||||
|
||||
if (characterBase->HasModelInSlotLoaded != 0)
|
||||
return false;
|
||||
|
||||
if (characterBase->HasModelFilesInSlotLoaded != 0)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private sealed class OwnedObjectTracker
|
||||
{
|
||||
private readonly HashSet<nint> _renderedPlayers = new();
|
||||
private readonly HashSet<nint> _renderedCompanions = new();
|
||||
private readonly Dictionary<nint, LightlessObjectKind> _ownedObjects = new();
|
||||
private nint _localPlayerAddress = nint.Zero;
|
||||
private nint _localPetAddress = nint.Zero;
|
||||
private nint _localMinionMountAddress = nint.Zero;
|
||||
private nint _localCompanionAddress = nint.Zero;
|
||||
|
||||
public void OnDescriptorAdded(ActorDescriptor descriptor)
|
||||
{
|
||||
if (descriptor.ObjectKind == DalamudObjectKind.Player)
|
||||
{
|
||||
_renderedPlayers.Add(descriptor.Address);
|
||||
if (descriptor.IsLocalPlayer)
|
||||
_localPlayerAddress = descriptor.Address;
|
||||
}
|
||||
else if (descriptor.ObjectKind == DalamudObjectKind.Companion)
|
||||
{
|
||||
_renderedCompanions.Add(descriptor.Address);
|
||||
}
|
||||
|
||||
if (descriptor.OwnedKind is { } ownedKind)
|
||||
{
|
||||
_ownedObjects[descriptor.Address] = ownedKind;
|
||||
switch (ownedKind)
|
||||
{
|
||||
case LightlessObjectKind.Player:
|
||||
_localPlayerAddress = descriptor.Address;
|
||||
break;
|
||||
case LightlessObjectKind.Pet:
|
||||
_localPetAddress = descriptor.Address;
|
||||
break;
|
||||
case LightlessObjectKind.MinionOrMount:
|
||||
_localMinionMountAddress = descriptor.Address;
|
||||
break;
|
||||
case LightlessObjectKind.Companion:
|
||||
_localCompanionAddress = descriptor.Address;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void OnDescriptorRemoved(ActorDescriptor descriptor)
|
||||
{
|
||||
if (descriptor.ObjectKind == DalamudObjectKind.Player)
|
||||
{
|
||||
_renderedPlayers.Remove(descriptor.Address);
|
||||
if (descriptor.IsLocalPlayer && _localPlayerAddress == descriptor.Address)
|
||||
_localPlayerAddress = nint.Zero;
|
||||
}
|
||||
else if (descriptor.ObjectKind == DalamudObjectKind.Companion)
|
||||
{
|
||||
_renderedCompanions.Remove(descriptor.Address);
|
||||
if (_localCompanionAddress == descriptor.Address)
|
||||
_localCompanionAddress = nint.Zero;
|
||||
}
|
||||
|
||||
if (descriptor.OwnedKind is { } ownedKind)
|
||||
{
|
||||
_ownedObjects.Remove(descriptor.Address);
|
||||
switch (ownedKind)
|
||||
{
|
||||
case LightlessObjectKind.Player when _localPlayerAddress == descriptor.Address:
|
||||
_localPlayerAddress = nint.Zero;
|
||||
break;
|
||||
case LightlessObjectKind.Pet when _localPetAddress == descriptor.Address:
|
||||
_localPetAddress = nint.Zero;
|
||||
break;
|
||||
case LightlessObjectKind.MinionOrMount when _localMinionMountAddress == descriptor.Address:
|
||||
_localMinionMountAddress = nint.Zero;
|
||||
break;
|
||||
case LightlessObjectKind.Companion when _localCompanionAddress == descriptor.Address:
|
||||
_localCompanionAddress = nint.Zero;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public OwnedObjectSnapshot CreateSnapshot()
|
||||
=> new(
|
||||
_renderedPlayers.ToArray(),
|
||||
_renderedCompanions.ToArray(),
|
||||
_ownedObjects.Keys.ToArray(),
|
||||
new Dictionary<nint, LightlessObjectKind>(_ownedObjects),
|
||||
_localPlayerAddress,
|
||||
_localPetAddress,
|
||||
_localMinionMountAddress,
|
||||
_localCompanionAddress);
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
_renderedPlayers.Clear();
|
||||
_renderedCompanions.Clear();
|
||||
_ownedObjects.Clear();
|
||||
_localPlayerAddress = nint.Zero;
|
||||
_localPetAddress = nint.Zero;
|
||||
_localMinionMountAddress = nint.Zero;
|
||||
_localCompanionAddress = nint.Zero;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed record OwnedObjectSnapshot(
|
||||
IReadOnlyList<nint> RenderedPlayers,
|
||||
IReadOnlyList<nint> RenderedCompanions,
|
||||
IReadOnlyList<nint> OwnedAddresses,
|
||||
IReadOnlyDictionary<nint, LightlessObjectKind> Map,
|
||||
nint LocalPlayer,
|
||||
nint LocalPet,
|
||||
nint LocalMinionOrMount,
|
||||
nint LocalCompanion)
|
||||
{
|
||||
public static OwnedObjectSnapshot Empty { get; } = new(
|
||||
Array.Empty<nint>(),
|
||||
Array.Empty<nint>(),
|
||||
Array.Empty<nint>(),
|
||||
new Dictionary<nint, LightlessObjectKind>(),
|
||||
nint.Zero,
|
||||
nint.Zero,
|
||||
nint.Zero,
|
||||
nint.Zero);
|
||||
}
|
||||
|
||||
private sealed record ActorSnapshot(
|
||||
IReadOnlyList<ActorDescriptor> PlayerDescriptors,
|
||||
IReadOnlyList<nint> PlayerAddresses,
|
||||
OwnedObjectSnapshot OwnedObjects,
|
||||
int Generation)
|
||||
{
|
||||
public static ActorSnapshot Empty { get; } = new(
|
||||
Array.Empty<ActorDescriptor>(),
|
||||
Array.Empty<nint>(),
|
||||
OwnedObjectSnapshot.Empty,
|
||||
0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using LightlessSync.API.Dto;
|
||||
using LightlessSync.API.Dto.Chat;
|
||||
using LightlessSync.Services.ActorTracking;
|
||||
using LightlessSync.Services.Mediator;
|
||||
@@ -21,12 +20,11 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
private const int MaxReportContextLength = 1000;
|
||||
|
||||
private readonly ApiController _apiController;
|
||||
private readonly ChatConfigService _chatConfigService;
|
||||
private readonly DalamudUtilService _dalamudUtilService;
|
||||
private readonly ActorObjectService _actorObjectService;
|
||||
private readonly PairUiService _pairUiService;
|
||||
|
||||
private readonly object _sync = new();
|
||||
private readonly Lock _sync = new();
|
||||
|
||||
private readonly Dictionary<string, ChatChannelState> _channels = new(StringComparer.Ordinal);
|
||||
private readonly List<string> _channelOrder = new();
|
||||
@@ -55,7 +53,6 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
PairUiService pairUiService)
|
||||
: base(logger, mediator)
|
||||
{
|
||||
_chatConfigService = chatConfigService;
|
||||
_apiController = apiController;
|
||||
_dalamudUtilService = dalamudUtilService;
|
||||
_actorObjectService = actorObjectService;
|
||||
@@ -63,12 +60,12 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
|
||||
_isLoggedIn = _dalamudUtilService.IsLoggedIn;
|
||||
_isConnected = _apiController.IsConnected;
|
||||
_chatEnabled = _chatConfigService.Current.AutoEnableChatOnLogin;
|
||||
_chatEnabled = chatConfigService.Current.AutoEnableChatOnLogin;
|
||||
}
|
||||
|
||||
public IReadOnlyList<ChatChannelSnapshot> GetChannelsSnapshot()
|
||||
{
|
||||
lock (_sync)
|
||||
using (_sync.EnterScope())
|
||||
{
|
||||
var snapshots = new List<ChatChannelSnapshot>(_channelOrder.Count);
|
||||
foreach (var key in _channelOrder)
|
||||
@@ -107,7 +104,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_sync)
|
||||
using (_sync.EnterScope())
|
||||
{
|
||||
return _chatEnabled;
|
||||
}
|
||||
@@ -118,7 +115,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_sync)
|
||||
using (_sync.EnterScope())
|
||||
{
|
||||
return _chatEnabled && _isConnected;
|
||||
}
|
||||
@@ -127,7 +124,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
|
||||
public void SetActiveChannel(string? key)
|
||||
{
|
||||
lock (_sync)
|
||||
using (_sync.EnterScope())
|
||||
{
|
||||
_activeChannelKey = key;
|
||||
if (key is not null && _channels.TryGetValue(key, out var state))
|
||||
@@ -145,7 +142,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
private async Task EnableChatAsync()
|
||||
{
|
||||
bool wasEnabled;
|
||||
lock (_sync)
|
||||
using (_sync.EnterScope())
|
||||
{
|
||||
wasEnabled = _chatEnabled;
|
||||
if (!wasEnabled)
|
||||
@@ -170,7 +167,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
List<ChatChannelDescriptor> groupDescriptors;
|
||||
ChatChannelDescriptor? zoneDescriptor;
|
||||
|
||||
lock (_sync)
|
||||
using (_sync.EnterScope())
|
||||
{
|
||||
wasEnabled = _chatEnabled;
|
||||
if (!wasEnabled)
|
||||
@@ -259,7 +256,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
return Task.FromResult(new ChatReportResult(false, "Please describe why you are reporting this message."));
|
||||
}
|
||||
|
||||
lock (_sync)
|
||||
using (_sync.EnterScope())
|
||||
{
|
||||
if (!_chatEnabled)
|
||||
{
|
||||
@@ -311,8 +308,8 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
Mediator.Subscribe<DalamudLogoutMessage>(this, _ => HandleLogout());
|
||||
Mediator.Subscribe<ZoneSwitchEndMessage>(this, _ => ScheduleZonePresenceUpdate());
|
||||
Mediator.Subscribe<WorldChangedMessage>(this, _ => ScheduleZonePresenceUpdate(force: true));
|
||||
Mediator.Subscribe<ConnectedMessage>(this, msg => HandleConnected(msg.Connection));
|
||||
Mediator.Subscribe<HubReconnectedMessage>(this, _ => HandleConnected(null));
|
||||
Mediator.Subscribe<ConnectedMessage>(this, _ => HandleConnected());
|
||||
Mediator.Subscribe<HubReconnectedMessage>(this, _ => HandleConnected());
|
||||
Mediator.Subscribe<HubReconnectingMessage>(this, _ => HandleReconnecting());
|
||||
Mediator.Subscribe<DisconnectedMessage>(this, _ => HandleReconnecting());
|
||||
Mediator.Subscribe<PairUiUpdatedMessage>(this, _ => RefreshGroupsFromPairManager());
|
||||
@@ -371,11 +368,11 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleConnected(ConnectionDto? connection)
|
||||
private void HandleConnected()
|
||||
{
|
||||
_isConnected = true;
|
||||
|
||||
lock (_sync)
|
||||
using (_sync.EnterScope())
|
||||
{
|
||||
_selfTokens.Clear();
|
||||
_pendingSelfMessages.Clear();
|
||||
@@ -410,7 +407,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
{
|
||||
_isConnected = false;
|
||||
|
||||
lock (_sync)
|
||||
using (_sync.EnterScope())
|
||||
{
|
||||
_selfTokens.Clear();
|
||||
_pendingSelfMessages.Clear();
|
||||
@@ -475,7 +472,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
|
||||
private void UpdateChannelsForDisabledState()
|
||||
{
|
||||
lock (_sync)
|
||||
using (_sync.EnterScope())
|
||||
{
|
||||
foreach (var state in _channels.Values)
|
||||
{
|
||||
@@ -513,7 +510,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
string? zoneKey;
|
||||
ZoneChannelDefinition? definition = null;
|
||||
|
||||
lock (_sync)
|
||||
using (_sync.EnterScope())
|
||||
{
|
||||
_territoryToZoneKey.TryGetValue(territoryId, out zoneKey);
|
||||
if (zoneKey is not null)
|
||||
@@ -538,7 +535,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
|
||||
bool shouldForceSend;
|
||||
|
||||
lock (_sync)
|
||||
using (_sync.EnterScope())
|
||||
{
|
||||
var state = EnsureZoneStateLocked();
|
||||
state.DisplayName = definition.Value.DisplayName;
|
||||
@@ -566,7 +563,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
ChatChannelDescriptor? descriptor = null;
|
||||
bool clearedHistory = false;
|
||||
|
||||
lock (_sync)
|
||||
using (_sync.EnterScope())
|
||||
{
|
||||
descriptor = _lastZoneDescriptor;
|
||||
_lastZoneDescriptor = null;
|
||||
@@ -590,9 +587,9 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
state.DisplayName = "Zone Chat";
|
||||
}
|
||||
|
||||
if (_activeChannelKey == ZoneChannelKey)
|
||||
if (string.Equals(_activeChannelKey, ZoneChannelKey, StringComparison.Ordinal))
|
||||
{
|
||||
_activeChannelKey = _channelOrder.FirstOrDefault(key => key != ZoneChannelKey);
|
||||
_activeChannelKey = _channelOrder.FirstOrDefault(key => !string.Equals(key, ZoneChannelKey, StringComparison.Ordinal));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -627,7 +624,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
{
|
||||
var infoList = infos ?? Array.Empty<ZoneChatChannelInfoDto>();
|
||||
|
||||
lock (_sync)
|
||||
using (_sync.EnterScope())
|
||||
{
|
||||
_zoneDefinitions.Clear();
|
||||
_territoryToZoneKey.Clear();
|
||||
@@ -657,7 +654,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
{
|
||||
if (def.TerritoryNames.Contains(variant))
|
||||
{
|
||||
_territoryToZoneKey[(uint)kvp.Key] = def.Key;
|
||||
_territoryToZoneKey[kvp.Key] = def.Key;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -689,7 +686,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
var descriptorsToJoin = new List<ChatChannelDescriptor>();
|
||||
var descriptorsToLeave = new List<ChatChannelDescriptor>();
|
||||
|
||||
lock (_sync)
|
||||
using (_sync.EnterScope())
|
||||
{
|
||||
var remainingGroups = new HashSet<string>(_groupDefinitions.Keys, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
@@ -807,7 +804,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
return;
|
||||
|
||||
List<ChatChannelDescriptor> descriptors;
|
||||
lock (_sync)
|
||||
using (_sync.EnterScope())
|
||||
{
|
||||
descriptors = _channels.Values
|
||||
.Where(state => state.Type == ChatChannelType.Group)
|
||||
@@ -832,7 +829,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
var presenceKey = BuildPresenceKey(descriptor);
|
||||
bool stateMatches;
|
||||
|
||||
lock (_sync)
|
||||
using (_sync.EnterScope())
|
||||
{
|
||||
stateMatches = !force
|
||||
&& _lastPresenceStates.TryGetValue(presenceKey, out var lastState)
|
||||
@@ -846,7 +843,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
{
|
||||
await _apiController.UpdateChatPresence(new ChatPresenceUpdateDto(descriptor, territoryId, isActive)).ConfigureAwait(false);
|
||||
|
||||
lock (_sync)
|
||||
using (_sync.EnterScope())
|
||||
{
|
||||
if (isActive)
|
||||
{
|
||||
@@ -870,7 +867,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
var key = normalized.Type == ChatChannelType.Zone ? ZoneChannelKey : BuildChannelKey(normalized);
|
||||
var pending = new PendingSelfMessage(key, message);
|
||||
|
||||
lock (_sync)
|
||||
using (_sync.EnterScope())
|
||||
{
|
||||
_pendingSelfMessages.Add(pending);
|
||||
while (_pendingSelfMessages.Count > 20)
|
||||
@@ -884,7 +881,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
|
||||
private void RemovePendingSelfMessage(PendingSelfMessage pending)
|
||||
{
|
||||
lock (_sync)
|
||||
using (_sync.EnterScope())
|
||||
{
|
||||
var index = _pendingSelfMessages.FindIndex(p =>
|
||||
string.Equals(p.ChannelKey, pending.ChannelKey, StringComparison.Ordinal) &&
|
||||
@@ -905,7 +902,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
var message = BuildMessage(dto, fromSelf);
|
||||
bool publishChannelList = false;
|
||||
|
||||
lock (_sync)
|
||||
using (_sync.EnterScope())
|
||||
{
|
||||
if (!_channels.TryGetValue(key, out var state))
|
||||
{
|
||||
@@ -960,7 +957,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
|
||||
if (publishChannelList)
|
||||
{
|
||||
lock (_sync)
|
||||
using (_sync.EnterScope())
|
||||
{
|
||||
UpdateChannelOrderLocked();
|
||||
}
|
||||
@@ -973,7 +970,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
{
|
||||
if (dto.Sender.User?.UID is { } uid && string.Equals(uid, _apiController.UID, StringComparison.Ordinal))
|
||||
{
|
||||
lock (_sync)
|
||||
using (_sync.EnterScope())
|
||||
{
|
||||
_selfTokens[channelKey] = dto.Sender.Token;
|
||||
}
|
||||
@@ -981,7 +978,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
return true;
|
||||
}
|
||||
|
||||
lock (_sync)
|
||||
using (_sync.EnterScope())
|
||||
{
|
||||
if (_selfTokens.TryGetValue(channelKey, out var token) &&
|
||||
string.Equals(token, dto.Sender.Token, StringComparison.Ordinal))
|
||||
@@ -1014,7 +1011,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
{
|
||||
var isZone = dto.Channel.Type == ChatChannelType.Zone;
|
||||
if (!string.IsNullOrEmpty(dto.Sender.HashedCid) &&
|
||||
_actorObjectService.TryGetActorByHash(dto.Sender.HashedCid, out var descriptor) &&
|
||||
_actorObjectService.TryGetValidatedActorByHash(dto.Sender.HashedCid, out var descriptor) &&
|
||||
!string.IsNullOrWhiteSpace(descriptor.Name))
|
||||
{
|
||||
return descriptor.Name;
|
||||
@@ -1065,7 +1062,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
{
|
||||
_activeChannelKey = _channelOrder[0];
|
||||
}
|
||||
else if (_activeChannelKey is not null && !_channelOrder.Contains(_activeChannelKey))
|
||||
else if (_activeChannelKey is not null && !_channelOrder.Contains(_activeChannelKey, StringComparer.Ordinal))
|
||||
{
|
||||
_activeChannelKey = _channelOrder.Count > 0 ? _channelOrder[0] : null;
|
||||
}
|
||||
@@ -1108,7 +1105,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
|
||||
private static bool ChannelDescriptorsMatch(ChatChannelDescriptor left, ChatChannelDescriptor right)
|
||||
=> left.Type == right.Type
|
||||
&& NormalizeKey(left.CustomKey) == NormalizeKey(right.CustomKey)
|
||||
&& string.Equals(NormalizeKey(left.CustomKey), NormalizeKey(right.CustomKey), StringComparison.Ordinal)
|
||||
&& left.WorldId == right.WorldId;
|
||||
|
||||
private ChatChannelState EnsureZoneStateLocked()
|
||||
@@ -1180,6 +1177,3 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
|
||||
private readonly record struct PendingSelfMessage(string ChannelKey, string Message);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -360,7 +360,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
|
||||
public IntPtr GetPlayerCharacterFromCachedTableByIdent(string characterName)
|
||||
{
|
||||
if (_actorObjectService.TryGetActorByHash(characterName, out var actor))
|
||||
if (_actorObjectService.TryGetValidatedActorByHash(characterName, out var actor))
|
||||
return actor.Address;
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
@@ -639,7 +639,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
|
||||
internal (string Name, nint Address) FindPlayerByNameHash(string ident)
|
||||
{
|
||||
if (_actorObjectService.TryGetActorByHash(ident, out var descriptor))
|
||||
if (_actorObjectService.TryGetValidatedActorByHash(ident, out var descriptor))
|
||||
{
|
||||
return (descriptor.Name, descriptor.Address);
|
||||
}
|
||||
|
||||
@@ -1,249 +1,692 @@
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Game.Addon.Lifecycle;
|
||||
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
|
||||
using Dalamud.Game.ClientState.Objects.Enums;
|
||||
using Dalamud.Game.Text;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Plugin;
|
||||
using Dalamud.Plugin.Services;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.Object;
|
||||
using FFXIVClientStructs.FFXIV.Client.System.Framework;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using LightlessSync.LightlessConfiguration;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.Services.Rendering;
|
||||
using LightlessSync.UI;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using LightlessSync.UI.Services;
|
||||
using LightlessSync.Utils;
|
||||
using LightlessSync.UtilsEnum.Enum;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Pictomancy;
|
||||
using System.Collections.Immutable;
|
||||
using System.Globalization;
|
||||
using System.Numerics;
|
||||
using Task = System.Threading.Tasks.Task;
|
||||
|
||||
namespace LightlessSync.Services.LightFinder
|
||||
namespace LightlessSync.Services.LightFinder;
|
||||
|
||||
public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscriber
|
||||
{
|
||||
public class LightFinderPlateHandler : IHostedService, IMediatorSubscriber
|
||||
private readonly ILogger<LightFinderPlateHandler> _logger;
|
||||
private readonly IAddonLifecycle _addonLifecycle;
|
||||
private readonly IGameGui _gameGui;
|
||||
private readonly IObjectTable _objectTable;
|
||||
private readonly LightlessConfigService _configService;
|
||||
private readonly PairUiService _pairUiService;
|
||||
private readonly LightlessMediator _mediator;
|
||||
public LightlessMediator Mediator => _mediator;
|
||||
|
||||
private readonly IUiBuilder _uiBuilder;
|
||||
private bool _mEnabled;
|
||||
private bool _needsLabelRefresh;
|
||||
private bool _drawSubscribed;
|
||||
private AddonNamePlate* _mpNameplateAddon;
|
||||
private readonly object _labelLock = new();
|
||||
private readonly NameplateBuffers _buffers = new();
|
||||
private int _labelRenderCount;
|
||||
|
||||
private const string DefaultLabelText = "LightFinder";
|
||||
private const SeIconChar DefaultIcon = SeIconChar.Hyadelyn;
|
||||
private static readonly string DefaultIconGlyph = SeIconCharExtensions.ToIconString(DefaultIcon);
|
||||
private static readonly Vector2 DefaultPivot = new(0.5f, 1f);
|
||||
|
||||
private ImmutableHashSet<string> _activeBroadcastingCids = [];
|
||||
|
||||
public LightFinderPlateHandler(
|
||||
ILogger<LightFinderPlateHandler> logger,
|
||||
IAddonLifecycle addonLifecycle,
|
||||
IGameGui gameGui,
|
||||
LightlessConfigService configService,
|
||||
LightlessMediator mediator,
|
||||
IObjectTable objectTable,
|
||||
PairUiService pairUiService,
|
||||
IDalamudPluginInterface pluginInterface,
|
||||
PictomancyService pictomancyService)
|
||||
{
|
||||
private readonly ILogger<LightFinderPlateHandler> _logger;
|
||||
private readonly LightlessConfigService _configService;
|
||||
private readonly IDalamudPluginInterface _pluginInterface;
|
||||
private readonly IObjectTable _gameObjects;
|
||||
private readonly IGameGui _gameGui;
|
||||
_logger = logger;
|
||||
_addonLifecycle = addonLifecycle;
|
||||
_gameGui = gameGui;
|
||||
_configService = configService;
|
||||
_mediator = mediator;
|
||||
_objectTable = objectTable;
|
||||
_pairUiService = pairUiService;
|
||||
_uiBuilder = pluginInterface.UiBuilder ?? throw new ArgumentNullException(nameof(pluginInterface));
|
||||
_ = pictomancyService ?? throw new ArgumentNullException(nameof(pictomancyService));
|
||||
|
||||
private const float _defaultNameplateDistance = 15.0f;
|
||||
private ImmutableHashSet<string> _activeBroadcastingCids = [];
|
||||
private readonly Dictionary<IGameObject, Vector3> _smoothed = [];
|
||||
private readonly float _defaultHeightOffset = 0f;
|
||||
}
|
||||
|
||||
public LightlessMediator Mediator { get; }
|
||||
|
||||
public LightFinderPlateHandler(
|
||||
ILogger<LightFinderPlateHandler> logger,
|
||||
LightlessMediator mediator,
|
||||
IDalamudPluginInterface dalamudPluginInterface,
|
||||
LightlessConfigService configService,
|
||||
IObjectTable gameObjects,
|
||||
IGameGui gameGui)
|
||||
internal void Init()
|
||||
{
|
||||
if (!_drawSubscribed)
|
||||
{
|
||||
_logger = logger;
|
||||
Mediator = mediator;
|
||||
_pluginInterface = dalamudPluginInterface;
|
||||
_configService = configService;
|
||||
_gameObjects = gameObjects;
|
||||
_gameGui = gameGui;
|
||||
_uiBuilder.Draw += OnUiBuilderDraw;
|
||||
_drawSubscribed = true;
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
EnableNameplate();
|
||||
_mediator.Subscribe<PriorityFrameworkUpdateMessage>(this, OnTick);
|
||||
}
|
||||
|
||||
internal void Uninit()
|
||||
{
|
||||
DisableNameplate();
|
||||
if (_drawSubscribed)
|
||||
{
|
||||
_logger.LogInformation("Starting LightFinderPlateHandler...");
|
||||
|
||||
_pluginInterface.UiBuilder.Draw += OnDraw;
|
||||
|
||||
_logger.LogInformation("LightFinderPlateHandler started.");
|
||||
return Task.CompletedTask;
|
||||
_uiBuilder.Draw -= OnUiBuilderDraw;
|
||||
_drawSubscribed = false;
|
||||
}
|
||||
ClearLabelBuffer();
|
||||
_mediator.Unsubscribe<PriorityFrameworkUpdateMessage>(this);
|
||||
_mpNameplateAddon = null;
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
internal void EnableNameplate()
|
||||
{
|
||||
if (!_mEnabled)
|
||||
{
|
||||
_logger.LogInformation("Stopping LightFinderPlateHandler...");
|
||||
|
||||
_pluginInterface.UiBuilder.Draw -= OnDraw;
|
||||
|
||||
_logger.LogInformation("LightFinderPlateHandler stopped.");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private unsafe void OnDraw()
|
||||
{
|
||||
if (!_configService.Current.BroadcastEnabled)
|
||||
return;
|
||||
|
||||
if (_activeBroadcastingCids.Count == 0)
|
||||
return;
|
||||
|
||||
var drawList = ImGui.GetForegroundDrawList();
|
||||
|
||||
foreach (var obj in _gameObjects.PlayerObjects.OfType<IPlayerCharacter>())
|
||||
try
|
||||
{
|
||||
//Double check to be sure, should always be true due to OfType filter above
|
||||
if (obj is not IPlayerCharacter player)
|
||||
continue;
|
||||
|
||||
if (player.Address == IntPtr.Zero)
|
||||
continue;
|
||||
|
||||
var hashedCID = DalamudUtilService.GetHashedCIDFromPlayerPointer(player.Address);
|
||||
if (!_activeBroadcastingCids.Contains(hashedCID))
|
||||
continue;
|
||||
|
||||
//Approximate check if nameplate should be visible (at short distances)
|
||||
if (!ShouldApproximateNameplateVisible(player))
|
||||
continue;
|
||||
|
||||
if (!TryGetApproxNameplateScreenPos(player, out var rawScreenPos))
|
||||
continue;
|
||||
|
||||
var rawVector3 = new Vector3(rawScreenPos.X, rawScreenPos.Y, 0f);
|
||||
|
||||
if (rawVector3 == Vector3.Zero)
|
||||
{
|
||||
_smoothed.Remove(obj);
|
||||
continue;
|
||||
}
|
||||
|
||||
//Possible have to rework this. Currently just a simple distance check to avoid jitter.
|
||||
Vector3 smoothedVector3;
|
||||
|
||||
if (_smoothed.TryGetValue(obj, out var lastVector3))
|
||||
{
|
||||
var deltaVector2 = new Vector2(rawVector3.X - lastVector3.X, rawVector3.Y - lastVector3.Y);
|
||||
if (deltaVector2.Length() < 1f)
|
||||
smoothedVector3 = lastVector3;
|
||||
else
|
||||
smoothedVector3 = rawVector3;
|
||||
}
|
||||
else
|
||||
{
|
||||
smoothedVector3 = rawVector3;
|
||||
}
|
||||
|
||||
_smoothed[obj] = smoothedVector3;
|
||||
|
||||
var screenPos = new Vector2(smoothedVector3.X, smoothedVector3.Y);
|
||||
|
||||
var radiusWorld = Math.Max(player.HitboxRadius, 0.5f);
|
||||
var radiusPx = radiusWorld * 8.0f;
|
||||
var offsetPx = GetScreenOffset(player);
|
||||
var drawPos = new Vector2(screenPos.X, screenPos.Y - offsetPx);
|
||||
|
||||
var fillColor = ImGui.GetColorU32(UiSharedService.Color(UIColors.Get("Lightfinder")));
|
||||
var outlineColor = ImGui.GetColorU32(UiSharedService.Color(UIColors.Get("LightfinderEdge")));
|
||||
|
||||
drawList.AddCircleFilled(drawPos, radiusPx, fillColor);
|
||||
drawList.AddCircle(drawPos, radiusPx, outlineColor, 0, 2.0f);
|
||||
|
||||
var label = "LightFinder";
|
||||
var icon = FontAwesomeIcon.Bullseye.ToIconString();
|
||||
|
||||
ImGui.PushFont(UiBuilder.IconFont);
|
||||
var iconSize = ImGui.CalcTextSize(icon);
|
||||
var iconPos = new Vector2(drawPos.X - iconSize.X / 2f, drawPos.Y - radiusPx - iconSize.Y - 2f);
|
||||
drawList.AddText(iconPos, fillColor, icon);
|
||||
ImGui.PopFont();
|
||||
|
||||
/* var scale = 1.4f;
|
||||
var font = ImGui.GetFont();
|
||||
var baseFontSize = ImGui.GetFontSize();
|
||||
var fontSize = baseFontSize * scale;
|
||||
|
||||
var baseTextSize = ImGui.CalcTextSize(label);
|
||||
var textSize = baseTextSize * scale;
|
||||
|
||||
var textPos = new Vector2(
|
||||
drawPos.X - textSize.X / 2f,
|
||||
drawPos.Y - radiusPx - textSize.Y - 2f
|
||||
);
|
||||
|
||||
drawList.AddText(font, fontSize, textPos, fillColor, label); */
|
||||
_addonLifecycle.RegisterListener(AddonEvent.PostDraw, "NamePlate", NameplateDrawDetour);
|
||||
_mEnabled = true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Unknown error while trying to enable nameplate.");
|
||||
DisableNameplate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get screen offset based on distance to local player (to scale size appropriately)
|
||||
// I need to fine tune these values still
|
||||
private float GetScreenOffset(IPlayerCharacter player)
|
||||
internal void DisableNameplate()
|
||||
{
|
||||
if (_mEnabled)
|
||||
{
|
||||
var local = _gameObjects.LocalPlayer;
|
||||
if (local == null)
|
||||
return 32.1f;
|
||||
try
|
||||
{
|
||||
_addonLifecycle.UnregisterListener(NameplateDrawDetour);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Unknown error while unregistering nameplate listener.");
|
||||
}
|
||||
|
||||
var delta = player.Position - local.Position;
|
||||
var dist = MathF.Sqrt(delta.X * delta.X + delta.Z * delta.Z);
|
||||
_mEnabled = false;
|
||||
ClearNameplateCaches();
|
||||
}
|
||||
}
|
||||
|
||||
const float minDist = 2.1f;
|
||||
const float maxDist = 30.4f;
|
||||
dist = Math.Clamp(dist, minDist, maxDist);
|
||||
|
||||
var t = 1f - (dist - minDist) / (maxDist - minDist);
|
||||
|
||||
const float minOffset = 24.4f;
|
||||
const float maxOffset = 56.4f;
|
||||
return minOffset + (maxOffset - minOffset) * t;
|
||||
private void NameplateDrawDetour(AddonEvent type, AddonArgs args)
|
||||
{
|
||||
if (args.Addon.Address == nint.Zero)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Warning))
|
||||
_logger.LogWarning("Nameplate draw detour received a null addon address, skipping update.");
|
||||
return;
|
||||
}
|
||||
|
||||
private bool TryGetApproxNameplateScreenPos(IPlayerCharacter player, out Vector2 screenPos)
|
||||
var pNameplateAddon = (AddonNamePlate*)args.Addon.Address;
|
||||
|
||||
if (_mpNameplateAddon != pNameplateAddon)
|
||||
{
|
||||
screenPos = default;
|
||||
ClearNameplateCaches();
|
||||
_mpNameplateAddon = pNameplateAddon;
|
||||
}
|
||||
|
||||
var worldPos = player.Position;
|
||||
UpdateNameplateNodes();
|
||||
}
|
||||
|
||||
var visualHeight = GetVisualHeight(player);
|
||||
private void UpdateNameplateNodes()
|
||||
{
|
||||
var currentHandle = _gameGui.GetAddonByName("NamePlate");
|
||||
if (currentHandle.Address == nint.Zero)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
_logger.LogDebug("NamePlate addon unavailable during update, skipping label refresh.");
|
||||
ClearLabelBuffer();
|
||||
return;
|
||||
}
|
||||
|
||||
worldPos.Y += (visualHeight + 1.2f) + _defaultHeightOffset;
|
||||
var currentAddon = (AddonNamePlate*)currentHandle.Address;
|
||||
if (_mpNameplateAddon == null || currentAddon == null || currentAddon != _mpNameplateAddon)
|
||||
{
|
||||
if (_mpNameplateAddon != null && _logger.IsEnabled(LogLevel.Debug))
|
||||
_logger.LogDebug("Cached NamePlate addon pointer differs from current: waiting for new hook (cached {Cached}, current {Current}).", (IntPtr)_mpNameplateAddon, (IntPtr)currentAddon);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_gameGui.WorldToScreen(worldPos, out var raw))
|
||||
return false;
|
||||
var framework = Framework.Instance();
|
||||
if (framework == null)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
_logger.LogDebug("Framework instance unavailable during nameplate update, skipping.");
|
||||
return;
|
||||
}
|
||||
|
||||
screenPos = raw;
|
||||
var uiModule = framework->GetUIModule();
|
||||
if (uiModule == null)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
_logger.LogDebug("UI module unavailable during nameplate update, skipping.");
|
||||
return;
|
||||
}
|
||||
|
||||
var ui3DModule = uiModule->GetUI3DModule();
|
||||
if (ui3DModule == null)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
_logger.LogDebug("UI3D module unavailable during nameplate update, skipping.");
|
||||
return;
|
||||
}
|
||||
|
||||
var vec = ui3DModule->NamePlateObjectInfoPointers;
|
||||
if (vec.IsEmpty)
|
||||
{
|
||||
ClearLabelBuffer();
|
||||
return;
|
||||
}
|
||||
|
||||
var visibleUserIdsSnapshot = VisibleUserIds;
|
||||
var safeCount = System.Math.Min(ui3DModule->NamePlateObjectInfoCount, vec.Length);
|
||||
var currentConfig = _configService.Current;
|
||||
var labelColor = UIColors.Get("Lightfinder");
|
||||
var edgeColor = UIColors.Get("LightfinderEdge");
|
||||
var scratchCount = 0;
|
||||
|
||||
for (int i = 0; i < safeCount; ++i)
|
||||
{
|
||||
var objectInfoPtr = vec[i];
|
||||
if (objectInfoPtr == null)
|
||||
continue;
|
||||
|
||||
var objectInfo = objectInfoPtr.Value;
|
||||
if (objectInfo == null || objectInfo->GameObject == null)
|
||||
continue;
|
||||
|
||||
var nameplateIndex = objectInfo->NamePlateIndex;
|
||||
if (nameplateIndex < 0 || nameplateIndex >= AddonNamePlate.NumNamePlateObjects)
|
||||
continue;
|
||||
|
||||
var gameObject = objectInfo->GameObject;
|
||||
if ((ObjectKind)gameObject->ObjectKind != ObjectKind.Player)
|
||||
continue;
|
||||
|
||||
// CID gating
|
||||
var cid = DalamudUtilService.GetHashedCIDFromPlayerPointer((nint)gameObject);
|
||||
if (cid == null || !_activeBroadcastingCids.Contains(cid))
|
||||
continue;
|
||||
|
||||
var local = _objectTable.LocalPlayer;
|
||||
if (!currentConfig.LightfinderLabelShowOwn && local != null &&
|
||||
objectInfo->GameObject->GetGameObjectId() == local.GameObjectId)
|
||||
continue;
|
||||
|
||||
var hidePaired = !currentConfig.LightfinderLabelShowPaired;
|
||||
var goId = gameObject->GetGameObjectId();
|
||||
if (hidePaired && visibleUserIdsSnapshot.Contains(goId))
|
||||
continue;
|
||||
|
||||
var nameplateObject = _mpNameplateAddon->NamePlateObjectArray[nameplateIndex];
|
||||
var root = nameplateObject.RootComponentNode;
|
||||
var nameContainer = nameplateObject.NameContainer;
|
||||
var nameText = nameplateObject.NameText;
|
||||
var marker = nameplateObject.MarkerIcon;
|
||||
|
||||
if (root == null || root->Component == null || nameContainer == null || nameText == null)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
_logger.LogDebug("Nameplate {Index} missing required nodes during update, skipping.", nameplateIndex);
|
||||
continue;
|
||||
}
|
||||
|
||||
root->Component->UldManager.UpdateDrawNodeList();
|
||||
|
||||
bool isVisible =
|
||||
(marker != null && marker->AtkResNode.IsVisible()) ||
|
||||
(nameContainer->IsVisible() && nameText->AtkResNode.IsVisible()) ||
|
||||
currentConfig.LightfinderLabelShowHidden;
|
||||
|
||||
if (!isVisible)
|
||||
continue;
|
||||
|
||||
var scaleMultiplier = System.Math.Clamp(currentConfig.LightfinderLabelScale, 0.5f, 2.0f);
|
||||
var baseScale = currentConfig.LightfinderLabelUseIcon ? 1.0f : 0.5f;
|
||||
var effectiveScale = baseScale * scaleMultiplier;
|
||||
var baseFontSize = currentConfig.LightfinderLabelUseIcon ? 36f : 24f;
|
||||
var targetFontSize = (int)System.Math.Round(baseFontSize * scaleMultiplier);
|
||||
var labelContent = currentConfig.LightfinderLabelUseIcon
|
||||
? NormalizeIconGlyph(currentConfig.LightfinderLabelIconGlyph)
|
||||
: DefaultLabelText;
|
||||
|
||||
if (!currentConfig.LightfinderLabelUseIcon && (string.IsNullOrWhiteSpace(labelContent) || string.Equals(labelContent, "-", StringComparison.Ordinal)))
|
||||
labelContent = DefaultLabelText;
|
||||
|
||||
var nodeWidth = (int)System.Math.Round(AtkNodeHelpers.DefaultTextNodeWidth * effectiveScale);
|
||||
var nodeHeight = (int)System.Math.Round(AtkNodeHelpers.DefaultTextNodeHeight * effectiveScale);
|
||||
AlignmentType alignment;
|
||||
|
||||
var textScaleY = nameText->AtkResNode.ScaleY;
|
||||
if (textScaleY <= 0f)
|
||||
textScaleY = 1f;
|
||||
|
||||
var blockHeight = ResolveCache(
|
||||
_buffers.TextHeights,
|
||||
nameplateIndex,
|
||||
System.Math.Abs((int)nameplateObject.TextH),
|
||||
() => GetScaledTextHeight(nameText),
|
||||
nodeHeight);
|
||||
|
||||
var containerHeight = ResolveCache(
|
||||
_buffers.ContainerHeights,
|
||||
nameplateIndex,
|
||||
(int)nameContainer->Height,
|
||||
() =>
|
||||
{
|
||||
var computed = blockHeight + (int)System.Math.Round(8 * textScaleY);
|
||||
return computed <= blockHeight ? blockHeight + 1 : computed;
|
||||
},
|
||||
blockHeight + 1);
|
||||
|
||||
var blockTop = containerHeight - blockHeight;
|
||||
if (blockTop < 0)
|
||||
blockTop = 0;
|
||||
var verticalPadding = (int)System.Math.Round(4 * effectiveScale);
|
||||
|
||||
var positionY = blockTop - verticalPadding;
|
||||
|
||||
var rawTextWidth = (int)nameplateObject.TextW;
|
||||
var textWidth = ResolveCache(
|
||||
_buffers.TextWidths,
|
||||
nameplateIndex,
|
||||
System.Math.Abs(rawTextWidth),
|
||||
() => GetScaledTextWidth(nameText),
|
||||
nodeWidth);
|
||||
|
||||
var textOffset = (int)System.Math.Round(nameText->AtkResNode.X);
|
||||
var hasValidOffset = TryCacheTextOffset(nameplateIndex, rawTextWidth, textOffset);
|
||||
|
||||
if (nameContainer == null)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
_logger.LogDebug("Nameplate {Index} container became unavailable during update, skipping.", nameplateIndex);
|
||||
continue;
|
||||
}
|
||||
|
||||
float finalX;
|
||||
if (currentConfig.LightfinderAutoAlign)
|
||||
{
|
||||
var measuredWidth = System.Math.Max(1, textWidth > 0 ? textWidth : nodeWidth);
|
||||
var measuredWidthF = (float)measuredWidth;
|
||||
var alignmentType = currentConfig.LabelAlignment;
|
||||
|
||||
var containerScale = nameContainer->ScaleX;
|
||||
if (containerScale <= 0f)
|
||||
containerScale = 1f;
|
||||
var containerWidthRaw = (float)nameContainer->Width;
|
||||
if (containerWidthRaw <= 0f)
|
||||
containerWidthRaw = measuredWidthF;
|
||||
var containerWidth = containerWidthRaw * containerScale;
|
||||
if (containerWidth <= 0f)
|
||||
containerWidth = measuredWidthF;
|
||||
|
||||
var containerLeft = nameContainer->ScreenX;
|
||||
var containerRight = containerLeft + containerWidth;
|
||||
var containerCenter = containerLeft + (containerWidth * 0.5f);
|
||||
|
||||
var iconMargin = currentConfig.LightfinderLabelUseIcon
|
||||
? System.Math.Min(containerWidth * 0.1f, 14f * containerScale)
|
||||
: 0f;
|
||||
|
||||
switch (alignmentType)
|
||||
{
|
||||
case LabelAlignment.Left:
|
||||
finalX = containerLeft + iconMargin;
|
||||
alignment = AlignmentType.BottomLeft;
|
||||
break;
|
||||
case LabelAlignment.Right:
|
||||
finalX = containerRight - iconMargin;
|
||||
alignment = AlignmentType.BottomRight;
|
||||
break;
|
||||
default:
|
||||
finalX = containerCenter;
|
||||
alignment = AlignmentType.Bottom;
|
||||
break;
|
||||
}
|
||||
|
||||
finalX += currentConfig.LightfinderLabelOffsetX;
|
||||
}
|
||||
else
|
||||
{
|
||||
var cachedTextOffset = _buffers.TextOffsets[nameplateIndex];
|
||||
var hasCachedOffset = cachedTextOffset != int.MinValue;
|
||||
var baseOffsetX = (!currentConfig.LightfinderLabelUseIcon && hasValidOffset && hasCachedOffset) ? cachedTextOffset : 0;
|
||||
finalX = nameContainer->ScreenX + baseOffsetX + 58 + currentConfig.LightfinderLabelOffsetX;
|
||||
alignment = AlignmentType.Bottom;
|
||||
}
|
||||
|
||||
positionY += currentConfig.LightfinderLabelOffsetY;
|
||||
alignment = (AlignmentType)System.Math.Clamp((int)alignment, 0, 8);
|
||||
|
||||
var finalPosition = new Vector2(finalX, nameContainer->ScreenY + positionY);
|
||||
var pivot = (currentConfig.LightfinderAutoAlign || currentConfig.LightfinderLabelUseIcon)
|
||||
? AlignmentToPivot(alignment)
|
||||
: DefaultPivot;
|
||||
var textColorPacked = PackColor(labelColor);
|
||||
var edgeColorPacked = PackColor(edgeColor);
|
||||
|
||||
_buffers.LabelScratch[scratchCount++] = new NameplateLabelInfo(
|
||||
finalPosition,
|
||||
labelContent,
|
||||
textColorPacked,
|
||||
edgeColorPacked,
|
||||
targetFontSize,
|
||||
pivot,
|
||||
currentConfig.LightfinderLabelUseIcon);
|
||||
}
|
||||
|
||||
lock (_labelLock)
|
||||
{
|
||||
if (scratchCount == 0)
|
||||
{
|
||||
_labelRenderCount = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
Array.Copy(_buffers.LabelScratch, _buffers.LabelRender, scratchCount);
|
||||
_labelRenderCount = scratchCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnUiBuilderDraw()
|
||||
{
|
||||
if (!_mEnabled)
|
||||
return;
|
||||
|
||||
int copyCount;
|
||||
lock (_labelLock)
|
||||
{
|
||||
copyCount = _labelRenderCount;
|
||||
if (copyCount == 0)
|
||||
return;
|
||||
|
||||
Array.Copy(_buffers.LabelRender, _buffers.LabelCopy, copyCount);
|
||||
}
|
||||
|
||||
using var drawList = PictoService.Draw();
|
||||
if (drawList == null)
|
||||
return;
|
||||
|
||||
for (int i = 0; i < copyCount; ++i)
|
||||
{
|
||||
ref var info = ref _buffers.LabelCopy[i];
|
||||
var font = default(ImFontPtr);
|
||||
if (info.UseIcon)
|
||||
{
|
||||
var ioFonts = ImGui.GetIO().Fonts;
|
||||
font = ioFonts.Fonts.Size > 1 ? new ImFontPtr(ioFonts.Fonts[1]) : ImGui.GetFont();
|
||||
}
|
||||
|
||||
drawList.AddScreenText(info.ScreenPosition, info.Text, info.TextColor, info.FontSize, info.Pivot, info.EdgeColor, font);
|
||||
}
|
||||
}
|
||||
|
||||
private static Vector2 AlignmentToPivot(AlignmentType alignment) => alignment switch
|
||||
{
|
||||
AlignmentType.BottomLeft => new Vector2(0f, 1f),
|
||||
AlignmentType.BottomRight => new Vector2(1f, 1f),
|
||||
AlignmentType.TopLeft => new Vector2(0f, 0f),
|
||||
AlignmentType.TopRight => new Vector2(1f, 0f),
|
||||
AlignmentType.Top => new Vector2(0.5f, 0f),
|
||||
AlignmentType.Left => new Vector2(0f, 0.5f),
|
||||
AlignmentType.Right => new Vector2(1f, 0.5f),
|
||||
_ => DefaultPivot
|
||||
};
|
||||
|
||||
private static uint PackColor(Vector4 color)
|
||||
{
|
||||
var r = (byte)System.Math.Clamp(color.X * 255f, 0f, 255f);
|
||||
var g = (byte)System.Math.Clamp(color.Y * 255f, 0f, 255f);
|
||||
var b = (byte)System.Math.Clamp(color.Z * 255f, 0f, 255f);
|
||||
var a = (byte)System.Math.Clamp(color.W * 255f, 0f, 255f);
|
||||
return (uint)((a << 24) | (b << 16) | (g << 8) | r);
|
||||
}
|
||||
|
||||
private void ClearLabelBuffer()
|
||||
{
|
||||
lock (_labelLock)
|
||||
{
|
||||
_labelRenderCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static unsafe int GetScaledTextHeight(AtkTextNode* node)
|
||||
{
|
||||
if (node == null)
|
||||
return 0;
|
||||
|
||||
var resNode = &node->AtkResNode;
|
||||
var rawHeight = (int)resNode->GetHeight();
|
||||
if (rawHeight <= 0 && node->LineSpacing > 0)
|
||||
rawHeight = node->LineSpacing;
|
||||
if (rawHeight <= 0)
|
||||
rawHeight = AtkNodeHelpers.DefaultTextNodeHeight;
|
||||
|
||||
var scale = resNode->ScaleY;
|
||||
if (scale <= 0f)
|
||||
scale = 1f;
|
||||
|
||||
var computed = (int)System.Math.Round(rawHeight * scale);
|
||||
return System.Math.Max(1, computed);
|
||||
}
|
||||
|
||||
private static unsafe int GetScaledTextWidth(AtkTextNode* node)
|
||||
{
|
||||
if (node == null)
|
||||
return 0;
|
||||
|
||||
var resNode = &node->AtkResNode;
|
||||
var rawWidth = (int)resNode->GetWidth();
|
||||
if (rawWidth <= 0)
|
||||
rawWidth = AtkNodeHelpers.DefaultTextNodeWidth;
|
||||
|
||||
var scale = resNode->ScaleX;
|
||||
if (scale <= 0f)
|
||||
scale = 1f;
|
||||
|
||||
var computed = (int)System.Math.Round(rawWidth * scale);
|
||||
return System.Math.Max(1, computed);
|
||||
}
|
||||
|
||||
private static int ResolveCache(
|
||||
int[] cache,
|
||||
int index,
|
||||
int rawValue,
|
||||
Func<int> fallback,
|
||||
int fallbackWhenZero)
|
||||
{
|
||||
if (rawValue > 0)
|
||||
{
|
||||
cache[index] = rawValue;
|
||||
return rawValue;
|
||||
}
|
||||
|
||||
var cachedValue = cache[index];
|
||||
if (cachedValue > 0)
|
||||
return cachedValue;
|
||||
|
||||
var computed = fallback();
|
||||
if (computed <= 0)
|
||||
computed = fallbackWhenZero;
|
||||
|
||||
cache[index] = computed;
|
||||
return computed;
|
||||
}
|
||||
|
||||
private bool TryCacheTextOffset(int nameplateIndex, int measuredTextWidth, int textOffset)
|
||||
{
|
||||
if (System.Math.Abs(measuredTextWidth) > 0 || textOffset != 0)
|
||||
{
|
||||
_buffers.TextOffsets[nameplateIndex] = textOffset;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Approximate check to see if nameplate would be visible based on distance and screen position
|
||||
// Also has to be fine tuned still
|
||||
private bool ShouldApproximateNameplateVisible(IPlayerCharacter player)
|
||||
return false;
|
||||
}
|
||||
|
||||
internal static string NormalizeIconGlyph(string? rawInput)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(rawInput))
|
||||
return DefaultIconGlyph;
|
||||
|
||||
var trimmed = rawInput.Trim();
|
||||
|
||||
if (Enum.TryParse<SeIconChar>(trimmed, true, out var iconEnum))
|
||||
return SeIconCharExtensions.ToIconString(iconEnum);
|
||||
|
||||
var hexCandidate = trimmed.StartsWith("0x", StringComparison.OrdinalIgnoreCase)
|
||||
? trimmed[2..]
|
||||
: trimmed;
|
||||
|
||||
if (ushort.TryParse(hexCandidate, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var hexValue))
|
||||
return char.ConvertFromUtf32(hexValue);
|
||||
|
||||
var enumerator = trimmed.EnumerateRunes();
|
||||
if (enumerator.MoveNext())
|
||||
return enumerator.Current.ToString();
|
||||
|
||||
return DefaultIconGlyph;
|
||||
}
|
||||
|
||||
internal static string ToIconEditorString(string? rawInput)
|
||||
{
|
||||
var normalized = NormalizeIconGlyph(rawInput);
|
||||
var runeEnumerator = normalized.EnumerateRunes();
|
||||
return runeEnumerator.MoveNext()
|
||||
? runeEnumerator.Current.Value.ToString("X4", CultureInfo.InvariantCulture)
|
||||
: DefaultIconGlyph;
|
||||
}
|
||||
private readonly struct NameplateLabelInfo
|
||||
{
|
||||
public NameplateLabelInfo(
|
||||
Vector2 screenPosition,
|
||||
string text,
|
||||
uint textColor,
|
||||
uint edgeColor,
|
||||
float fontSize,
|
||||
Vector2 pivot,
|
||||
bool useIcon)
|
||||
{
|
||||
var local = _gameObjects.LocalPlayer;
|
||||
if (local == null)
|
||||
return false;
|
||||
|
||||
var delta = player.Position - local.Position;
|
||||
var distance2D = MathF.Sqrt(delta.X * delta.X + delta.Z * delta.Z);
|
||||
if (distance2D > _defaultNameplateDistance)
|
||||
return false;
|
||||
|
||||
var verticalDelta = MathF.Abs(delta.Y);
|
||||
if (verticalDelta > 3.4f)
|
||||
return false;
|
||||
|
||||
return TryGetApproxNameplateScreenPos(player, out _);
|
||||
ScreenPosition = screenPosition;
|
||||
Text = text;
|
||||
TextColor = textColor;
|
||||
EdgeColor = edgeColor;
|
||||
FontSize = fontSize;
|
||||
Pivot = pivot;
|
||||
UseIcon = useIcon;
|
||||
}
|
||||
|
||||
private static unsafe float GetVisualHeight(IPlayerCharacter player)
|
||||
public Vector2 ScreenPosition { get; }
|
||||
public string Text { get; }
|
||||
public uint TextColor { get; }
|
||||
public uint EdgeColor { get; }
|
||||
public float FontSize { get; }
|
||||
public Vector2 Pivot { get; }
|
||||
public bool UseIcon { get; }
|
||||
}
|
||||
|
||||
private HashSet<ulong> VisibleUserIds
|
||||
=> [.. _pairUiService.GetSnapshot().PairsByUid.Values
|
||||
.Where(u => u.IsVisible && u.PlayerCharacterId != uint.MaxValue)
|
||||
.Select(u => (ulong)u.PlayerCharacterId)];
|
||||
|
||||
public void FlagRefresh()
|
||||
{
|
||||
_needsLabelRefresh = true;
|
||||
}
|
||||
|
||||
public void OnTick(PriorityFrameworkUpdateMessage _)
|
||||
{
|
||||
if (_needsLabelRefresh)
|
||||
{
|
||||
var gameObject = (GameObject*)player.Address;
|
||||
if (gameObject == null)
|
||||
return Math.Max(player.HitboxRadius * 2.0f, 1.7f); // fallback
|
||||
|
||||
// This should account for transformations (sitting, crouching, etc.)
|
||||
var radius = gameObject->GetRadius(adjustByTransformation: true);
|
||||
if (radius <= 0)
|
||||
radius = Math.Max(player.HitboxRadius * 2.0f, 1.7f);
|
||||
|
||||
return radius;
|
||||
}
|
||||
|
||||
// Update the set of active broadcasting CIDs (Same uses as in NameplateHnadler before)
|
||||
public void UpdateBroadcastingCids(IEnumerable<string> cids)
|
||||
{
|
||||
var newSet = cids.ToImmutableHashSet(StringComparer.Ordinal);
|
||||
if (ReferenceEquals(_activeBroadcastingCids, newSet) || _activeBroadcastingCids.SetEquals(newSet))
|
||||
return;
|
||||
|
||||
_activeBroadcastingCids = newSet;
|
||||
if (_logger.IsEnabled(LogLevel.Information))
|
||||
_logger.LogInformation("Active broadcast CIDs: {Cids}", string.Join(',', _activeBroadcastingCids));
|
||||
UpdateNameplateNodes();
|
||||
_needsLabelRefresh = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateBroadcastingCids(IEnumerable<string> cids)
|
||||
{
|
||||
var newSet = cids.ToImmutableHashSet(StringComparer.Ordinal);
|
||||
if (ReferenceEquals(_activeBroadcastingCids, newSet) || _activeBroadcastingCids.SetEquals(newSet))
|
||||
return;
|
||||
|
||||
_activeBroadcastingCids = newSet;
|
||||
if (_logger.IsEnabled(LogLevel.Information))
|
||||
_logger.LogInformation("Active broadcast CIDs: {Cids}", string.Join(',', _activeBroadcastingCids));
|
||||
FlagRefresh();
|
||||
}
|
||||
|
||||
public void ClearNameplateCaches()
|
||||
{
|
||||
_buffers.Clear();
|
||||
ClearLabelBuffer();
|
||||
}
|
||||
|
||||
private sealed class NameplateBuffers
|
||||
{
|
||||
public NameplateBuffers()
|
||||
{
|
||||
TextOffsets = new int[AddonNamePlate.NumNamePlateObjects];
|
||||
System.Array.Fill(TextOffsets, int.MinValue);
|
||||
}
|
||||
|
||||
public int[] TextWidths { get; } = new int[AddonNamePlate.NumNamePlateObjects];
|
||||
public int[] TextHeights { get; } = new int[AddonNamePlate.NumNamePlateObjects];
|
||||
public int[] ContainerHeights { get; } = new int[AddonNamePlate.NumNamePlateObjects];
|
||||
public int[] TextOffsets { get; }
|
||||
public NameplateLabelInfo[] LabelScratch { get; } = new NameplateLabelInfo[AddonNamePlate.NumNamePlateObjects];
|
||||
public NameplateLabelInfo[] LabelRender { get; } = new NameplateLabelInfo[AddonNamePlate.NumNamePlateObjects];
|
||||
public NameplateLabelInfo[] LabelCopy { get; } = new NameplateLabelInfo[AddonNamePlate.NumNamePlateObjects];
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
System.Array.Clear(TextWidths, 0, TextWidths.Length);
|
||||
System.Array.Clear(TextHeights, 0, TextHeights.Length);
|
||||
System.Array.Clear(ContainerHeights, 0, ContainerHeights.Length);
|
||||
System.Array.Fill(TextOffsets, int.MinValue);
|
||||
}
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
Init();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
Uninit();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
}
|
||||
82
LightlessSync/Services/Rendering/PctDrawListExtensions.cs
Normal file
82
LightlessSync/Services/Rendering/PctDrawListExtensions.cs
Normal file
@@ -0,0 +1,82 @@
|
||||
using System.Numerics;
|
||||
using System.Reflection;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Pictomancy;
|
||||
|
||||
namespace LightlessSync.Services.Rendering;
|
||||
|
||||
internal static class PctDrawListExtensions
|
||||
{
|
||||
private static readonly FieldInfo? DrawListField = typeof(PctDrawList).GetField("_drawList", BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
|
||||
private static bool TryGetImDrawList(PctDrawList drawList, out ImDrawListPtr ptr)
|
||||
{
|
||||
ptr = default;
|
||||
if (DrawListField == null)
|
||||
return false;
|
||||
|
||||
if (DrawListField.GetValue(drawList) is ImDrawListPtr list)
|
||||
{
|
||||
ptr = list;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static void AddScreenText(this PctDrawList drawList, Vector2 screenPosition, string text, uint color, float fontSize, Vector2? pivot = null, uint? outlineColor = null, ImFontPtr fontOverride = default)
|
||||
{
|
||||
if (drawList == null || string.IsNullOrEmpty(text))
|
||||
return;
|
||||
|
||||
if (!TryGetImDrawList(drawList, out var imDrawList))
|
||||
return;
|
||||
|
||||
var font = fontOverride.IsNull ? ImGui.GetFont() : fontOverride;
|
||||
if (font.IsNull)
|
||||
return;
|
||||
|
||||
var size = MathF.Max(1f, fontSize);
|
||||
var pivotValue = pivot ?? new Vector2(0.5f, 0.5f);
|
||||
|
||||
Vector2 measured;
|
||||
float calcFontSize;
|
||||
if (!fontOverride.IsNull)
|
||||
{
|
||||
ImGui.PushFont(font);
|
||||
measured = ImGui.CalcTextSize(text);
|
||||
calcFontSize = ImGui.GetFontSize();
|
||||
ImGui.PopFont();
|
||||
}
|
||||
else
|
||||
{
|
||||
measured = ImGui.CalcTextSize(text);
|
||||
calcFontSize = ImGui.GetFontSize();
|
||||
}
|
||||
|
||||
if (calcFontSize > 0f && MathF.Abs(size - calcFontSize) > 0.001f)
|
||||
{
|
||||
measured *= size / calcFontSize;
|
||||
}
|
||||
|
||||
var drawPos = screenPosition - measured * pivotValue;
|
||||
if (outlineColor.HasValue)
|
||||
{
|
||||
var thickness = MathF.Max(1f, size / 24f);
|
||||
Span<Vector2> offsets = stackalloc Vector2[4]
|
||||
{
|
||||
new Vector2(1f, 0f),
|
||||
new Vector2(-1f, 0f),
|
||||
new Vector2(0f, 1f),
|
||||
new Vector2(0f, -1f)
|
||||
};
|
||||
|
||||
foreach (var offset in offsets)
|
||||
{
|
||||
imDrawList.AddText(font, size, drawPos + offset * thickness, outlineColor.Value, text);
|
||||
}
|
||||
}
|
||||
|
||||
imDrawList.AddText(font, size, drawPos, color, text);
|
||||
}
|
||||
}
|
||||
47
LightlessSync/Services/Rendering/PictomancyService.cs
Normal file
47
LightlessSync/Services/Rendering/PictomancyService.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
using Dalamud.Plugin;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Pictomancy;
|
||||
|
||||
namespace LightlessSync.Services.Rendering;
|
||||
|
||||
public sealed class PictomancyService : IDisposable
|
||||
{
|
||||
private readonly ILogger<PictomancyService> _logger;
|
||||
private bool _initialized;
|
||||
|
||||
public PictomancyService(ILogger<PictomancyService> logger, IDalamudPluginInterface pluginInterface)
|
||||
{
|
||||
_logger = logger;
|
||||
|
||||
try
|
||||
{
|
||||
PictoService.Initialize(pluginInterface);
|
||||
_initialized = true;
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
_logger.LogDebug("Pictomancy initialized");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to initialize Pictomancy");
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (!_initialized)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
PictoService.Dispose();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to dispose Pictomancy");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_initialized = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Buffers;
|
||||
using System.Buffers.Binary;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using OtterTex;
|
||||
using OtterImage = OtterTex.Image;
|
||||
using LightlessSync.LightlessConfiguration;
|
||||
@@ -11,7 +12,6 @@ using Microsoft.Extensions.Logging;
|
||||
using Lumina.Data.Files;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
|
||||
/*
|
||||
* OtterTex made by Ottermandias
|
||||
@@ -33,6 +33,7 @@ public sealed class TextureDownscaleService
|
||||
|
||||
private readonly ConcurrentDictionary<string, Task> _activeJobs = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly ConcurrentDictionary<string, string> _downscaledPaths = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly SemaphoreSlim _downscaleSemaphore = new(4);
|
||||
private static readonly IReadOnlyDictionary<int, TextureCompressionTarget> BlockCompressedFormatMap =
|
||||
new Dictionary<int, TextureCompressionTarget>
|
||||
{
|
||||
@@ -80,7 +81,10 @@ public sealed class TextureDownscaleService
|
||||
if (!filePath.EndsWith(".tex", StringComparison.OrdinalIgnoreCase)) return;
|
||||
if (_activeJobs.ContainsKey(hash)) return;
|
||||
|
||||
_activeJobs[hash] = Task.Run(() => DownscaleInternalAsync(hash, filePath, mapKind), CancellationToken.None);
|
||||
_activeJobs[hash] = Task.Run(async () =>
|
||||
{
|
||||
await DownscaleInternalAsync(hash, filePath, mapKind).ConfigureAwait(false);
|
||||
}, CancellationToken.None);
|
||||
}
|
||||
|
||||
public string GetPreferredPath(string hash, string originalPath)
|
||||
@@ -108,6 +112,7 @@ public sealed class TextureDownscaleService
|
||||
bool onlyDownscaleUncompressed = false;
|
||||
bool? isIndexTexture = null;
|
||||
|
||||
await _downscaleSemaphore.WaitAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
if (!File.Exists(sourcePath))
|
||||
@@ -157,6 +162,15 @@ public sealed class TextureDownscaleService
|
||||
return;
|
||||
}
|
||||
|
||||
if (headerInfo is { } headerValue &&
|
||||
headerValue.Width <= targetMaxDimension &&
|
||||
headerValue.Height <= targetMaxDimension)
|
||||
{
|
||||
_downscaledPaths[hash] = sourcePath;
|
||||
_logger.LogTrace("Skipping downscale for index texture {Hash}; header dimensions {Width}x{Height} within target.", hash, headerValue.Width, headerValue.Height);
|
||||
return;
|
||||
}
|
||||
|
||||
if (onlyDownscaleUncompressed && headerInfo.HasValue && IsBlockCompressedFormat(headerInfo.Value.Format))
|
||||
{
|
||||
_downscaledPaths[hash] = sourcePath;
|
||||
@@ -172,21 +186,20 @@ public sealed class TextureDownscaleService
|
||||
var height = rgbaInfo.Meta.Height;
|
||||
var requiredLength = width * height * bytesPerPixel;
|
||||
|
||||
var rgbaPixels = rgbaScratch.Pixels[..requiredLength].ToArray();
|
||||
var rgbaPixels = rgbaScratch.Pixels.Slice(0, requiredLength);
|
||||
using var originalImage = SixLabors.ImageSharp.Image.LoadPixelData<Rgba32>(rgbaPixels, width, height);
|
||||
|
||||
var targetSize = CalculateTargetSize(originalImage.Width, originalImage.Height, targetMaxDimension);
|
||||
if (targetSize.width == originalImage.Width && targetSize.height == originalImage.Height)
|
||||
{
|
||||
_downscaledPaths[hash] = sourcePath;
|
||||
_logger.LogTrace("Skipping downscale for index texture {Hash}; already within bounds.", hash);
|
||||
return;
|
||||
}
|
||||
|
||||
using var resized = IndexDownscaler.Downscale(originalImage, targetSize.width, targetSize.height, BlockMultiple);
|
||||
|
||||
var resizedPixels = new byte[targetSize.width * targetSize.height * 4];
|
||||
resized.CopyPixelDataTo(resizedPixels);
|
||||
|
||||
using var resizedScratch = ScratchImage.FromRGBA(resizedPixels, targetSize.width, targetSize.height, out var creationInfo).ThrowIfError(creationInfo);
|
||||
using var resizedScratch = CreateScratchImage(resized, targetSize.width, targetSize.height);
|
||||
using var finalScratch = resizedScratch.Convert(DXGIFormat.B8G8R8A8UNorm);
|
||||
|
||||
TexFileHelper.Save(destination, finalScratch);
|
||||
@@ -209,6 +222,7 @@ public sealed class TextureDownscaleService
|
||||
}
|
||||
finally
|
||||
{
|
||||
_downscaleSemaphore.Release();
|
||||
_activeJobs.TryRemove(hash, out _);
|
||||
}
|
||||
}
|
||||
@@ -227,6 +241,41 @@ public sealed class TextureDownscaleService
|
||||
return (resultWidth, resultHeight);
|
||||
}
|
||||
|
||||
private static ScratchImage CreateScratchImage(Image<Rgba32> image, int width, int height)
|
||||
{
|
||||
const int BytesPerPixel = 4;
|
||||
var requiredLength = width * height * BytesPerPixel;
|
||||
|
||||
static ScratchImage Create(ReadOnlySpan<byte> pixels, int width, int height)
|
||||
{
|
||||
var scratchResult = ScratchImage.FromRGBA(pixels, width, height, out var creationInfo);
|
||||
return scratchResult.ThrowIfError(creationInfo);
|
||||
}
|
||||
|
||||
if (image.DangerousTryGetSinglePixelMemory(out var pixelMemory))
|
||||
{
|
||||
var byteSpan = MemoryMarshal.AsBytes(pixelMemory.Span);
|
||||
if (byteSpan.Length < requiredLength)
|
||||
{
|
||||
throw new InvalidOperationException($"Image buffer shorter than expected ({byteSpan.Length} < {requiredLength}).");
|
||||
}
|
||||
|
||||
return Create(byteSpan.Slice(0, requiredLength), width, height);
|
||||
}
|
||||
|
||||
var rented = ArrayPool<byte>.Shared.Rent(requiredLength);
|
||||
try
|
||||
{
|
||||
var rentedSpan = rented.AsSpan(0, requiredLength);
|
||||
image.CopyPixelDataTo(rentedSpan);
|
||||
return Create(rentedSpan, width, height);
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(rented);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsIndexMap(TextureMapKind kind)
|
||||
=> kind is TextureMapKind.Mask
|
||||
or TextureMapKind.Index;
|
||||
@@ -420,21 +469,6 @@ public sealed class TextureDownscaleService
|
||||
private static int ReduceDimension(int value)
|
||||
=> value <= 1 ? 1 : Math.Max(1, value / 2);
|
||||
|
||||
private static Image<Rgba32> ReduceLinearTexture(Image<Rgba32> source, int targetWidth, int targetHeight)
|
||||
{
|
||||
var clone = source.Clone();
|
||||
|
||||
while (clone.Width > targetWidth || clone.Height > targetHeight)
|
||||
{
|
||||
var nextWidth = Math.Max(targetWidth, Math.Max(BlockMultiple, clone.Width / 2));
|
||||
var nextHeight = Math.Max(targetHeight, Math.Max(BlockMultiple, clone.Height / 2));
|
||||
clone.Mutate(ctx => ctx.Resize(nextWidth, nextHeight, KnownResamplers.Lanczos3));
|
||||
}
|
||||
|
||||
return clone;
|
||||
}
|
||||
|
||||
|
||||
private static bool ShouldTrim(in TexMeta meta, int targetMaxDimension)
|
||||
{
|
||||
var depth = meta.Dimension == TexDimension.Tex3D ? Math.Max(1, meta.Depth) : 1;
|
||||
@@ -443,12 +477,7 @@ public sealed class TextureDownscaleService
|
||||
|
||||
private static bool ShouldTrimDimensions(int width, int height, int depth, int targetMaxDimension)
|
||||
{
|
||||
if (width <= targetMaxDimension || height <= targetMaxDimension)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (depth > 1 && depth <= targetMaxDimension)
|
||||
if (width <= targetMaxDimension && height <= targetMaxDimension && depth <= targetMaxDimension)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user