hopefully it's fine now?

This commit is contained in:
2025-12-24 07:43:23 +09:00
parent 2eba5a1f30
commit 7d151dac2b
24 changed files with 1101 additions and 352 deletions

View File

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

View File

@@ -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)
{

View File

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

View File

@@ -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];

View File

@@ -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;