hopefully it's fine now?
This commit is contained in:
@@ -59,7 +59,7 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
||||
_playerRelatedPointers.Remove(msg.GameObjectHandler);
|
||||
});
|
||||
|
||||
foreach (var descriptor in _actorObjectService.PlayerDescriptors)
|
||||
foreach (var descriptor in _actorObjectService.ObjectDescriptors)
|
||||
{
|
||||
HandleActorTracked(descriptor);
|
||||
}
|
||||
@@ -291,7 +291,7 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
||||
}
|
||||
|
||||
var activeDescriptors = new Dictionary<nint, ObjectKind>();
|
||||
foreach (var descriptor in _actorObjectService.PlayerDescriptors)
|
||||
foreach (var descriptor in _actorObjectService.ObjectDescriptors)
|
||||
{
|
||||
if (TryResolveObjectKind(descriptor, out var resolvedKind))
|
||||
{
|
||||
|
||||
@@ -92,6 +92,9 @@ public sealed class IpcCallerPenumbra : IpcServiceBase
|
||||
public string GetMetaManipulations()
|
||||
=> _resources.GetMetaManipulations();
|
||||
|
||||
public Task<Dictionary<ushort, Dictionary<string, HashSet<string>>>> GetClientOnScreenResourcePaths() // why did i add this, i honestly don't know but i'm keeping it anyways, fuck you
|
||||
=> _resources.GetClientOnScreenResourcePathsAsync();
|
||||
|
||||
public Task<(string[] forward, string[][] reverse)> ResolvePathsAsync(string[] forward, string[] reverse)
|
||||
=> _resources.ResolvePathsAsync(forward, reverse);
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
||||
private readonly Func<IntPtr> _getAddress;
|
||||
private readonly bool _isOwnedObject;
|
||||
private readonly PerformanceCollectorService _performanceCollector;
|
||||
private readonly object _frameworkUpdateGate = new();
|
||||
private bool _frameworkUpdateSubscribed;
|
||||
private byte _classJob = 0;
|
||||
private Task? _delayedZoningTask;
|
||||
private bool _haltProcessing = false;
|
||||
@@ -47,7 +49,10 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
||||
});
|
||||
}
|
||||
|
||||
Mediator.Subscribe<FrameworkUpdateMessage>(this, (_) => FrameworkUpdate());
|
||||
if (_isOwnedObject)
|
||||
{
|
||||
EnableFrameworkUpdates();
|
||||
}
|
||||
|
||||
Mediator.Subscribe<ZoneSwitchEndMessage>(this, (_) => ZoneSwitchEnd());
|
||||
Mediator.Subscribe<ZoneSwitchStartMessage>(this, (_) => ZoneSwitchStart());
|
||||
@@ -109,7 +114,7 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
||||
{
|
||||
while (await _dalamudUtil.RunOnFrameworkThread(() =>
|
||||
{
|
||||
if (_haltProcessing) CheckAndUpdateObject();
|
||||
EnsureLatestObjectState();
|
||||
if (CurrentDrawCondition != DrawCondition.None) return true;
|
||||
var gameObj = _dalamudUtil.CreateGameObject(Address);
|
||||
if (gameObj is Dalamud.Game.ClientState.Objects.Types.ICharacter chara)
|
||||
@@ -148,6 +153,11 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
||||
_haltProcessing = false;
|
||||
}
|
||||
|
||||
public void Refresh()
|
||||
{
|
||||
_dalamudUtil.RunOnFrameworkThread(CheckAndUpdateObject).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public async Task<bool> IsBeingDrawnRunOnFrameworkAsync()
|
||||
{
|
||||
return await _dalamudUtil.RunOnFrameworkThread(IsBeingDrawn).ConfigureAwait(false);
|
||||
@@ -361,7 +371,7 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
||||
|
||||
private bool IsBeingDrawn()
|
||||
{
|
||||
if (_haltProcessing) CheckAndUpdateObject();
|
||||
EnsureLatestObjectState();
|
||||
|
||||
if (_dalamudUtil.IsAnythingDrawing)
|
||||
{
|
||||
@@ -373,6 +383,28 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
||||
return CurrentDrawCondition != DrawCondition.None;
|
||||
}
|
||||
|
||||
private void EnsureLatestObjectState()
|
||||
{
|
||||
if (_haltProcessing || !_frameworkUpdateSubscribed)
|
||||
{
|
||||
CheckAndUpdateObject();
|
||||
}
|
||||
}
|
||||
|
||||
private void EnableFrameworkUpdates()
|
||||
{
|
||||
lock (_frameworkUpdateGate)
|
||||
{
|
||||
if (_frameworkUpdateSubscribed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Mediator.Subscribe<FrameworkUpdateMessage>(this, _ => FrameworkUpdate());
|
||||
_frameworkUpdateSubscribed = true;
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe DrawCondition IsBeingDrawnUnsafe()
|
||||
{
|
||||
if (Address == IntPtr.Zero) return DrawCondition.ObjectZero;
|
||||
|
||||
@@ -25,6 +25,11 @@
|
||||
bool IsDownloading { get; }
|
||||
int PendingDownloadCount { get; }
|
||||
int ForbiddenDownloadCount { get; }
|
||||
bool PendingModReapply { get; }
|
||||
bool ModApplyDeferred { get; }
|
||||
int MissingCriticalMods { get; }
|
||||
int MissingNonCriticalMods { get; }
|
||||
int MissingForbiddenMods { get; }
|
||||
DateTime? InvisibleSinceUtc { get; }
|
||||
DateTime? VisibilityEvictionDueAtUtc { get; }
|
||||
|
||||
|
||||
@@ -87,22 +87,25 @@ public class Pair
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.Target is not MenuTargetDefault target || target.TargetObjectId != handler.PlayerCharacterId || IsPaused)
|
||||
if (args.Target is not MenuTargetDefault target || target.TargetObjectId != handler.PlayerCharacterId)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
UiSharedService.AddContextMenuItem(args, name: "Open Profile", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
|
||||
if (!IsPaused)
|
||||
{
|
||||
_mediator.Publish(new ProfileOpenStandaloneMessage(this));
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
UiSharedService.AddContextMenuItem(args, name: "Open Profile", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
|
||||
{
|
||||
_mediator.Publish(new ProfileOpenStandaloneMessage(this));
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
|
||||
UiSharedService.AddContextMenuItem(args, name: "Reapply last data", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
|
||||
{
|
||||
ApplyLastReceivedData(forced: true);
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
UiSharedService.AddContextMenuItem(args, name: "Reapply last data", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
|
||||
{
|
||||
ApplyLastReceivedData(forced: true);
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
}
|
||||
|
||||
UiSharedService.AddContextMenuItem(args, name: "Change Permissions", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
|
||||
{
|
||||
@@ -110,7 +113,24 @@ public class Pair
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
|
||||
UiSharedService.AddContextMenuItem(args, name: "Cycle pause state", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
|
||||
if (IsPaused)
|
||||
{
|
||||
UiSharedService.AddContextMenuItem(args, name: "Toggle Unpause State", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
|
||||
{
|
||||
_ = _apiController.Value.UnpauseAsync(UserData);
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
UiSharedService.AddContextMenuItem(args, name: "Toggle Pause State", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
|
||||
{
|
||||
_ = _apiController.Value.PauseAsync(UserData);
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
}
|
||||
|
||||
UiSharedService.AddContextMenuItem(args, name: "Cycle Pause State", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
|
||||
{
|
||||
TriggerCyclePause();
|
||||
return Task.CompletedTask;
|
||||
@@ -218,6 +238,11 @@ public class Pair
|
||||
handler.IsApplying,
|
||||
handler.IsDownloading,
|
||||
handler.PendingDownloadCount,
|
||||
handler.ForbiddenDownloadCount);
|
||||
handler.ForbiddenDownloadCount,
|
||||
handler.PendingModReapply,
|
||||
handler.ModApplyDeferred,
|
||||
handler.MissingCriticalMods,
|
||||
handler.MissingNonCriticalMods,
|
||||
handler.MissingForbiddenMods);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,12 @@ public sealed record PairDebugInfo(
|
||||
bool IsApplying,
|
||||
bool IsDownloading,
|
||||
int PendingDownloadCount,
|
||||
int ForbiddenDownloadCount)
|
||||
int ForbiddenDownloadCount,
|
||||
bool PendingModReapply,
|
||||
bool ModApplyDeferred,
|
||||
int MissingCriticalMods,
|
||||
int MissingNonCriticalMods,
|
||||
int MissingForbiddenMods)
|
||||
{
|
||||
public static PairDebugInfo Empty { get; } = new(
|
||||
false,
|
||||
@@ -34,5 +39,10 @@ public sealed record PairDebugInfo(
|
||||
false,
|
||||
false,
|
||||
0,
|
||||
0,
|
||||
false,
|
||||
false,
|
||||
0,
|
||||
0,
|
||||
0);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ using LightlessSync.Interop.Ipc;
|
||||
using LightlessSync.PlayerData.Factories;
|
||||
using LightlessSync.PlayerData.Handlers;
|
||||
using LightlessSync.Services;
|
||||
using LightlessSync.Services.ActorTracking;
|
||||
using LightlessSync.Services.Events;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.Services.PairProcessing;
|
||||
@@ -18,6 +19,7 @@ using LightlessSync.WebAPI.Files;
|
||||
using LightlessSync.WebAPI.Files.Models;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using DalamudObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind;
|
||||
using ObjectKind = LightlessSync.API.Data.Enum.ObjectKind;
|
||||
using FileReplacementDataComparer = LightlessSync.PlayerData.Data.FileReplacementDataComparer;
|
||||
|
||||
@@ -31,6 +33,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
private sealed record CombatData(Guid ApplicationId, CharacterData CharacterData, bool Forced);
|
||||
|
||||
private readonly DalamudUtilService _dalamudUtil;
|
||||
private readonly ActorObjectService _actorObjectService;
|
||||
private readonly FileDownloadManager _downloadManager;
|
||||
private readonly FileCacheManager _fileDbManager;
|
||||
private readonly GameObjectHandlerFactory _gameObjectHandlerFactory;
|
||||
@@ -56,11 +59,15 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
private bool _forceFullReapply;
|
||||
private Dictionary<(string GamePath, string? Hash), string>? _lastAppliedModdedPaths;
|
||||
private bool _needsCollectionRebuild;
|
||||
private bool _pendingModReapply;
|
||||
private bool _lastModApplyDeferred;
|
||||
private int _lastMissingCriticalMods;
|
||||
private int _lastMissingNonCriticalMods;
|
||||
private int _lastMissingForbiddenMods;
|
||||
private bool _isVisible;
|
||||
private Guid _penumbraCollection;
|
||||
private readonly object _collectionGate = new();
|
||||
private bool _redrawOnNextApplication = false;
|
||||
private bool _explicitRedrawQueued;
|
||||
private readonly object _initializationGate = new();
|
||||
private readonly object _pauseLock = new();
|
||||
private Task _pauseTransitionTask = Task.CompletedTask;
|
||||
@@ -73,8 +80,23 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
private readonly object _visibilityGraceGate = new();
|
||||
private CancellationTokenSource? _visibilityGraceCts;
|
||||
private static readonly TimeSpan VisibilityEvictionGrace = TimeSpan.FromMinutes(1);
|
||||
private static readonly HashSet<string> NonPriorityModExtensions = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
".tmb",
|
||||
".pap",
|
||||
".atex",
|
||||
".avfx",
|
||||
".scd"
|
||||
};
|
||||
private DateTime? _invisibleSinceUtc;
|
||||
private DateTime? _visibilityEvictionDueAtUtc;
|
||||
private DateTime _nextActorLookupUtc = DateTime.MinValue;
|
||||
private static readonly TimeSpan ActorLookupInterval = TimeSpan.FromSeconds(1);
|
||||
private static readonly SemaphoreSlim ActorInitializationLimiter = new(1, 1);
|
||||
private readonly object _actorInitializationGate = new();
|
||||
private ActorObjectService.ActorDescriptor? _pendingActorDescriptor;
|
||||
private bool _actorInitializationInProgress;
|
||||
private bool _frameworkUpdateSubscribed;
|
||||
|
||||
public DateTime? InvisibleSinceUtc => _invisibleSinceUtc;
|
||||
public DateTime? VisibilityEvictionDueAtUtc => _visibilityEvictionDueAtUtc;
|
||||
@@ -126,6 +148,11 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
public long LastAppliedApproximateVRAMBytes { get; set; } = -1;
|
||||
public long LastAppliedApproximateEffectiveVRAMBytes { get; set; } = -1;
|
||||
public CharacterData? LastReceivedCharacterData { get; private set; }
|
||||
public bool PendingModReapply => _pendingModReapply;
|
||||
public bool ModApplyDeferred => _lastModApplyDeferred;
|
||||
public int MissingCriticalMods => _lastMissingCriticalMods;
|
||||
public int MissingNonCriticalMods => _lastMissingNonCriticalMods;
|
||||
public int MissingForbiddenMods => _lastMissingForbiddenMods;
|
||||
public DateTime? LastDataReceivedAt => _lastDataReceivedAt;
|
||||
public DateTime? LastApplyAttemptAt => _lastApplyAttemptAt;
|
||||
public DateTime? LastSuccessfulApplyAt => _lastSuccessfulApplyAt;
|
||||
@@ -146,6 +173,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
FileDownloadManager transferManager,
|
||||
PluginWarningNotificationService pluginWarningNotificationManager,
|
||||
DalamudUtilService dalamudUtil,
|
||||
ActorObjectService actorObjectService,
|
||||
IHostApplicationLifetime lifetime,
|
||||
FileCacheManager fileDbManager,
|
||||
PlayerPerformanceService playerPerformanceService,
|
||||
@@ -162,6 +190,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
_downloadManager = transferManager;
|
||||
_pluginWarningNotificationManager = pluginWarningNotificationManager;
|
||||
_dalamudUtil = dalamudUtil;
|
||||
_actorObjectService = actorObjectService;
|
||||
_lifetime = lifetime;
|
||||
_fileDbManager = fileDbManager;
|
||||
_playerPerformanceService = playerPerformanceService;
|
||||
@@ -185,6 +214,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
return;
|
||||
}
|
||||
|
||||
ActorObjectService.ActorDescriptor? trackedDescriptor = null;
|
||||
lock (_initializationGate)
|
||||
{
|
||||
if (Initialized)
|
||||
@@ -198,7 +228,12 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
_forceApplyMods = true;
|
||||
}
|
||||
|
||||
Mediator.Subscribe<FrameworkUpdateMessage>(this, _ => FrameworkUpdate());
|
||||
var useFrameworkUpdate = !_actorObjectService.HooksActive;
|
||||
if (useFrameworkUpdate)
|
||||
{
|
||||
Mediator.Subscribe<FrameworkUpdateMessage>(this, _ => FrameworkUpdate());
|
||||
_frameworkUpdateSubscribed = true;
|
||||
}
|
||||
Mediator.Subscribe<ZoneSwitchStartMessage>(this, _ =>
|
||||
{
|
||||
_downloadCancellationTokenSource?.CancelDispose();
|
||||
@@ -234,17 +269,49 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
Mediator.Subscribe<CutsceneEndMessage>(this, _ => EnableSync());
|
||||
Mediator.Subscribe<GposeStartMessage>(this, _ => DisableSync());
|
||||
Mediator.Subscribe<GposeEndMessage>(this, _ => EnableSync());
|
||||
Mediator.Subscribe<DownloadFinishedMessage>(this, msg =>
|
||||
{
|
||||
if (_charaHandler is null || !ReferenceEquals(msg.DownloadId, _charaHandler))
|
||||
Mediator.Subscribe<ActorTrackedMessage>(this, msg => HandleActorTracked(msg.Descriptor));
|
||||
Mediator.Subscribe<ActorUntrackedMessage>(this, msg => HandleActorUntracked(msg.Descriptor));
|
||||
Mediator.Subscribe<DownloadFinishedMessage>(this, msg =>
|
||||
{
|
||||
return;
|
||||
if (_charaHandler is null || !ReferenceEquals(msg.DownloadId, _charaHandler))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_pendingModReapply && IsVisible)
|
||||
{
|
||||
if (LastReceivedCharacterData is not null)
|
||||
{
|
||||
Logger.LogDebug("Downloads finished for {handler}, reapplying pending mod data", GetLogIdentifier());
|
||||
ApplyLastReceivedData(forced: true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_cachedData is not null)
|
||||
{
|
||||
Logger.LogDebug("Downloads finished for {handler}, reapplying pending mod data from cache", GetLogIdentifier());
|
||||
ApplyCharacterData(Guid.NewGuid(), _cachedData, forceApplyCustomization: true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
TryApplyQueuedData();
|
||||
});
|
||||
|
||||
if (!useFrameworkUpdate
|
||||
&& _actorObjectService.TryGetActorByHash(Ident, out var descriptor)
|
||||
&& descriptor.Address != nint.Zero)
|
||||
{
|
||||
trackedDescriptor = descriptor;
|
||||
}
|
||||
TryApplyQueuedData();
|
||||
});
|
||||
|
||||
Initialized = true;
|
||||
}
|
||||
|
||||
if (trackedDescriptor.HasValue)
|
||||
{
|
||||
HandleActorTracked(trackedDescriptor.Value);
|
||||
}
|
||||
}
|
||||
|
||||
private IReadOnlyList<PairConnection> GetCurrentPairs()
|
||||
@@ -737,6 +804,67 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool IsForbiddenHash(string hash)
|
||||
=> _downloadManager.ForbiddenTransfers.Exists(f => string.Equals(f.Hash, hash, StringComparison.Ordinal));
|
||||
|
||||
private static bool IsNonPriorityModPath(string? gamePath)
|
||||
{
|
||||
if (string.IsNullOrEmpty(gamePath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var extension = Path.GetExtension(gamePath);
|
||||
return !string.IsNullOrEmpty(extension) && NonPriorityModExtensions.Contains(extension);
|
||||
}
|
||||
|
||||
private static bool IsCriticalModReplacement(FileReplacementData replacement)
|
||||
{
|
||||
foreach (var gamePath in replacement.GamePaths)
|
||||
{
|
||||
if (!IsNonPriorityModPath(gamePath))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void CountMissingReplacements(IEnumerable<FileReplacementData> missing, out int critical, out int nonCritical, out int forbidden)
|
||||
{
|
||||
critical = 0;
|
||||
nonCritical = 0;
|
||||
forbidden = 0;
|
||||
|
||||
foreach (var replacement in missing)
|
||||
{
|
||||
if (IsForbiddenHash(replacement.Hash))
|
||||
{
|
||||
forbidden++;
|
||||
}
|
||||
|
||||
if (IsCriticalModReplacement(replacement))
|
||||
{
|
||||
critical++;
|
||||
}
|
||||
else
|
||||
{
|
||||
nonCritical++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void RemoveModApplyChanges(Dictionary<ObjectKind, HashSet<PlayerChanges>> updatedData)
|
||||
{
|
||||
foreach (var changes in updatedData.Values)
|
||||
{
|
||||
changes.Remove(PlayerChanges.ModFiles);
|
||||
changes.Remove(PlayerChanges.ModManip);
|
||||
changes.Remove(PlayerChanges.ForcedRedraw);
|
||||
}
|
||||
}
|
||||
|
||||
private bool CanApplyNow()
|
||||
{
|
||||
return !_dalamudUtil.IsInCombat
|
||||
@@ -760,6 +888,16 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
_lastBlockingConditions = Array.Empty<string>();
|
||||
}
|
||||
|
||||
private void DeferApplication(Guid applicationBase, CharacterData characterData, bool forceApplyCustomization, UserData user, string reason,
|
||||
string failureKey, LogLevel logLevel, string logMessage, params object?[] logArgs)
|
||||
{
|
||||
Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter), EventSeverity.Warning, reason)));
|
||||
Logger.Log(logLevel, logMessage, logArgs);
|
||||
RecordFailure(reason, failureKey);
|
||||
_dataReceivedInDowntime = new(applicationBase, characterData, forceApplyCustomization);
|
||||
SetUploading(false);
|
||||
}
|
||||
|
||||
public void ApplyCharacterData(Guid applicationBase, CharacterData characterData, bool forceApplyCustomization = false)
|
||||
{
|
||||
_lastApplyAttemptAt = DateTime.UtcNow;
|
||||
@@ -777,72 +915,48 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
if (_dalamudUtil.IsInCombat)
|
||||
{
|
||||
const string reason = "Cannot apply character data: you are in combat, deferring application";
|
||||
Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter), EventSeverity.Warning,
|
||||
reason)));
|
||||
Logger.LogDebug("[BASE-{appBase}] Received data but player is in combat", applicationBase);
|
||||
RecordFailure(reason, "Combat");
|
||||
_dataReceivedInDowntime = new(applicationBase, characterData, forceApplyCustomization);
|
||||
SetUploading(false);
|
||||
DeferApplication(applicationBase, characterData, forceApplyCustomization, user, reason, "Combat", LogLevel.Debug,
|
||||
"[BASE-{appBase}] Received data but player is in combat", applicationBase);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_dalamudUtil.IsPerforming)
|
||||
{
|
||||
const string reason = "Cannot apply character data: you are performing music, deferring application";
|
||||
Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter), EventSeverity.Warning,
|
||||
reason)));
|
||||
Logger.LogDebug("[BASE-{appBase}] Received data but player is performing", applicationBase);
|
||||
RecordFailure(reason, "Performance");
|
||||
_dataReceivedInDowntime = new(applicationBase, characterData, forceApplyCustomization);
|
||||
SetUploading(false);
|
||||
DeferApplication(applicationBase, characterData, forceApplyCustomization, user, reason, "Performance", LogLevel.Debug,
|
||||
"[BASE-{appBase}] Received data but player is performing", applicationBase);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_dalamudUtil.IsInInstance)
|
||||
{
|
||||
const string reason = "Cannot apply character data: you are in an instance, deferring application";
|
||||
Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter), EventSeverity.Warning,
|
||||
reason)));
|
||||
Logger.LogDebug("[BASE-{appBase}] Received data but player is in instance", applicationBase);
|
||||
RecordFailure(reason, "Instance");
|
||||
_dataReceivedInDowntime = new(applicationBase, characterData, forceApplyCustomization);
|
||||
SetUploading(false);
|
||||
DeferApplication(applicationBase, characterData, forceApplyCustomization, user, reason, "Instance", LogLevel.Debug,
|
||||
"[BASE-{appBase}] Received data but player is in instance", applicationBase);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_dalamudUtil.IsInCutscene)
|
||||
{
|
||||
const string reason = "Cannot apply character data: you are in a cutscene, deferring application";
|
||||
Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter), EventSeverity.Warning,
|
||||
reason)));
|
||||
Logger.LogDebug("[BASE-{appBase}] Received data but player is in a cutscene", applicationBase);
|
||||
RecordFailure(reason, "Cutscene");
|
||||
_dataReceivedInDowntime = new(applicationBase, characterData, forceApplyCustomization);
|
||||
SetUploading(false);
|
||||
DeferApplication(applicationBase, characterData, forceApplyCustomization, user, reason, "Cutscene", LogLevel.Debug,
|
||||
"[BASE-{appBase}] Received data but player is in a cutscene", applicationBase);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_dalamudUtil.IsInGpose)
|
||||
{
|
||||
const string reason = "Cannot apply character data: you are in GPose, deferring application";
|
||||
Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter), EventSeverity.Warning,
|
||||
reason)));
|
||||
Logger.LogDebug("[BASE-{appBase}] Received data but player is in GPose", applicationBase);
|
||||
RecordFailure(reason, "GPose");
|
||||
_dataReceivedInDowntime = new(applicationBase, characterData, forceApplyCustomization);
|
||||
SetUploading(false);
|
||||
DeferApplication(applicationBase, characterData, forceApplyCustomization, user, reason, "GPose", LogLevel.Debug,
|
||||
"[BASE-{appBase}] Received data but player is in GPose", applicationBase);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_ipcManager.Penumbra.APIAvailable || !_ipcManager.Glamourer.APIAvailable)
|
||||
{
|
||||
const string reason = "Cannot apply character data: Penumbra or Glamourer is not available, deferring application";
|
||||
Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter), EventSeverity.Warning,
|
||||
reason)));
|
||||
Logger.LogInformation("[BASE-{appbase}] Application of data for {player} while Penumbra/Glamourer unavailable, returning", applicationBase, GetLogIdentifier());
|
||||
RecordFailure(reason, "PluginUnavailable");
|
||||
_dataReceivedInDowntime = new(applicationBase, characterData, forceApplyCustomization);
|
||||
SetUploading(false);
|
||||
DeferApplication(applicationBase, characterData, forceApplyCustomization, user, reason, "PluginUnavailable", LogLevel.Information,
|
||||
"[BASE-{appbase}] Application of data for {player} while Penumbra/Glamourer unavailable, returning", applicationBase, GetLogIdentifier());
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -885,13 +999,10 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
_forceApplyMods = false;
|
||||
}
|
||||
|
||||
_explicitRedrawQueued = false;
|
||||
|
||||
if (_redrawOnNextApplication && charaDataToUpdate.TryGetValue(ObjectKind.Player, out var player))
|
||||
{
|
||||
player.Add(PlayerChanges.ForcedRedraw);
|
||||
_redrawOnNextApplication = false;
|
||||
_explicitRedrawQueued = true;
|
||||
}
|
||||
|
||||
if (charaDataToUpdate.TryGetValue(ObjectKind.Player, out var playerChanges))
|
||||
@@ -1085,7 +1196,14 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
|
||||
Logger.LogDebug("[{applicationId}] Applying Customization Data for {handler}", applicationId, handler);
|
||||
await _dalamudUtil.WaitWhileCharacterIsDrawing(Logger, handler, applicationId, 30000, token).ConfigureAwait(false);
|
||||
if (handler.Address != nint.Zero)
|
||||
{
|
||||
await _actorObjectService.WaitForFullyLoadedAsync(handler.Address, token).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
token.ThrowIfCancellationRequested();
|
||||
var tasks = new List<Task>();
|
||||
bool needsRedraw = false;
|
||||
foreach (var change in changes.Value.OrderBy(p => (int)p))
|
||||
{
|
||||
Logger.LogDebug("[{applicationId}] Processing {change} for {handler}", applicationId, change, handler);
|
||||
@@ -1094,45 +1212,39 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
case PlayerChanges.Customize:
|
||||
if (charaData.CustomizePlusData.TryGetValue(changes.Key, out var customizePlusData))
|
||||
{
|
||||
_customizeIds[changes.Key] = await _ipcManager.CustomizePlus.SetBodyScaleAsync(handler.Address, customizePlusData).ConfigureAwait(false);
|
||||
tasks.Add(ApplyCustomizeAsync(handler.Address, customizePlusData, changes.Key));
|
||||
}
|
||||
else if (_customizeIds.TryGetValue(changes.Key, out var customizeId))
|
||||
{
|
||||
await _ipcManager.CustomizePlus.RevertByIdAsync(customizeId).ConfigureAwait(false);
|
||||
_customizeIds.Remove(changes.Key);
|
||||
tasks.Add(RevertCustomizeAsync(customizeId, changes.Key));
|
||||
}
|
||||
break;
|
||||
|
||||
case PlayerChanges.Heels:
|
||||
await _ipcManager.Heels.SetOffsetForPlayerAsync(handler.Address, charaData.HeelsData).ConfigureAwait(false);
|
||||
tasks.Add(_ipcManager.Heels.SetOffsetForPlayerAsync(handler.Address, charaData.HeelsData));
|
||||
break;
|
||||
|
||||
case PlayerChanges.Honorific:
|
||||
await _ipcManager.Honorific.SetTitleAsync(handler.Address, charaData.HonorificData).ConfigureAwait(false);
|
||||
tasks.Add(_ipcManager.Honorific.SetTitleAsync(handler.Address, charaData.HonorificData));
|
||||
break;
|
||||
|
||||
case PlayerChanges.Glamourer:
|
||||
if (charaData.GlamourerData.TryGetValue(changes.Key, out var glamourerData))
|
||||
{
|
||||
await _ipcManager.Glamourer.ApplyAllAsync(Logger, handler, glamourerData, applicationId, token).ConfigureAwait(false);
|
||||
tasks.Add(_ipcManager.Glamourer.ApplyAllAsync(Logger, handler, glamourerData, applicationId, token));
|
||||
}
|
||||
break;
|
||||
|
||||
case PlayerChanges.Moodles:
|
||||
await _ipcManager.Moodles.SetStatusAsync(handler.Address, charaData.MoodlesData).ConfigureAwait(false);
|
||||
tasks.Add(_ipcManager.Moodles.SetStatusAsync(handler.Address, charaData.MoodlesData));
|
||||
break;
|
||||
|
||||
case PlayerChanges.PetNames:
|
||||
await _ipcManager.PetNames.SetPlayerData(handler.Address, charaData.PetNamesData).ConfigureAwait(false);
|
||||
tasks.Add(_ipcManager.PetNames.SetPlayerData(handler.Address, charaData.PetNamesData));
|
||||
break;
|
||||
|
||||
case PlayerChanges.ForcedRedraw:
|
||||
if (!ShouldPerformForcedRedraw(changes.Key, changes.Value, charaData))
|
||||
{
|
||||
Logger.LogTrace("[{applicationId}] Skipping forced redraw for {handler}", applicationId, handler);
|
||||
break;
|
||||
}
|
||||
await _ipcManager.Penumbra.RedrawAsync(Logger, handler, applicationId, token).ConfigureAwait(false);
|
||||
needsRedraw = true;
|
||||
break;
|
||||
|
||||
default:
|
||||
@@ -1140,6 +1252,16 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
}
|
||||
token.ThrowIfCancellationRequested();
|
||||
}
|
||||
|
||||
if (tasks.Count > 0)
|
||||
{
|
||||
await Task.WhenAll(tasks).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (needsRedraw)
|
||||
{
|
||||
await _ipcManager.Penumbra.RedrawAsync(Logger, handler, applicationId, token).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -1147,44 +1269,6 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
}
|
||||
}
|
||||
|
||||
private bool ShouldPerformForcedRedraw(ObjectKind objectKind, ICollection<PlayerChanges> changeSet, CharacterData newData)
|
||||
{
|
||||
if (objectKind != ObjectKind.Player)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var hasModFiles = changeSet.Contains(PlayerChanges.ModFiles);
|
||||
var hasManip = changeSet.Contains(PlayerChanges.ModManip);
|
||||
var modsChanged = hasModFiles && PlayerModFilesChanged(newData, _cachedData);
|
||||
var manipChanged = hasManip && !string.Equals(_cachedData?.ManipulationData, newData.ManipulationData, StringComparison.Ordinal);
|
||||
|
||||
if (modsChanged)
|
||||
{
|
||||
_explicitRedrawQueued = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (manipChanged)
|
||||
{
|
||||
_explicitRedrawQueued = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_explicitRedrawQueued)
|
||||
{
|
||||
_explicitRedrawQueued = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
if ((hasModFiles || hasManip) && (_forceFullReapply || _needsCollectionRebuild))
|
||||
{
|
||||
_explicitRedrawQueued = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static Dictionary<ObjectKind, HashSet<PlayerChanges>> BuildFullChangeSet(CharacterData characterData)
|
||||
{
|
||||
@@ -1339,6 +1423,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
bool skipDownscaleForPair = ShouldSkipDownscale();
|
||||
var user = GetPrimaryUserData();
|
||||
Dictionary<(string GamePath, string? Hash), string> moddedPaths;
|
||||
List<FileReplacementData> missingReplacements = [];
|
||||
|
||||
if (updateModdedPaths)
|
||||
{
|
||||
@@ -1350,6 +1435,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
{
|
||||
int attempts = 0;
|
||||
List<FileReplacementData> toDownloadReplacements = TryCalculateModdedDictionary(applicationBase, charaData, out moddedPaths, downloadToken);
|
||||
missingReplacements = toDownloadReplacements;
|
||||
|
||||
while (toDownloadReplacements.Count > 0 && attempts++ <= 10 && !downloadToken.IsCancellationRequested)
|
||||
{
|
||||
@@ -1399,6 +1485,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
}
|
||||
|
||||
toDownloadReplacements = TryCalculateModdedDictionary(applicationBase, charaData, out moddedPaths, downloadToken);
|
||||
missingReplacements = toDownloadReplacements;
|
||||
|
||||
if (toDownloadReplacements.TrueForAll(c => _downloadManager.ForbiddenTransfers.Exists(f => string.Equals(f.Hash, c.Hash, StringComparison.Ordinal))))
|
||||
{
|
||||
@@ -1422,6 +1509,54 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
: [];
|
||||
}
|
||||
|
||||
var wantsModApply = updateModdedPaths || updateManip;
|
||||
var pendingModReapply = false;
|
||||
var deferModApply = false;
|
||||
|
||||
if (wantsModApply && missingReplacements.Count > 0)
|
||||
{
|
||||
CountMissingReplacements(missingReplacements, out var missingCritical, out var missingNonCritical, out var missingForbidden);
|
||||
_lastMissingCriticalMods = missingCritical;
|
||||
_lastMissingNonCriticalMods = missingNonCritical;
|
||||
_lastMissingForbiddenMods = missingForbidden;
|
||||
|
||||
var hasCriticalMissing = missingCritical > 0;
|
||||
var hasNonCriticalMissing = missingNonCritical > 0;
|
||||
var hasDownloadableMissing = missingReplacements.Any(replacement => !IsForbiddenHash(replacement.Hash));
|
||||
var hasDownloadableCriticalMissing = hasCriticalMissing
|
||||
&& missingReplacements.Any(replacement => !IsForbiddenHash(replacement.Hash) && IsCriticalModReplacement(replacement));
|
||||
|
||||
pendingModReapply = hasDownloadableMissing;
|
||||
_lastModApplyDeferred = false;
|
||||
|
||||
if (hasDownloadableCriticalMissing)
|
||||
{
|
||||
deferModApply = true;
|
||||
_lastModApplyDeferred = true;
|
||||
Logger.LogDebug("[BASE-{appBase}] Critical mod files missing for {handler}, deferring mod apply ({count} missing)",
|
||||
applicationBase, GetLogIdentifier(), missingReplacements.Count);
|
||||
}
|
||||
else if (hasNonCriticalMissing && hasDownloadableMissing)
|
||||
{
|
||||
Logger.LogDebug("[BASE-{appBase}] Non-critical mod files missing for {handler}, applying partial mods and reapplying after downloads ({count} missing)",
|
||||
applicationBase, GetLogIdentifier(), missingReplacements.Count);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_lastMissingCriticalMods = 0;
|
||||
_lastMissingNonCriticalMods = 0;
|
||||
_lastMissingForbiddenMods = 0;
|
||||
_lastModApplyDeferred = false;
|
||||
}
|
||||
|
||||
if (deferModApply)
|
||||
{
|
||||
updateModdedPaths = false;
|
||||
updateManip = false;
|
||||
RemoveModApplyChanges(updatedData);
|
||||
}
|
||||
|
||||
downloadToken.ThrowIfCancellationRequested();
|
||||
|
||||
var handlerForApply = _charaHandler;
|
||||
@@ -1454,7 +1589,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
_applicationCancellationTokenSource = _applicationCancellationTokenSource.CancelRecreate() ?? new CancellationTokenSource();
|
||||
var token = _applicationCancellationTokenSource.Token;
|
||||
|
||||
_applicationTask = ApplyCharacterDataAsync(applicationBase, handlerForApply, charaData, updatedData, updateModdedPaths, updateManip, moddedPaths, token);
|
||||
_applicationTask = ApplyCharacterDataAsync(applicationBase, handlerForApply, charaData, updatedData, updateModdedPaths, updateManip, moddedPaths, wantsModApply, pendingModReapply, token);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -1463,7 +1598,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
}
|
||||
|
||||
private async Task ApplyCharacterDataAsync(Guid applicationBase, GameObjectHandler handlerForApply, CharacterData charaData, Dictionary<ObjectKind, HashSet<PlayerChanges>> updatedData, bool updateModdedPaths, bool updateManip,
|
||||
Dictionary<(string GamePath, string? Hash), string> moddedPaths, CancellationToken token)
|
||||
Dictionary<(string GamePath, string? Hash), string> moddedPaths, bool wantsModApply, bool pendingModReapply, CancellationToken token)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -1472,6 +1607,10 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
|
||||
Logger.LogDebug("[{applicationId}] Waiting for initial draw for for {handler}", _applicationId, handlerForApply);
|
||||
await _dalamudUtil.WaitWhileCharacterIsDrawing(Logger, handlerForApply, _applicationId, 30000, token).ConfigureAwait(false);
|
||||
if (handlerForApply.Address != nint.Zero)
|
||||
{
|
||||
await _actorObjectService.WaitForFullyLoadedAsync(handlerForApply.Address, token).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
@@ -1538,7 +1677,11 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
|
||||
_cachedData = charaData;
|
||||
_pairStateCache.Store(Ident, charaData);
|
||||
_forceFullReapply = false;
|
||||
if (wantsModApply)
|
||||
{
|
||||
_pendingModReapply = pendingModReapply;
|
||||
}
|
||||
_forceFullReapply = _pendingModReapply;
|
||||
_needsCollectionRebuild = false;
|
||||
if (LastAppliedApproximateVRAMBytes < 0 || LastAppliedApproximateEffectiveVRAMBytes < 0)
|
||||
{
|
||||
@@ -1584,8 +1727,15 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
|
||||
private void FrameworkUpdate()
|
||||
{
|
||||
if (string.IsNullOrEmpty(PlayerName))
|
||||
if (string.IsNullOrEmpty(PlayerName) && _charaHandler is null)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
if (now < _nextActorLookupUtc)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_nextActorLookupUtc = now + ActorLookupInterval;
|
||||
var pc = _dalamudUtil.FindPlayerByNameHash(Ident);
|
||||
if (pc == default((string, nint))) return;
|
||||
Logger.LogDebug("One-Time Initializing {handler}", GetLogIdentifier());
|
||||
@@ -1595,6 +1745,11 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
$"Initializing User For Character {pc.Name}")));
|
||||
}
|
||||
|
||||
TryHandleVisibilityUpdate();
|
||||
}
|
||||
|
||||
private void TryHandleVisibilityUpdate()
|
||||
{
|
||||
if (_charaHandler?.Address != nint.Zero && !IsVisible && !_pauseRequested)
|
||||
{
|
||||
Guid appData = Guid.NewGuid();
|
||||
@@ -1641,16 +1796,24 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
}
|
||||
else if (_charaHandler?.Address == nint.Zero && IsVisible)
|
||||
{
|
||||
IsVisible = false;
|
||||
_charaHandler.Invalidate();
|
||||
_downloadCancellationTokenSource?.CancelDispose();
|
||||
_downloadCancellationTokenSource = null;
|
||||
Logger.LogTrace("{handler} visibility changed, now: {visi}", GetLogIdentifier(), IsVisible);
|
||||
HandleVisibilityLoss(logChange: true);
|
||||
}
|
||||
|
||||
TryApplyQueuedData();
|
||||
}
|
||||
|
||||
private void HandleVisibilityLoss(bool logChange)
|
||||
{
|
||||
IsVisible = false;
|
||||
_charaHandler?.Invalidate();
|
||||
_downloadCancellationTokenSource?.CancelDispose();
|
||||
_downloadCancellationTokenSource = null;
|
||||
if (logChange)
|
||||
{
|
||||
Logger.LogTrace("{handler} visibility changed, now: {visi}", GetLogIdentifier(), IsVisible);
|
||||
}
|
||||
}
|
||||
|
||||
private void Initialize(string name)
|
||||
{
|
||||
PlayerName = name;
|
||||
@@ -1977,7 +2140,164 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
}
|
||||
|
||||
_dataReceivedInDowntime = null;
|
||||
ApplyCharacterData(pending.ApplicationId,
|
||||
pending.CharacterData, pending.Forced);
|
||||
_ = Task.Run(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
ApplyCharacterData(pending.ApplicationId, pending.CharacterData, pending.Forced);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Failed applying queued data for {handler}", GetLogIdentifier());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void HandleActorTracked(ActorObjectService.ActorDescriptor descriptor)
|
||||
{
|
||||
if (!TryResolveDescriptorHash(descriptor, out var hashedCid))
|
||||
return;
|
||||
|
||||
if (!string.Equals(hashedCid, Ident, StringComparison.Ordinal))
|
||||
return;
|
||||
|
||||
if (descriptor.Address == nint.Zero)
|
||||
return;
|
||||
|
||||
RefreshTrackedHandler(descriptor);
|
||||
QueueActorInitialization(descriptor);
|
||||
}
|
||||
|
||||
private void QueueActorInitialization(ActorObjectService.ActorDescriptor descriptor)
|
||||
{
|
||||
lock (_actorInitializationGate)
|
||||
{
|
||||
_pendingActorDescriptor = descriptor;
|
||||
if (_actorInitializationInProgress)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_actorInitializationInProgress = true;
|
||||
}
|
||||
|
||||
_ = Task.Run(InitializeFromTrackedAsync);
|
||||
}
|
||||
|
||||
private async Task InitializeFromTrackedAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
await ActorInitializationLimiter.WaitAsync().ConfigureAwait(false);
|
||||
while (true)
|
||||
{
|
||||
ActorObjectService.ActorDescriptor? descriptor;
|
||||
lock (_actorInitializationGate)
|
||||
{
|
||||
descriptor = _pendingActorDescriptor;
|
||||
_pendingActorDescriptor = null;
|
||||
}
|
||||
|
||||
if (!descriptor.HasValue)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (_frameworkUpdateSubscribed && _actorObjectService.HooksActive)
|
||||
{
|
||||
Mediator.Unsubscribe<FrameworkUpdateMessage>(this);
|
||||
_frameworkUpdateSubscribed = false;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(PlayerName) || _charaHandler is null)
|
||||
{
|
||||
Logger.LogDebug("Actor tracked for {handler}, initializing from hook", GetLogIdentifier());
|
||||
Initialize(descriptor.Value.Name);
|
||||
Mediator.Publish(new EventMessage(new Event(PlayerName, GetPrimaryUserData(), nameof(PairHandlerAdapter), EventSeverity.Informational,
|
||||
$"Initializing User For Character {descriptor.Value.Name}")));
|
||||
}
|
||||
|
||||
RefreshTrackedHandler(descriptor.Value);
|
||||
TryHandleVisibilityUpdate();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
ActorInitializationLimiter.Release();
|
||||
lock (_actorInitializationGate)
|
||||
{
|
||||
_actorInitializationInProgress = false;
|
||||
if (_pendingActorDescriptor.HasValue)
|
||||
{
|
||||
_actorInitializationInProgress = true;
|
||||
_ = Task.Run(InitializeFromTrackedAsync);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshTrackedHandler(ActorObjectService.ActorDescriptor descriptor)
|
||||
{
|
||||
if (_charaHandler is null)
|
||||
return;
|
||||
|
||||
if (descriptor.Address == nint.Zero)
|
||||
return;
|
||||
|
||||
if (_charaHandler.Address == descriptor.Address)
|
||||
return;
|
||||
|
||||
_charaHandler.Refresh();
|
||||
}
|
||||
|
||||
private void HandleActorUntracked(ActorObjectService.ActorDescriptor descriptor)
|
||||
{
|
||||
if (!TryResolveDescriptorHash(descriptor, out var hashedCid))
|
||||
{
|
||||
if (_charaHandler is null || _charaHandler.Address == nint.Zero)
|
||||
return;
|
||||
|
||||
if (descriptor.Address != _charaHandler.Address)
|
||||
return;
|
||||
}
|
||||
else if (!string.Equals(hashedCid, Ident, StringComparison.Ordinal))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_charaHandler is null || _charaHandler.Address == nint.Zero)
|
||||
return;
|
||||
|
||||
if (descriptor.Address != _charaHandler.Address)
|
||||
return;
|
||||
|
||||
HandleVisibilityLoss(logChange: false);
|
||||
}
|
||||
|
||||
private bool TryResolveDescriptorHash(ActorObjectService.ActorDescriptor descriptor, out string hashedCid)
|
||||
{
|
||||
hashedCid = descriptor.HashedContentId ?? string.Empty;
|
||||
if (!string.IsNullOrEmpty(hashedCid))
|
||||
return true;
|
||||
|
||||
if (descriptor.ObjectKind != DalamudObjectKind.Player || descriptor.Address == nint.Zero)
|
||||
return false;
|
||||
|
||||
hashedCid = DalamudUtilService.GetHashedCIDFromPlayerPointer(descriptor.Address);
|
||||
return !string.IsNullOrEmpty(hashedCid);
|
||||
}
|
||||
|
||||
private async Task ApplyCustomizeAsync(nint address, string customizeData, ObjectKind kind)
|
||||
{
|
||||
_customizeIds[kind] = await _ipcManager.CustomizePlus.SetBodyScaleAsync(address, customizeData).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task RevertCustomizeAsync(Guid? customizeId, ObjectKind kind)
|
||||
{
|
||||
if (!customizeId.HasValue)
|
||||
return;
|
||||
|
||||
await _ipcManager.CustomizePlus.RevertByIdAsync(customizeId.Value).ConfigureAwait(false);
|
||||
_customizeIds.Remove(kind);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ using LightlessSync.FileCache;
|
||||
using LightlessSync.Interop.Ipc;
|
||||
using LightlessSync.PlayerData.Factories;
|
||||
using LightlessSync.Services;
|
||||
using LightlessSync.Services.ActorTracking;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.Services.PairProcessing;
|
||||
using LightlessSync.Services.ServerConfiguration;
|
||||
@@ -71,6 +72,7 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory
|
||||
{
|
||||
var downloadManager = _fileDownloadManagerFactory.Create();
|
||||
var dalamudUtilService = _serviceProvider.GetRequiredService<DalamudUtilService>();
|
||||
var actorObjectService = _serviceProvider.GetRequiredService<ActorObjectService>();
|
||||
return new PairHandlerAdapter(
|
||||
_loggerFactory.CreateLogger<PairHandlerAdapter>(),
|
||||
_mediator,
|
||||
@@ -81,6 +83,7 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory
|
||||
downloadManager,
|
||||
_pluginWarningNotificationManager,
|
||||
dalamudUtilService,
|
||||
actorObjectService,
|
||||
_lifetime,
|
||||
_fileCacheManager,
|
||||
_playerPerformanceService,
|
||||
|
||||
@@ -201,6 +201,7 @@ public sealed class Plugin : IDalamudPlugin
|
||||
gameInteropProvider,
|
||||
objectTable,
|
||||
clientState,
|
||||
condition,
|
||||
sp.GetRequiredService<LightlessMediator>()));
|
||||
|
||||
services.AddSingleton(sp => new DalamudUtilService(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -133,6 +133,26 @@ public class DrawUserPair
|
||||
UiSharedService.AttachToolTip("This reapplies the last received character data to this character");
|
||||
}
|
||||
|
||||
var isPaused = _pair.UserPair!.OwnPermissions.IsPaused();
|
||||
if (!isPaused)
|
||||
{
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Pause, "Toggle Pause State", _menuWidth, true))
|
||||
{
|
||||
_ = _apiController.PauseAsync(_pair.UserData);
|
||||
ImGui.CloseCurrentPopup();
|
||||
}
|
||||
UiSharedService.AttachToolTip("Pauses syncing with this user.");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Play, "Toggle Unpause State", _menuWidth, true))
|
||||
{
|
||||
_ = _apiController.UnpauseAsync(_pair.UserData);
|
||||
ImGui.CloseCurrentPopup();
|
||||
}
|
||||
UiSharedService.AttachToolTip("Resumes syncing with this user.");
|
||||
}
|
||||
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.PlayCircle, "Cycle pause state", _menuWidth, true))
|
||||
{
|
||||
_ = _apiController.CyclePauseAsync(_pair);
|
||||
|
||||
@@ -1546,6 +1546,11 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
||||
DrawPairPropertyRow("Downloading", FormatBool(debugInfo.IsDownloading));
|
||||
DrawPairPropertyRow("Pending Downloads", debugInfo.PendingDownloadCount.ToString(CultureInfo.InvariantCulture));
|
||||
DrawPairPropertyRow("Forbidden Downloads", debugInfo.ForbiddenDownloadCount.ToString(CultureInfo.InvariantCulture));
|
||||
DrawPairPropertyRow("Pending Mod Reapply", FormatBool(debugInfo.PendingModReapply));
|
||||
DrawPairPropertyRow("Mod Apply Deferred", FormatBool(debugInfo.ModApplyDeferred));
|
||||
DrawPairPropertyRow("Missing Critical Mods", debugInfo.MissingCriticalMods.ToString(CultureInfo.InvariantCulture));
|
||||
DrawPairPropertyRow("Missing Non-Critical Mods", debugInfo.MissingNonCriticalMods.ToString(CultureInfo.InvariantCulture));
|
||||
DrawPairPropertyRow("Missing Forbidden Mods", debugInfo.MissingForbiddenMods.ToString(CultureInfo.InvariantCulture));
|
||||
ImGui.EndTable();
|
||||
}
|
||||
|
||||
|
||||
@@ -297,6 +297,25 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
|
||||
var ownerTab = ImRaii.TabItem("Owner Settings");
|
||||
if (ownerTab)
|
||||
{
|
||||
bool isChatDisabled = perm.IsDisableChat();
|
||||
ImGui.AlignTextToFramePadding();
|
||||
ImGui.TextUnformatted("Syncshell Chat");
|
||||
_uiSharedService.BooleanToColoredIcon(!isChatDisabled);
|
||||
ImGui.SameLine(230);
|
||||
using (ImRaii.PushColor(ImGuiCol.Text, isChatDisabled ? UIColors.Get("PairBlue") : UIColors.Get("DimRed")))
|
||||
{
|
||||
if (_uiSharedService.IconTextButton(
|
||||
isChatDisabled ? FontAwesomeIcon.Comment : FontAwesomeIcon.Ban,
|
||||
isChatDisabled ? "Enable syncshell chat" : "Disable syncshell chat"))
|
||||
{
|
||||
perm.SetDisableChat(!isChatDisabled);
|
||||
_ = _apiController.GroupChangeGroupPermissionState(new(GroupFullInfo.Group, perm));
|
||||
}
|
||||
}
|
||||
UiSharedService.AttachToolTip("Disables syncshell chat for all members.");
|
||||
|
||||
ImGuiHelpers.ScaledDummy(6f);
|
||||
|
||||
ImGui.AlignTextToFramePadding();
|
||||
ImGui.TextUnformatted("New Password");
|
||||
var availableWidth = ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X;
|
||||
|
||||
@@ -140,19 +140,10 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
||||
return;
|
||||
}
|
||||
|
||||
string? myHashedCid = null;
|
||||
try
|
||||
{
|
||||
var cid = _dalamudUtilService.GetCID();
|
||||
myHashedCid = cid.ToString().GetHash256();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogDebug(ex, "Failed to get CID, not excluding own broadcast.");
|
||||
}
|
||||
var broadcasts = _broadcastScannerService.GetActiveSyncshellBroadcasts().Where(b => !string.Equals(b.HashedCID, myHashedCid, StringComparison.Ordinal)).ToList() ?? [];
|
||||
var broadcasts = _broadcastScannerService.GetActiveSyncshellBroadcasts().ToList() ?? [];
|
||||
_broadcastScannerService.TryGetLocalHashedCid(out var localHashedCid);
|
||||
|
||||
var cardData = new List<(GroupJoinDto Shell, string BroadcasterName)>();
|
||||
var cardData = new List<(GroupJoinDto Shell, string BroadcasterName, bool IsSelfBroadcast)>();
|
||||
|
||||
foreach (var shell in _nearbySyncshells)
|
||||
{
|
||||
@@ -185,9 +176,15 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
||||
broadcasterName = !string.IsNullOrEmpty(worldName)
|
||||
? $"{name} ({worldName})"
|
||||
: name;
|
||||
|
||||
var isSelfBroadcast = !string.IsNullOrEmpty(localHashedCid)
|
||||
&& string.Equals(broadcast.HashedCID, localHashedCid, StringComparison.Ordinal);
|
||||
|
||||
cardData.Add((shell, broadcasterName, isSelfBroadcast));
|
||||
continue;
|
||||
}
|
||||
|
||||
cardData.Add((shell, broadcasterName));
|
||||
cardData.Add((shell, broadcasterName, false));
|
||||
}
|
||||
|
||||
if (cardData.Count == 0)
|
||||
@@ -210,7 +207,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
||||
DrawConfirmation();
|
||||
}
|
||||
|
||||
private void DrawSyncshellList(List<(GroupJoinDto Shell, string BroadcasterName)> listData)
|
||||
private void DrawSyncshellList(List<(GroupJoinDto Shell, string BroadcasterName, bool IsSelfBroadcast)> listData)
|
||||
{
|
||||
const int shellsPerPage = 3;
|
||||
var totalPages = (int)Math.Ceiling(listData.Count / (float)shellsPerPage);
|
||||
@@ -227,7 +224,10 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
||||
|
||||
for (int index = firstIndex; index < lastExclusive; index++)
|
||||
{
|
||||
var (shell, broadcasterName) = listData[index];
|
||||
var (shell, broadcasterName, isSelfBroadcast) = listData[index];
|
||||
var broadcasterLabel = string.IsNullOrEmpty(broadcasterName)
|
||||
? (isSelfBroadcast ? "You" : string.Empty)
|
||||
: (isSelfBroadcast ? $"{broadcasterName} (You)" : broadcasterName);
|
||||
|
||||
ImGui.PushID(shell.Group.GID);
|
||||
float rowHeight = 74f * ImGuiHelpers.GlobalScale;
|
||||
@@ -239,7 +239,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
||||
var style = ImGui.GetStyle();
|
||||
float startX = ImGui.GetCursorPosX();
|
||||
float regionW = ImGui.GetContentRegionAvail().X;
|
||||
float rightTxtW = ImGui.CalcTextSize(broadcasterName).X;
|
||||
float rightTxtW = ImGui.CalcTextSize(broadcasterLabel).X;
|
||||
|
||||
_uiSharedService.MediumText(displayName, UIColors.Get("LightlessPurple"));
|
||||
if (ImGui.IsItemHovered())
|
||||
@@ -252,7 +252,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
||||
float rightX = startX + regionW - rightTxtW - style.ItemSpacing.X;
|
||||
ImGui.SameLine();
|
||||
ImGui.SetCursorPosX(rightX);
|
||||
ImGui.TextUnformatted(broadcasterName);
|
||||
ImGui.TextUnformatted(broadcasterLabel);
|
||||
if (ImGui.IsItemHovered())
|
||||
ImGui.SetTooltip("Broadcaster of the syncshell.");
|
||||
|
||||
@@ -291,7 +291,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
||||
float joinX = rowStartLocal.X + (tagsWidth > 0 ? tagsWidth + style.ItemSpacing.X : 0f);
|
||||
|
||||
ImGui.SetCursorPos(new Vector2(joinX, btnBaselineY));
|
||||
DrawJoinButton(shell);
|
||||
DrawJoinButton(shell, isSelfBroadcast);
|
||||
|
||||
float btnHeight = ImGui.GetFrameHeightWithSpacing();
|
||||
float rowHeightUsed = MathF.Max(tagsHeight, btnHeight);
|
||||
@@ -311,7 +311,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
||||
DrawPagination(totalPages);
|
||||
}
|
||||
|
||||
private void DrawSyncshellGrid(List<(GroupJoinDto Shell, string BroadcasterName)> cardData)
|
||||
private void DrawSyncshellGrid(List<(GroupJoinDto Shell, string BroadcasterName, bool IsSelfBroadcast)> cardData)
|
||||
{
|
||||
const int shellsPerPage = 4;
|
||||
var totalPages = (int)Math.Ceiling(cardData.Count / (float)shellsPerPage);
|
||||
@@ -336,7 +336,10 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
||||
for (int index = firstIndex; index < lastExclusive; index++)
|
||||
{
|
||||
var localIndex = index - firstIndex;
|
||||
var (shell, broadcasterName) = cardData[index];
|
||||
var (shell, broadcasterName, isSelfBroadcast) = cardData[index];
|
||||
var broadcasterLabel = string.IsNullOrEmpty(broadcasterName)
|
||||
? (isSelfBroadcast ? "You" : string.Empty)
|
||||
: (isSelfBroadcast ? $"{broadcasterName} (You)" : broadcasterName);
|
||||
|
||||
if (localIndex % 2 != 0)
|
||||
ImGui.SameLine();
|
||||
@@ -373,17 +376,17 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
||||
|
||||
float maxBroadcasterWidth = regionRightX - minBroadcasterX;
|
||||
|
||||
string broadcasterToShow = broadcasterName;
|
||||
string broadcasterToShow = broadcasterLabel;
|
||||
|
||||
if (!string.IsNullOrEmpty(broadcasterName) && maxBroadcasterWidth > 0f)
|
||||
if (!string.IsNullOrEmpty(broadcasterLabel) && maxBroadcasterWidth > 0f)
|
||||
{
|
||||
float bcFullWidth = ImGui.CalcTextSize(broadcasterName).X;
|
||||
float bcFullWidth = ImGui.CalcTextSize(broadcasterLabel).X;
|
||||
string toolTip;
|
||||
|
||||
if (bcFullWidth > maxBroadcasterWidth)
|
||||
{
|
||||
broadcasterToShow = TruncateTextToWidth(broadcasterName, maxBroadcasterWidth);
|
||||
toolTip = broadcasterName + Environment.NewLine + Environment.NewLine + "Broadcaster of the syncshell.";
|
||||
broadcasterToShow = TruncateTextToWidth(broadcasterLabel, maxBroadcasterWidth);
|
||||
toolTip = broadcasterLabel + Environment.NewLine + Environment.NewLine + "Broadcaster of the syncshell.";
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -443,7 +446,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
||||
if (remainingY > 0)
|
||||
ImGui.Dummy(new Vector2(0, remainingY));
|
||||
|
||||
DrawJoinButton(shell);
|
||||
DrawJoinButton(shell, isSelfBroadcast);
|
||||
|
||||
ImGui.EndChild();
|
||||
ImGui.EndGroup();
|
||||
@@ -489,7 +492,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawJoinButton(dynamic shell)
|
||||
private void DrawJoinButton(GroupJoinDto shell, bool isSelfBroadcast)
|
||||
{
|
||||
const string visibleLabel = "Join";
|
||||
var label = $"{visibleLabel}##{shell.Group.GID}";
|
||||
@@ -517,7 +520,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
||||
buttonSize = new Vector2(-1, 0);
|
||||
}
|
||||
|
||||
if (!isAlreadyMember && !isRecentlyJoined)
|
||||
if (!isAlreadyMember && !isRecentlyJoined && !isSelfBroadcast)
|
||||
{
|
||||
ImGui.PushStyleColor(ImGuiCol.Button, UIColors.Get("LightlessGreen"));
|
||||
ImGui.PushStyleColor(ImGuiCol.ButtonHovered, UIColors.Get("LightlessGreen").WithAlpha(0.85f));
|
||||
@@ -567,7 +570,9 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
||||
ImGui.Button(label, buttonSize);
|
||||
}
|
||||
|
||||
UiSharedService.AttachToolTip("Already a member or owner of this Syncshell.");
|
||||
UiSharedService.AttachToolTip(isSelfBroadcast
|
||||
? "This is your own Syncshell."
|
||||
: "Already a member or owner of this Syncshell.");
|
||||
}
|
||||
|
||||
ImGui.PopStyleColor(3);
|
||||
|
||||
@@ -800,22 +800,9 @@ public class TopTabMenu
|
||||
if (!_lightFinderService.IsBroadcasting)
|
||||
return "Syncshell Finder";
|
||||
|
||||
string? myHashedCid = null;
|
||||
try
|
||||
{
|
||||
var cid = _dalamudUtilService.GetCID();
|
||||
myHashedCid = cid.ToString().GetHash256();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Couldnt get own CID, log and return default table
|
||||
}
|
||||
|
||||
var nearbyCount = _lightFinderScannerService
|
||||
.GetActiveSyncshellBroadcasts()
|
||||
.Where(b =>
|
||||
!string.IsNullOrEmpty(b.GID) &&
|
||||
!string.Equals(b.HashedCID, myHashedCid, StringComparison.Ordinal))
|
||||
.GetActiveSyncshellBroadcasts(excludeLocal: true)
|
||||
.Where(b => !string.IsNullOrEmpty(b.GID))
|
||||
.Select(b => b.GID!)
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.Count();
|
||||
|
||||
@@ -947,13 +947,16 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
|
||||
}
|
||||
}
|
||||
|
||||
if (_discordOAuthCheck != null && _discordOAuthCheck.IsCompleted)
|
||||
if (_discordOAuthCheck != null && _discordOAuthCheck.IsCompleted && _discordOAuthCheck.Result != null)
|
||||
{
|
||||
if (IconTextButton(FontAwesomeIcon.ArrowRight, "Authenticate with Server"))
|
||||
if (_discordOAuthGetCode == null)
|
||||
{
|
||||
_discordOAuthGetCode = _serverConfigurationManager.GetDiscordOAuthToken(_discordOAuthCheck.Result!, selectedServer.ServerUri, _discordOAuthGetCts.Token);
|
||||
if (IconTextButton(FontAwesomeIcon.ArrowRight, "Authenticate with Server"))
|
||||
{
|
||||
_discordOAuthGetCode = _serverConfigurationManager.GetDiscordOAuthToken(_discordOAuthCheck.Result, selectedServer.ServerUri, _discordOAuthGetCts.Token);
|
||||
}
|
||||
}
|
||||
else if (_discordOAuthGetCode != null && !_discordOAuthGetCode.IsCompleted)
|
||||
else if (!_discordOAuthGetCode.IsCompleted)
|
||||
{
|
||||
TextWrapped("A browser window has been opened, follow it to authenticate. Click the button below if you accidentally closed the window and need to restart the authentication.");
|
||||
if (IconTextButton(FontAwesomeIcon.Ban, "Cancel Authentication"))
|
||||
@@ -962,7 +965,7 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
|
||||
_discordOAuthGetCode = null;
|
||||
}
|
||||
}
|
||||
else if (_discordOAuthGetCode != null && _discordOAuthGetCode.IsCompleted)
|
||||
else
|
||||
{
|
||||
TextWrapped("Discord OAuth is completed, status: ");
|
||||
ImGui.SameLine();
|
||||
|
||||
@@ -17,6 +17,7 @@ using LightlessSync.Services.ServerConfiguration;
|
||||
using LightlessSync.UI.Services;
|
||||
using LightlessSync.UI.Style;
|
||||
using LightlessSync.Utils;
|
||||
using OtterGui.Text;
|
||||
using LightlessSync.WebAPI;
|
||||
using LightlessSync.WebAPI.SignalR.Utils;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -211,12 +212,6 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
||||
|
||||
protected override void DrawInternal()
|
||||
{
|
||||
if (_titleBarStylePopCount > 0)
|
||||
{
|
||||
ImGui.PopStyleColor(_titleBarStylePopCount);
|
||||
_titleBarStylePopCount = 0;
|
||||
}
|
||||
|
||||
var config = _chatConfigService.Current;
|
||||
var isFocused = ImGui.IsWindowFocused(ImGuiFocusedFlags.RootAndChildWindows);
|
||||
var isHovered = ImGui.IsWindowHovered(ImGuiHoveredFlags.RootAndChildWindows);
|
||||
@@ -400,52 +395,57 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
||||
}
|
||||
else
|
||||
{
|
||||
for (var i = 0; i < channel.Messages.Count; i++)
|
||||
var itemHeight = ImGui.GetTextLineHeightWithSpacing();
|
||||
using var clipper = ImUtf8.ListClipper(channel.Messages.Count, itemHeight);
|
||||
while (clipper.Step())
|
||||
{
|
||||
var message = channel.Messages[i];
|
||||
ImGui.PushID(i);
|
||||
|
||||
if (message.IsSystem)
|
||||
for (var i = clipper.DisplayStart; i < clipper.DisplayEnd; i++)
|
||||
{
|
||||
DrawSystemEntry(message);
|
||||
ImGui.PopID();
|
||||
continue;
|
||||
}
|
||||
var message = channel.Messages[i];
|
||||
ImGui.PushID(i);
|
||||
|
||||
if (message.Payload is not { } payload)
|
||||
{
|
||||
ImGui.PopID();
|
||||
continue;
|
||||
}
|
||||
|
||||
var timestampText = string.Empty;
|
||||
if (showTimestamps)
|
||||
{
|
||||
timestampText = $"[{message.ReceivedAtUtc.ToLocalTime().ToString("HH:mm", CultureInfo.InvariantCulture)}] ";
|
||||
}
|
||||
var color = message.FromSelf ? UIColors.Get("LightlessBlue") : ImGuiColors.DalamudWhite;
|
||||
|
||||
ImGui.PushStyleColor(ImGuiCol.Text, color);
|
||||
ImGui.TextWrapped($"{timestampText}{message.DisplayName}: {payload.Message}");
|
||||
ImGui.PopStyleColor();
|
||||
|
||||
if (ImGui.BeginPopupContextItem($"chat_msg_ctx##{channel.Key}_{i}"))
|
||||
{
|
||||
var contextLocalTimestamp = payload.SentAtUtc.ToLocalTime();
|
||||
var contextTimestampText = contextLocalTimestamp.ToString("yyyy-MM-dd HH:mm:ss 'UTC'z", CultureInfo.InvariantCulture);
|
||||
ImGui.TextDisabled(contextTimestampText);
|
||||
ImGui.Separator();
|
||||
|
||||
var actionIndex = 0;
|
||||
foreach (var action in GetContextMenuActions(channel, message))
|
||||
if (message.IsSystem)
|
||||
{
|
||||
DrawContextMenuAction(action, actionIndex++);
|
||||
DrawSystemEntry(message);
|
||||
ImGui.PopID();
|
||||
continue;
|
||||
}
|
||||
|
||||
ImGui.EndPopup();
|
||||
}
|
||||
if (message.Payload is not { } payload)
|
||||
{
|
||||
ImGui.PopID();
|
||||
continue;
|
||||
}
|
||||
|
||||
ImGui.PopID();
|
||||
var timestampText = string.Empty;
|
||||
if (showTimestamps)
|
||||
{
|
||||
timestampText = $"[{message.ReceivedAtUtc.ToLocalTime().ToString("HH:mm", CultureInfo.InvariantCulture)}] ";
|
||||
}
|
||||
var color = message.FromSelf ? UIColors.Get("LightlessBlue") : ImGuiColors.DalamudWhite;
|
||||
|
||||
ImGui.PushStyleColor(ImGuiCol.Text, color);
|
||||
ImGui.TextWrapped($"{timestampText}{message.DisplayName}: {payload.Message}");
|
||||
ImGui.PopStyleColor();
|
||||
|
||||
if (ImGui.BeginPopupContextItem($"chat_msg_ctx##{channel.Key}_{i}"))
|
||||
{
|
||||
var contextLocalTimestamp = payload.SentAtUtc.ToLocalTime();
|
||||
var contextTimestampText = contextLocalTimestamp.ToString("yyyy-MM-dd HH:mm:ss 'UTC'z", CultureInfo.InvariantCulture);
|
||||
ImGui.TextDisabled(contextTimestampText);
|
||||
ImGui.Separator();
|
||||
|
||||
var actionIndex = 0;
|
||||
foreach (var action in GetContextMenuActions(channel, message))
|
||||
{
|
||||
DrawContextMenuAction(action, actionIndex++);
|
||||
}
|
||||
|
||||
ImGui.EndPopup();
|
||||
}
|
||||
|
||||
ImGui.PopID();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -833,6 +833,11 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
||||
ImGui.PopStyleVar(1);
|
||||
_pushedStyle = false;
|
||||
}
|
||||
if (_titleBarStylePopCount > 0)
|
||||
{
|
||||
ImGui.PopStyleColor(_titleBarStylePopCount);
|
||||
_titleBarStylePopCount = 0;
|
||||
}
|
||||
base.PostDraw();
|
||||
}
|
||||
|
||||
|
||||
@@ -60,16 +60,6 @@ public static class VariousExtensions
|
||||
CharacterData? oldData, ILogger logger, IPairPerformanceSubject cachedPlayer, bool forceApplyCustomization, bool forceApplyMods)
|
||||
{
|
||||
oldData ??= new();
|
||||
static bool FileReplacementsEquivalent(ICollection<FileReplacementData> left, ICollection<FileReplacementData> right)
|
||||
{
|
||||
if (left.Count != right.Count)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var comparer = LightlessSync.PlayerData.Data.FileReplacementDataComparer.Instance;
|
||||
return !left.Except(right, comparer).Any() && !right.Except(left, comparer).Any();
|
||||
}
|
||||
|
||||
var charaDataToUpdate = new Dictionary<ObjectKind, HashSet<PlayerChanges>>();
|
||||
foreach (ObjectKind objectKind in Enum.GetValues<ObjectKind>())
|
||||
@@ -105,7 +95,7 @@ public static class VariousExtensions
|
||||
{
|
||||
var oldList = oldData.FileReplacements[objectKind];
|
||||
var newList = newData.FileReplacements[objectKind];
|
||||
var listsAreEqual = FileReplacementsEquivalent(oldList, newList);
|
||||
var listsAreEqual = oldList.SequenceEqual(newList, PlayerData.Data.FileReplacementDataComparer.Instance);
|
||||
if (!listsAreEqual || forceApplyMods)
|
||||
{
|
||||
logger.LogDebug("[BASE-{appBase}] Updating {object}/{kind} (FileReplacements not equal) => {change}", applicationBase, cachedPlayer, objectKind, PlayerChanges.ModFiles);
|
||||
@@ -128,9 +118,9 @@ public static class VariousExtensions
|
||||
.OrderBy(g => string.IsNullOrEmpty(g.Hash) ? g.FileSwapPath : g.Hash, StringComparer.OrdinalIgnoreCase).ToList();
|
||||
var newTail = newFileReplacements.Where(g => g.GamePaths.Any(p => p.Contains("/tail/", StringComparison.OrdinalIgnoreCase)))
|
||||
.OrderBy(g => string.IsNullOrEmpty(g.Hash) ? g.FileSwapPath : g.Hash, StringComparer.OrdinalIgnoreCase).ToList();
|
||||
var existingTransients = existingFileReplacements.Where(g => g.GamePaths.Any(g => !g.EndsWith("mdl", StringComparison.OrdinalIgnoreCase) && !g.EndsWith("tex", StringComparison.OrdinalIgnoreCase) && !g.EndsWith("mtrl", StringComparison.OrdinalIgnoreCase)))
|
||||
var existingTransients = existingFileReplacements.Where(g => g.GamePaths.Any(g => !g.EndsWith("mdl") && !g.EndsWith("tex") && !g.EndsWith("mtrl")))
|
||||
.OrderBy(g => string.IsNullOrEmpty(g.Hash) ? g.FileSwapPath : g.Hash, StringComparer.OrdinalIgnoreCase).ToList();
|
||||
var newTransients = newFileReplacements.Where(g => g.GamePaths.Any(g => !g.EndsWith("mdl", StringComparison.OrdinalIgnoreCase) && !g.EndsWith("tex", StringComparison.OrdinalIgnoreCase) && !g.EndsWith("mtrl", StringComparison.OrdinalIgnoreCase)))
|
||||
var newTransients = newFileReplacements.Where(g => g.GamePaths.Any(g => !g.EndsWith("mdl") && !g.EndsWith("tex") && !g.EndsWith("mtrl")))
|
||||
.OrderBy(g => string.IsNullOrEmpty(g.Hash) ? g.FileSwapPath : g.Hash, StringComparer.OrdinalIgnoreCase).ToList();
|
||||
|
||||
logger.LogTrace("[BASE-{appbase}] ExistingFace: {of}, NewFace: {fc}; ExistingHair: {eh}, NewHair: {nh}; ExistingTail: {et}, NewTail: {nt}; ExistingTransient: {etr}, NewTransient: {ntr}", applicationBase,
|
||||
@@ -177,8 +167,7 @@ public static class VariousExtensions
|
||||
if (objectKind != ObjectKind.Player) continue;
|
||||
|
||||
bool manipDataDifferent = !string.Equals(oldData.ManipulationData, newData.ManipulationData, StringComparison.Ordinal);
|
||||
var hasManipulationData = !string.IsNullOrEmpty(newData.ManipulationData);
|
||||
if (manipDataDifferent || (forceApplyMods && hasManipulationData))
|
||||
if (manipDataDifferent || forceApplyMods)
|
||||
{
|
||||
logger.LogDebug("[BASE-{appBase}] Updating {object}/{kind} (Diff manip data) => {change}", applicationBase, cachedPlayer, objectKind, PlayerChanges.ModManip);
|
||||
charaDataToUpdate[objectKind].Add(PlayerChanges.ModManip);
|
||||
|
||||
@@ -563,7 +563,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
||||
|
||||
if (directDownloads.Count > 0 || downloadBatches.Length > 0)
|
||||
{
|
||||
Logger.LogWarning("Downloading {direct} files directly, and {batchtotal} in {batches} batches.", directDownloads.Count, batchDownloads.Count, downloadBatches.Length);
|
||||
Logger.LogInformation("Downloading {direct} files directly, and {batchtotal} in {batches} batches.", directDownloads.Count, batchDownloads.Count, downloadBatches.Length);
|
||||
}
|
||||
|
||||
if (gameObjectHandler is not null)
|
||||
|
||||
@@ -418,7 +418,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
|
||||
|
||||
public Task CyclePauseAsync(PairUniqueIdentifier ident)
|
||||
{
|
||||
var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(8));
|
||||
var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
var token = timeoutCts.Token;
|
||||
@@ -430,20 +430,19 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
|
||||
return;
|
||||
}
|
||||
|
||||
var originalPermissions = entry.SelfPermissions;
|
||||
var targetPermissions = originalPermissions;
|
||||
targetPermissions.SetPaused(!originalPermissions.IsPaused());
|
||||
var targetPermissions = entry.SelfPermissions;
|
||||
targetPermissions.SetPaused(paused: true);
|
||||
|
||||
await UserSetPairPermissions(new UserPermissionsDto(entry.User, targetPermissions)).ConfigureAwait(false);
|
||||
|
||||
var applied = false;
|
||||
var pauseApplied = false;
|
||||
while (!token.IsCancellationRequested)
|
||||
{
|
||||
if (_pairCoordinator.Ledger.TryGetEntry(ident, out var updated) && updated is not null)
|
||||
{
|
||||
if (updated.SelfPermissions == targetPermissions)
|
||||
{
|
||||
applied = true;
|
||||
pauseApplied = true;
|
||||
entry = updated;
|
||||
break;
|
||||
}
|
||||
@@ -453,13 +452,16 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
|
||||
Logger.LogTrace("Waiting for permissions change for {uid}", ident.UserId);
|
||||
}
|
||||
|
||||
if (!applied)
|
||||
if (!pauseApplied)
|
||||
{
|
||||
Logger.LogWarning("CyclePauseAsync timed out waiting for pause acknowledgement for {uid}", ident.UserId);
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.LogDebug("CyclePauseAsync toggled paused for {uid} to {state}", ident.UserId, targetPermissions.IsPaused());
|
||||
targetPermissions.SetPaused(paused: false);
|
||||
await UserSetPairPermissions(new UserPermissionsDto(entry.User, targetPermissions)).ConfigureAwait(false);
|
||||
|
||||
Logger.LogDebug("CyclePauseAsync completed pause cycle for {uid}", ident.UserId);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
@@ -479,16 +481,26 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
|
||||
}
|
||||
|
||||
public async Task PauseAsync(UserData userData)
|
||||
{
|
||||
await SetPausedStateAsync(userData, paused: true).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task UnpauseAsync(UserData userData)
|
||||
{
|
||||
await SetPausedStateAsync(userData, paused: false).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task SetPausedStateAsync(UserData userData, bool paused)
|
||||
{
|
||||
var pairIdent = new PairUniqueIdentifier(userData.UID);
|
||||
if (!_pairCoordinator.Ledger.TryGetEntry(pairIdent, out var entry) || entry is null)
|
||||
{
|
||||
Logger.LogWarning("PauseAsync: pair {uid} not found in ledger", userData.UID);
|
||||
Logger.LogWarning("SetPausedStateAsync: pair {uid} not found in ledger", userData.UID);
|
||||
return;
|
||||
}
|
||||
|
||||
var permissions = entry.SelfPermissions;
|
||||
permissions.SetPaused(paused: true);
|
||||
permissions.SetPaused(paused);
|
||||
await UserSetPairPermissions(new UserPermissionsDto(userData, permissions)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user