Merge remote-tracking branch 'origin/2.0.2' into 2.0.0-crashing-bugfixes
# Conflicts: # LightlessSync/LightlessConfiguration/Configurations/LightlessConfig.cs # LightlessSync/UI/SyncshellFinderUI.cs # LightlessSync/UI/TopTabMenu.cs # LightlessSync/WebAPI/Files/FileDownloadManager.cs
This commit is contained in:
@@ -1,11 +1,44 @@
|
|||||||
tagline: "Lightless Sync v2.0.0"
|
tagline: "Lightless Sync v2.0.1"
|
||||||
subline: "LIGHTLESS IS EVOLVING!!"
|
subline: "LIGHTLESS IS EVOLVING!!"
|
||||||
changelog:
|
changelog:
|
||||||
|
- name: "v2.0.1"
|
||||||
|
tagline: "Some Fixes"
|
||||||
|
date: "December 23 2025"
|
||||||
|
# be sure to set this every new version
|
||||||
|
isCurrent: true
|
||||||
|
versions:
|
||||||
|
- number: "Chat"
|
||||||
|
icon: ""
|
||||||
|
items:
|
||||||
|
- "You can turn off the syncshell chat as Owner by going to the Syncshell Admin panel -> Owner -> Enable/Disable Chat."
|
||||||
|
- "Fixed an issue where you can't chat due to regions being in a different language."
|
||||||
|
- number: "LightFinder"
|
||||||
|
icon: ""
|
||||||
|
items:
|
||||||
|
- "The icon/Lightfinder Text will be hidden when Game UI is hidden and behind game elements/UI"
|
||||||
|
- "Able to select an icon for the selected list or a custom glyph if you know the code."
|
||||||
|
- "Smoothing and reducing jitter on the icon/Lightfinder Text."
|
||||||
|
- "Fixed so higher scaled UI options (100/150/200% UI scale) wouldn't break the element."
|
||||||
|
- "Detects if GPose is active, wouldn't render the elements"
|
||||||
|
- number: "Miscellaneous fixes"
|
||||||
|
icon: ""
|
||||||
|
items:
|
||||||
|
- "Fixed the null error given on GetCID when transferring between zones/housing."
|
||||||
|
- "Added push/pop on certain ImGUI elements to remove them after being used. "
|
||||||
|
- "Having all tabs open in the Main UI wouldn't lag out the game anymore."
|
||||||
|
- "Cycle pause has been adjusted to the old function. There is a separate button to pause normally, now called 'Toggle (Un)Pause State'."
|
||||||
|
- "Changes have been made to the character redraw to address the issues with the building character data constantly being redrawn and the redrawn behavior with Honorific titles."
|
||||||
|
- "GPose characters should appear again in the actor screen"
|
||||||
|
- "Lightspeed download console messages are no longer shown as warnings."
|
||||||
|
- number: "Server Updates"
|
||||||
|
icon: ""
|
||||||
|
items:
|
||||||
|
- "Changes have been made to the disabling of your profile. It should save again."
|
||||||
|
- "Ability added to toggle chats from syncshell to be disabled."
|
||||||
|
- "Files are continuously being deleted due to high volumes in storage, potentially causing MCDOs to have missing files. We have increased the limit of the storage in our configurations to see if that helps."
|
||||||
- name: "v2.0.0"
|
- name: "v2.0.0"
|
||||||
tagline: "Thank you for 4 months!"
|
tagline: "Thank you for 4 months!"
|
||||||
date: "December 2025"
|
date: "December 2025"
|
||||||
# be sure to set this every new version
|
|
||||||
isCurrent: true
|
|
||||||
versions:
|
versions:
|
||||||
- number: "Lightless Chat"
|
- number: "Lightless Chat"
|
||||||
icon: ""
|
icon: ""
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
|||||||
_playerRelatedPointers.Remove(msg.GameObjectHandler);
|
_playerRelatedPointers.Remove(msg.GameObjectHandler);
|
||||||
});
|
});
|
||||||
|
|
||||||
foreach (var descriptor in _actorObjectService.PlayerDescriptors)
|
foreach (var descriptor in _actorObjectService.ObjectDescriptors)
|
||||||
{
|
{
|
||||||
HandleActorTracked(descriptor);
|
HandleActorTracked(descriptor);
|
||||||
}
|
}
|
||||||
@@ -291,7 +291,7 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
var activeDescriptors = new Dictionary<nint, ObjectKind>();
|
var activeDescriptors = new Dictionary<nint, ObjectKind>();
|
||||||
foreach (var descriptor in _actorObjectService.PlayerDescriptors)
|
foreach (var descriptor in _actorObjectService.ObjectDescriptors)
|
||||||
{
|
{
|
||||||
if (TryResolveObjectKind(descriptor, out var resolvedKind))
|
if (TryResolveObjectKind(descriptor, out var resolvedKind))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -155,4 +155,5 @@ public class LightlessConfig : ILightlessConfiguration
|
|||||||
public string? SelectedFinderSyncshell { get; set; } = null;
|
public string? SelectedFinderSyncshell { get; set; } = null;
|
||||||
public string LastSeenVersion { get; set; } = string.Empty;
|
public string LastSeenVersion { get; set; } = string.Empty;
|
||||||
public bool EnableParticleEffects { get; set; } = true;
|
public bool EnableParticleEffects { get; set; } = true;
|
||||||
|
public HashSet<Guid> OrphanableTempCollections { get; set; } = [];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Authors></Authors>
|
<Authors></Authors>
|
||||||
<Company></Company>
|
<Company></Company>
|
||||||
<Version>2.0.1</Version>
|
<Version>2.0.2</Version>
|
||||||
<Description></Description>
|
<Description></Description>
|
||||||
<Copyright></Copyright>
|
<Copyright></Copyright>
|
||||||
<PackageProjectUrl>https://github.com/Light-Public-Syncshells/LightlessClient</PackageProjectUrl>
|
<PackageProjectUrl>https://github.com/Light-Public-Syncshells/LightlessClient</PackageProjectUrl>
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
|||||||
private readonly Func<IntPtr> _getAddress;
|
private readonly Func<IntPtr> _getAddress;
|
||||||
private readonly bool _isOwnedObject;
|
private readonly bool _isOwnedObject;
|
||||||
private readonly PerformanceCollectorService _performanceCollector;
|
private readonly PerformanceCollectorService _performanceCollector;
|
||||||
|
private readonly object _frameworkUpdateGate = new();
|
||||||
|
private bool _frameworkUpdateSubscribed;
|
||||||
private byte _classJob = 0;
|
private byte _classJob = 0;
|
||||||
private Task? _delayedZoningTask;
|
private Task? _delayedZoningTask;
|
||||||
private bool _haltProcessing = false;
|
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<ZoneSwitchEndMessage>(this, (_) => ZoneSwitchEnd());
|
||||||
Mediator.Subscribe<ZoneSwitchStartMessage>(this, (_) => ZoneSwitchStart());
|
Mediator.Subscribe<ZoneSwitchStartMessage>(this, (_) => ZoneSwitchStart());
|
||||||
@@ -109,7 +114,7 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
|||||||
{
|
{
|
||||||
while (await _dalamudUtil.RunOnFrameworkThread(() =>
|
while (await _dalamudUtil.RunOnFrameworkThread(() =>
|
||||||
{
|
{
|
||||||
if (_haltProcessing) CheckAndUpdateObject();
|
EnsureLatestObjectState();
|
||||||
if (CurrentDrawCondition != DrawCondition.None) return true;
|
if (CurrentDrawCondition != DrawCondition.None) return true;
|
||||||
var gameObj = _dalamudUtil.CreateGameObject(Address);
|
var gameObj = _dalamudUtil.CreateGameObject(Address);
|
||||||
if (gameObj is Dalamud.Game.ClientState.Objects.Types.ICharacter chara)
|
if (gameObj is Dalamud.Game.ClientState.Objects.Types.ICharacter chara)
|
||||||
@@ -148,6 +153,11 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
|||||||
_haltProcessing = false;
|
_haltProcessing = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void Refresh()
|
||||||
|
{
|
||||||
|
_dalamudUtil.RunOnFrameworkThread(CheckAndUpdateObject).GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<bool> IsBeingDrawnRunOnFrameworkAsync()
|
public async Task<bool> IsBeingDrawnRunOnFrameworkAsync()
|
||||||
{
|
{
|
||||||
return await _dalamudUtil.RunOnFrameworkThread(IsBeingDrawn).ConfigureAwait(false);
|
return await _dalamudUtil.RunOnFrameworkThread(IsBeingDrawn).ConfigureAwait(false);
|
||||||
@@ -361,7 +371,7 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
|||||||
|
|
||||||
private bool IsBeingDrawn()
|
private bool IsBeingDrawn()
|
||||||
{
|
{
|
||||||
if (_haltProcessing) CheckAndUpdateObject();
|
EnsureLatestObjectState();
|
||||||
|
|
||||||
if (_dalamudUtil.IsAnythingDrawing)
|
if (_dalamudUtil.IsAnythingDrawing)
|
||||||
{
|
{
|
||||||
@@ -373,6 +383,28 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
|||||||
return CurrentDrawCondition != DrawCondition.None;
|
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()
|
private unsafe DrawCondition IsBeingDrawnUnsafe()
|
||||||
{
|
{
|
||||||
if (Address == IntPtr.Zero) return DrawCondition.ObjectZero;
|
if (Address == IntPtr.Zero) return DrawCondition.ObjectZero;
|
||||||
|
|||||||
@@ -25,6 +25,11 @@
|
|||||||
bool IsDownloading { get; }
|
bool IsDownloading { get; }
|
||||||
int PendingDownloadCount { get; }
|
int PendingDownloadCount { get; }
|
||||||
int ForbiddenDownloadCount { get; }
|
int ForbiddenDownloadCount { get; }
|
||||||
|
bool PendingModReapply { get; }
|
||||||
|
bool ModApplyDeferred { get; }
|
||||||
|
int MissingCriticalMods { get; }
|
||||||
|
int MissingNonCriticalMods { get; }
|
||||||
|
int MissingForbiddenMods { get; }
|
||||||
DateTime? InvisibleSinceUtc { get; }
|
DateTime? InvisibleSinceUtc { get; }
|
||||||
DateTime? VisibilityEvictionDueAtUtc { get; }
|
DateTime? VisibilityEvictionDueAtUtc { get; }
|
||||||
|
|
||||||
|
|||||||
@@ -87,22 +87,25 @@ public class Pair
|
|||||||
return;
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
UiSharedService.AddContextMenuItem(args, name: "Open Profile", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
|
if (!IsPaused)
|
||||||
{
|
{
|
||||||
_mediator.Publish(new ProfileOpenStandaloneMessage(this));
|
UiSharedService.AddContextMenuItem(args, name: "Open Profile", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
|
||||||
return Task.CompletedTask;
|
{
|
||||||
});
|
_mediator.Publish(new ProfileOpenStandaloneMessage(this));
|
||||||
|
return Task.CompletedTask;
|
||||||
|
});
|
||||||
|
|
||||||
UiSharedService.AddContextMenuItem(args, name: "Reapply last data", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
|
UiSharedService.AddContextMenuItem(args, name: "Reapply last data", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
|
||||||
{
|
{
|
||||||
ApplyLastReceivedData(forced: true);
|
ApplyLastReceivedData(forced: true);
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
UiSharedService.AddContextMenuItem(args, name: "Change Permissions", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
|
UiSharedService.AddContextMenuItem(args, name: "Change Permissions", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
|
||||||
{
|
{
|
||||||
@@ -110,7 +113,24 @@ public class Pair
|
|||||||
return Task.CompletedTask;
|
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();
|
TriggerCyclePause();
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
@@ -218,6 +238,11 @@ public class Pair
|
|||||||
handler.IsApplying,
|
handler.IsApplying,
|
||||||
handler.IsDownloading,
|
handler.IsDownloading,
|
||||||
handler.PendingDownloadCount,
|
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 IsApplying,
|
||||||
bool IsDownloading,
|
bool IsDownloading,
|
||||||
int PendingDownloadCount,
|
int PendingDownloadCount,
|
||||||
int ForbiddenDownloadCount)
|
int ForbiddenDownloadCount,
|
||||||
|
bool PendingModReapply,
|
||||||
|
bool ModApplyDeferred,
|
||||||
|
int MissingCriticalMods,
|
||||||
|
int MissingNonCriticalMods,
|
||||||
|
int MissingForbiddenMods)
|
||||||
{
|
{
|
||||||
public static PairDebugInfo Empty { get; } = new(
|
public static PairDebugInfo Empty { get; } = new(
|
||||||
false,
|
false,
|
||||||
@@ -34,5 +39,10 @@ public sealed record PairDebugInfo(
|
|||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
0,
|
0,
|
||||||
|
0,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
0);
|
0);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ using LightlessSync.Interop.Ipc;
|
|||||||
using LightlessSync.PlayerData.Factories;
|
using LightlessSync.PlayerData.Factories;
|
||||||
using LightlessSync.PlayerData.Handlers;
|
using LightlessSync.PlayerData.Handlers;
|
||||||
using LightlessSync.Services;
|
using LightlessSync.Services;
|
||||||
|
using LightlessSync.Services.ActorTracking;
|
||||||
using LightlessSync.Services.Events;
|
using LightlessSync.Services.Events;
|
||||||
using LightlessSync.Services.Mediator;
|
using LightlessSync.Services.Mediator;
|
||||||
using LightlessSync.Services.PairProcessing;
|
using LightlessSync.Services.PairProcessing;
|
||||||
@@ -18,6 +19,7 @@ using LightlessSync.WebAPI.Files;
|
|||||||
using LightlessSync.WebAPI.Files.Models;
|
using LightlessSync.WebAPI.Files.Models;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using DalamudObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind;
|
||||||
using ObjectKind = LightlessSync.API.Data.Enum.ObjectKind;
|
using ObjectKind = LightlessSync.API.Data.Enum.ObjectKind;
|
||||||
using FileReplacementDataComparer = LightlessSync.PlayerData.Data.FileReplacementDataComparer;
|
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 sealed record CombatData(Guid ApplicationId, CharacterData CharacterData, bool Forced);
|
||||||
|
|
||||||
private readonly DalamudUtilService _dalamudUtil;
|
private readonly DalamudUtilService _dalamudUtil;
|
||||||
|
private readonly ActorObjectService _actorObjectService;
|
||||||
private readonly FileDownloadManager _downloadManager;
|
private readonly FileDownloadManager _downloadManager;
|
||||||
private readonly FileCacheManager _fileDbManager;
|
private readonly FileCacheManager _fileDbManager;
|
||||||
private readonly GameObjectHandlerFactory _gameObjectHandlerFactory;
|
private readonly GameObjectHandlerFactory _gameObjectHandlerFactory;
|
||||||
@@ -43,6 +46,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
private readonly TextureDownscaleService _textureDownscaleService;
|
private readonly TextureDownscaleService _textureDownscaleService;
|
||||||
private readonly PairStateCache _pairStateCache;
|
private readonly PairStateCache _pairStateCache;
|
||||||
private readonly PairPerformanceMetricsCache _performanceMetricsCache;
|
private readonly PairPerformanceMetricsCache _performanceMetricsCache;
|
||||||
|
private readonly PenumbraTempCollectionJanitor _tempCollectionJanitor;
|
||||||
private readonly PairManager _pairManager;
|
private readonly PairManager _pairManager;
|
||||||
private CancellationTokenSource? _applicationCancellationTokenSource;
|
private CancellationTokenSource? _applicationCancellationTokenSource;
|
||||||
private Guid _applicationId;
|
private Guid _applicationId;
|
||||||
@@ -56,11 +60,15 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
private bool _forceFullReapply;
|
private bool _forceFullReapply;
|
||||||
private Dictionary<(string GamePath, string? Hash), string>? _lastAppliedModdedPaths;
|
private Dictionary<(string GamePath, string? Hash), string>? _lastAppliedModdedPaths;
|
||||||
private bool _needsCollectionRebuild;
|
private bool _needsCollectionRebuild;
|
||||||
|
private bool _pendingModReapply;
|
||||||
|
private bool _lastModApplyDeferred;
|
||||||
|
private int _lastMissingCriticalMods;
|
||||||
|
private int _lastMissingNonCriticalMods;
|
||||||
|
private int _lastMissingForbiddenMods;
|
||||||
private bool _isVisible;
|
private bool _isVisible;
|
||||||
private Guid _penumbraCollection;
|
private Guid _penumbraCollection;
|
||||||
private readonly object _collectionGate = new();
|
private readonly object _collectionGate = new();
|
||||||
private bool _redrawOnNextApplication = false;
|
private bool _redrawOnNextApplication = false;
|
||||||
private bool _explicitRedrawQueued;
|
|
||||||
private readonly object _initializationGate = new();
|
private readonly object _initializationGate = new();
|
||||||
private readonly object _pauseLock = new();
|
private readonly object _pauseLock = new();
|
||||||
private Task _pauseTransitionTask = Task.CompletedTask;
|
private Task _pauseTransitionTask = Task.CompletedTask;
|
||||||
@@ -73,8 +81,23 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
private readonly object _visibilityGraceGate = new();
|
private readonly object _visibilityGraceGate = new();
|
||||||
private CancellationTokenSource? _visibilityGraceCts;
|
private CancellationTokenSource? _visibilityGraceCts;
|
||||||
private static readonly TimeSpan VisibilityEvictionGrace = TimeSpan.FromMinutes(1);
|
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? _invisibleSinceUtc;
|
||||||
private DateTime? _visibilityEvictionDueAtUtc;
|
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? InvisibleSinceUtc => _invisibleSinceUtc;
|
||||||
public DateTime? VisibilityEvictionDueAtUtc => _visibilityEvictionDueAtUtc;
|
public DateTime? VisibilityEvictionDueAtUtc => _visibilityEvictionDueAtUtc;
|
||||||
@@ -126,6 +149,11 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
public long LastAppliedApproximateVRAMBytes { get; set; } = -1;
|
public long LastAppliedApproximateVRAMBytes { get; set; } = -1;
|
||||||
public long LastAppliedApproximateEffectiveVRAMBytes { get; set; } = -1;
|
public long LastAppliedApproximateEffectiveVRAMBytes { get; set; } = -1;
|
||||||
public CharacterData? LastReceivedCharacterData { get; private set; }
|
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? LastDataReceivedAt => _lastDataReceivedAt;
|
||||||
public DateTime? LastApplyAttemptAt => _lastApplyAttemptAt;
|
public DateTime? LastApplyAttemptAt => _lastApplyAttemptAt;
|
||||||
public DateTime? LastSuccessfulApplyAt => _lastSuccessfulApplyAt;
|
public DateTime? LastSuccessfulApplyAt => _lastSuccessfulApplyAt;
|
||||||
@@ -146,6 +174,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
FileDownloadManager transferManager,
|
FileDownloadManager transferManager,
|
||||||
PluginWarningNotificationService pluginWarningNotificationManager,
|
PluginWarningNotificationService pluginWarningNotificationManager,
|
||||||
DalamudUtilService dalamudUtil,
|
DalamudUtilService dalamudUtil,
|
||||||
|
ActorObjectService actorObjectService,
|
||||||
IHostApplicationLifetime lifetime,
|
IHostApplicationLifetime lifetime,
|
||||||
FileCacheManager fileDbManager,
|
FileCacheManager fileDbManager,
|
||||||
PlayerPerformanceService playerPerformanceService,
|
PlayerPerformanceService playerPerformanceService,
|
||||||
@@ -153,7 +182,8 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
ServerConfigurationManager serverConfigManager,
|
ServerConfigurationManager serverConfigManager,
|
||||||
TextureDownscaleService textureDownscaleService,
|
TextureDownscaleService textureDownscaleService,
|
||||||
PairStateCache pairStateCache,
|
PairStateCache pairStateCache,
|
||||||
PairPerformanceMetricsCache performanceMetricsCache) : base(logger, mediator)
|
PairPerformanceMetricsCache performanceMetricsCache,
|
||||||
|
PenumbraTempCollectionJanitor tempCollectionJanitor) : base(logger, mediator)
|
||||||
{
|
{
|
||||||
_pairManager = pairManager;
|
_pairManager = pairManager;
|
||||||
Ident = ident;
|
Ident = ident;
|
||||||
@@ -162,6 +192,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
_downloadManager = transferManager;
|
_downloadManager = transferManager;
|
||||||
_pluginWarningNotificationManager = pluginWarningNotificationManager;
|
_pluginWarningNotificationManager = pluginWarningNotificationManager;
|
||||||
_dalamudUtil = dalamudUtil;
|
_dalamudUtil = dalamudUtil;
|
||||||
|
_actorObjectService = actorObjectService;
|
||||||
_lifetime = lifetime;
|
_lifetime = lifetime;
|
||||||
_fileDbManager = fileDbManager;
|
_fileDbManager = fileDbManager;
|
||||||
_playerPerformanceService = playerPerformanceService;
|
_playerPerformanceService = playerPerformanceService;
|
||||||
@@ -170,7 +201,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
_textureDownscaleService = textureDownscaleService;
|
_textureDownscaleService = textureDownscaleService;
|
||||||
_pairStateCache = pairStateCache;
|
_pairStateCache = pairStateCache;
|
||||||
_performanceMetricsCache = performanceMetricsCache;
|
_performanceMetricsCache = performanceMetricsCache;
|
||||||
LastAppliedDataBytes = -1;
|
_tempCollectionJanitor = tempCollectionJanitor;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Initialize()
|
public void Initialize()
|
||||||
@@ -185,6 +216,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ActorObjectService.ActorDescriptor? trackedDescriptor = null;
|
||||||
lock (_initializationGate)
|
lock (_initializationGate)
|
||||||
{
|
{
|
||||||
if (Initialized)
|
if (Initialized)
|
||||||
@@ -198,7 +230,12 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
_forceApplyMods = true;
|
_forceApplyMods = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Mediator.Subscribe<FrameworkUpdateMessage>(this, _ => FrameworkUpdate());
|
var useFrameworkUpdate = !_actorObjectService.HooksActive;
|
||||||
|
if (useFrameworkUpdate)
|
||||||
|
{
|
||||||
|
Mediator.Subscribe<FrameworkUpdateMessage>(this, _ => FrameworkUpdate());
|
||||||
|
_frameworkUpdateSubscribed = true;
|
||||||
|
}
|
||||||
Mediator.Subscribe<ZoneSwitchStartMessage>(this, _ =>
|
Mediator.Subscribe<ZoneSwitchStartMessage>(this, _ =>
|
||||||
{
|
{
|
||||||
_downloadCancellationTokenSource?.CancelDispose();
|
_downloadCancellationTokenSource?.CancelDispose();
|
||||||
@@ -234,17 +271,49 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
Mediator.Subscribe<CutsceneEndMessage>(this, _ => EnableSync());
|
Mediator.Subscribe<CutsceneEndMessage>(this, _ => EnableSync());
|
||||||
Mediator.Subscribe<GposeStartMessage>(this, _ => DisableSync());
|
Mediator.Subscribe<GposeStartMessage>(this, _ => DisableSync());
|
||||||
Mediator.Subscribe<GposeEndMessage>(this, _ => EnableSync());
|
Mediator.Subscribe<GposeEndMessage>(this, _ => EnableSync());
|
||||||
Mediator.Subscribe<DownloadFinishedMessage>(this, msg =>
|
Mediator.Subscribe<ActorTrackedMessage>(this, msg => HandleActorTracked(msg.Descriptor));
|
||||||
{
|
Mediator.Subscribe<ActorUntrackedMessage>(this, msg => HandleActorUntracked(msg.Descriptor));
|
||||||
if (_charaHandler is null || !ReferenceEquals(msg.DownloadId, _charaHandler))
|
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;
|
Initialized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (trackedDescriptor.HasValue)
|
||||||
|
{
|
||||||
|
HandleActorTracked(trackedDescriptor.Value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private IReadOnlyList<PairConnection> GetCurrentPairs()
|
private IReadOnlyList<PairConnection> GetCurrentPairs()
|
||||||
@@ -355,6 +424,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
{
|
{
|
||||||
_penumbraCollection = created;
|
_penumbraCollection = created;
|
||||||
_pairStateCache.StoreTemporaryCollection(Ident, created);
|
_pairStateCache.StoreTemporaryCollection(Ident, created);
|
||||||
|
_tempCollectionJanitor.Register(created);
|
||||||
}
|
}
|
||||||
|
|
||||||
return _penumbraCollection;
|
return _penumbraCollection;
|
||||||
@@ -387,6 +457,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
_needsCollectionRebuild = true;
|
_needsCollectionRebuild = true;
|
||||||
_forceFullReapply = true;
|
_forceFullReapply = true;
|
||||||
_forceApplyMods = true;
|
_forceApplyMods = true;
|
||||||
|
_tempCollectionJanitor.Unregister(toRelease);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!releaseFromPenumbra || toRelease == Guid.Empty || !_ipcManager.Penumbra.APIAvailable)
|
if (!releaseFromPenumbra || toRelease == Guid.Empty || !_ipcManager.Penumbra.APIAvailable)
|
||||||
@@ -737,6 +808,67 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
return true;
|
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()
|
private bool CanApplyNow()
|
||||||
{
|
{
|
||||||
return !_dalamudUtil.IsInCombat
|
return !_dalamudUtil.IsInCombat
|
||||||
@@ -760,6 +892,16 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
_lastBlockingConditions = Array.Empty<string>();
|
_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)
|
public void ApplyCharacterData(Guid applicationBase, CharacterData characterData, bool forceApplyCustomization = false)
|
||||||
{
|
{
|
||||||
_lastApplyAttemptAt = DateTime.UtcNow;
|
_lastApplyAttemptAt = DateTime.UtcNow;
|
||||||
@@ -777,72 +919,48 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
if (_dalamudUtil.IsInCombat)
|
if (_dalamudUtil.IsInCombat)
|
||||||
{
|
{
|
||||||
const string reason = "Cannot apply character data: you are in combat, deferring application";
|
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,
|
DeferApplication(applicationBase, characterData, forceApplyCustomization, user, reason, "Combat", LogLevel.Debug,
|
||||||
reason)));
|
"[BASE-{appBase}] Received data but player is in combat", applicationBase);
|
||||||
Logger.LogDebug("[BASE-{appBase}] Received data but player is in combat", applicationBase);
|
|
||||||
RecordFailure(reason, "Combat");
|
|
||||||
_dataReceivedInDowntime = new(applicationBase, characterData, forceApplyCustomization);
|
|
||||||
SetUploading(false);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_dalamudUtil.IsPerforming)
|
if (_dalamudUtil.IsPerforming)
|
||||||
{
|
{
|
||||||
const string reason = "Cannot apply character data: you are performing music, deferring application";
|
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,
|
DeferApplication(applicationBase, characterData, forceApplyCustomization, user, reason, "Performance", LogLevel.Debug,
|
||||||
reason)));
|
"[BASE-{appBase}] Received data but player is performing", applicationBase);
|
||||||
Logger.LogDebug("[BASE-{appBase}] Received data but player is performing", applicationBase);
|
|
||||||
RecordFailure(reason, "Performance");
|
|
||||||
_dataReceivedInDowntime = new(applicationBase, characterData, forceApplyCustomization);
|
|
||||||
SetUploading(false);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_dalamudUtil.IsInInstance)
|
if (_dalamudUtil.IsInInstance)
|
||||||
{
|
{
|
||||||
const string reason = "Cannot apply character data: you are in an instance, deferring application";
|
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,
|
DeferApplication(applicationBase, characterData, forceApplyCustomization, user, reason, "Instance", LogLevel.Debug,
|
||||||
reason)));
|
"[BASE-{appBase}] Received data but player is in instance", applicationBase);
|
||||||
Logger.LogDebug("[BASE-{appBase}] Received data but player is in instance", applicationBase);
|
|
||||||
RecordFailure(reason, "Instance");
|
|
||||||
_dataReceivedInDowntime = new(applicationBase, characterData, forceApplyCustomization);
|
|
||||||
SetUploading(false);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_dalamudUtil.IsInCutscene)
|
if (_dalamudUtil.IsInCutscene)
|
||||||
{
|
{
|
||||||
const string reason = "Cannot apply character data: you are in a cutscene, deferring application";
|
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,
|
DeferApplication(applicationBase, characterData, forceApplyCustomization, user, reason, "Cutscene", LogLevel.Debug,
|
||||||
reason)));
|
"[BASE-{appBase}] Received data but player is in a cutscene", applicationBase);
|
||||||
Logger.LogDebug("[BASE-{appBase}] Received data but player is in a cutscene", applicationBase);
|
|
||||||
RecordFailure(reason, "Cutscene");
|
|
||||||
_dataReceivedInDowntime = new(applicationBase, characterData, forceApplyCustomization);
|
|
||||||
SetUploading(false);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_dalamudUtil.IsInGpose)
|
if (_dalamudUtil.IsInGpose)
|
||||||
{
|
{
|
||||||
const string reason = "Cannot apply character data: you are in GPose, deferring application";
|
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,
|
DeferApplication(applicationBase, characterData, forceApplyCustomization, user, reason, "GPose", LogLevel.Debug,
|
||||||
reason)));
|
"[BASE-{appBase}] Received data but player is in GPose", applicationBase);
|
||||||
Logger.LogDebug("[BASE-{appBase}] Received data but player is in GPose", applicationBase);
|
|
||||||
RecordFailure(reason, "GPose");
|
|
||||||
_dataReceivedInDowntime = new(applicationBase, characterData, forceApplyCustomization);
|
|
||||||
SetUploading(false);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_ipcManager.Penumbra.APIAvailable || !_ipcManager.Glamourer.APIAvailable)
|
if (!_ipcManager.Penumbra.APIAvailable || !_ipcManager.Glamourer.APIAvailable)
|
||||||
{
|
{
|
||||||
const string reason = "Cannot apply character data: Penumbra or Glamourer is not available, deferring application";
|
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,
|
DeferApplication(applicationBase, characterData, forceApplyCustomization, user, reason, "PluginUnavailable", LogLevel.Information,
|
||||||
reason)));
|
"[BASE-{appbase}] Application of data for {player} while Penumbra/Glamourer unavailable, returning", applicationBase, GetLogIdentifier());
|
||||||
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);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -885,13 +1003,10 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
_forceApplyMods = false;
|
_forceApplyMods = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
_explicitRedrawQueued = false;
|
|
||||||
|
|
||||||
if (_redrawOnNextApplication && charaDataToUpdate.TryGetValue(ObjectKind.Player, out var player))
|
if (_redrawOnNextApplication && charaDataToUpdate.TryGetValue(ObjectKind.Player, out var player))
|
||||||
{
|
{
|
||||||
player.Add(PlayerChanges.ForcedRedraw);
|
player.Add(PlayerChanges.ForcedRedraw);
|
||||||
_redrawOnNextApplication = false;
|
_redrawOnNextApplication = false;
|
||||||
_explicitRedrawQueued = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (charaDataToUpdate.TryGetValue(ObjectKind.Player, out var playerChanges))
|
if (charaDataToUpdate.TryGetValue(ObjectKind.Player, out var playerChanges))
|
||||||
@@ -1085,7 +1200,14 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
|
|
||||||
Logger.LogDebug("[{applicationId}] Applying Customization Data for {handler}", applicationId, handler);
|
Logger.LogDebug("[{applicationId}] Applying Customization Data for {handler}", applicationId, handler);
|
||||||
await _dalamudUtil.WaitWhileCharacterIsDrawing(Logger, handler, applicationId, 30000, token).ConfigureAwait(false);
|
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();
|
token.ThrowIfCancellationRequested();
|
||||||
|
var tasks = new List<Task>();
|
||||||
|
bool needsRedraw = false;
|
||||||
foreach (var change in changes.Value.OrderBy(p => (int)p))
|
foreach (var change in changes.Value.OrderBy(p => (int)p))
|
||||||
{
|
{
|
||||||
Logger.LogDebug("[{applicationId}] Processing {change} for {handler}", applicationId, change, handler);
|
Logger.LogDebug("[{applicationId}] Processing {change} for {handler}", applicationId, change, handler);
|
||||||
@@ -1094,45 +1216,39 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
case PlayerChanges.Customize:
|
case PlayerChanges.Customize:
|
||||||
if (charaData.CustomizePlusData.TryGetValue(changes.Key, out var customizePlusData))
|
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))
|
else if (_customizeIds.TryGetValue(changes.Key, out var customizeId))
|
||||||
{
|
{
|
||||||
await _ipcManager.CustomizePlus.RevertByIdAsync(customizeId).ConfigureAwait(false);
|
tasks.Add(RevertCustomizeAsync(customizeId, changes.Key));
|
||||||
_customizeIds.Remove(changes.Key);
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case PlayerChanges.Heels:
|
case PlayerChanges.Heels:
|
||||||
await _ipcManager.Heels.SetOffsetForPlayerAsync(handler.Address, charaData.HeelsData).ConfigureAwait(false);
|
tasks.Add(_ipcManager.Heels.SetOffsetForPlayerAsync(handler.Address, charaData.HeelsData));
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case PlayerChanges.Honorific:
|
case PlayerChanges.Honorific:
|
||||||
await _ipcManager.Honorific.SetTitleAsync(handler.Address, charaData.HonorificData).ConfigureAwait(false);
|
tasks.Add(_ipcManager.Honorific.SetTitleAsync(handler.Address, charaData.HonorificData));
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case PlayerChanges.Glamourer:
|
case PlayerChanges.Glamourer:
|
||||||
if (charaData.GlamourerData.TryGetValue(changes.Key, out var glamourerData))
|
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;
|
break;
|
||||||
|
|
||||||
case PlayerChanges.Moodles:
|
case PlayerChanges.Moodles:
|
||||||
await _ipcManager.Moodles.SetStatusAsync(handler.Address, charaData.MoodlesData).ConfigureAwait(false);
|
tasks.Add(_ipcManager.Moodles.SetStatusAsync(handler.Address, charaData.MoodlesData));
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case PlayerChanges.PetNames:
|
case PlayerChanges.PetNames:
|
||||||
await _ipcManager.PetNames.SetPlayerData(handler.Address, charaData.PetNamesData).ConfigureAwait(false);
|
tasks.Add(_ipcManager.PetNames.SetPlayerData(handler.Address, charaData.PetNamesData));
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case PlayerChanges.ForcedRedraw:
|
case PlayerChanges.ForcedRedraw:
|
||||||
if (!ShouldPerformForcedRedraw(changes.Key, changes.Value, charaData))
|
needsRedraw = true;
|
||||||
{
|
|
||||||
Logger.LogTrace("[{applicationId}] Skipping forced redraw for {handler}", applicationId, handler);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
await _ipcManager.Penumbra.RedrawAsync(Logger, handler, applicationId, token).ConfigureAwait(false);
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@@ -1140,6 +1256,16 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
}
|
}
|
||||||
token.ThrowIfCancellationRequested();
|
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
|
finally
|
||||||
{
|
{
|
||||||
@@ -1147,44 +1273,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)
|
private static Dictionary<ObjectKind, HashSet<PlayerChanges>> BuildFullChangeSet(CharacterData characterData)
|
||||||
{
|
{
|
||||||
@@ -1339,6 +1427,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
bool skipDownscaleForPair = ShouldSkipDownscale();
|
bool skipDownscaleForPair = ShouldSkipDownscale();
|
||||||
var user = GetPrimaryUserData();
|
var user = GetPrimaryUserData();
|
||||||
Dictionary<(string GamePath, string? Hash), string> moddedPaths;
|
Dictionary<(string GamePath, string? Hash), string> moddedPaths;
|
||||||
|
List<FileReplacementData> missingReplacements = [];
|
||||||
|
|
||||||
if (updateModdedPaths)
|
if (updateModdedPaths)
|
||||||
{
|
{
|
||||||
@@ -1350,6 +1439,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
{
|
{
|
||||||
int attempts = 0;
|
int attempts = 0;
|
||||||
List<FileReplacementData> toDownloadReplacements = TryCalculateModdedDictionary(applicationBase, charaData, out moddedPaths, downloadToken);
|
List<FileReplacementData> toDownloadReplacements = TryCalculateModdedDictionary(applicationBase, charaData, out moddedPaths, downloadToken);
|
||||||
|
missingReplacements = toDownloadReplacements;
|
||||||
|
|
||||||
while (toDownloadReplacements.Count > 0 && attempts++ <= 10 && !downloadToken.IsCancellationRequested)
|
while (toDownloadReplacements.Count > 0 && attempts++ <= 10 && !downloadToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
@@ -1399,6 +1489,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
}
|
}
|
||||||
|
|
||||||
toDownloadReplacements = TryCalculateModdedDictionary(applicationBase, charaData, out moddedPaths, downloadToken);
|
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))))
|
if (toDownloadReplacements.TrueForAll(c => _downloadManager.ForbiddenTransfers.Exists(f => string.Equals(f.Hash, c.Hash, StringComparison.Ordinal))))
|
||||||
{
|
{
|
||||||
@@ -1422,6 +1513,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();
|
downloadToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
var handlerForApply = _charaHandler;
|
var handlerForApply = _charaHandler;
|
||||||
@@ -1454,7 +1593,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
_applicationCancellationTokenSource = _applicationCancellationTokenSource.CancelRecreate() ?? new CancellationTokenSource();
|
_applicationCancellationTokenSource = _applicationCancellationTokenSource.CancelRecreate() ?? new CancellationTokenSource();
|
||||||
var token = _applicationCancellationTokenSource.Token;
|
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
|
finally
|
||||||
{
|
{
|
||||||
@@ -1463,7 +1602,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,
|
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
|
try
|
||||||
{
|
{
|
||||||
@@ -1472,6 +1611,10 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
|
|
||||||
Logger.LogDebug("[{applicationId}] Waiting for initial draw for for {handler}", _applicationId, handlerForApply);
|
Logger.LogDebug("[{applicationId}] Waiting for initial draw for for {handler}", _applicationId, handlerForApply);
|
||||||
await _dalamudUtil.WaitWhileCharacterIsDrawing(Logger, handlerForApply, _applicationId, 30000, token).ConfigureAwait(false);
|
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();
|
token.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
@@ -1538,7 +1681,11 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
|
|
||||||
_cachedData = charaData;
|
_cachedData = charaData;
|
||||||
_pairStateCache.Store(Ident, charaData);
|
_pairStateCache.Store(Ident, charaData);
|
||||||
_forceFullReapply = false;
|
if (wantsModApply)
|
||||||
|
{
|
||||||
|
_pendingModReapply = pendingModReapply;
|
||||||
|
}
|
||||||
|
_forceFullReapply = _pendingModReapply;
|
||||||
_needsCollectionRebuild = false;
|
_needsCollectionRebuild = false;
|
||||||
if (LastAppliedApproximateVRAMBytes < 0 || LastAppliedApproximateEffectiveVRAMBytes < 0)
|
if (LastAppliedApproximateVRAMBytes < 0 || LastAppliedApproximateEffectiveVRAMBytes < 0)
|
||||||
{
|
{
|
||||||
@@ -1584,8 +1731,15 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
|
|
||||||
private void FrameworkUpdate()
|
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);
|
var pc = _dalamudUtil.FindPlayerByNameHash(Ident);
|
||||||
if (pc == default((string, nint))) return;
|
if (pc == default((string, nint))) return;
|
||||||
Logger.LogDebug("One-Time Initializing {handler}", GetLogIdentifier());
|
Logger.LogDebug("One-Time Initializing {handler}", GetLogIdentifier());
|
||||||
@@ -1595,6 +1749,11 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
$"Initializing User For Character {pc.Name}")));
|
$"Initializing User For Character {pc.Name}")));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TryHandleVisibilityUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TryHandleVisibilityUpdate()
|
||||||
|
{
|
||||||
if (_charaHandler?.Address != nint.Zero && !IsVisible && !_pauseRequested)
|
if (_charaHandler?.Address != nint.Zero && !IsVisible && !_pauseRequested)
|
||||||
{
|
{
|
||||||
Guid appData = Guid.NewGuid();
|
Guid appData = Guid.NewGuid();
|
||||||
@@ -1641,16 +1800,24 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
}
|
}
|
||||||
else if (_charaHandler?.Address == nint.Zero && IsVisible)
|
else if (_charaHandler?.Address == nint.Zero && IsVisible)
|
||||||
{
|
{
|
||||||
IsVisible = false;
|
HandleVisibilityLoss(logChange: true);
|
||||||
_charaHandler.Invalidate();
|
|
||||||
_downloadCancellationTokenSource?.CancelDispose();
|
|
||||||
_downloadCancellationTokenSource = null;
|
|
||||||
Logger.LogTrace("{handler} visibility changed, now: {visi}", GetLogIdentifier(), IsVisible);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
TryApplyQueuedData();
|
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)
|
private void Initialize(string name)
|
||||||
{
|
{
|
||||||
PlayerName = name;
|
PlayerName = name;
|
||||||
@@ -1977,7 +2144,164 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
}
|
}
|
||||||
|
|
||||||
_dataReceivedInDowntime = null;
|
_dataReceivedInDowntime = null;
|
||||||
ApplyCharacterData(pending.ApplicationId,
|
_ = Task.Run(() =>
|
||||||
pending.CharacterData, pending.Forced);
|
{
|
||||||
|
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.Interop.Ipc;
|
||||||
using LightlessSync.PlayerData.Factories;
|
using LightlessSync.PlayerData.Factories;
|
||||||
using LightlessSync.Services;
|
using LightlessSync.Services;
|
||||||
|
using LightlessSync.Services.ActorTracking;
|
||||||
using LightlessSync.Services.Mediator;
|
using LightlessSync.Services.Mediator;
|
||||||
using LightlessSync.Services.PairProcessing;
|
using LightlessSync.Services.PairProcessing;
|
||||||
using LightlessSync.Services.ServerConfiguration;
|
using LightlessSync.Services.ServerConfiguration;
|
||||||
@@ -30,6 +31,7 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory
|
|||||||
private readonly TextureDownscaleService _textureDownscaleService;
|
private readonly TextureDownscaleService _textureDownscaleService;
|
||||||
private readonly PairStateCache _pairStateCache;
|
private readonly PairStateCache _pairStateCache;
|
||||||
private readonly PairPerformanceMetricsCache _pairPerformanceMetricsCache;
|
private readonly PairPerformanceMetricsCache _pairPerformanceMetricsCache;
|
||||||
|
private readonly PenumbraTempCollectionJanitor _tempCollectionJanitor;
|
||||||
|
|
||||||
public PairHandlerAdapterFactory(
|
public PairHandlerAdapterFactory(
|
||||||
ILoggerFactory loggerFactory,
|
ILoggerFactory loggerFactory,
|
||||||
@@ -47,7 +49,8 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory
|
|||||||
ServerConfigurationManager serverConfigManager,
|
ServerConfigurationManager serverConfigManager,
|
||||||
TextureDownscaleService textureDownscaleService,
|
TextureDownscaleService textureDownscaleService,
|
||||||
PairStateCache pairStateCache,
|
PairStateCache pairStateCache,
|
||||||
PairPerformanceMetricsCache pairPerformanceMetricsCache)
|
PairPerformanceMetricsCache pairPerformanceMetricsCache,
|
||||||
|
PenumbraTempCollectionJanitor tempCollectionJanitor)
|
||||||
{
|
{
|
||||||
_loggerFactory = loggerFactory;
|
_loggerFactory = loggerFactory;
|
||||||
_mediator = mediator;
|
_mediator = mediator;
|
||||||
@@ -65,12 +68,14 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory
|
|||||||
_textureDownscaleService = textureDownscaleService;
|
_textureDownscaleService = textureDownscaleService;
|
||||||
_pairStateCache = pairStateCache;
|
_pairStateCache = pairStateCache;
|
||||||
_pairPerformanceMetricsCache = pairPerformanceMetricsCache;
|
_pairPerformanceMetricsCache = pairPerformanceMetricsCache;
|
||||||
|
_tempCollectionJanitor = tempCollectionJanitor;
|
||||||
}
|
}
|
||||||
|
|
||||||
public IPairHandlerAdapter Create(string ident)
|
public IPairHandlerAdapter Create(string ident)
|
||||||
{
|
{
|
||||||
var downloadManager = _fileDownloadManagerFactory.Create();
|
var downloadManager = _fileDownloadManagerFactory.Create();
|
||||||
var dalamudUtilService = _serviceProvider.GetRequiredService<DalamudUtilService>();
|
var dalamudUtilService = _serviceProvider.GetRequiredService<DalamudUtilService>();
|
||||||
|
var actorObjectService = _serviceProvider.GetRequiredService<ActorObjectService>();
|
||||||
return new PairHandlerAdapter(
|
return new PairHandlerAdapter(
|
||||||
_loggerFactory.CreateLogger<PairHandlerAdapter>(),
|
_loggerFactory.CreateLogger<PairHandlerAdapter>(),
|
||||||
_mediator,
|
_mediator,
|
||||||
@@ -81,6 +86,7 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory
|
|||||||
downloadManager,
|
downloadManager,
|
||||||
_pluginWarningNotificationManager,
|
_pluginWarningNotificationManager,
|
||||||
dalamudUtilService,
|
dalamudUtilService,
|
||||||
|
actorObjectService,
|
||||||
_lifetime,
|
_lifetime,
|
||||||
_fileCacheManager,
|
_fileCacheManager,
|
||||||
_playerPerformanceService,
|
_playerPerformanceService,
|
||||||
@@ -88,6 +94,7 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory
|
|||||||
_serverConfigManager,
|
_serverConfigManager,
|
||||||
_textureDownscaleService,
|
_textureDownscaleService,
|
||||||
_pairStateCache,
|
_pairStateCache,
|
||||||
_pairPerformanceMetricsCache);
|
_pairPerformanceMetricsCache,
|
||||||
|
_tempCollectionJanitor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ using System.Reflection;
|
|||||||
using OtterTex;
|
using OtterTex;
|
||||||
using LightlessSync.Services.LightFinder;
|
using LightlessSync.Services.LightFinder;
|
||||||
using LightlessSync.Services.PairProcessing;
|
using LightlessSync.Services.PairProcessing;
|
||||||
|
using LightlessSync.UI.Models;
|
||||||
|
|
||||||
namespace LightlessSync;
|
namespace LightlessSync;
|
||||||
|
|
||||||
@@ -135,6 +136,7 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
services.AddSingleton<ZoneChatService>();
|
services.AddSingleton<ZoneChatService>();
|
||||||
services.AddSingleton<IdDisplayHandler>();
|
services.AddSingleton<IdDisplayHandler>();
|
||||||
services.AddSingleton<PlayerPerformanceService>();
|
services.AddSingleton<PlayerPerformanceService>();
|
||||||
|
services.AddSingleton<PenumbraTempCollectionJanitor>();
|
||||||
|
|
||||||
services.AddSingleton<TextureMetadataHelper>(sp =>
|
services.AddSingleton<TextureMetadataHelper>(sp =>
|
||||||
new TextureMetadataHelper(sp.GetRequiredService<ILogger<TextureMetadataHelper>>(), gameData));
|
new TextureMetadataHelper(sp.GetRequiredService<ILogger<TextureMetadataHelper>>(), gameData));
|
||||||
@@ -201,6 +203,7 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
gameInteropProvider,
|
gameInteropProvider,
|
||||||
objectTable,
|
objectTable,
|
||||||
clientState,
|
clientState,
|
||||||
|
condition,
|
||||||
sp.GetRequiredService<LightlessMediator>()));
|
sp.GetRequiredService<LightlessMediator>()));
|
||||||
|
|
||||||
services.AddSingleton(sp => new DalamudUtilService(
|
services.AddSingleton(sp => new DalamudUtilService(
|
||||||
@@ -298,7 +301,10 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
sp.GetRequiredService<LightFinderScannerService>(),
|
sp.GetRequiredService<LightFinderScannerService>(),
|
||||||
sp.GetRequiredService<LightFinderService>(),
|
sp.GetRequiredService<LightFinderService>(),
|
||||||
sp.GetRequiredService<LightlessProfileManager>(),
|
sp.GetRequiredService<LightlessProfileManager>(),
|
||||||
sp.GetRequiredService<LightlessMediator>()));
|
sp.GetRequiredService<LightlessMediator>(),
|
||||||
|
chatGui,
|
||||||
|
sp.GetRequiredService<NotificationService>())
|
||||||
|
);
|
||||||
|
|
||||||
// IPC callers / manager
|
// IPC callers / manager
|
||||||
services.AddSingleton(sp => new IpcCallerPenumbra(
|
services.AddSingleton(sp => new IpcCallerPenumbra(
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
|
using Dalamud.Game.ClientState.Conditions;
|
||||||
using Dalamud.Game.ClientState.Objects.SubKinds;
|
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||||
using Dalamud.Hooking;
|
using Dalamud.Hooking;
|
||||||
using Dalamud.Plugin.Services;
|
using Dalamud.Plugin.Services;
|
||||||
@@ -31,13 +32,18 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
|||||||
private readonly IFramework _framework;
|
private readonly IFramework _framework;
|
||||||
private readonly IGameInteropProvider _interop;
|
private readonly IGameInteropProvider _interop;
|
||||||
private readonly IObjectTable _objectTable;
|
private readonly IObjectTable _objectTable;
|
||||||
|
private readonly IClientState _clientState;
|
||||||
|
private readonly ICondition _condition;
|
||||||
private readonly LightlessMediator _mediator;
|
private readonly LightlessMediator _mediator;
|
||||||
|
|
||||||
private readonly ConcurrentDictionary<nint, ActorDescriptor> _activePlayers = new();
|
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, ActorDescriptor> _actorsByHash = new(StringComparer.Ordinal);
|
||||||
private readonly ConcurrentDictionary<string, ConcurrentDictionary<nint, ActorDescriptor>> _actorsByName = 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 readonly OwnedObjectTracker _ownedTracker = new();
|
||||||
private ActorSnapshot _snapshot = ActorSnapshot.Empty;
|
private ActorSnapshot _snapshot = ActorSnapshot.Empty;
|
||||||
|
private GposeSnapshot _gposeSnapshot = GposeSnapshot.Empty;
|
||||||
|
|
||||||
private Hook<Character.Delegates.OnInitialize>? _onInitializeHook;
|
private Hook<Character.Delegates.OnInitialize>? _onInitializeHook;
|
||||||
private Hook<Character.Delegates.Terminate>? _onTerminateHook;
|
private Hook<Character.Delegates.Terminate>? _onTerminateHook;
|
||||||
@@ -55,21 +61,29 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
|||||||
IGameInteropProvider interop,
|
IGameInteropProvider interop,
|
||||||
IObjectTable objectTable,
|
IObjectTable objectTable,
|
||||||
IClientState clientState,
|
IClientState clientState,
|
||||||
|
ICondition condition,
|
||||||
LightlessMediator mediator)
|
LightlessMediator mediator)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_framework = framework;
|
_framework = framework;
|
||||||
_interop = interop;
|
_interop = interop;
|
||||||
_objectTable = objectTable;
|
_objectTable = objectTable;
|
||||||
|
_clientState = clientState;
|
||||||
|
_condition = condition;
|
||||||
_mediator = mediator;
|
_mediator = mediator;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool IsZoning => _condition[ConditionFlag.BetweenAreas] || _condition[ConditionFlag.BetweenAreas51];
|
||||||
|
|
||||||
private ActorSnapshot Snapshot => Volatile.Read(ref _snapshot);
|
private ActorSnapshot Snapshot => Volatile.Read(ref _snapshot);
|
||||||
|
private GposeSnapshot CurrentGposeSnapshot => Volatile.Read(ref _gposeSnapshot);
|
||||||
|
|
||||||
public IReadOnlyList<nint> PlayerAddresses => Snapshot.PlayerAddresses;
|
public IReadOnlyList<nint> PlayerAddresses => Snapshot.PlayerAddresses;
|
||||||
|
|
||||||
public IEnumerable<ActorDescriptor> PlayerDescriptors => _activePlayers.Values;
|
public IEnumerable<ActorDescriptor> ObjectDescriptors => _activePlayers.Values;
|
||||||
public IReadOnlyList<ActorDescriptor> PlayerCharacterDescriptors => Snapshot.PlayerDescriptors;
|
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 TryGetActorByHash(string hash, out ActorDescriptor descriptor) => _actorsByHash.TryGetValue(hash, out descriptor);
|
||||||
public bool TryGetValidatedActorByHash(string hash, out ActorDescriptor descriptor)
|
public bool TryGetValidatedActorByHash(string hash, out ActorDescriptor descriptor)
|
||||||
@@ -113,6 +127,7 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
public bool HooksActive => _hooksActive;
|
public bool HooksActive => _hooksActive;
|
||||||
|
public bool HasPendingHashResolutions => !_pendingHashResolutions.IsEmpty;
|
||||||
public IReadOnlyList<nint> RenderedPlayerAddresses => Snapshot.OwnedObjects.RenderedPlayers;
|
public IReadOnlyList<nint> RenderedPlayerAddresses => Snapshot.OwnedObjects.RenderedPlayers;
|
||||||
public IReadOnlyList<nint> RenderedCompanionAddresses => Snapshot.OwnedObjects.RenderedCompanions;
|
public IReadOnlyList<nint> RenderedCompanionAddresses => Snapshot.OwnedObjects.RenderedCompanions;
|
||||||
public IReadOnlyList<nint> OwnedObjectAddresses => Snapshot.OwnedObjects.OwnedAddresses;
|
public IReadOnlyList<nint> OwnedObjectAddresses => Snapshot.OwnedObjects.OwnedAddresses;
|
||||||
@@ -207,7 +222,7 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
|||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
var isLoaded = await _framework.RunOnFrameworkThread(() => IsObjectFullyLoaded(address)).ConfigureAwait(false);
|
var isLoaded = await _framework.RunOnFrameworkThread(() => IsObjectFullyLoaded(address)).ConfigureAwait(false);
|
||||||
if (isLoaded)
|
if (!IsZoning && isLoaded)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
await Task.Delay(100, cancellationToken).ConfigureAwait(false);
|
await Task.Delay(100, cancellationToken).ConfigureAwait(false);
|
||||||
@@ -297,10 +312,13 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
|||||||
{
|
{
|
||||||
DisposeHooks();
|
DisposeHooks();
|
||||||
_activePlayers.Clear();
|
_activePlayers.Clear();
|
||||||
|
_gposePlayers.Clear();
|
||||||
_actorsByHash.Clear();
|
_actorsByHash.Clear();
|
||||||
_actorsByName.Clear();
|
_actorsByName.Clear();
|
||||||
|
_pendingHashResolutions.Clear();
|
||||||
_ownedTracker.Reset();
|
_ownedTracker.Reset();
|
||||||
Volatile.Write(ref _snapshot, ActorSnapshot.Empty);
|
Volatile.Write(ref _snapshot, ActorSnapshot.Empty);
|
||||||
|
Volatile.Write(ref _gposeSnapshot, GposeSnapshot.Empty);
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -336,7 +354,7 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
|||||||
_onCompanionTerminateHook.Enable();
|
_onCompanionTerminateHook.Enable();
|
||||||
|
|
||||||
_hooksActive = true;
|
_hooksActive = true;
|
||||||
_logger.LogDebug("ActorObjectService hooks enabled.");
|
_logger.LogTrace("ActorObjectService hooks enabled.");
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task WarmupExistingActors()
|
private Task WarmupExistingActors()
|
||||||
@@ -350,36 +368,21 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
|||||||
|
|
||||||
private unsafe void OnCharacterInitialized(Character* chara)
|
private unsafe void OnCharacterInitialized(Character* chara)
|
||||||
{
|
{
|
||||||
try
|
ExecuteOriginal(() => _onInitializeHook!.Original(chara), "Error invoking original character initialize.");
|
||||||
{
|
QueueTrack((GameObject*)chara);
|
||||||
_onInitializeHook!.Original(chara);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error invoking original character initialize.");
|
|
||||||
}
|
|
||||||
|
|
||||||
QueueFrameworkUpdate(() => TrackGameObject((GameObject*)chara));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private unsafe void OnCharacterTerminated(Character* chara)
|
private unsafe void OnCharacterTerminated(Character* chara)
|
||||||
{
|
{
|
||||||
var address = (nint)chara;
|
var address = (nint)chara;
|
||||||
QueueFrameworkUpdate(() => UntrackGameObject(address));
|
QueueUntrack(address);
|
||||||
try
|
ExecuteOriginal(() => _onTerminateHook!.Original(chara), "Error invoking original character terminate.");
|
||||||
{
|
|
||||||
_onTerminateHook!.Original(chara);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error invoking original character terminate.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private unsafe GameObject* OnCharacterDisposed(Character* chara, byte freeMemory)
|
private unsafe GameObject* OnCharacterDisposed(Character* chara, byte freeMemory)
|
||||||
{
|
{
|
||||||
var address = (nint)chara;
|
var address = (nint)chara;
|
||||||
QueueFrameworkUpdate(() => UntrackGameObject(address));
|
QueueUntrack(address);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return _onDestructorHook!.Original(chara, freeMemory);
|
return _onDestructorHook!.Original(chara, freeMemory);
|
||||||
@@ -416,7 +419,7 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
|||||||
|
|
||||||
if (_logger.IsEnabled(LogLevel.Debug))
|
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.Name,
|
||||||
descriptor.Address,
|
descriptor.Address,
|
||||||
descriptor.ObjectIndex,
|
descriptor.ObjectIndex,
|
||||||
@@ -534,7 +537,7 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
|||||||
RemoveDescriptor(descriptor);
|
RemoveDescriptor(descriptor);
|
||||||
if (_logger.IsEnabled(LogLevel.Debug))
|
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.Name,
|
||||||
descriptor.Address,
|
descriptor.Address,
|
||||||
descriptor.ObjectIndex,
|
descriptor.ObjectIndex,
|
||||||
@@ -558,10 +561,14 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
|||||||
if (!seen.Add(address))
|
if (!seen.Add(address))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (_activePlayers.ContainsKey(address))
|
var gameObject = (GameObject*)address;
|
||||||
|
if (_activePlayers.TryGetValue(address, out var existing))
|
||||||
|
{
|
||||||
|
RefreshDescriptorIfNeeded(existing, gameObject);
|
||||||
continue;
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
TrackGameObject((GameObject*)address);
|
TrackGameObject(gameObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
var stale = _activePlayers.Keys.Where(addr => !seen.Contains(addr)).ToList();
|
var stale = _activePlayers.Keys.Where(addr => !seen.Contains(addr)).ToList();
|
||||||
@@ -574,6 +581,50 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
|||||||
{
|
{
|
||||||
_nextRefreshAllowed = DateTime.UtcNow + SnapshotRefreshInterval;
|
_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)
|
private void IndexDescriptor(ActorDescriptor descriptor)
|
||||||
@@ -605,30 +656,15 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
|||||||
|
|
||||||
private unsafe void OnCompanionInitialized(Companion* companion)
|
private unsafe void OnCompanionInitialized(Companion* companion)
|
||||||
{
|
{
|
||||||
try
|
ExecuteOriginal(() => _onCompanionInitializeHook!.Original(companion), "Error invoking original companion initialize.");
|
||||||
{
|
QueueTrack((GameObject*)companion);
|
||||||
_onCompanionInitializeHook!.Original(companion);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error invoking original companion initialize.");
|
|
||||||
}
|
|
||||||
|
|
||||||
QueueFrameworkUpdate(() => TrackGameObject((GameObject*)companion));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private unsafe void OnCompanionTerminated(Companion* companion)
|
private unsafe void OnCompanionTerminated(Companion* companion)
|
||||||
{
|
{
|
||||||
var address = (nint)companion;
|
var address = (nint)companion;
|
||||||
QueueFrameworkUpdate(() => UntrackGameObject(address));
|
QueueUntrack(address);
|
||||||
try
|
ExecuteOriginal(() => _onCompanionTerminateHook!.Original(companion), "Error invoking original companion terminate.");
|
||||||
{
|
|
||||||
_onCompanionTerminateHook!.Original(companion);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error invoking original companion terminate.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RemoveDescriptorFromIndexes(ActorDescriptor descriptor)
|
private void RemoveDescriptorFromIndexes(ActorDescriptor descriptor)
|
||||||
@@ -655,6 +691,7 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
|||||||
_activePlayers[descriptor.Address] = descriptor;
|
_activePlayers[descriptor.Address] = descriptor;
|
||||||
IndexDescriptor(descriptor);
|
IndexDescriptor(descriptor);
|
||||||
_ownedTracker.OnDescriptorAdded(descriptor);
|
_ownedTracker.OnDescriptorAdded(descriptor);
|
||||||
|
UpdatePendingHashResolutions(descriptor);
|
||||||
PublishSnapshot();
|
PublishSnapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -662,21 +699,42 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
|||||||
{
|
{
|
||||||
RemoveDescriptorFromIndexes(descriptor);
|
RemoveDescriptorFromIndexes(descriptor);
|
||||||
_ownedTracker.OnDescriptorRemoved(descriptor);
|
_ownedTracker.OnDescriptorRemoved(descriptor);
|
||||||
|
_pendingHashResolutions.TryRemove(descriptor.Address, out _);
|
||||||
PublishSnapshot();
|
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()
|
private void PublishSnapshot()
|
||||||
{
|
{
|
||||||
var playerDescriptors = _activePlayers.Values
|
var playerDescriptors = _activePlayers.Values
|
||||||
.Where(descriptor => descriptor.ObjectKind == DalamudObjectKind.Player)
|
.Where(descriptor => descriptor.ObjectKind == DalamudObjectKind.Player)
|
||||||
.ToArray();
|
.ToArray();
|
||||||
|
var ownedDescriptors = _activePlayers.Values
|
||||||
|
.Where(descriptor => descriptor.OwnedKind is not null)
|
||||||
|
.ToArray();
|
||||||
var playerAddresses = new nint[playerDescriptors.Length];
|
var playerAddresses = new nint[playerDescriptors.Length];
|
||||||
for (var i = 0; i < playerDescriptors.Length; i++)
|
for (var i = 0; i < playerDescriptors.Length; i++)
|
||||||
playerAddresses[i] = playerDescriptors[i].Address;
|
playerAddresses[i] = playerDescriptors[i].Address;
|
||||||
|
|
||||||
var ownedSnapshot = _ownedTracker.CreateSnapshot();
|
var ownedSnapshot = _ownedTracker.CreateSnapshot();
|
||||||
var nextGeneration = Snapshot.Generation + 1;
|
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);
|
Volatile.Write(ref _snapshot, snapshot);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -694,6 +752,24 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
|||||||
_ = _framework.RunOnFrameworkThread(action);
|
_ = _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()
|
private void DisposeHooks()
|
||||||
{
|
{
|
||||||
var hadHooks = _hooksActive
|
var hadHooks = _hooksActive
|
||||||
@@ -725,7 +801,7 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
|||||||
|
|
||||||
if (hadHooks)
|
if (hadHooks)
|
||||||
{
|
{
|
||||||
_logger.LogDebug("ActorObjectService hooks disabled.");
|
_logger.LogTrace("ActorObjectService hooks disabled.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -770,6 +846,89 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
|||||||
return results;
|
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)
|
private static unsafe bool IsObjectFullyLoaded(nint address)
|
||||||
{
|
{
|
||||||
if (address == nint.Zero)
|
if (address == nint.Zero)
|
||||||
@@ -783,13 +942,10 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
|||||||
if (drawObject == null)
|
if (drawObject == null)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if ((gameObject->RenderFlags & VisibilityFlags.Nameplate) != VisibilityFlags.None)
|
if ((ulong)gameObject->RenderFlags == 2048)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
var characterBase = (CharacterBase*)drawObject;
|
var characterBase = (CharacterBase*)drawObject;
|
||||||
if (characterBase == null)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (characterBase->HasModelInSlotLoaded != 0)
|
if (characterBase->HasModelInSlotLoaded != 0)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
@@ -925,14 +1081,27 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
|||||||
|
|
||||||
private sealed record ActorSnapshot(
|
private sealed record ActorSnapshot(
|
||||||
IReadOnlyList<ActorDescriptor> PlayerDescriptors,
|
IReadOnlyList<ActorDescriptor> PlayerDescriptors,
|
||||||
|
IReadOnlyList<ActorDescriptor> OwnedDescriptors,
|
||||||
IReadOnlyList<nint> PlayerAddresses,
|
IReadOnlyList<nint> PlayerAddresses,
|
||||||
OwnedObjectSnapshot OwnedObjects,
|
OwnedObjectSnapshot OwnedObjects,
|
||||||
int Generation)
|
int Generation)
|
||||||
{
|
{
|
||||||
public static ActorSnapshot Empty { get; } = new(
|
public static ActorSnapshot Empty { get; } = new(
|
||||||
|
Array.Empty<ActorDescriptor>(),
|
||||||
Array.Empty<ActorDescriptor>(),
|
Array.Empty<ActorDescriptor>(),
|
||||||
Array.Empty<nint>(),
|
Array.Empty<nint>(),
|
||||||
OwnedObjectSnapshot.Empty,
|
OwnedObjectSnapshot.Empty,
|
||||||
0);
|
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.Dto.Chat;
|
||||||
|
using LightlessSync.API.Data.Extensions;
|
||||||
using LightlessSync.Services.ActorTracking;
|
using LightlessSync.Services.ActorTracking;
|
||||||
using LightlessSync.Services.Mediator;
|
using LightlessSync.Services.Mediator;
|
||||||
using LightlessSync.WebAPI;
|
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, bool> _lastPresenceStates = new(StringComparer.Ordinal);
|
||||||
private readonly Dictionary<string, string> _selfTokens = new(StringComparer.Ordinal);
|
private readonly Dictionary<string, string> _selfTokens = new(StringComparer.Ordinal);
|
||||||
private readonly List<PendingSelfMessage> _pendingSelfMessages = new();
|
private readonly List<PendingSelfMessage> _pendingSelfMessages = new();
|
||||||
|
private List<ChatChannelSnapshot>? _cachedChannelSnapshots;
|
||||||
|
private bool _channelsSnapshotDirty = true;
|
||||||
|
|
||||||
private bool _isLoggedIn;
|
private bool _isLoggedIn;
|
||||||
private bool _isConnected;
|
private bool _isConnected;
|
||||||
@@ -69,6 +72,11 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
|||||||
{
|
{
|
||||||
using (_sync.EnterScope())
|
using (_sync.EnterScope())
|
||||||
{
|
{
|
||||||
|
if (!_channelsSnapshotDirty && _cachedChannelSnapshots is not null)
|
||||||
|
{
|
||||||
|
return _cachedChannelSnapshots;
|
||||||
|
}
|
||||||
|
|
||||||
var snapshots = new List<ChatChannelSnapshot>(_channelOrder.Count);
|
var snapshots = new List<ChatChannelSnapshot>(_channelOrder.Count);
|
||||||
foreach (var key in _channelOrder)
|
foreach (var key in _channelOrder)
|
||||||
{
|
{
|
||||||
@@ -98,6 +106,8 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
|||||||
state.Messages.ToList()));
|
state.Messages.ToList()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_cachedChannelSnapshots = snapshots;
|
||||||
|
_channelsSnapshotDirty = false;
|
||||||
return snapshots;
|
return snapshots;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -135,6 +145,8 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
|||||||
state.UnreadCount = 0;
|
state.UnreadCount = 0;
|
||||||
_lastReadCounts[key] = state.Messages.Count;
|
_lastReadCounts[key] = state.Messages.Count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MarkChannelsSnapshotDirtyLocked();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,6 +198,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
|||||||
if (!wasEnabled)
|
if (!wasEnabled)
|
||||||
{
|
{
|
||||||
_chatEnabled = true;
|
_chatEnabled = true;
|
||||||
|
MarkChannelsSnapshotDirtyLocked();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,6 +244,8 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
|||||||
state.IsAvailable = false;
|
state.IsAvailable = false;
|
||||||
state.StatusText = "Chat services disabled";
|
state.StatusText = "Chat services disabled";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MarkChannelsSnapshotDirtyLocked();
|
||||||
}
|
}
|
||||||
|
|
||||||
UnregisterChatHandler();
|
UnregisterChatHandler();
|
||||||
@@ -717,7 +732,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
|||||||
_zoneDefinitions[key] = new ZoneChannelDefinition(key, info.DisplayName ?? key, descriptor, territories);
|
_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 kvp in territoryData)
|
||||||
{
|
{
|
||||||
foreach (var variant in EnumerateTerritoryKeys(kvp.Value))
|
foreach (var variant in EnumerateTerritoryKeys(kvp.Value))
|
||||||
@@ -853,6 +868,12 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
|||||||
var infos = new List<GroupChatChannelInfoDto>(groups.Count);
|
var infos = new List<GroupChatChannelInfoDto>(groups.Count);
|
||||||
foreach (var group in groups)
|
foreach (var group in groups)
|
||||||
{
|
{
|
||||||
|
// basically prune the channel if it's disabled
|
||||||
|
if (group.GroupPermissions.IsDisableChat())
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
var descriptor = new ChatChannelDescriptor
|
var descriptor = new ChatChannelDescriptor
|
||||||
{
|
{
|
||||||
Type = ChatChannelType.Group,
|
Type = ChatChannelType.Group,
|
||||||
@@ -1023,6 +1044,8 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
|||||||
state.UnreadCount = Math.Min(Math.Max(unreadFromHistory, incrementalUnread), MaxUnreadCount);
|
state.UnreadCount = Math.Min(Math.Max(unreadFromHistory, incrementalUnread), MaxUnreadCount);
|
||||||
state.HasUnread = state.UnreadCount > 0;
|
state.HasUnread = state.UnreadCount > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MarkChannelsSnapshotDirtyLocked();
|
||||||
}
|
}
|
||||||
|
|
||||||
Mediator.Publish(new ChatChannelMessageAdded(key, message));
|
Mediator.Publish(new ChatChannelMessageAdded(key, message));
|
||||||
@@ -1204,9 +1227,25 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
|||||||
{
|
{
|
||||||
_activeChannelKey = _channelOrder.Count > 0 ? _channelOrder[0] : null;
|
_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)
|
private static IEnumerable<string> EnumerateTerritoryKeys(string? value)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,21 +4,22 @@ using Dalamud.Plugin;
|
|||||||
using Dalamud.Plugin.Services;
|
using Dalamud.Plugin.Services;
|
||||||
using LightlessSync.LightlessConfiguration;
|
using LightlessSync.LightlessConfiguration;
|
||||||
using LightlessSync.LightlessConfiguration.Models;
|
using LightlessSync.LightlessConfiguration.Models;
|
||||||
|
using LightlessSync.Services.LightFinder;
|
||||||
using LightlessSync.Services.Mediator;
|
using LightlessSync.Services.Mediator;
|
||||||
|
using LightlessSync.UI;
|
||||||
|
using LightlessSync.UI.Services;
|
||||||
using LightlessSync.Utils;
|
using LightlessSync.Utils;
|
||||||
using LightlessSync.WebAPI;
|
using LightlessSync.WebAPI;
|
||||||
using Lumina.Excel.Sheets;
|
using Lumina.Excel.Sheets;
|
||||||
using LightlessSync.UI.Services;
|
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using LightlessSync.UI;
|
|
||||||
using LightlessSync.Services.LightFinder;
|
|
||||||
|
|
||||||
namespace LightlessSync.Services;
|
namespace LightlessSync.Services;
|
||||||
|
|
||||||
internal class ContextMenuService : IHostedService
|
internal class ContextMenuService : IHostedService
|
||||||
{
|
{
|
||||||
private readonly IContextMenu _contextMenu;
|
private readonly IContextMenu _contextMenu;
|
||||||
|
private readonly IChatGui _chatGui;
|
||||||
private readonly IDalamudPluginInterface _pluginInterface;
|
private readonly IDalamudPluginInterface _pluginInterface;
|
||||||
private readonly IDataManager _gameData;
|
private readonly IDataManager _gameData;
|
||||||
private readonly ILogger<ContextMenuService> _logger;
|
private readonly ILogger<ContextMenuService> _logger;
|
||||||
@@ -29,6 +30,7 @@ internal class ContextMenuService : IHostedService
|
|||||||
private readonly ApiController _apiController;
|
private readonly ApiController _apiController;
|
||||||
private readonly IObjectTable _objectTable;
|
private readonly IObjectTable _objectTable;
|
||||||
private readonly LightlessConfigService _configService;
|
private readonly LightlessConfigService _configService;
|
||||||
|
private readonly NotificationService _lightlessNotification;
|
||||||
private readonly LightFinderScannerService _broadcastScannerService;
|
private readonly LightFinderScannerService _broadcastScannerService;
|
||||||
private readonly LightFinderService _broadcastService;
|
private readonly LightFinderService _broadcastService;
|
||||||
private readonly LightlessProfileManager _lightlessProfileManager;
|
private readonly LightlessProfileManager _lightlessProfileManager;
|
||||||
@@ -43,7 +45,7 @@ internal class ContextMenuService : IHostedService
|
|||||||
ILogger<ContextMenuService> logger,
|
ILogger<ContextMenuService> logger,
|
||||||
DalamudUtilService dalamudUtil,
|
DalamudUtilService dalamudUtil,
|
||||||
ApiController apiController,
|
ApiController apiController,
|
||||||
IObjectTable objectTable,
|
IObjectTable objectTable,
|
||||||
LightlessConfigService configService,
|
LightlessConfigService configService,
|
||||||
PairRequestService pairRequestService,
|
PairRequestService pairRequestService,
|
||||||
PairUiService pairUiService,
|
PairUiService pairUiService,
|
||||||
@@ -51,7 +53,9 @@ internal class ContextMenuService : IHostedService
|
|||||||
LightFinderScannerService broadcastScannerService,
|
LightFinderScannerService broadcastScannerService,
|
||||||
LightFinderService broadcastService,
|
LightFinderService broadcastService,
|
||||||
LightlessProfileManager lightlessProfileManager,
|
LightlessProfileManager lightlessProfileManager,
|
||||||
LightlessMediator mediator)
|
LightlessMediator mediator,
|
||||||
|
IChatGui chatGui,
|
||||||
|
NotificationService lightlessNotification)
|
||||||
{
|
{
|
||||||
_contextMenu = contextMenu;
|
_contextMenu = contextMenu;
|
||||||
_pluginInterface = pluginInterface;
|
_pluginInterface = pluginInterface;
|
||||||
@@ -68,6 +72,8 @@ internal class ContextMenuService : IHostedService
|
|||||||
_broadcastService = broadcastService;
|
_broadcastService = broadcastService;
|
||||||
_lightlessProfileManager = lightlessProfileManager;
|
_lightlessProfileManager = lightlessProfileManager;
|
||||||
_mediator = mediator;
|
_mediator = mediator;
|
||||||
|
_chatGui = chatGui;
|
||||||
|
_lightlessNotification = lightlessNotification;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task StartAsync(CancellationToken cancellationToken)
|
public Task StartAsync(CancellationToken cancellationToken)
|
||||||
@@ -99,6 +105,12 @@ internal class ContextMenuService : IHostedService
|
|||||||
if (!_pluginInterface.UiBuilder.ShouldModifyUi)
|
if (!_pluginInterface.UiBuilder.ShouldModifyUi)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
if (!_configService.Current.EnableRightClickMenus)
|
||||||
|
{
|
||||||
|
_logger.LogTrace("Right-click menus are disabled in configuration.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (args.AddonName != null)
|
if (args.AddonName != null)
|
||||||
{
|
{
|
||||||
var addonName = args.AddonName;
|
var addonName = args.AddonName;
|
||||||
@@ -129,7 +141,6 @@ internal class ContextMenuService : IHostedService
|
|||||||
|
|
||||||
var snapshot = _pairUiService.GetSnapshot();
|
var snapshot = _pairUiService.GetSnapshot();
|
||||||
var pair = snapshot.PairsByUid.Values.FirstOrDefault(p =>
|
var pair = snapshot.PairsByUid.Values.FirstOrDefault(p =>
|
||||||
p.IsVisible &&
|
|
||||||
p.PlayerCharacterId != uint.MaxValue &&
|
p.PlayerCharacterId != uint.MaxValue &&
|
||||||
p.PlayerCharacterId == target.TargetObjectId);
|
p.PlayerCharacterId == target.TargetObjectId);
|
||||||
|
|
||||||
@@ -199,6 +210,18 @@ internal class ContextMenuService : IHostedService
|
|||||||
.Where(p => p.IsVisible && p.PlayerCharacterId != uint.MaxValue)
|
.Where(p => p.IsVisible && p.PlayerCharacterId != uint.MaxValue)
|
||||||
.Select(p => (ulong)p.PlayerCharacterId)];
|
.Select(p => (ulong)p.PlayerCharacterId)];
|
||||||
|
|
||||||
|
private void NotifyInChat(string message, NotificationType type = NotificationType.Info)
|
||||||
|
{
|
||||||
|
if (!_configService.Current.UseLightlessNotifications || (_configService.Current.LightlessPairRequestNotification == NotificationLocation.Chat || _configService.Current.LightlessPairRequestNotification == NotificationLocation.ChatAndLightlessUi))
|
||||||
|
{
|
||||||
|
var chatMsg = $"[Lightless] {message}";
|
||||||
|
if (type == NotificationType.Error)
|
||||||
|
_chatGui.PrintError(chatMsg);
|
||||||
|
else
|
||||||
|
_chatGui.Print(chatMsg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async Task HandleSelection(IMenuArgs args)
|
private async Task HandleSelection(IMenuArgs args)
|
||||||
{
|
{
|
||||||
if (args.Target is not MenuTargetDefault target)
|
if (args.Target is not MenuTargetDefault target)
|
||||||
@@ -227,6 +250,9 @@ internal class ContextMenuService : IHostedService
|
|||||||
{
|
{
|
||||||
_pairRequestService.RemoveRequest(receiverCid);
|
_pairRequestService.RemoveRequest(receiverCid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Notify in chat when NotificationService is disabled
|
||||||
|
NotifyInChat($"Pair request sent to {target.TargetName}@{world.Name}.", NotificationType.Info);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -91,43 +91,10 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
return gameData.GetExcelSheet<ClassJob>(Dalamud.Game.ClientLanguage.English)!
|
return gameData.GetExcelSheet<ClassJob>(Dalamud.Game.ClientLanguage.English)!
|
||||||
.ToDictionary(k => k.RowId, k => k.NameEnglish.ToString());
|
.ToDictionary(k => k.RowId, k => k.NameEnglish.ToString());
|
||||||
});
|
});
|
||||||
TerritoryData = new(() =>
|
var clientLanguage = _clientState.ClientLanguage;
|
||||||
{
|
TerritoryData = new(() => BuildTerritoryData(clientLanguage));
|
||||||
return gameData.GetExcelSheet<TerritoryType>(Dalamud.Game.ClientLanguage.English)!
|
TerritoryDataEnglish = new(() => BuildTerritoryData(Dalamud.Game.ClientLanguage.English));
|
||||||
.Where(w => w.RowId != 0)
|
MapData = new(() => BuildMapData(clientLanguage));
|
||||||
.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());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
mediator.Subscribe<TargetPairMessage>(this, (msg) =>
|
mediator.Subscribe<TargetPairMessage>(this, (msg) =>
|
||||||
{
|
{
|
||||||
if (clientState.IsPvP) return;
|
if (clientState.IsPvP) return;
|
||||||
@@ -158,6 +125,71 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
private Lazy<ulong> RebuildCID() => new(GetCID);
|
private Lazy<ulong> RebuildCID() => new(GetCID);
|
||||||
|
|
||||||
public bool IsWine { get; init; }
|
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)
|
private bool ResolvePairAddress(Pair pair, out Pair resolvedPair, out nint address)
|
||||||
{
|
{
|
||||||
resolvedPair = _pairFactory.Value.Create(pair.UniqueIdent) ?? pair;
|
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<uint, string>> JobData { get; private set; }
|
||||||
public Lazy<Dictionary<ushort, string>> WorldData { 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>> 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 Lazy<Dictionary<uint, (Map Map, string MapName)>> MapData { get; private set; }
|
||||||
public bool IsLodEnabled { get; private set; }
|
public bool IsLodEnabled { get; private set; }
|
||||||
public LightlessMediator Mediator { get; }
|
public LightlessMediator Mediator { get; }
|
||||||
@@ -264,7 +297,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
return false;
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -355,7 +388,8 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
|
|
||||||
var playerAddress = playerPointer.Value;
|
var playerAddress = playerPointer.Value;
|
||||||
var ownerEntityId = ((Character*)playerAddress)->EntityId;
|
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)
|
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 =>
|
var ownedObject = FindOwnedObject(ownerEntityId, playerAddress, static kind =>
|
||||||
kind == DalamudObjectKind.MountType || kind == DalamudObjectKind.Companion);
|
kind == DalamudObjectKind.MountType || kind == DalamudObjectKind.Companion);
|
||||||
if (ownedObject != nint.Zero)
|
if (ownedObject != nint.Zero)
|
||||||
@@ -373,7 +418,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
return ownedObject;
|
return ownedObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
return _objectTable.GetObjectAddress(((GameObject*)playerAddress)->ObjectIndex + 1);
|
return candidateAddress;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IntPtr> GetMinionOrMountAsync(IntPtr? playerPointer = null)
|
public async Task<IntPtr> GetMinionOrMountAsync(IntPtr? playerPointer = null)
|
||||||
@@ -806,7 +851,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
bool isDrawingChanged = false;
|
bool isDrawingChanged = false;
|
||||||
if ((nint)drawObj != IntPtr.Zero)
|
if ((nint)drawObj != IntPtr.Zero)
|
||||||
{
|
{
|
||||||
isDrawing = (gameObj->RenderFlags & VisibilityFlags.Nameplate) != VisibilityFlags.None;
|
isDrawing = gameObj->RenderFlags == (VisibilityFlags)0b100000000000;
|
||||||
if (!isDrawing)
|
if (!isDrawing)
|
||||||
{
|
{
|
||||||
isDrawing = ((CharacterBase*)drawObj)->HasModelInSlotLoaded != 0;
|
isDrawing = ((CharacterBase*)drawObj)->HasModelInSlotLoaded != 0;
|
||||||
@@ -872,9 +917,12 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
_performanceCollector.LogPerformance(this, $"TrackedActorsToState",
|
_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++)
|
for (var i = 0; i < playerDescriptors.Count; i++)
|
||||||
{
|
{
|
||||||
var actor = playerDescriptors[i];
|
var actor = playerDescriptors[i];
|
||||||
|
|||||||
@@ -168,10 +168,14 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
var now = DateTime.UtcNow;
|
var now = DateTime.UtcNow;
|
||||||
var newSet = _broadcastCache
|
var nearbyCids = GetNearbyHashedCids(out _);
|
||||||
.Where(e => e.Value.IsBroadcasting && e.Value.ExpiryTime > now && !string.IsNullOrEmpty(e.Value.GID))
|
var newSet = nearbyCids.Count == 0
|
||||||
.Select(e => e.Key)
|
? new HashSet<string>(StringComparer.Ordinal)
|
||||||
.ToHashSet(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))
|
if (!_syncshellCids.SetEquals(newSet))
|
||||||
{
|
{
|
||||||
@@ -183,12 +187,17 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<BroadcastStatusInfoDto> GetActiveSyncshellBroadcasts()
|
public List<BroadcastStatusInfoDto> GetActiveSyncshellBroadcasts(bool excludeLocal = false)
|
||||||
{
|
{
|
||||||
var now = DateTime.UtcNow;
|
var now = DateTime.UtcNow;
|
||||||
|
var nearbyCids = GetNearbyHashedCids(out var localCid);
|
||||||
|
if (nearbyCids.Count == 0)
|
||||||
|
return [];
|
||||||
|
|
||||||
return [.. _broadcastCache
|
return [.. _broadcastCache
|
||||||
.Where(e => e.Value.IsBroadcasting && e.Value.ExpiryTime > now && !string.IsNullOrEmpty(e.Value.GID))
|
.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
|
.Select(e => new BroadcastStatusInfoDto
|
||||||
{
|
{
|
||||||
HashedCID = e.Key,
|
HashedCID = e.Key,
|
||||||
@@ -198,6 +207,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()
|
private async Task ExpiredBroadcastCleanupLoop()
|
||||||
{
|
{
|
||||||
var token = _cleanupCts.Token;
|
var token = _cleanupCts.Token;
|
||||||
|
|||||||
71
LightlessSync/Services/PenumbraTempCollectionJanitor.cs
Normal file
71
LightlessSync/Services/PenumbraTempCollectionJanitor.cs
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
using LightlessSync.Interop.Ipc;
|
||||||
|
using LightlessSync.LightlessConfiguration;
|
||||||
|
using LightlessSync.Services.Mediator;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace LightlessSync.Services;
|
||||||
|
|
||||||
|
public sealed class PenumbraTempCollectionJanitor : DisposableMediatorSubscriberBase
|
||||||
|
{
|
||||||
|
private readonly IpcManager _ipc;
|
||||||
|
private readonly LightlessConfigService _config;
|
||||||
|
private int _ran;
|
||||||
|
|
||||||
|
public PenumbraTempCollectionJanitor(
|
||||||
|
ILogger<PenumbraTempCollectionJanitor> logger,
|
||||||
|
LightlessMediator mediator,
|
||||||
|
IpcManager ipc,
|
||||||
|
LightlessConfigService config) : base(logger, mediator)
|
||||||
|
{
|
||||||
|
_ipc = ipc;
|
||||||
|
_config = config;
|
||||||
|
|
||||||
|
Mediator.Subscribe<PenumbraInitializedMessage>(this, _ => CleanupOrphansOnBoot());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Register(Guid id)
|
||||||
|
{
|
||||||
|
if (id == Guid.Empty) return;
|
||||||
|
if (_config.Current.OrphanableTempCollections.Add(id))
|
||||||
|
_config.Save();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Unregister(Guid id)
|
||||||
|
{
|
||||||
|
if (id == Guid.Empty) return;
|
||||||
|
if (_config.Current.OrphanableTempCollections.Remove(id))
|
||||||
|
_config.Save();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CleanupOrphansOnBoot()
|
||||||
|
{
|
||||||
|
if (Interlocked.Exchange(ref _ran, 1) == 1)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!_ipc.Penumbra.APIAvailable)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var ids = _config.Current.OrphanableTempCollections.ToArray();
|
||||||
|
if (ids.Length == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var appId = Guid.NewGuid();
|
||||||
|
Logger.LogInformation("Cleaning up {count} orphaned Lightless temp collections found in configuration", ids.Length);
|
||||||
|
|
||||||
|
foreach (var id in ids)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_ipc.Penumbra.RemoveTemporaryCollectionAsync(Logger, appId, id)
|
||||||
|
.GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogDebug(ex, "Failed removing orphaned temp collection {id}", id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_config.Current.OrphanableTempCollections.Clear();
|
||||||
|
_config.Save();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -133,6 +133,26 @@ public class DrawUserPair
|
|||||||
UiSharedService.AttachToolTip("This reapplies the last received character data to this character");
|
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))
|
if (_uiSharedService.IconTextButton(FontAwesomeIcon.PlayCircle, "Cycle pause state", _menuWidth, true))
|
||||||
{
|
{
|
||||||
_ = _apiController.CyclePauseAsync(_pair);
|
_ = _apiController.CyclePauseAsync(_pair);
|
||||||
|
|||||||
@@ -164,9 +164,25 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
|||||||
const float rounding = 6f;
|
const float rounding = 6f;
|
||||||
var shadowOffset = new Vector2(2, 2);
|
var shadowOffset = new Vector2(2, 2);
|
||||||
|
|
||||||
foreach (var transfer in _currentDownloads.ToList())
|
List<KeyValuePair<GameObjectHandler, Dictionary<string, FileDownloadStatus>>> transfers;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
transfers = _currentDownloads.ToList();
|
||||||
|
}
|
||||||
|
catch (ArgumentException)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
foreach (var transfer in transfers)
|
||||||
{
|
{
|
||||||
var transferKey = transfer.Key;
|
var transferKey = transfer.Key;
|
||||||
|
|
||||||
|
// Skip if no valid game object
|
||||||
|
if (transferKey.GetGameObject() == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
var rawPos = _dalamudUtilService.WorldToScreen(transferKey.GetGameObject());
|
var rawPos = _dalamudUtilService.WorldToScreen(transferKey.GetGameObject());
|
||||||
|
|
||||||
// If RawPos is zero, remove it from smoothed dictionary
|
// If RawPos is zero, remove it from smoothed dictionary
|
||||||
|
|||||||
@@ -1552,6 +1552,11 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
DrawPairPropertyRow("Downloading", FormatBool(debugInfo.IsDownloading));
|
DrawPairPropertyRow("Downloading", FormatBool(debugInfo.IsDownloading));
|
||||||
DrawPairPropertyRow("Pending Downloads", debugInfo.PendingDownloadCount.ToString(CultureInfo.InvariantCulture));
|
DrawPairPropertyRow("Pending Downloads", debugInfo.PendingDownloadCount.ToString(CultureInfo.InvariantCulture));
|
||||||
DrawPairPropertyRow("Forbidden Downloads", debugInfo.ForbiddenDownloadCount.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();
|
ImGui.EndTable();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -297,6 +297,25 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
|
|||||||
var ownerTab = ImRaii.TabItem("Owner Settings");
|
var ownerTab = ImRaii.TabItem("Owner Settings");
|
||||||
if (ownerTab)
|
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.AlignTextToFramePadding();
|
||||||
ImGui.TextUnformatted("New Password");
|
ImGui.TextUnformatted("New Password");
|
||||||
var availableWidth = ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X;
|
var availableWidth = ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X;
|
||||||
|
|||||||
@@ -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.");
|
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"))
|
if (IconTextButton(FontAwesomeIcon.Ban, "Cancel Authentication"))
|
||||||
@@ -962,7 +965,7 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
|
|||||||
_discordOAuthGetCode = null;
|
_discordOAuthGetCode = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (_discordOAuthGetCode != null && _discordOAuthGetCode.IsCompleted)
|
else
|
||||||
{
|
{
|
||||||
TextWrapped("Discord OAuth is completed, status: ");
|
TextWrapped("Discord OAuth is completed, status: ");
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ using LightlessSync.Services.ServerConfiguration;
|
|||||||
using LightlessSync.UI.Services;
|
using LightlessSync.UI.Services;
|
||||||
using LightlessSync.UI.Style;
|
using LightlessSync.UI.Style;
|
||||||
using LightlessSync.Utils;
|
using LightlessSync.Utils;
|
||||||
|
using OtterGui.Text;
|
||||||
using LightlessSync.WebAPI;
|
using LightlessSync.WebAPI;
|
||||||
using LightlessSync.WebAPI.SignalR.Utils;
|
using LightlessSync.WebAPI.SignalR.Utils;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@@ -211,12 +212,6 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
|||||||
|
|
||||||
protected override void DrawInternal()
|
protected override void DrawInternal()
|
||||||
{
|
{
|
||||||
if (_titleBarStylePopCount > 0)
|
|
||||||
{
|
|
||||||
ImGui.PopStyleColor(_titleBarStylePopCount);
|
|
||||||
_titleBarStylePopCount = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
var config = _chatConfigService.Current;
|
var config = _chatConfigService.Current;
|
||||||
var isFocused = ImGui.IsWindowFocused(ImGuiFocusedFlags.RootAndChildWindows);
|
var isFocused = ImGui.IsWindowFocused(ImGuiFocusedFlags.RootAndChildWindows);
|
||||||
var isHovered = ImGui.IsWindowHovered(ImGuiHoveredFlags.RootAndChildWindows);
|
var isHovered = ImGui.IsWindowHovered(ImGuiHoveredFlags.RootAndChildWindows);
|
||||||
@@ -400,52 +395,57 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
else
|
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];
|
for (var i = clipper.DisplayStart; i < clipper.DisplayEnd; i++)
|
||||||
ImGui.PushID(i);
|
|
||||||
|
|
||||||
if (message.IsSystem)
|
|
||||||
{
|
{
|
||||||
DrawSystemEntry(message);
|
var message = channel.Messages[i];
|
||||||
ImGui.PopID();
|
ImGui.PushID(i);
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.Payload is not { } payload)
|
if (message.IsSystem)
|
||||||
{
|
|
||||||
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))
|
|
||||||
{
|
{
|
||||||
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);
|
ImGui.PopStyleVar(1);
|
||||||
_pushedStyle = false;
|
_pushedStyle = false;
|
||||||
}
|
}
|
||||||
|
if (_titleBarStylePopCount > 0)
|
||||||
|
{
|
||||||
|
ImGui.PopStyleColor(_titleBarStylePopCount);
|
||||||
|
_titleBarStylePopCount = 0;
|
||||||
|
}
|
||||||
base.PostDraw();
|
base.PostDraw();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -60,16 +60,6 @@ public static class VariousExtensions
|
|||||||
CharacterData? oldData, ILogger logger, IPairPerformanceSubject cachedPlayer, bool forceApplyCustomization, bool forceApplyMods)
|
CharacterData? oldData, ILogger logger, IPairPerformanceSubject cachedPlayer, bool forceApplyCustomization, bool forceApplyMods)
|
||||||
{
|
{
|
||||||
oldData ??= new();
|
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>>();
|
var charaDataToUpdate = new Dictionary<ObjectKind, HashSet<PlayerChanges>>();
|
||||||
foreach (ObjectKind objectKind in Enum.GetValues<ObjectKind>())
|
foreach (ObjectKind objectKind in Enum.GetValues<ObjectKind>())
|
||||||
@@ -105,7 +95,7 @@ public static class VariousExtensions
|
|||||||
{
|
{
|
||||||
var oldList = oldData.FileReplacements[objectKind];
|
var oldList = oldData.FileReplacements[objectKind];
|
||||||
var newList = newData.FileReplacements[objectKind];
|
var newList = newData.FileReplacements[objectKind];
|
||||||
var listsAreEqual = FileReplacementsEquivalent(oldList, newList);
|
var listsAreEqual = oldList.SequenceEqual(newList, PlayerData.Data.FileReplacementDataComparer.Instance);
|
||||||
if (!listsAreEqual || forceApplyMods)
|
if (!listsAreEqual || forceApplyMods)
|
||||||
{
|
{
|
||||||
logger.LogDebug("[BASE-{appBase}] Updating {object}/{kind} (FileReplacements not equal) => {change}", applicationBase, cachedPlayer, objectKind, PlayerChanges.ModFiles);
|
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();
|
.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)))
|
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();
|
.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();
|
.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();
|
.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,
|
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;
|
if (objectKind != ObjectKind.Player) continue;
|
||||||
|
|
||||||
bool manipDataDifferent = !string.Equals(oldData.ManipulationData, newData.ManipulationData, StringComparison.Ordinal);
|
bool manipDataDifferent = !string.Equals(oldData.ManipulationData, newData.ManipulationData, StringComparison.Ordinal);
|
||||||
var hasManipulationData = !string.IsNullOrEmpty(newData.ManipulationData);
|
if (manipDataDifferent || forceApplyMods)
|
||||||
if (manipDataDifferent || (forceApplyMods && hasManipulationData))
|
|
||||||
{
|
{
|
||||||
logger.LogDebug("[BASE-{appBase}] Updating {object}/{kind} (Diff manip data) => {change}", applicationBase, cachedPlayer, objectKind, PlayerChanges.ModManip);
|
logger.LogDebug("[BASE-{appBase}] Updating {object}/{kind} (Diff manip data) => {change}", applicationBase, cachedPlayer, objectKind, PlayerChanges.ModManip);
|
||||||
charaDataToUpdate[objectKind].Add(PlayerChanges.ModManip);
|
charaDataToUpdate[objectKind].Add(PlayerChanges.ModManip);
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -18,56 +18,72 @@ public class FileTransferOrchestrator : DisposableMediatorSubscriberBase
|
|||||||
private readonly LightlessConfigService _lightlessConfig;
|
private readonly LightlessConfigService _lightlessConfig;
|
||||||
private readonly object _semaphoreModificationLock = new();
|
private readonly object _semaphoreModificationLock = new();
|
||||||
private readonly TokenProvider _tokenProvider;
|
private readonly TokenProvider _tokenProvider;
|
||||||
|
|
||||||
private int _availableDownloadSlots;
|
private int _availableDownloadSlots;
|
||||||
private SemaphoreSlim _downloadSemaphore;
|
private SemaphoreSlim _downloadSemaphore;
|
||||||
|
|
||||||
private int CurrentlyUsedDownloadSlots => _availableDownloadSlots - _downloadSemaphore.CurrentCount;
|
private int CurrentlyUsedDownloadSlots => _availableDownloadSlots - _downloadSemaphore.CurrentCount;
|
||||||
|
|
||||||
public FileTransferOrchestrator(ILogger<FileTransferOrchestrator> logger, LightlessConfigService lightlessConfig,
|
public FileTransferOrchestrator(
|
||||||
LightlessMediator mediator, TokenProvider tokenProvider, HttpClient httpClient) : base(logger, mediator)
|
ILogger<FileTransferOrchestrator> logger,
|
||||||
|
LightlessConfigService lightlessConfig,
|
||||||
|
LightlessMediator mediator,
|
||||||
|
TokenProvider tokenProvider,
|
||||||
|
HttpClient httpClient) : base(logger, mediator)
|
||||||
{
|
{
|
||||||
_lightlessConfig = lightlessConfig;
|
_lightlessConfig = lightlessConfig;
|
||||||
_tokenProvider = tokenProvider;
|
_tokenProvider = tokenProvider;
|
||||||
_httpClient = httpClient;
|
_httpClient = httpClient;
|
||||||
|
|
||||||
var ver = Assembly.GetExecutingAssembly().GetName().Version;
|
var ver = Assembly.GetExecutingAssembly().GetName().Version;
|
||||||
_httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("LightlessSync", ver!.Major + "." + ver!.Minor + "." + ver!.Build));
|
_httpClient.DefaultRequestHeaders.UserAgent.Add(
|
||||||
|
new ProductInfoHeaderValue("LightlessSync", $"{ver!.Major}.{ver.Minor}.{ver.Build}"));
|
||||||
|
|
||||||
_availableDownloadSlots = lightlessConfig.Current.ParallelDownloads;
|
_availableDownloadSlots = Math.Max(1, lightlessConfig.Current.ParallelDownloads);
|
||||||
_downloadSemaphore = new(_availableDownloadSlots, _availableDownloadSlots);
|
_downloadSemaphore = new SemaphoreSlim(_availableDownloadSlots, _availableDownloadSlots);
|
||||||
|
|
||||||
Mediator.Subscribe<ConnectedMessage>(this, (msg) =>
|
Mediator.Subscribe<ConnectedMessage>(this, msg => FilesCdnUri = msg.Connection.ServerInfo.FileServerAddress);
|
||||||
{
|
Mediator.Subscribe<DisconnectedMessage>(this, _ => FilesCdnUri = null);
|
||||||
FilesCdnUri = msg.Connection.ServerInfo.FileServerAddress;
|
Mediator.Subscribe<DownloadReadyMessage>(this, msg => _downloadReady[msg.RequestId] = true);
|
||||||
});
|
|
||||||
|
|
||||||
Mediator.Subscribe<DisconnectedMessage>(this, (msg) =>
|
|
||||||
{
|
|
||||||
FilesCdnUri = null;
|
|
||||||
});
|
|
||||||
Mediator.Subscribe<DownloadReadyMessage>(this, (msg) =>
|
|
||||||
{
|
|
||||||
_downloadReady[msg.RequestId] = true;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Files CDN Uri from server
|
||||||
|
/// </summary>
|
||||||
public Uri? FilesCdnUri { private set; get; }
|
public Uri? FilesCdnUri { private set; get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Forbidden file transfers given by server
|
||||||
|
/// </summary>
|
||||||
public List<FileTransfer> ForbiddenTransfers { get; } = [];
|
public List<FileTransfer> ForbiddenTransfers { get; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Is the FileTransferOrchestrator initialized
|
||||||
|
/// </summary>
|
||||||
public bool IsInitialized => FilesCdnUri != null;
|
public bool IsInitialized => FilesCdnUri != null;
|
||||||
|
|
||||||
public void ClearDownloadRequest(Guid guid)
|
/// <summary>
|
||||||
{
|
/// Configured parallel downloads in settings (ParallelDownloads)
|
||||||
_downloadReady.Remove(guid, out _);
|
/// </summary>
|
||||||
}
|
public int ConfiguredParallelDownloads => Math.Max(1, _lightlessConfig.Current.ParallelDownloads);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Clears the download request for the given guid
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="guid">Guid of download request</param>
|
||||||
|
public void ClearDownloadRequest(Guid guid) => _downloadReady.Remove(guid, out _);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Is the download ready for the given guid
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="guid">Guid of download request</param>
|
||||||
|
/// <returns>Completion of the download</returns>
|
||||||
public bool IsDownloadReady(Guid guid)
|
public bool IsDownloadReady(Guid guid)
|
||||||
{
|
=> _downloadReady.TryGetValue(guid, out bool isReady) && isReady;
|
||||||
if (_downloadReady.TryGetValue(guid, out bool isReady) && isReady)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Release a download slot after download is complete
|
||||||
|
/// </summary>
|
||||||
public void ReleaseDownloadSlot()
|
public void ReleaseDownloadSlot()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -81,60 +97,26 @@ public class FileTransferOrchestrator : DisposableMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<HttpResponseMessage> SendRequestAsync(HttpMethod method, Uri uri,
|
/// <summary>
|
||||||
CancellationToken? ct = null, HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseContentRead,
|
/// Wait for an available download slot asyncronously
|
||||||
bool withToken = true)
|
/// </summary>
|
||||||
{
|
/// <param name="token">Cancellation Token</param>
|
||||||
return await SendRequestInternalAsync(() => new HttpRequestMessage(method, uri),
|
/// <returns>Task of the slot</returns>
|
||||||
ct, httpCompletionOption, withToken, allowRetry: true).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<HttpResponseMessage> SendRequestAsync<T>(HttpMethod method, Uri uri, T content, CancellationToken ct,
|
|
||||||
bool withToken = true) where T : class
|
|
||||||
{
|
|
||||||
return await SendRequestInternalAsync(() =>
|
|
||||||
{
|
|
||||||
var requestMessage = new HttpRequestMessage(method, uri);
|
|
||||||
if (content is not ByteArrayContent byteArrayContent)
|
|
||||||
{
|
|
||||||
requestMessage.Content = JsonContent.Create(content);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var clonedContent = new ByteArrayContent(byteArrayContent.ReadAsByteArrayAsync().GetAwaiter().GetResult());
|
|
||||||
foreach (var header in byteArrayContent.Headers)
|
|
||||||
{
|
|
||||||
clonedContent.Headers.TryAddWithoutValidation(header.Key, header.Value);
|
|
||||||
}
|
|
||||||
requestMessage.Content = clonedContent;
|
|
||||||
}
|
|
||||||
|
|
||||||
return requestMessage;
|
|
||||||
}, ct, HttpCompletionOption.ResponseContentRead, withToken,
|
|
||||||
allowRetry: content is not HttpContent || content is ByteArrayContent).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<HttpResponseMessage> SendRequestStreamAsync(HttpMethod method, Uri uri, ProgressableStreamContent content,
|
|
||||||
CancellationToken ct, bool withToken = true)
|
|
||||||
{
|
|
||||||
return await SendRequestInternalAsync(() =>
|
|
||||||
{
|
|
||||||
var requestMessage = new HttpRequestMessage(method, uri)
|
|
||||||
{
|
|
||||||
Content = content
|
|
||||||
};
|
|
||||||
return requestMessage;
|
|
||||||
}, ct, HttpCompletionOption.ResponseContentRead, withToken, allowRetry: false).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task WaitForDownloadSlotAsync(CancellationToken token)
|
public async Task WaitForDownloadSlotAsync(CancellationToken token)
|
||||||
{
|
{
|
||||||
lock (_semaphoreModificationLock)
|
lock (_semaphoreModificationLock)
|
||||||
{
|
{
|
||||||
if (_availableDownloadSlots != _lightlessConfig.Current.ParallelDownloads && _availableDownloadSlots == _downloadSemaphore.CurrentCount)
|
var desired = Math.Max(1, _lightlessConfig.Current.ParallelDownloads);
|
||||||
|
|
||||||
|
if (_availableDownloadSlots != desired &&
|
||||||
|
_availableDownloadSlots == _downloadSemaphore.CurrentCount)
|
||||||
{
|
{
|
||||||
_availableDownloadSlots = _lightlessConfig.Current.ParallelDownloads;
|
_availableDownloadSlots = desired;
|
||||||
_downloadSemaphore = new(_availableDownloadSlots, _availableDownloadSlots);
|
|
||||||
|
var old = _downloadSemaphore;
|
||||||
|
_downloadSemaphore = new SemaphoreSlim(_availableDownloadSlots, _availableDownloadSlots);
|
||||||
|
|
||||||
|
try { old.Dispose(); } catch { /* ignore */ }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,10 +124,15 @@ public class FileTransferOrchestrator : DisposableMediatorSubscriberBase
|
|||||||
Mediator.Publish(new DownloadLimitChangedMessage());
|
Mediator.Publish(new DownloadLimitChangedMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Download limit per slot in bytes
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Bytes of the download limit</returns>
|
||||||
public long DownloadLimitPerSlot()
|
public long DownloadLimitPerSlot()
|
||||||
{
|
{
|
||||||
var limit = _lightlessConfig.Current.DownloadSpeedLimitInBytes;
|
var limit = _lightlessConfig.Current.DownloadSpeedLimitInBytes;
|
||||||
if (limit <= 0) return 0;
|
if (limit <= 0) return 0;
|
||||||
|
|
||||||
limit = _lightlessConfig.Current.DownloadSpeedType switch
|
limit = _lightlessConfig.Current.DownloadSpeedType switch
|
||||||
{
|
{
|
||||||
LightlessConfiguration.Models.DownloadSpeeds.Bps => limit,
|
LightlessConfiguration.Models.DownloadSpeeds.Bps => limit,
|
||||||
@@ -153,22 +140,113 @@ public class FileTransferOrchestrator : DisposableMediatorSubscriberBase
|
|||||||
LightlessConfiguration.Models.DownloadSpeeds.MBps => limit * 1024 * 1024,
|
LightlessConfiguration.Models.DownloadSpeeds.MBps => limit * 1024 * 1024,
|
||||||
_ => limit,
|
_ => limit,
|
||||||
};
|
};
|
||||||
var currentUsedDlSlots = CurrentlyUsedDownloadSlots;
|
|
||||||
var avaialble = _availableDownloadSlots;
|
var usedSlots = CurrentlyUsedDownloadSlots;
|
||||||
var currentCount = _downloadSemaphore.CurrentCount;
|
var divided = limit / (usedSlots <= 0 ? 1 : usedSlots);
|
||||||
var dividedLimit = limit / (currentUsedDlSlots == 0 ? 1 : currentUsedDlSlots);
|
|
||||||
if (dividedLimit < 0)
|
if (divided < 0)
|
||||||
{
|
{
|
||||||
Logger.LogWarning("Calculated Bandwidth Limit is negative, returning Infinity: {value}, CurrentlyUsedDownloadSlots is {currentSlots}, " +
|
Logger.LogWarning(
|
||||||
"DownloadSpeedLimit is {limit}, available slots: {avail}, current count: {count}", dividedLimit, currentUsedDlSlots, limit, avaialble, currentCount);
|
"Calculated Bandwidth Limit is negative, returning Infinity: {value}, usedSlots={usedSlots}, limit={limit}, avail={avail}, currentCount={count}",
|
||||||
|
divided, usedSlots, limit, _availableDownloadSlots, _downloadSemaphore.CurrentCount);
|
||||||
return long.MaxValue;
|
return long.MaxValue;
|
||||||
}
|
}
|
||||||
return Math.Clamp(dividedLimit, 1, long.MaxValue);
|
|
||||||
|
return Math.Clamp(divided, 1, long.MaxValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<HttpResponseMessage> SendRequestInternalAsync(Func<HttpRequestMessage> requestFactory,
|
/// <summary>
|
||||||
CancellationToken? ct = null, HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseContentRead,
|
/// sends an HTTP request without content serialization
|
||||||
bool withToken = true, bool allowRetry = true)
|
/// </summary>
|
||||||
|
/// <param name="method">HttpMethod for the request</param>
|
||||||
|
/// <param name="uri">Uri for the request</param>
|
||||||
|
/// <param name="ct">Cancellation Token</param>
|
||||||
|
/// <param name="httpCompletionOption">Enum of HttpCollectionOption</param>
|
||||||
|
/// <param name="withToken">Include Cancellation Token</param>
|
||||||
|
/// <returns>Http response of the request</returns>
|
||||||
|
public async Task<HttpResponseMessage> SendRequestAsync(
|
||||||
|
HttpMethod method,
|
||||||
|
Uri uri,
|
||||||
|
CancellationToken? ct = null,
|
||||||
|
HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseContentRead,
|
||||||
|
bool withToken = true)
|
||||||
|
{
|
||||||
|
return await SendRequestInternalAsync(
|
||||||
|
() => new HttpRequestMessage(method, uri),
|
||||||
|
ct,
|
||||||
|
httpCompletionOption,
|
||||||
|
withToken,
|
||||||
|
allowRetry: true).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sends an HTTP request with JSON content serialization
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">HttpResponseMessage</typeparam>
|
||||||
|
/// <param name="method">Http method</param>
|
||||||
|
/// <param name="uri">Url of the direct download link</param>
|
||||||
|
/// <param name="content">content of the request</param>
|
||||||
|
/// <param name="ct">cancellation token</param>
|
||||||
|
/// <param name="withToken">include cancellation token</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task<HttpResponseMessage> SendRequestAsync<T>(
|
||||||
|
HttpMethod method,
|
||||||
|
Uri uri,
|
||||||
|
T content,
|
||||||
|
CancellationToken ct,
|
||||||
|
bool withToken = true) where T : class
|
||||||
|
{
|
||||||
|
return await SendRequestInternalAsync(() =>
|
||||||
|
{
|
||||||
|
var requestMessage = new HttpRequestMessage(method, uri);
|
||||||
|
|
||||||
|
if (content is ByteArrayContent byteArrayContent)
|
||||||
|
{
|
||||||
|
var bytes = byteArrayContent.ReadAsByteArrayAsync(ct).GetAwaiter().GetResult();
|
||||||
|
var cloned = new ByteArrayContent(bytes);
|
||||||
|
foreach (var header in byteArrayContent.Headers)
|
||||||
|
cloned.Headers.TryAddWithoutValidation(header.Key, header.Value);
|
||||||
|
|
||||||
|
requestMessage.Content = cloned;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
requestMessage.Content = JsonContent.Create(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
return requestMessage;
|
||||||
|
}, ct, HttpCompletionOption.ResponseContentRead, withToken,
|
||||||
|
allowRetry: content is not HttpContent || content is ByteArrayContent).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<HttpResponseMessage> SendRequestStreamAsync(
|
||||||
|
HttpMethod method,
|
||||||
|
Uri uri,
|
||||||
|
ProgressableStreamContent content,
|
||||||
|
CancellationToken ct,
|
||||||
|
bool withToken = true)
|
||||||
|
{
|
||||||
|
return await SendRequestInternalAsync(() =>
|
||||||
|
{
|
||||||
|
return new HttpRequestMessage(method, uri) { Content = content };
|
||||||
|
}, ct, HttpCompletionOption.ResponseContentRead, withToken, allowRetry: false).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// sends an HTTP request with optional retry logic for transient network errors
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="requestFactory">Request factory</param>
|
||||||
|
/// <param name="ct">Cancellation Token</param>
|
||||||
|
/// <param name="httpCompletionOption">Http Options</param>
|
||||||
|
/// <param name="withToken">With cancellation token</param>
|
||||||
|
/// <param name="allowRetry">Allows retry of request</param>
|
||||||
|
/// <returns>Response message of request</returns>
|
||||||
|
private async Task<HttpResponseMessage> SendRequestInternalAsync(
|
||||||
|
Func<HttpRequestMessage> requestFactory,
|
||||||
|
CancellationToken? ct = null,
|
||||||
|
HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseContentRead,
|
||||||
|
bool withToken = true,
|
||||||
|
bool allowRetry = true)
|
||||||
{
|
{
|
||||||
const int maxAttempts = 2;
|
const int maxAttempts = 2;
|
||||||
var attempt = 0;
|
var attempt = 0;
|
||||||
@@ -184,8 +262,11 @@ public class FileTransferOrchestrator : DisposableMediatorSubscriberBase
|
|||||||
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (requestMessage.Content != null && requestMessage.Content is not StreamContent && requestMessage.Content is not ByteArrayContent)
|
if (requestMessage.Content != null &&
|
||||||
|
requestMessage.Content is not StreamContent &&
|
||||||
|
requestMessage.Content is not ByteArrayContent)
|
||||||
{
|
{
|
||||||
|
// log content for debugging
|
||||||
var content = await ((JsonContent)requestMessage.Content).ReadAsStringAsync().ConfigureAwait(false);
|
var content = await ((JsonContent)requestMessage.Content).ReadAsStringAsync().ConfigureAwait(false);
|
||||||
Logger.LogDebug("Sending {method} to {uri} (Content: {content})", requestMessage.Method, requestMessage.RequestUri, content);
|
Logger.LogDebug("Sending {method} to {uri} (Content: {content})", requestMessage.Method, requestMessage.RequestUri, content);
|
||||||
}
|
}
|
||||||
@@ -196,9 +277,10 @@ public class FileTransferOrchestrator : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (ct != null)
|
// send request
|
||||||
return await _httpClient.SendAsync(requestMessage, httpCompletionOption, ct.Value).ConfigureAwait(false);
|
return ct != null
|
||||||
return await _httpClient.SendAsync(requestMessage, httpCompletionOption).ConfigureAwait(false);
|
? await _httpClient.SendAsync(requestMessage, httpCompletionOption, ct.Value).ConfigureAwait(false)
|
||||||
|
: await _httpClient.SendAsync(requestMessage, httpCompletionOption).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (TaskCanceledException)
|
catch (TaskCanceledException)
|
||||||
{
|
{
|
||||||
@@ -208,14 +290,11 @@ public class FileTransferOrchestrator : DisposableMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
Logger.LogWarning(ex, "Transient error during SendRequestInternal for {uri}, retrying attempt {attempt}/{maxAttempts}",
|
Logger.LogWarning(ex, "Transient error during SendRequestInternal for {uri}, retrying attempt {attempt}/{maxAttempts}",
|
||||||
requestMessage.RequestUri, attempt, maxAttempts);
|
requestMessage.RequestUri, attempt, maxAttempts);
|
||||||
|
|
||||||
if (ct.HasValue)
|
if (ct.HasValue)
|
||||||
{
|
|
||||||
await Task.Delay(TimeSpan.FromMilliseconds(200), ct.Value).ConfigureAwait(false);
|
await Task.Delay(TimeSpan.FromMilliseconds(200), ct.Value).ConfigureAwait(false);
|
||||||
}
|
|
||||||
else
|
else
|
||||||
{
|
|
||||||
await Task.Delay(TimeSpan.FromMilliseconds(200)).ConfigureAwait(false);
|
await Task.Delay(TimeSpan.FromMilliseconds(200)).ConfigureAwait(false);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -225,6 +304,11 @@ public class FileTransferOrchestrator : DisposableMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Is the exception a transient network exception
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="ex">expection</param>
|
||||||
|
/// <returns>Is transient network expection</returns>
|
||||||
private static bool IsTransientNetworkException(Exception ex)
|
private static bool IsTransientNetworkException(Exception ex)
|
||||||
{
|
{
|
||||||
var current = ex;
|
var current = ex;
|
||||||
@@ -232,12 +316,13 @@ public class FileTransferOrchestrator : DisposableMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
if (current is SocketException socketEx)
|
if (current is SocketException socketEx)
|
||||||
{
|
{
|
||||||
return socketEx.SocketErrorCode is SocketError.ConnectionReset or SocketError.ConnectionAborted or SocketError.TimedOut;
|
return socketEx.SocketErrorCode is
|
||||||
|
SocketError.ConnectionReset or
|
||||||
|
SocketError.ConnectionAborted or
|
||||||
|
SocketError.TimedOut;
|
||||||
}
|
}
|
||||||
|
|
||||||
current = current.InnerException;
|
current = current.InnerException;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -418,7 +418,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
|
|||||||
|
|
||||||
public Task CyclePauseAsync(PairUniqueIdentifier ident)
|
public Task CyclePauseAsync(PairUniqueIdentifier ident)
|
||||||
{
|
{
|
||||||
var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(8));
|
var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
||||||
_ = Task.Run(async () =>
|
_ = Task.Run(async () =>
|
||||||
{
|
{
|
||||||
var token = timeoutCts.Token;
|
var token = timeoutCts.Token;
|
||||||
@@ -430,20 +430,19 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var originalPermissions = entry.SelfPermissions;
|
var targetPermissions = entry.SelfPermissions;
|
||||||
var targetPermissions = originalPermissions;
|
targetPermissions.SetPaused(paused: true);
|
||||||
targetPermissions.SetPaused(!originalPermissions.IsPaused());
|
|
||||||
|
|
||||||
await UserSetPairPermissions(new UserPermissionsDto(entry.User, targetPermissions)).ConfigureAwait(false);
|
await UserSetPairPermissions(new UserPermissionsDto(entry.User, targetPermissions)).ConfigureAwait(false);
|
||||||
|
|
||||||
var applied = false;
|
var pauseApplied = false;
|
||||||
while (!token.IsCancellationRequested)
|
while (!token.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
if (_pairCoordinator.Ledger.TryGetEntry(ident, out var updated) && updated is not null)
|
if (_pairCoordinator.Ledger.TryGetEntry(ident, out var updated) && updated is not null)
|
||||||
{
|
{
|
||||||
if (updated.SelfPermissions == targetPermissions)
|
if (updated.SelfPermissions == targetPermissions)
|
||||||
{
|
{
|
||||||
applied = true;
|
pauseApplied = true;
|
||||||
entry = updated;
|
entry = updated;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -453,13 +452,16 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
|
|||||||
Logger.LogTrace("Waiting for permissions change for {uid}", ident.UserId);
|
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);
|
Logger.LogWarning("CyclePauseAsync timed out waiting for pause acknowledgement for {uid}", ident.UserId);
|
||||||
return;
|
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)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
@@ -479,16 +481,26 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async Task PauseAsync(UserData userData)
|
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);
|
var pairIdent = new PairUniqueIdentifier(userData.UID);
|
||||||
if (!_pairCoordinator.Ledger.TryGetEntry(pairIdent, out var entry) || entry is null)
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var permissions = entry.SelfPermissions;
|
var permissions = entry.SelfPermissions;
|
||||||
permissions.SetPaused(paused: true);
|
permissions.SetPaused(paused);
|
||||||
await UserSetPairPermissions(new UserPermissionsDto(userData, permissions)).ConfigureAwait(false);
|
await UserSetPairPermissions(new UserPermissionsDto(userData, permissions)).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user