hopefully it's fine now?
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
using System.Collections.Concurrent;
|
||||
using Dalamud.Game.ClientState.Conditions;
|
||||
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||
using Dalamud.Hooking;
|
||||
using Dalamud.Plugin.Services;
|
||||
@@ -31,13 +32,18 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
||||
private readonly IFramework _framework;
|
||||
private readonly IGameInteropProvider _interop;
|
||||
private readonly IObjectTable _objectTable;
|
||||
private readonly IClientState _clientState;
|
||||
private readonly ICondition _condition;
|
||||
private readonly LightlessMediator _mediator;
|
||||
|
||||
private readonly ConcurrentDictionary<nint, ActorDescriptor> _activePlayers = new();
|
||||
private readonly ConcurrentDictionary<nint, ActorDescriptor> _gposePlayers = new();
|
||||
private readonly ConcurrentDictionary<string, ActorDescriptor> _actorsByHash = new(StringComparer.Ordinal);
|
||||
private readonly ConcurrentDictionary<string, ConcurrentDictionary<nint, ActorDescriptor>> _actorsByName = new(StringComparer.Ordinal);
|
||||
private readonly ConcurrentDictionary<nint, byte> _pendingHashResolutions = new();
|
||||
private readonly OwnedObjectTracker _ownedTracker = new();
|
||||
private ActorSnapshot _snapshot = ActorSnapshot.Empty;
|
||||
private GposeSnapshot _gposeSnapshot = GposeSnapshot.Empty;
|
||||
|
||||
private Hook<Character.Delegates.OnInitialize>? _onInitializeHook;
|
||||
private Hook<Character.Delegates.Terminate>? _onTerminateHook;
|
||||
@@ -55,21 +61,29 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
||||
IGameInteropProvider interop,
|
||||
IObjectTable objectTable,
|
||||
IClientState clientState,
|
||||
ICondition condition,
|
||||
LightlessMediator mediator)
|
||||
{
|
||||
_logger = logger;
|
||||
_framework = framework;
|
||||
_interop = interop;
|
||||
_objectTable = objectTable;
|
||||
_clientState = clientState;
|
||||
_condition = condition;
|
||||
_mediator = mediator;
|
||||
}
|
||||
|
||||
private bool IsZoning => _condition[ConditionFlag.BetweenAreas] || _condition[ConditionFlag.BetweenAreas51];
|
||||
|
||||
private ActorSnapshot Snapshot => Volatile.Read(ref _snapshot);
|
||||
private GposeSnapshot CurrentGposeSnapshot => Volatile.Read(ref _gposeSnapshot);
|
||||
|
||||
public IReadOnlyList<nint> PlayerAddresses => Snapshot.PlayerAddresses;
|
||||
|
||||
public IEnumerable<ActorDescriptor> PlayerDescriptors => _activePlayers.Values;
|
||||
public IReadOnlyList<ActorDescriptor> PlayerCharacterDescriptors => Snapshot.PlayerDescriptors;
|
||||
public IEnumerable<ActorDescriptor> ObjectDescriptors => _activePlayers.Values;
|
||||
public IReadOnlyList<ActorDescriptor> PlayerDescriptors => Snapshot.PlayerDescriptors;
|
||||
public IReadOnlyList<ActorDescriptor> OwnedDescriptors => Snapshot.OwnedDescriptors;
|
||||
public IReadOnlyList<ActorDescriptor> GposeDescriptors => CurrentGposeSnapshot.GposeDescriptors;
|
||||
|
||||
public bool TryGetActorByHash(string hash, out ActorDescriptor descriptor) => _actorsByHash.TryGetValue(hash, out descriptor);
|
||||
public bool TryGetValidatedActorByHash(string hash, out ActorDescriptor descriptor)
|
||||
@@ -113,6 +127,7 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
||||
return false;
|
||||
}
|
||||
public bool HooksActive => _hooksActive;
|
||||
public bool HasPendingHashResolutions => !_pendingHashResolutions.IsEmpty;
|
||||
public IReadOnlyList<nint> RenderedPlayerAddresses => Snapshot.OwnedObjects.RenderedPlayers;
|
||||
public IReadOnlyList<nint> RenderedCompanionAddresses => Snapshot.OwnedObjects.RenderedCompanions;
|
||||
public IReadOnlyList<nint> OwnedObjectAddresses => Snapshot.OwnedObjects.OwnedAddresses;
|
||||
@@ -207,7 +222,7 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var isLoaded = await _framework.RunOnFrameworkThread(() => IsObjectFullyLoaded(address)).ConfigureAwait(false);
|
||||
if (isLoaded)
|
||||
if (!IsZoning && isLoaded)
|
||||
return;
|
||||
|
||||
await Task.Delay(100, cancellationToken).ConfigureAwait(false);
|
||||
@@ -297,10 +312,13 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
||||
{
|
||||
DisposeHooks();
|
||||
_activePlayers.Clear();
|
||||
_gposePlayers.Clear();
|
||||
_actorsByHash.Clear();
|
||||
_actorsByName.Clear();
|
||||
_pendingHashResolutions.Clear();
|
||||
_ownedTracker.Reset();
|
||||
Volatile.Write(ref _snapshot, ActorSnapshot.Empty);
|
||||
Volatile.Write(ref _gposeSnapshot, GposeSnapshot.Empty);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
@@ -336,7 +354,7 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
||||
_onCompanionTerminateHook.Enable();
|
||||
|
||||
_hooksActive = true;
|
||||
_logger.LogDebug("ActorObjectService hooks enabled.");
|
||||
_logger.LogTrace("ActorObjectService hooks enabled.");
|
||||
}
|
||||
|
||||
private Task WarmupExistingActors()
|
||||
@@ -350,36 +368,21 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
||||
|
||||
private unsafe void OnCharacterInitialized(Character* chara)
|
||||
{
|
||||
try
|
||||
{
|
||||
_onInitializeHook!.Original(chara);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error invoking original character initialize.");
|
||||
}
|
||||
|
||||
QueueFrameworkUpdate(() => TrackGameObject((GameObject*)chara));
|
||||
ExecuteOriginal(() => _onInitializeHook!.Original(chara), "Error invoking original character initialize.");
|
||||
QueueTrack((GameObject*)chara);
|
||||
}
|
||||
|
||||
private unsafe void OnCharacterTerminated(Character* chara)
|
||||
{
|
||||
var address = (nint)chara;
|
||||
QueueFrameworkUpdate(() => UntrackGameObject(address));
|
||||
try
|
||||
{
|
||||
_onTerminateHook!.Original(chara);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error invoking original character terminate.");
|
||||
}
|
||||
QueueUntrack(address);
|
||||
ExecuteOriginal(() => _onTerminateHook!.Original(chara), "Error invoking original character terminate.");
|
||||
}
|
||||
|
||||
private unsafe GameObject* OnCharacterDisposed(Character* chara, byte freeMemory)
|
||||
{
|
||||
var address = (nint)chara;
|
||||
QueueFrameworkUpdate(() => UntrackGameObject(address));
|
||||
QueueUntrack(address);
|
||||
try
|
||||
{
|
||||
return _onDestructorHook!.Original(chara, freeMemory);
|
||||
@@ -416,7 +419,7 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
||||
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Actor tracked: {Name} addr={Address:X} idx={Index} owned={OwnedKind} local={Local} gpose={Gpose}",
|
||||
_logger.LogTrace("Actor tracked: {Name} addr={Address:X} idx={Index} owned={OwnedKind} local={Local} gpose={Gpose}",
|
||||
descriptor.Name,
|
||||
descriptor.Address,
|
||||
descriptor.ObjectIndex,
|
||||
@@ -534,7 +537,7 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
||||
RemoveDescriptor(descriptor);
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Actor untracked: {Name} addr={Address:X} idx={Index} owned={OwnedKind}",
|
||||
_logger.LogTrace("Actor untracked: {Name} addr={Address:X} idx={Index} owned={OwnedKind}",
|
||||
descriptor.Name,
|
||||
descriptor.Address,
|
||||
descriptor.ObjectIndex,
|
||||
@@ -558,10 +561,14 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
||||
if (!seen.Add(address))
|
||||
continue;
|
||||
|
||||
if (_activePlayers.ContainsKey(address))
|
||||
var gameObject = (GameObject*)address;
|
||||
if (_activePlayers.TryGetValue(address, out var existing))
|
||||
{
|
||||
RefreshDescriptorIfNeeded(existing, gameObject);
|
||||
continue;
|
||||
}
|
||||
|
||||
TrackGameObject((GameObject*)address);
|
||||
TrackGameObject(gameObject);
|
||||
}
|
||||
|
||||
var stale = _activePlayers.Keys.Where(addr => !seen.Contains(addr)).ToList();
|
||||
@@ -574,6 +581,50 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
||||
{
|
||||
_nextRefreshAllowed = DateTime.UtcNow + SnapshotRefreshInterval;
|
||||
}
|
||||
|
||||
if (_clientState.IsGPosing)
|
||||
{
|
||||
RefreshGposeActorsInternal();
|
||||
}
|
||||
else if (!_gposePlayers.IsEmpty)
|
||||
{
|
||||
_gposePlayers.Clear();
|
||||
PublishGposeSnapshot();
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe void RefreshDescriptorIfNeeded(ActorDescriptor existing, GameObject* gameObject)
|
||||
{
|
||||
if (gameObject == null)
|
||||
return;
|
||||
|
||||
if (existing.ObjectKind != DalamudObjectKind.Player || !string.IsNullOrEmpty(existing.HashedContentId))
|
||||
return;
|
||||
|
||||
var objectKind = (DalamudObjectKind)gameObject->ObjectKind;
|
||||
if (!IsSupportedObjectKind(objectKind))
|
||||
return;
|
||||
|
||||
if (BuildDescriptor(gameObject, objectKind) is not { } updated)
|
||||
return;
|
||||
|
||||
if (string.IsNullOrEmpty(updated.HashedContentId))
|
||||
return;
|
||||
|
||||
ReplaceDescriptor(existing, updated);
|
||||
_mediator.Publish(new ActorTrackedMessage(updated));
|
||||
}
|
||||
|
||||
private void ReplaceDescriptor(ActorDescriptor existing, ActorDescriptor updated)
|
||||
{
|
||||
RemoveDescriptorFromIndexes(existing);
|
||||
_ownedTracker.OnDescriptorRemoved(existing);
|
||||
|
||||
_activePlayers[updated.Address] = updated;
|
||||
IndexDescriptor(updated);
|
||||
_ownedTracker.OnDescriptorAdded(updated);
|
||||
UpdatePendingHashResolutions(updated);
|
||||
PublishSnapshot();
|
||||
}
|
||||
|
||||
private void IndexDescriptor(ActorDescriptor descriptor)
|
||||
@@ -605,30 +656,15 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
||||
|
||||
private unsafe void OnCompanionInitialized(Companion* companion)
|
||||
{
|
||||
try
|
||||
{
|
||||
_onCompanionInitializeHook!.Original(companion);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error invoking original companion initialize.");
|
||||
}
|
||||
|
||||
QueueFrameworkUpdate(() => TrackGameObject((GameObject*)companion));
|
||||
ExecuteOriginal(() => _onCompanionInitializeHook!.Original(companion), "Error invoking original companion initialize.");
|
||||
QueueTrack((GameObject*)companion);
|
||||
}
|
||||
|
||||
private unsafe void OnCompanionTerminated(Companion* companion)
|
||||
{
|
||||
var address = (nint)companion;
|
||||
QueueFrameworkUpdate(() => UntrackGameObject(address));
|
||||
try
|
||||
{
|
||||
_onCompanionTerminateHook!.Original(companion);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error invoking original companion terminate.");
|
||||
}
|
||||
QueueUntrack(address);
|
||||
ExecuteOriginal(() => _onCompanionTerminateHook!.Original(companion), "Error invoking original companion terminate.");
|
||||
}
|
||||
|
||||
private void RemoveDescriptorFromIndexes(ActorDescriptor descriptor)
|
||||
@@ -655,6 +691,7 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
||||
_activePlayers[descriptor.Address] = descriptor;
|
||||
IndexDescriptor(descriptor);
|
||||
_ownedTracker.OnDescriptorAdded(descriptor);
|
||||
UpdatePendingHashResolutions(descriptor);
|
||||
PublishSnapshot();
|
||||
}
|
||||
|
||||
@@ -662,21 +699,42 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
||||
{
|
||||
RemoveDescriptorFromIndexes(descriptor);
|
||||
_ownedTracker.OnDescriptorRemoved(descriptor);
|
||||
_pendingHashResolutions.TryRemove(descriptor.Address, out _);
|
||||
PublishSnapshot();
|
||||
}
|
||||
|
||||
private void UpdatePendingHashResolutions(ActorDescriptor descriptor)
|
||||
{
|
||||
if (descriptor.ObjectKind != DalamudObjectKind.Player)
|
||||
{
|
||||
_pendingHashResolutions.TryRemove(descriptor.Address, out _);
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(descriptor.HashedContentId))
|
||||
{
|
||||
_pendingHashResolutions[descriptor.Address] = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
_pendingHashResolutions.TryRemove(descriptor.Address, out _);
|
||||
}
|
||||
|
||||
private void PublishSnapshot()
|
||||
{
|
||||
var playerDescriptors = _activePlayers.Values
|
||||
.Where(descriptor => descriptor.ObjectKind == DalamudObjectKind.Player)
|
||||
.ToArray();
|
||||
var ownedDescriptors = _activePlayers.Values
|
||||
.Where(descriptor => descriptor.OwnedKind is not null)
|
||||
.ToArray();
|
||||
var playerAddresses = new nint[playerDescriptors.Length];
|
||||
for (var i = 0; i < playerDescriptors.Length; i++)
|
||||
playerAddresses[i] = playerDescriptors[i].Address;
|
||||
|
||||
var ownedSnapshot = _ownedTracker.CreateSnapshot();
|
||||
var nextGeneration = Snapshot.Generation + 1;
|
||||
var snapshot = new ActorSnapshot(playerDescriptors, playerAddresses, ownedSnapshot, nextGeneration);
|
||||
var snapshot = new ActorSnapshot(playerDescriptors, ownedDescriptors, playerAddresses, ownedSnapshot, nextGeneration);
|
||||
Volatile.Write(ref _snapshot, snapshot);
|
||||
}
|
||||
|
||||
@@ -694,6 +752,24 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
||||
_ = _framework.RunOnFrameworkThread(action);
|
||||
}
|
||||
|
||||
private void ExecuteOriginal(Action action, string errorMessage)
|
||||
{
|
||||
try
|
||||
{
|
||||
action();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe void QueueTrack(GameObject* gameObject)
|
||||
=> QueueFrameworkUpdate(() => TrackGameObject(gameObject));
|
||||
|
||||
private void QueueUntrack(nint address)
|
||||
=> QueueFrameworkUpdate(() => UntrackGameObject(address));
|
||||
|
||||
private void DisposeHooks()
|
||||
{
|
||||
var hadHooks = _hooksActive
|
||||
@@ -725,7 +801,7 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
||||
|
||||
if (hadHooks)
|
||||
{
|
||||
_logger.LogDebug("ActorObjectService hooks disabled.");
|
||||
_logger.LogTrace("ActorObjectService hooks disabled.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -770,6 +846,89 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
||||
return results;
|
||||
}
|
||||
|
||||
private unsafe void RefreshGposeActorsInternal()
|
||||
{
|
||||
var addresses = EnumerateGposeCharacterAddresses();
|
||||
HashSet<nint> seen = new(addresses.Count);
|
||||
|
||||
foreach (var address in addresses)
|
||||
{
|
||||
if (address == nint.Zero)
|
||||
continue;
|
||||
|
||||
if (!seen.Add(address))
|
||||
continue;
|
||||
|
||||
if (_gposePlayers.ContainsKey(address))
|
||||
continue;
|
||||
|
||||
TrackGposeObject((GameObject*)address);
|
||||
}
|
||||
|
||||
var stale = _gposePlayers.Keys.Where(addr => !seen.Contains(addr)).ToList();
|
||||
foreach (var staleAddress in stale)
|
||||
{
|
||||
UntrackGposeObject(staleAddress);
|
||||
}
|
||||
|
||||
PublishGposeSnapshot();
|
||||
}
|
||||
|
||||
private unsafe void TrackGposeObject(GameObject* gameObject)
|
||||
{
|
||||
if (gameObject == null)
|
||||
return;
|
||||
|
||||
var objectKind = (DalamudObjectKind)gameObject->ObjectKind;
|
||||
if (objectKind != DalamudObjectKind.Player)
|
||||
return;
|
||||
|
||||
if (BuildDescriptor(gameObject, objectKind) is not { } descriptor)
|
||||
return;
|
||||
|
||||
if (!descriptor.IsInGpose)
|
||||
return;
|
||||
|
||||
_gposePlayers[descriptor.Address] = descriptor;
|
||||
}
|
||||
|
||||
private void UntrackGposeObject(nint address)
|
||||
{
|
||||
if (address == nint.Zero)
|
||||
return;
|
||||
|
||||
_gposePlayers.TryRemove(address, out _);
|
||||
}
|
||||
|
||||
private void PublishGposeSnapshot()
|
||||
{
|
||||
var gposeDescriptors = _gposePlayers.Values.ToArray();
|
||||
var gposeAddresses = new nint[gposeDescriptors.Length];
|
||||
for (var i = 0; i < gposeDescriptors.Length; i++)
|
||||
gposeAddresses[i] = gposeDescriptors[i].Address;
|
||||
|
||||
var nextGeneration = CurrentGposeSnapshot.Generation + 1;
|
||||
var snapshot = new GposeSnapshot(gposeDescriptors, gposeAddresses, nextGeneration);
|
||||
Volatile.Write(ref _gposeSnapshot, snapshot);
|
||||
}
|
||||
|
||||
private List<nint> EnumerateGposeCharacterAddresses()
|
||||
{
|
||||
var results = new List<nint>(16);
|
||||
foreach (var obj in _objectTable)
|
||||
{
|
||||
if (obj.ObjectKind != DalamudObjectKind.Player)
|
||||
continue;
|
||||
|
||||
if (obj.ObjectIndex < 200)
|
||||
continue;
|
||||
|
||||
results.Add(obj.Address);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private static unsafe bool IsObjectFullyLoaded(nint address)
|
||||
{
|
||||
if (address == nint.Zero)
|
||||
@@ -783,13 +942,10 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
||||
if (drawObject == null)
|
||||
return false;
|
||||
|
||||
if ((gameObject->RenderFlags & VisibilityFlags.Nameplate) != VisibilityFlags.None)
|
||||
if ((ulong)gameObject->RenderFlags == 2048)
|
||||
return false;
|
||||
|
||||
var characterBase = (CharacterBase*)drawObject;
|
||||
if (characterBase == null)
|
||||
return false;
|
||||
|
||||
if (characterBase->HasModelInSlotLoaded != 0)
|
||||
return false;
|
||||
|
||||
@@ -925,14 +1081,27 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
||||
|
||||
private sealed record ActorSnapshot(
|
||||
IReadOnlyList<ActorDescriptor> PlayerDescriptors,
|
||||
IReadOnlyList<ActorDescriptor> OwnedDescriptors,
|
||||
IReadOnlyList<nint> PlayerAddresses,
|
||||
OwnedObjectSnapshot OwnedObjects,
|
||||
int Generation)
|
||||
{
|
||||
public static ActorSnapshot Empty { get; } = new(
|
||||
Array.Empty<ActorDescriptor>(),
|
||||
Array.Empty<ActorDescriptor>(),
|
||||
Array.Empty<nint>(),
|
||||
OwnedObjectSnapshot.Empty,
|
||||
0);
|
||||
}
|
||||
|
||||
private sealed record GposeSnapshot(
|
||||
IReadOnlyList<ActorDescriptor> GposeDescriptors,
|
||||
IReadOnlyList<nint> GposeAddresses,
|
||||
int Generation)
|
||||
{
|
||||
public static GposeSnapshot Empty { get; } = new(
|
||||
Array.Empty<ActorDescriptor>(),
|
||||
Array.Empty<nint>(),
|
||||
0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using LightlessSync.API.Dto.Chat;
|
||||
using LightlessSync.API.Data.Extensions;
|
||||
using LightlessSync.Services.ActorTracking;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.WebAPI;
|
||||
@@ -36,6 +37,8 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
private readonly Dictionary<string, bool> _lastPresenceStates = new(StringComparer.Ordinal);
|
||||
private readonly Dictionary<string, string> _selfTokens = new(StringComparer.Ordinal);
|
||||
private readonly List<PendingSelfMessage> _pendingSelfMessages = new();
|
||||
private List<ChatChannelSnapshot>? _cachedChannelSnapshots;
|
||||
private bool _channelsSnapshotDirty = true;
|
||||
|
||||
private bool _isLoggedIn;
|
||||
private bool _isConnected;
|
||||
@@ -69,6 +72,11 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
{
|
||||
using (_sync.EnterScope())
|
||||
{
|
||||
if (!_channelsSnapshotDirty && _cachedChannelSnapshots is not null)
|
||||
{
|
||||
return _cachedChannelSnapshots;
|
||||
}
|
||||
|
||||
var snapshots = new List<ChatChannelSnapshot>(_channelOrder.Count);
|
||||
foreach (var key in _channelOrder)
|
||||
{
|
||||
@@ -98,6 +106,8 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
state.Messages.ToList()));
|
||||
}
|
||||
|
||||
_cachedChannelSnapshots = snapshots;
|
||||
_channelsSnapshotDirty = false;
|
||||
return snapshots;
|
||||
}
|
||||
}
|
||||
@@ -135,6 +145,8 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
state.UnreadCount = 0;
|
||||
_lastReadCounts[key] = state.Messages.Count;
|
||||
}
|
||||
|
||||
MarkChannelsSnapshotDirtyLocked();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,6 +198,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
if (!wasEnabled)
|
||||
{
|
||||
_chatEnabled = true;
|
||||
MarkChannelsSnapshotDirtyLocked();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,6 +244,8 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
state.IsAvailable = false;
|
||||
state.StatusText = "Chat services disabled";
|
||||
}
|
||||
|
||||
MarkChannelsSnapshotDirtyLocked();
|
||||
}
|
||||
|
||||
UnregisterChatHandler();
|
||||
@@ -717,7 +732,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
_zoneDefinitions[key] = new ZoneChannelDefinition(key, info.DisplayName ?? key, descriptor, territories);
|
||||
}
|
||||
|
||||
var territoryData = _dalamudUtilService.TerritoryData.Value;
|
||||
var territoryData = _dalamudUtilService.TerritoryDataEnglish.Value;
|
||||
foreach (var kvp in territoryData)
|
||||
{
|
||||
foreach (var variant in EnumerateTerritoryKeys(kvp.Value))
|
||||
@@ -853,6 +868,12 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
var infos = new List<GroupChatChannelInfoDto>(groups.Count);
|
||||
foreach (var group in groups)
|
||||
{
|
||||
// basically prune the channel if it's disabled
|
||||
if (group.GroupPermissions.IsDisableChat())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var descriptor = new ChatChannelDescriptor
|
||||
{
|
||||
Type = ChatChannelType.Group,
|
||||
@@ -1023,6 +1044,8 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
state.UnreadCount = Math.Min(Math.Max(unreadFromHistory, incrementalUnread), MaxUnreadCount);
|
||||
state.HasUnread = state.UnreadCount > 0;
|
||||
}
|
||||
|
||||
MarkChannelsSnapshotDirtyLocked();
|
||||
}
|
||||
|
||||
Mediator.Publish(new ChatChannelMessageAdded(key, message));
|
||||
@@ -1204,9 +1227,25 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
{
|
||||
_activeChannelKey = _channelOrder.Count > 0 ? _channelOrder[0] : null;
|
||||
}
|
||||
|
||||
MarkChannelsSnapshotDirtyLocked();
|
||||
}
|
||||
|
||||
private void PublishChannelListChanged() => Mediator.Publish(new ChatChannelsUpdated());
|
||||
private void MarkChannelsSnapshotDirty()
|
||||
{
|
||||
using (_sync.EnterScope())
|
||||
{
|
||||
_channelsSnapshotDirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void MarkChannelsSnapshotDirtyLocked() => _channelsSnapshotDirty = true;
|
||||
|
||||
private void PublishChannelListChanged()
|
||||
{
|
||||
MarkChannelsSnapshotDirty();
|
||||
Mediator.Publish(new ChatChannelsUpdated());
|
||||
}
|
||||
|
||||
private static IEnumerable<string> EnumerateTerritoryKeys(string? value)
|
||||
{
|
||||
|
||||
@@ -129,7 +129,6 @@ internal class ContextMenuService : IHostedService
|
||||
|
||||
var snapshot = _pairUiService.GetSnapshot();
|
||||
var pair = snapshot.PairsByUid.Values.FirstOrDefault(p =>
|
||||
p.IsVisible &&
|
||||
p.PlayerCharacterId != uint.MaxValue &&
|
||||
p.PlayerCharacterId == target.TargetObjectId);
|
||||
|
||||
|
||||
@@ -91,43 +91,10 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
return gameData.GetExcelSheet<ClassJob>(Dalamud.Game.ClientLanguage.English)!
|
||||
.ToDictionary(k => k.RowId, k => k.NameEnglish.ToString());
|
||||
});
|
||||
TerritoryData = new(() =>
|
||||
{
|
||||
return gameData.GetExcelSheet<TerritoryType>(Dalamud.Game.ClientLanguage.English)!
|
||||
.Where(w => w.RowId != 0)
|
||||
.ToDictionary(w => w.RowId, w =>
|
||||
{
|
||||
StringBuilder sb = new();
|
||||
sb.Append(w.PlaceNameRegion.Value.Name);
|
||||
if (w.PlaceName.ValueNullable != null)
|
||||
{
|
||||
sb.Append(" - ");
|
||||
sb.Append(w.PlaceName.Value.Name);
|
||||
}
|
||||
return sb.ToString();
|
||||
});
|
||||
});
|
||||
MapData = new(() =>
|
||||
{
|
||||
return gameData.GetExcelSheet<Map>(Dalamud.Game.ClientLanguage.English)!
|
||||
.Where(w => w.RowId != 0)
|
||||
.ToDictionary(w => w.RowId, w =>
|
||||
{
|
||||
StringBuilder sb = new();
|
||||
sb.Append(w.PlaceNameRegion.Value.Name);
|
||||
if (w.PlaceName.ValueNullable != null)
|
||||
{
|
||||
sb.Append(" - ");
|
||||
sb.Append(w.PlaceName.Value.Name);
|
||||
}
|
||||
if (w.PlaceNameSub.ValueNullable != null && !string.IsNullOrEmpty(w.PlaceNameSub.Value.Name.ToString()))
|
||||
{
|
||||
sb.Append(" - ");
|
||||
sb.Append(w.PlaceNameSub.Value.Name);
|
||||
}
|
||||
return (w, sb.ToString());
|
||||
});
|
||||
});
|
||||
var clientLanguage = _clientState.ClientLanguage;
|
||||
TerritoryData = new(() => BuildTerritoryData(clientLanguage));
|
||||
TerritoryDataEnglish = new(() => BuildTerritoryData(Dalamud.Game.ClientLanguage.English));
|
||||
MapData = new(() => BuildMapData(clientLanguage));
|
||||
mediator.Subscribe<TargetPairMessage>(this, (msg) =>
|
||||
{
|
||||
if (clientState.IsPvP) return;
|
||||
@@ -158,6 +125,71 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
private Lazy<ulong> RebuildCID() => new(GetCID);
|
||||
|
||||
public bool IsWine { get; init; }
|
||||
private Dictionary<uint, string> BuildTerritoryData(Dalamud.Game.ClientLanguage language)
|
||||
{
|
||||
var placeNames = _gameData.GetExcelSheet<PlaceName>(language)!;
|
||||
return _gameData.GetExcelSheet<TerritoryType>(language)!
|
||||
.Where(w => w.RowId != 0)
|
||||
.ToDictionary(w => w.RowId, w =>
|
||||
{
|
||||
var regionName = GetPlaceName(placeNames, w.PlaceNameRegion.RowId);
|
||||
var placeName = GetPlaceName(placeNames, w.PlaceName.RowId);
|
||||
return BuildPlaceName(regionName, placeName, string.Empty);
|
||||
});
|
||||
}
|
||||
|
||||
private Dictionary<uint, (Map Map, string MapName)> BuildMapData(Dalamud.Game.ClientLanguage language)
|
||||
{
|
||||
var placeNames = _gameData.GetExcelSheet<PlaceName>(language)!;
|
||||
return _gameData.GetExcelSheet<Map>(language)!
|
||||
.Where(w => w.RowId != 0)
|
||||
.ToDictionary(w => w.RowId, w =>
|
||||
{
|
||||
var regionName = GetPlaceName(placeNames, w.PlaceNameRegion.RowId);
|
||||
var placeName = GetPlaceName(placeNames, w.PlaceName.RowId);
|
||||
var subPlaceName = GetPlaceName(placeNames, w.PlaceNameSub.RowId);
|
||||
var displayName = BuildPlaceName(regionName, placeName, subPlaceName);
|
||||
return (w, displayName);
|
||||
});
|
||||
}
|
||||
private static string GetPlaceName(Lumina.Excel.ExcelSheet<PlaceName> placeNames, uint rowId)
|
||||
{
|
||||
if (rowId == 0)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return placeNames.GetRow(rowId).Name.ToString();
|
||||
}
|
||||
|
||||
private static string BuildPlaceName(string regionName, string placeName, string subPlaceName)
|
||||
{
|
||||
StringBuilder sb = new();
|
||||
if (!string.IsNullOrWhiteSpace(regionName))
|
||||
{
|
||||
sb.Append(regionName);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(placeName))
|
||||
{
|
||||
if (sb.Length > 0)
|
||||
{
|
||||
sb.Append(" - ");
|
||||
}
|
||||
sb.Append(placeName);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(subPlaceName))
|
||||
{
|
||||
if (sb.Length > 0)
|
||||
{
|
||||
sb.Append(" - ");
|
||||
}
|
||||
sb.Append(subPlaceName);
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
private bool ResolvePairAddress(Pair pair, out Pair resolvedPair, out nint address)
|
||||
{
|
||||
resolvedPair = _pairFactory.Value.Create(pair.UniqueIdent) ?? pair;
|
||||
@@ -245,6 +277,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
public Lazy<Dictionary<uint, string>> JobData { get; private set; }
|
||||
public Lazy<Dictionary<ushort, string>> WorldData { get; private set; }
|
||||
public Lazy<Dictionary<uint, string>> TerritoryData { get; private set; }
|
||||
public Lazy<Dictionary<uint, string>> TerritoryDataEnglish { get; private set; }
|
||||
public Lazy<Dictionary<uint, (Map Map, string MapName)>> MapData { get; private set; }
|
||||
public bool IsLodEnabled { get; private set; }
|
||||
public LightlessMediator Mediator { get; }
|
||||
@@ -264,7 +297,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TerritoryData.Value.TryGetValue(territoryId, out var name) || string.IsNullOrWhiteSpace(name))
|
||||
if (!TerritoryDataEnglish.Value.TryGetValue(territoryId, out var name) || string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -355,7 +388,8 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
|
||||
var playerAddress = playerPointer.Value;
|
||||
var ownerEntityId = ((Character*)playerAddress)->EntityId;
|
||||
if (ownerEntityId == 0) return IntPtr.Zero;
|
||||
var candidateAddress = _objectTable.GetObjectAddress(((GameObject*)playerAddress)->ObjectIndex + 1);
|
||||
if (ownerEntityId == 0) return candidateAddress;
|
||||
|
||||
if (playerAddress == _actorObjectService.LocalPlayerAddress)
|
||||
{
|
||||
@@ -366,6 +400,17 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
}
|
||||
}
|
||||
|
||||
if (candidateAddress != nint.Zero)
|
||||
{
|
||||
var candidate = (GameObject*)candidateAddress;
|
||||
var candidateKind = (DalamudObjectKind)candidate->ObjectKind;
|
||||
if ((candidateKind == DalamudObjectKind.MountType || candidateKind == DalamudObjectKind.Companion)
|
||||
&& ResolveOwnerId(candidate) == ownerEntityId)
|
||||
{
|
||||
return candidateAddress;
|
||||
}
|
||||
}
|
||||
|
||||
var ownedObject = FindOwnedObject(ownerEntityId, playerAddress, static kind =>
|
||||
kind == DalamudObjectKind.MountType || kind == DalamudObjectKind.Companion);
|
||||
if (ownedObject != nint.Zero)
|
||||
@@ -373,7 +418,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
return ownedObject;
|
||||
}
|
||||
|
||||
return _objectTable.GetObjectAddress(((GameObject*)playerAddress)->ObjectIndex + 1);
|
||||
return candidateAddress;
|
||||
}
|
||||
|
||||
public async Task<IntPtr> GetMinionOrMountAsync(IntPtr? playerPointer = null)
|
||||
@@ -784,7 +829,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
bool isDrawingChanged = false;
|
||||
if ((nint)drawObj != IntPtr.Zero)
|
||||
{
|
||||
isDrawing = (gameObj->RenderFlags & VisibilityFlags.Nameplate) != VisibilityFlags.None;
|
||||
isDrawing = gameObj->RenderFlags == (VisibilityFlags)0b100000000000;
|
||||
if (!isDrawing)
|
||||
{
|
||||
isDrawing = ((CharacterBase*)drawObj)->HasModelInSlotLoaded != 0;
|
||||
@@ -850,9 +895,12 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
_performanceCollector.LogPerformance(this, $"TrackedActorsToState",
|
||||
() =>
|
||||
{
|
||||
_actorObjectService.RefreshTrackedActors();
|
||||
if (!_actorObjectService.HooksActive || !isNormalFrameworkUpdate || _actorObjectService.HasPendingHashResolutions)
|
||||
{
|
||||
_actorObjectService.RefreshTrackedActors();
|
||||
}
|
||||
|
||||
var playerDescriptors = _actorObjectService.PlayerCharacterDescriptors;
|
||||
var playerDescriptors = _actorObjectService.PlayerDescriptors;
|
||||
for (var i = 0; i < playerDescriptors.Count; i++)
|
||||
{
|
||||
var actor = playerDescriptors[i];
|
||||
|
||||
@@ -148,10 +148,14 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase
|
||||
private void UpdateSyncshellBroadcasts()
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
var newSet = _broadcastCache
|
||||
.Where(e => e.Value.IsBroadcasting && e.Value.ExpiryTime > now && !string.IsNullOrEmpty(e.Value.GID))
|
||||
.Select(e => e.Key)
|
||||
.ToHashSet(StringComparer.Ordinal);
|
||||
var nearbyCids = GetNearbyHashedCids(out _);
|
||||
var newSet = nearbyCids.Count == 0
|
||||
? new HashSet<string>(StringComparer.Ordinal)
|
||||
: _broadcastCache
|
||||
.Where(e => e.Value.IsBroadcasting && e.Value.ExpiryTime > now && !string.IsNullOrEmpty(e.Value.GID))
|
||||
.Where(e => nearbyCids.Contains(e.Key))
|
||||
.Select(e => e.Key)
|
||||
.ToHashSet(StringComparer.Ordinal);
|
||||
|
||||
if (!_syncshellCids.SetEquals(newSet))
|
||||
{
|
||||
@@ -163,12 +167,17 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase
|
||||
}
|
||||
}
|
||||
|
||||
public List<BroadcastStatusInfoDto> GetActiveSyncshellBroadcasts()
|
||||
public List<BroadcastStatusInfoDto> GetActiveSyncshellBroadcasts(bool excludeLocal = false)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
var nearbyCids = GetNearbyHashedCids(out var localCid);
|
||||
if (nearbyCids.Count == 0)
|
||||
return [];
|
||||
|
||||
return [.. _broadcastCache
|
||||
.Where(e => e.Value.IsBroadcasting && e.Value.ExpiryTime > now && !string.IsNullOrEmpty(e.Value.GID))
|
||||
.Where(e => nearbyCids.Contains(e.Key))
|
||||
.Where(e => !excludeLocal || !string.Equals(e.Key, localCid, StringComparison.Ordinal))
|
||||
.Select(e => new BroadcastStatusInfoDto
|
||||
{
|
||||
HashedCID = e.Key,
|
||||
@@ -178,6 +187,47 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase
|
||||
})];
|
||||
}
|
||||
|
||||
public bool TryGetLocalHashedCid(out string hashedCid)
|
||||
{
|
||||
hashedCid = string.Empty;
|
||||
var descriptors = _actorTracker.PlayerDescriptors;
|
||||
if (descriptors.Count == 0)
|
||||
return false;
|
||||
|
||||
foreach (var descriptor in descriptors)
|
||||
{
|
||||
if (!descriptor.IsLocalPlayer || string.IsNullOrWhiteSpace(descriptor.HashedContentId))
|
||||
continue;
|
||||
|
||||
hashedCid = descriptor.HashedContentId;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private HashSet<string> GetNearbyHashedCids(out string? localCid)
|
||||
{
|
||||
localCid = null;
|
||||
var descriptors = _actorTracker.PlayerDescriptors;
|
||||
if (descriptors.Count == 0)
|
||||
return new HashSet<string>(StringComparer.Ordinal);
|
||||
|
||||
var set = new HashSet<string>(StringComparer.Ordinal);
|
||||
foreach (var descriptor in descriptors)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(descriptor.HashedContentId))
|
||||
continue;
|
||||
|
||||
if (descriptor.IsLocalPlayer)
|
||||
localCid = descriptor.HashedContentId;
|
||||
|
||||
set.Add(descriptor.HashedContentId);
|
||||
}
|
||||
|
||||
return set;
|
||||
}
|
||||
|
||||
private async Task ExpiredBroadcastCleanupLoop()
|
||||
{
|
||||
var token = _cleanupCts.Token;
|
||||
|
||||
Reference in New Issue
Block a user