diff --git a/.gitmodules b/.gitmodules index 7879cd2..7ae9eb6 100644 --- a/.gitmodules +++ b/.gitmodules @@ -13,3 +13,6 @@ [submodule "OtterGui"] path = OtterGui url = https://github.com/Ottermandias/OtterGui +[submodule "ffxiv_pictomancy"] + path = ffxiv_pictomancy + url = https://github.com/sourpuh/ffxiv_pictomancy diff --git a/LightlessSync.sln b/LightlessSync.sln index 8d92b53..55bddfd 100644 --- a/LightlessSync.sln +++ b/LightlessSync.sln @@ -20,6 +20,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Penumbra.GameData", "Penumb EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OtterGui", "OtterGui\OtterGui.csproj", "{C77A2833-3FE4-405B-811D-439B1FF859D9}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Pictomancy", "ffxiv_pictomancy\Pictomancy\Pictomancy.csproj", "{825F17D8-2704-24F6-DF8B-2542AC92C765}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -102,6 +104,18 @@ Global {C77A2833-3FE4-405B-811D-439B1FF859D9}.Release|x64.Build.0 = Release|x64 {C77A2833-3FE4-405B-811D-439B1FF859D9}.Release|x86.ActiveCfg = Release|x64 {C77A2833-3FE4-405B-811D-439B1FF859D9}.Release|x86.Build.0 = Release|x64 + {825F17D8-2704-24F6-DF8B-2542AC92C765}.Debug|Any CPU.ActiveCfg = Debug|x64 + {825F17D8-2704-24F6-DF8B-2542AC92C765}.Debug|Any CPU.Build.0 = Debug|x64 + {825F17D8-2704-24F6-DF8B-2542AC92C765}.Debug|x64.ActiveCfg = Debug|x64 + {825F17D8-2704-24F6-DF8B-2542AC92C765}.Debug|x64.Build.0 = Debug|x64 + {825F17D8-2704-24F6-DF8B-2542AC92C765}.Debug|x86.ActiveCfg = Debug|x64 + {825F17D8-2704-24F6-DF8B-2542AC92C765}.Debug|x86.Build.0 = Debug|x64 + {825F17D8-2704-24F6-DF8B-2542AC92C765}.Release|Any CPU.ActiveCfg = Release|x64 + {825F17D8-2704-24F6-DF8B-2542AC92C765}.Release|Any CPU.Build.0 = Release|x64 + {825F17D8-2704-24F6-DF8B-2542AC92C765}.Release|x64.ActiveCfg = Release|x64 + {825F17D8-2704-24F6-DF8B-2542AC92C765}.Release|x64.Build.0 = Release|x64 + {825F17D8-2704-24F6-DF8B-2542AC92C765}.Release|x86.ActiveCfg = Release|x64 + {825F17D8-2704-24F6-DF8B-2542AC92C765}.Release|x86.Build.0 = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/LightlessSync/FileCache/TransientResourceManager.cs b/LightlessSync/FileCache/TransientResourceManager.cs index 7f982a3..6d77a97 100644 --- a/LightlessSync/FileCache/TransientResourceManager.cs +++ b/LightlessSync/FileCache/TransientResourceManager.cs @@ -426,7 +426,7 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase () => { if (!string.IsNullOrEmpty(descriptor.HashedContentId) && - _actorObjectService.TryGetActorByHash(descriptor.HashedContentId, out var current) && + _actorObjectService.TryGetValidatedActorByHash(descriptor.HashedContentId, out var current) && current.OwnedKind == kind) { return current.Address; diff --git a/LightlessSync/LightlessSync.csproj b/LightlessSync/LightlessSync.csproj index 975e935..8930cb6 100644 --- a/LightlessSync/LightlessSync.csproj +++ b/LightlessSync/LightlessSync.csproj @@ -79,6 +79,7 @@ + diff --git a/LightlessSync/PlayerData/Factories/FileDownloadManagerFactory.cs b/LightlessSync/PlayerData/Factories/FileDownloadManagerFactory.cs index f9b522a..e3697cf 100644 --- a/LightlessSync/PlayerData/Factories/FileDownloadManagerFactory.cs +++ b/LightlessSync/PlayerData/Factories/FileDownloadManagerFactory.cs @@ -1,7 +1,6 @@ using LightlessSync.FileCache; using LightlessSync.LightlessConfiguration; using LightlessSync.Services.Mediator; -using LightlessSync.Services.PairProcessing; using LightlessSync.Services.TextureCompression; using LightlessSync.WebAPI.Files; using Microsoft.Extensions.Logging; @@ -15,7 +14,6 @@ public class FileDownloadManagerFactory private readonly FileTransferOrchestrator _fileTransferOrchestrator; private readonly FileCacheManager _fileCacheManager; private readonly FileCompactor _fileCompactor; - private readonly PairProcessingLimiter _pairProcessingLimiter; private readonly LightlessConfigService _configService; private readonly TextureDownscaleService _textureDownscaleService; private readonly TextureMetadataHelper _textureMetadataHelper; @@ -26,7 +24,6 @@ public class FileDownloadManagerFactory FileTransferOrchestrator fileTransferOrchestrator, FileCacheManager fileCacheManager, FileCompactor fileCompactor, - PairProcessingLimiter pairProcessingLimiter, LightlessConfigService configService, TextureDownscaleService textureDownscaleService, TextureMetadataHelper textureMetadataHelper) @@ -36,7 +33,6 @@ public class FileDownloadManagerFactory _fileTransferOrchestrator = fileTransferOrchestrator; _fileCacheManager = fileCacheManager; _fileCompactor = fileCompactor; - _pairProcessingLimiter = pairProcessingLimiter; _configService = configService; _textureDownscaleService = textureDownscaleService; _textureMetadataHelper = textureMetadataHelper; @@ -50,7 +46,6 @@ public class FileDownloadManagerFactory _fileTransferOrchestrator, _fileCacheManager, _fileCompactor, - _pairProcessingLimiter, _configService, _textureDownscaleService, _textureMetadataHelper); diff --git a/LightlessSync/PlayerData/Handlers/GameObjectHandler.cs b/LightlessSync/PlayerData/Handlers/GameObjectHandler.cs index 8d56b4f..829c737 100644 --- a/LightlessSync/PlayerData/Handlers/GameObjectHandler.cs +++ b/LightlessSync/PlayerData/Handlers/GameObjectHandler.cs @@ -94,6 +94,7 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP public DrawCondition CurrentDrawCondition { get; set; } = DrawCondition.None; public byte Gender { get; private set; } public string Name { get; private set; } + public uint EntityId { get; private set; } = uint.MaxValue; public ObjectKind ObjectKind { get; } public byte RaceId { get; private set; } public byte TribeId { get; private set; } @@ -142,6 +143,7 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP { Address = IntPtr.Zero; DrawObjectAddress = IntPtr.Zero; + EntityId = uint.MaxValue; _haltProcessing = false; } @@ -171,13 +173,16 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP Address = _getAddress(); if (Address != IntPtr.Zero) { - var drawObjAddr = (IntPtr)((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)Address)->DrawObject; + var gameObject = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)Address; + var drawObjAddr = (IntPtr)gameObject->DrawObject; DrawObjectAddress = drawObjAddr; + EntityId = gameObject->EntityId; CurrentDrawCondition = DrawCondition.None; } else { DrawObjectAddress = IntPtr.Zero; + EntityId = uint.MaxValue; CurrentDrawCondition = DrawCondition.DrawObjectZero; } diff --git a/LightlessSync/PlayerData/Pairs/IPairHandlerAdapter.cs b/LightlessSync/PlayerData/Pairs/IPairHandlerAdapter.cs new file mode 100644 index 0000000..c89d311 --- /dev/null +++ b/LightlessSync/PlayerData/Pairs/IPairHandlerAdapter.cs @@ -0,0 +1,27 @@ +using LightlessSync.API.Data; + +namespace LightlessSync.PlayerData.Pairs; + +/// +/// orchestrates the lifecycle of a paired character +/// +public interface IPairHandlerAdapter : IDisposable, IPairPerformanceSubject +{ + new string Ident { get; } + bool Initialized { get; } + bool IsVisible { get; } + bool ScheduledForDeletion { get; set; } + CharacterData? LastReceivedCharacterData { get; } + long LastAppliedDataBytes { get; } + new string? PlayerName { get; } + string PlayerNameHash { get; } + uint PlayerCharacterId { get; } + + void Initialize(); + void ApplyData(CharacterData data); + void ApplyLastReceivedData(bool forced = false); + bool FetchPerformanceMetricsFromCache(); + void LoadCachedCharacterData(CharacterData data); + void SetUploading(bool uploading); + void SetPaused(bool paused); +} diff --git a/LightlessSync/PlayerData/Pairs/IPairHandlerAdapterFactory.cs b/LightlessSync/PlayerData/Pairs/IPairHandlerAdapterFactory.cs new file mode 100644 index 0000000..167b5bc --- /dev/null +++ b/LightlessSync/PlayerData/Pairs/IPairHandlerAdapterFactory.cs @@ -0,0 +1,6 @@ +namespace LightlessSync.PlayerData.Pairs; + +public interface IPairHandlerAdapterFactory +{ + IPairHandlerAdapter Create(string ident); +} diff --git a/LightlessSync/PlayerData/Pairs/PairHandlerAdapter.cs b/LightlessSync/PlayerData/Pairs/PairHandlerAdapter.cs index 70f4f0b..925c42c 100644 --- a/LightlessSync/PlayerData/Pairs/PairHandlerAdapter.cs +++ b/LightlessSync/PlayerData/Pairs/PairHandlerAdapter.cs @@ -16,43 +16,17 @@ using LightlessSync.Services.TextureCompression; using LightlessSync.Utils; using LightlessSync.WebAPI.Files; using LightlessSync.WebAPI.Files.Models; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using ObjectKind = LightlessSync.API.Data.Enum.ObjectKind; +using FileReplacementDataComparer = LightlessSync.PlayerData.Data.FileReplacementDataComparer; namespace LightlessSync.PlayerData.Pairs; /// -/// orchestrates the lifecycle of a paired character +/// handles lifecycle, visibility, queued data, character data for a paired user /// -public interface IPairHandlerAdapter : IDisposable, IPairPerformanceSubject -{ - string Ident { get; } - bool Initialized { get; } - bool IsVisible { get; } - bool ScheduledForDeletion { get; set; } - CharacterData? LastReceivedCharacterData { get; } - long LastAppliedDataBytes { get; } - string? PlayerName { get; } - string PlayerNameHash { get; } - uint PlayerCharacterId { get; } - - void Initialize(); - void ApplyData(CharacterData data); - void ApplyLastReceivedData(bool forced = false); - bool FetchPerformanceMetricsFromCache(); - void LoadCachedCharacterData(CharacterData data); - void SetUploading(bool uploading); - void SetPaused(bool paused); -} - -public interface IPairHandlerAdapterFactory -{ - IPairHandlerAdapter Create(string ident); -} - -internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPairHandlerAdapter, IPairPerformanceSubject +internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPairHandlerAdapter { private sealed record CombatData(Guid ApplicationId, CharacterData CharacterData, bool Forced); @@ -70,14 +44,14 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa private readonly PairStateCache _pairStateCache; private readonly PairPerformanceMetricsCache _performanceMetricsCache; private readonly PairManager _pairManager; - private CancellationTokenSource? _applicationCancellationTokenSource = new(); + private CancellationTokenSource? _applicationCancellationTokenSource; private Guid _applicationId; private Task? _applicationTask; private CharacterData? _cachedData = null; private GameObjectHandler? _charaHandler; private readonly Dictionary _customizeIds = []; private CombatData? _dataReceivedInDowntime; - private CancellationTokenSource? _downloadCancellationTokenSource = new(); + private CancellationTokenSource? _downloadCancellationTokenSource; private bool _forceApplyMods = false; private bool _forceFullReapply; private Dictionary<(string GamePath, string? Hash), string>? _lastAppliedModdedPaths; @@ -86,6 +60,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa private Guid _penumbraCollection; private readonly object _collectionGate = new(); private bool _redrawOnNextApplication = false; + private bool _explicitRedrawQueued; private readonly object _initializationGate = new(); private readonly object _pauseLock = new(); private Task _pauseTransitionTask = Task.CompletedTask; @@ -183,7 +158,6 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa return; } - var user = GetPrimaryUserData(); if (LastAppliedDataBytes < 0 || LastAppliedDataTris < 0 || LastAppliedApproximateVRAMBytes < 0 || LastAppliedApproximateEffectiveVRAMBytes < 0) { @@ -441,9 +415,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa return combined; } public nint PlayerCharacter => _charaHandler?.Address ?? nint.Zero; - public unsafe uint PlayerCharacterId => (_charaHandler?.Address ?? nint.Zero) == nint.Zero - ? uint.MaxValue - : ((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)_charaHandler!.Address)->EntityId; + public uint PlayerCharacterId => _charaHandler?.EntityId ?? uint.MaxValue; public string? PlayerName { get; private set; } public string PlayerNameHash => Ident; @@ -490,14 +462,14 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa if (shouldForce) { _forceApplyMods = true; - _cachedData = null; + _forceFullReapply = true; LastAppliedDataBytes = -1; LastAppliedDataTris = -1; LastAppliedApproximateVRAMBytes = -1; LastAppliedApproximateEffectiveVRAMBytes = -1; } - var sanitized = CloneAndSanitizeLastReceived(out var dataHash); + var sanitized = CloneAndSanitizeLastReceived(out _); if (sanitized is null) { Logger.LogTrace("Sanitized data null for {Ident}", Ident); @@ -746,7 +718,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa if (characterData is null) { Logger.LogWarning("[BASE-{appBase}] Received null character data, skipping application for {handler}", applicationBase, GetLogIdentifier()); - SetUploading(isUploading: false); + SetUploading(false); return; } @@ -757,7 +729,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa "Cannot apply character data: you are in combat, deferring application"))); Logger.LogDebug("[BASE-{appBase}] Received data but player is in combat", applicationBase); _dataReceivedInDowntime = new(applicationBase, characterData, forceApplyCustomization); - SetUploading(isUploading: false); + SetUploading(false); return; } @@ -767,7 +739,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa "Cannot apply character data: you are performing music, deferring application"))); Logger.LogDebug("[BASE-{appBase}] Received data but player is performing", applicationBase); _dataReceivedInDowntime = new(applicationBase, characterData, forceApplyCustomization); - SetUploading(isUploading: false); + SetUploading(false); return; } @@ -777,7 +749,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa "Cannot apply character data: you are in an instance, deferring application"))); Logger.LogDebug("[BASE-{appBase}] Received data but player is in instance", applicationBase); _dataReceivedInDowntime = new(applicationBase, characterData, forceApplyCustomization); - SetUploading(isUploading: false); + SetUploading(false); return; } @@ -787,7 +759,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa "Cannot apply character data: you are in a cutscene, deferring application"))); Logger.LogDebug("[BASE-{appBase}] Received data but player is in a cutscene", applicationBase); _dataReceivedInDowntime = new(applicationBase, characterData, forceApplyCustomization); - SetUploading(isUploading: false); + SetUploading(false); return; } @@ -797,7 +769,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa "Cannot apply character data: you are in GPose, deferring application"))); Logger.LogDebug("[BASE-{appBase}] Received data but player is in GPose", applicationBase); _dataReceivedInDowntime = new(applicationBase, characterData, forceApplyCustomization); - SetUploading(isUploading: false); + SetUploading(false); return; } @@ -807,7 +779,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa "Cannot apply character data: Penumbra or Glamourer is not available, deferring application"))); Logger.LogInformation("[BASE-{appbase}] Application of data for {player} while Penumbra/Glamourer unavailable, returning", applicationBase, GetLogIdentifier()); _dataReceivedInDowntime = new(applicationBase, characterData, forceApplyCustomization); - SetUploading(isUploading: false); + SetUploading(false); return; } @@ -828,7 +800,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa Logger.LogDebug("[BASE-{appBase}] Setting data: {hash}, forceApplyMods: {force}", applicationBase, _cachedData.DataHash.Value, _forceApplyMods); } - SetUploading(isUploading: false); + SetUploading(false); Logger.LogDebug("[BASE-{appbase}] Applying data for {player}, forceApplyCustomization: {forced}, forceApplyMods: {forceMods}", applicationBase, GetLogIdentifier(), forceApplyCustomization, _forceApplyMods); Logger.LogDebug("[BASE-{appbase}] Hash for data is {newHash}, current cache hash is {oldHash}", applicationBase, characterData.DataHash.Value, _cachedData?.DataHash.Value ?? "NODATA"); @@ -850,10 +822,13 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa _forceApplyMods = false; } + _explicitRedrawQueued = false; + if (_redrawOnNextApplication && charaDataToUpdate.TryGetValue(ObjectKind.Player, out var player)) { player.Add(PlayerChanges.ForcedRedraw); _redrawOnNextApplication = false; + _explicitRedrawQueued = true; } if (charaDataToUpdate.TryGetValue(ObjectKind.Player, out var playerChanges)) @@ -863,7 +838,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa Logger.LogDebug("[BASE-{appbase}] Downloading and applying character for {name}", applicationBase, GetPrimaryAliasOrUidSafe()); - var forceFullReapply = _forceFullReapply || forceApplyCustomization + var forceFullReapply = _forceFullReapply || LastAppliedApproximateVRAMBytes < 0 || LastAppliedDataTris < 0; DownloadAndApplyCharacter(applicationBase, characterData.DeepClone(), charaDataToUpdate, forceFullReapply); @@ -875,12 +850,12 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa return $"{alias}:{PlayerName ?? string.Empty}:{(PlayerCharacter != nint.Zero ? "HasChar" : "NoChar")}"; } - public void SetUploading(bool isUploading = true) + public void SetUploading(bool uploading) { - Logger.LogTrace("Setting {name} uploading {uploading}", GetPrimaryAliasOrUidSafe(), isUploading); + Logger.LogTrace("Setting {name} uploading {uploading}", GetPrimaryAliasOrUidSafe(), uploading); if (_charaHandler != null) { - Mediator.Publish(new PlayerUploadingMessage(_charaHandler, isUploading)); + Mediator.Publish(new PlayerUploadingMessage(_charaHandler, uploading)); } } @@ -904,7 +879,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa { base.Dispose(disposing); - SetUploading(isUploading: false); + SetUploading(false); var name = PlayerName; var user = GetPrimaryUserDataSafe(); var alias = GetPrimaryAliasOrUidSafe(); @@ -1046,6 +1021,11 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa break; case PlayerChanges.ForcedRedraw: + if (!ShouldPerformForcedRedraw(changes.Key, changes.Value, charaData)) + { + Logger.LogTrace("[{applicationId}] Skipping forced redraw for {handler}", applicationId, handler); + break; + } await _ipcManager.Penumbra.RedrawAsync(Logger, handler, applicationId, token).ConfigureAwait(false); break; @@ -1061,6 +1041,45 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa } } + private bool ShouldPerformForcedRedraw(ObjectKind objectKind, ICollection 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> BuildFullChangeSet(CharacterData characterData) { var result = new Dictionary>(); @@ -1126,6 +1145,39 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa return result; } + private static bool PlayerModFilesChanged(CharacterData newData, CharacterData? previousData) + { + return !FileReplacementListsEqual( + TryGetFileReplacementList(newData, ObjectKind.Player), + TryGetFileReplacementList(previousData, ObjectKind.Player)); + } + + private static IReadOnlyCollection? TryGetFileReplacementList(CharacterData? data, ObjectKind objectKind) + { + if (data is null) + { + return null; + } + + return data.FileReplacements.TryGetValue(objectKind, out var list) ? list : null; + } + + private static bool FileReplacementListsEqual(IReadOnlyCollection? left, IReadOnlyCollection? right) + { + if (left is null || left.Count == 0) + { + return right is null || right.Count == 0; + } + + if (right is null || right.Count == 0) + { + return false; + } + + var comparer = FileReplacementDataComparer.Instance; + return !left.Except(right, comparer).Any() && !right.Except(left, comparer).Any(); + } + private void DownloadAndApplyCharacter(Guid applicationBase, CharacterData charaData, Dictionary> updatedData, bool forceFullReapply) { if (!updatedData.Any()) @@ -1165,7 +1217,8 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa _downloadCancellationTokenSource = _downloadCancellationTokenSource?.CancelRecreate() ?? new CancellationTokenSource(); var downloadToken = _downloadCancellationTokenSource.Token; - _ = DownloadAndApplyCharacterAsync(applicationBase, charaData, updatedData, updateModdedPaths, updateManip, cachedModdedPaths, downloadToken).ConfigureAwait(false); + _ = DownloadAndApplyCharacterAsync(applicationBase, charaData, updatedData, updateModdedPaths, updateManip, cachedModdedPaths, downloadToken) + .ConfigureAwait(false); } private Task? _pairDownloadTask; @@ -1173,107 +1226,114 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa private async Task DownloadAndApplyCharacterAsync(Guid applicationBase, CharacterData charaData, Dictionary> updatedData, bool updateModdedPaths, bool updateManip, Dictionary<(string GamePath, string? Hash), string>? cachedModdedPaths, CancellationToken downloadToken) { - await using var concurrencyLease = await _pairProcessingLimiter.AcquireAsync(downloadToken).ConfigureAwait(false); - bool skipDownscaleForPair = ShouldSkipDownscale(); - var user = GetPrimaryUserData(); - Dictionary<(string GamePath, string? Hash), string> moddedPaths; - - if (updateModdedPaths) + var concurrencyLease = await _pairProcessingLimiter.AcquireAsync(downloadToken).ConfigureAwait(false); + try { - if (cachedModdedPaths is not null) + bool skipDownscaleForPair = ShouldSkipDownscale(); + var user = GetPrimaryUserData(); + Dictionary<(string GamePath, string? Hash), string> moddedPaths; + + if (updateModdedPaths) { - moddedPaths = new Dictionary<(string GamePath, string? Hash), string>(cachedModdedPaths, cachedModdedPaths.Comparer); + if (cachedModdedPaths is not null) + { + moddedPaths = new Dictionary<(string GamePath, string? Hash), string>(cachedModdedPaths, cachedModdedPaths.Comparer); + } + else + { + int attempts = 0; + List toDownloadReplacements = TryCalculateModdedDictionary(applicationBase, charaData, out moddedPaths, downloadToken); + + while (toDownloadReplacements.Count > 0 && attempts++ <= 10 && !downloadToken.IsCancellationRequested) + { + if (_pairDownloadTask != null && !_pairDownloadTask.IsCompleted) + { + Logger.LogDebug("[BASE-{appBase}] Finishing prior running download task for player {name}, {kind}", applicationBase, PlayerName, updatedData); + await _pairDownloadTask.ConfigureAwait(false); + } + + Logger.LogDebug("[BASE-{appBase}] Downloading missing files for player {name}, {kind}", applicationBase, PlayerName, updatedData); + + Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter), EventSeverity.Informational, + $"Starting download for {toDownloadReplacements.Count} files"))); + var toDownloadFiles = await _downloadManager.InitiateDownloadList(_charaHandler!, toDownloadReplacements, downloadToken).ConfigureAwait(false); + + if (!_playerPerformanceService.ComputeAndAutoPauseOnVRAMUsageThresholds(this, charaData, toDownloadFiles)) + { + _downloadManager.ClearDownload(); + return; + } + + var handlerForDownload = _charaHandler; + _pairDownloadTask = Task.Run(async () => await _downloadManager.DownloadFiles(handlerForDownload, toDownloadReplacements, downloadToken, skipDownscaleForPair).ConfigureAwait(false)); + + await _pairDownloadTask.ConfigureAwait(false); + + if (downloadToken.IsCancellationRequested) + { + Logger.LogTrace("[BASE-{appBase}] Detected cancellation", applicationBase); + return; + } + + toDownloadReplacements = TryCalculateModdedDictionary(applicationBase, charaData, out moddedPaths, downloadToken); + + if (toDownloadReplacements.TrueForAll(c => _downloadManager.ForbiddenTransfers.Exists(f => string.Equals(f.Hash, c.Hash, StringComparison.Ordinal)))) + { + break; + } + + await Task.Delay(TimeSpan.FromSeconds(2), downloadToken).ConfigureAwait(false); + } + + if (!await _playerPerformanceService.CheckBothThresholds(this, charaData).ConfigureAwait(false)) + { + return; + } + } } else { - int attempts = 0; - List toDownloadReplacements = TryCalculateModdedDictionary(applicationBase, charaData, out moddedPaths, downloadToken); - - while (toDownloadReplacements.Count > 0 && attempts++ <= 10 && !downloadToken.IsCancellationRequested) - { - if (_pairDownloadTask != null && !_pairDownloadTask.IsCompleted) - { - Logger.LogDebug("[BASE-{appBase}] Finishing prior running download task for player {name}, {kind}", applicationBase, PlayerName, updatedData); - await _pairDownloadTask.ConfigureAwait(false); - } - - Logger.LogDebug("[BASE-{appBase}] Downloading missing files for player {name}, {kind}", applicationBase, PlayerName, updatedData); - - Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter), EventSeverity.Informational, - $"Starting download for {toDownloadReplacements.Count} files"))); - var toDownloadFiles = await _downloadManager.InitiateDownloadList(_charaHandler!, toDownloadReplacements, downloadToken).ConfigureAwait(false); - - if (!_playerPerformanceService.ComputeAndAutoPauseOnVRAMUsageThresholds(this, charaData, toDownloadFiles)) - { - _downloadManager.ClearDownload(); - return; - } - - var handlerForDownload = _charaHandler; - _pairDownloadTask = Task.Run(async () => await _downloadManager.DownloadFiles(handlerForDownload, toDownloadReplacements, downloadToken, skipDownscaleForPair).ConfigureAwait(false)); - - await _pairDownloadTask.ConfigureAwait(false); - - if (downloadToken.IsCancellationRequested) - { - Logger.LogTrace("[BASE-{appBase}] Detected cancellation", applicationBase); - return; - } - - toDownloadReplacements = TryCalculateModdedDictionary(applicationBase, charaData, out moddedPaths, downloadToken); - - if (toDownloadReplacements.TrueForAll(c => _downloadManager.ForbiddenTransfers.Exists(f => string.Equals(f.Hash, c.Hash, StringComparison.Ordinal)))) - { - break; - } - - await Task.Delay(TimeSpan.FromSeconds(2), downloadToken).ConfigureAwait(false); - } - - if (!await _playerPerformanceService.CheckBothThresholds(this, charaData).ConfigureAwait(false)) - { - return; - } + moddedPaths = cachedModdedPaths is not null + ? new Dictionary<(string GamePath, string? Hash), string>(cachedModdedPaths, cachedModdedPaths.Comparer) + : []; } + + downloadToken.ThrowIfCancellationRequested(); + + var handlerForApply = _charaHandler; + if (handlerForApply is null || handlerForApply.Address == nint.Zero) + { + Logger.LogDebug("[BASE-{appBase}] Handler not available for {player}, cached data for later application", applicationBase, GetLogIdentifier()); + _cachedData = charaData; + _pairStateCache.Store(Ident, charaData); + _forceFullReapply = true; + return; + } + + var appToken = _applicationCancellationTokenSource?.Token; + while ((!_applicationTask?.IsCompleted ?? false) + && !downloadToken.IsCancellationRequested + && (!appToken?.IsCancellationRequested ?? false)) + { + Logger.LogDebug("[BASE-{appBase}] Waiting for current data application (Id: {id}) for player ({handler}) to finish", applicationBase, _applicationId, PlayerName); + await Task.Delay(250).ConfigureAwait(false); + } + + if (downloadToken.IsCancellationRequested || (appToken?.IsCancellationRequested ?? false)) + { + _forceFullReapply = true; + return; + } + + _applicationCancellationTokenSource = _applicationCancellationTokenSource.CancelRecreate() ?? new CancellationTokenSource(); + var token = _applicationCancellationTokenSource.Token; + + _applicationTask = ApplyCharacterDataAsync(applicationBase, handlerForApply, charaData, updatedData, updateModdedPaths, updateManip, moddedPaths, token); } - else + finally { - moddedPaths = cachedModdedPaths is not null - ? new Dictionary<(string GamePath, string? Hash), string>(cachedModdedPaths, cachedModdedPaths.Comparer) - : []; + await concurrencyLease.DisposeAsync().ConfigureAwait(false); } - - downloadToken.ThrowIfCancellationRequested(); - - var handlerForApply = _charaHandler; - if (handlerForApply is null || handlerForApply.Address == nint.Zero) - { - Logger.LogDebug("[BASE-{appBase}] Handler not available for {player}, cached data for later application", applicationBase, GetLogIdentifier()); - _cachedData = charaData; - _pairStateCache.Store(Ident, charaData); - _forceFullReapply = true; - return; - } - - var appToken = _applicationCancellationTokenSource?.Token; - while ((!_applicationTask?.IsCompleted ?? false) - && !downloadToken.IsCancellationRequested - && (!appToken?.IsCancellationRequested ?? false)) - { - Logger.LogDebug("[BASE-{appBase}] Waiting for current data application (Id: {id}) for player ({handler}) to finish", applicationBase, _applicationId, PlayerName); - await Task.Delay(250).ConfigureAwait(false); - } - - if (downloadToken.IsCancellationRequested || (appToken?.IsCancellationRequested ?? false)) - { - _forceFullReapply = true; - return; - } - - _applicationCancellationTokenSource = _applicationCancellationTokenSource.CancelRecreate() ?? new CancellationTokenSource(); - var token = _applicationCancellationTokenSource.Token; - - _applicationTask = ApplyCharacterDataAsync(applicationBase, handlerForApply, charaData, updatedData, updateModdedPaths, updateManip, moddedPaths, token); } private async Task ApplyCharacterDataAsync(Guid applicationBase, GameObjectHandler handlerForApply, CharacterData charaData, Dictionary> updatedData, bool updateModdedPaths, bool updateManip, @@ -1416,6 +1476,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa { try { + _forceFullReapply = true; ApplyCharacterData(appData, cachedData!, forceApplyCustomization: true); } catch (Exception ex) @@ -1432,6 +1493,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa { try { + _forceFullReapply = true; ApplyLastReceivedData(forced: true); } catch (Exception ex) @@ -1468,21 +1530,37 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa _serverConfigManager.AutoPopulateNoteForUid(user.UID, name); } - Mediator.Subscribe(this, async (_) => + Mediator.Subscribe(this, _message => { - if (string.IsNullOrEmpty(_cachedData?.HonorificData)) return; - Logger.LogTrace("Reapplying Honorific data for {handler}", GetLogIdentifier()); - await _ipcManager.Honorific.SetTitleAsync(PlayerCharacter, _cachedData.HonorificData).ConfigureAwait(false); + var honorificData = _cachedData?.HonorificData; + if (string.IsNullOrEmpty(honorificData)) + return; + + _ = ReapplyHonorificAsync(honorificData!); }); - Mediator.Subscribe(this, async (_) => + Mediator.Subscribe(this, _message => { - if (string.IsNullOrEmpty(_cachedData?.PetNamesData)) return; - Logger.LogTrace("Reapplying Pet Names data for {handler}", GetLogIdentifier()); - await _ipcManager.PetNames.SetPlayerData(PlayerCharacter, _cachedData.PetNamesData).ConfigureAwait(false); + var petNamesData = _cachedData?.PetNamesData; + if (string.IsNullOrEmpty(petNamesData)) + return; + + _ = ReapplyPetNamesAsync(petNamesData!); }); } + private async Task ReapplyHonorificAsync(string honorificData) + { + Logger.LogTrace("Reapplying Honorific data for {handler}", GetLogIdentifier()); + await _ipcManager.Honorific.SetTitleAsync(PlayerCharacter, honorificData).ConfigureAwait(false); + } + + private async Task ReapplyPetNamesAsync(string petNamesData) + { + Logger.LogTrace("Reapplying Pet Names data for {handler}", GetLogIdentifier()); + await _ipcManager.PetNames.SetPlayerData(PlayerCharacter, petNamesData).ConfigureAwait(false); + } + private async Task RevertCustomizationDataAsync(ObjectKind objectKind, string name, Guid applicationId, CancellationToken cancelToken) { nint address = _dalamudUtil.GetPlayerCharacterFromCachedTableByIdent(Ident); @@ -1572,14 +1650,11 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa { token.ThrowIfCancellationRequested(); var fileCache = _fileDbManager.GetFileCacheByHash(item.Hash); - if (fileCache != null) + if (fileCache is not null && !File.Exists(fileCache.ResolvedFilepath)) { - if (!File.Exists(fileCache.ResolvedFilepath)) - { - Logger.LogTrace("[BASE-{appBase}] Cached path {Path} missing on disk for hash {Hash}, removing cache entry", applicationBase, fileCache.ResolvedFilepath, item.Hash); - _fileDbManager.RemoveHashedFile(fileCache.Hash, fileCache.PrefixedFilePath); - fileCache = null; - } + Logger.LogTrace("[BASE-{appBase}] Cached path {Path} missing on disk for hash {Hash}, removing cache entry", applicationBase, fileCache.ResolvedFilepath, item.Hash); + _fileDbManager.RemoveHashedFile(fileCache.Hash, fileCache.PrefixedFilePath); + fileCache = null; } if (fileCache != null) @@ -1701,7 +1776,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa if (penumbraCollection != Guid.Empty) { await _ipcManager.Penumbra.AssignTemporaryCollectionAsync(Logger, penumbraCollection, character.ObjectIndex).ConfigureAwait(false); - await _ipcManager.Penumbra.SetTemporaryModsAsync(Logger, applicationId, penumbraCollection, new Dictionary()).ConfigureAwait(false); + await _ipcManager.Penumbra.SetTemporaryModsAsync(Logger, applicationId, penumbraCollection, new Dictionary(StringComparer.Ordinal)).ConfigureAwait(false); await _ipcManager.Penumbra.SetManipulationDataAsync(Logger, applicationId, penumbraCollection, string.Empty).ConfigureAwait(false); } } @@ -1775,83 +1850,3 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa } } - -internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory -{ - private readonly ILoggerFactory _loggerFactory; - private readonly LightlessMediator _mediator; - private readonly PairManager _pairManager; - private readonly GameObjectHandlerFactory _gameObjectHandlerFactory; - private readonly IpcManager _ipcManager; - private readonly FileDownloadManagerFactory _fileDownloadManagerFactory; - private readonly PluginWarningNotificationService _pluginWarningNotificationManager; - private readonly IServiceProvider _serviceProvider; - private readonly IHostApplicationLifetime _lifetime; - private readonly FileCacheManager _fileCacheManager; - private readonly PlayerPerformanceService _playerPerformanceService; - private readonly PairProcessingLimiter _pairProcessingLimiter; - private readonly ServerConfigurationManager _serverConfigManager; - private readonly TextureDownscaleService _textureDownscaleService; - private readonly PairStateCache _pairStateCache; - private readonly PairPerformanceMetricsCache _pairPerformanceMetricsCache; - - public PairHandlerAdapterFactory( - ILoggerFactory loggerFactory, - LightlessMediator mediator, - PairManager pairManager, - GameObjectHandlerFactory gameObjectHandlerFactory, - IpcManager ipcManager, - FileDownloadManagerFactory fileDownloadManagerFactory, - PluginWarningNotificationService pluginWarningNotificationManager, - IServiceProvider serviceProvider, - IHostApplicationLifetime lifetime, - FileCacheManager fileCacheManager, - PlayerPerformanceService playerPerformanceService, - PairProcessingLimiter pairProcessingLimiter, - ServerConfigurationManager serverConfigManager, - TextureDownscaleService textureDownscaleService, - PairStateCache pairStateCache, - PairPerformanceMetricsCache pairPerformanceMetricsCache) - { - _loggerFactory = loggerFactory; - _mediator = mediator; - _pairManager = pairManager; - _gameObjectHandlerFactory = gameObjectHandlerFactory; - _ipcManager = ipcManager; - _fileDownloadManagerFactory = fileDownloadManagerFactory; - _pluginWarningNotificationManager = pluginWarningNotificationManager; - _serviceProvider = serviceProvider; - _lifetime = lifetime; - _fileCacheManager = fileCacheManager; - _playerPerformanceService = playerPerformanceService; - _pairProcessingLimiter = pairProcessingLimiter; - _serverConfigManager = serverConfigManager; - _textureDownscaleService = textureDownscaleService; - _pairStateCache = pairStateCache; - _pairPerformanceMetricsCache = pairPerformanceMetricsCache; - } - - public IPairHandlerAdapter Create(string ident) - { - var downloadManager = _fileDownloadManagerFactory.Create(); - var dalamudUtilService = _serviceProvider.GetRequiredService(); - return new PairHandlerAdapter( - _loggerFactory.CreateLogger(), - _mediator, - _pairManager, - ident, - _gameObjectHandlerFactory, - _ipcManager, - downloadManager, - _pluginWarningNotificationManager, - dalamudUtilService, - _lifetime, - _fileCacheManager, - _playerPerformanceService, - _pairProcessingLimiter, - _serverConfigManager, - _textureDownscaleService, - _pairStateCache, - _pairPerformanceMetricsCache); - } -} diff --git a/LightlessSync/PlayerData/Pairs/PairHandlerAdapterFactory.cs b/LightlessSync/PlayerData/Pairs/PairHandlerAdapterFactory.cs new file mode 100644 index 0000000..1fe2703 --- /dev/null +++ b/LightlessSync/PlayerData/Pairs/PairHandlerAdapterFactory.cs @@ -0,0 +1,93 @@ +using LightlessSync.FileCache; +using LightlessSync.Interop.Ipc; +using LightlessSync.PlayerData.Factories; +using LightlessSync.Services; +using LightlessSync.Services.Mediator; +using LightlessSync.Services.PairProcessing; +using LightlessSync.Services.ServerConfiguration; +using LightlessSync.Services.TextureCompression; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace LightlessSync.PlayerData.Pairs; + +internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory +{ + private readonly ILoggerFactory _loggerFactory; + private readonly LightlessMediator _mediator; + private readonly PairManager _pairManager; + private readonly GameObjectHandlerFactory _gameObjectHandlerFactory; + private readonly IpcManager _ipcManager; + private readonly FileDownloadManagerFactory _fileDownloadManagerFactory; + private readonly PluginWarningNotificationService _pluginWarningNotificationManager; + private readonly IServiceProvider _serviceProvider; + private readonly IHostApplicationLifetime _lifetime; + private readonly FileCacheManager _fileCacheManager; + private readonly PlayerPerformanceService _playerPerformanceService; + private readonly PairProcessingLimiter _pairProcessingLimiter; + private readonly ServerConfigurationManager _serverConfigManager; + private readonly TextureDownscaleService _textureDownscaleService; + private readonly PairStateCache _pairStateCache; + private readonly PairPerformanceMetricsCache _pairPerformanceMetricsCache; + + public PairHandlerAdapterFactory( + ILoggerFactory loggerFactory, + LightlessMediator mediator, + PairManager pairManager, + GameObjectHandlerFactory gameObjectHandlerFactory, + IpcManager ipcManager, + FileDownloadManagerFactory fileDownloadManagerFactory, + PluginWarningNotificationService pluginWarningNotificationManager, + IServiceProvider serviceProvider, + IHostApplicationLifetime lifetime, + FileCacheManager fileCacheManager, + PlayerPerformanceService playerPerformanceService, + PairProcessingLimiter pairProcessingLimiter, + ServerConfigurationManager serverConfigManager, + TextureDownscaleService textureDownscaleService, + PairStateCache pairStateCache, + PairPerformanceMetricsCache pairPerformanceMetricsCache) + { + _loggerFactory = loggerFactory; + _mediator = mediator; + _pairManager = pairManager; + _gameObjectHandlerFactory = gameObjectHandlerFactory; + _ipcManager = ipcManager; + _fileDownloadManagerFactory = fileDownloadManagerFactory; + _pluginWarningNotificationManager = pluginWarningNotificationManager; + _serviceProvider = serviceProvider; + _lifetime = lifetime; + _fileCacheManager = fileCacheManager; + _playerPerformanceService = playerPerformanceService; + _pairProcessingLimiter = pairProcessingLimiter; + _serverConfigManager = serverConfigManager; + _textureDownscaleService = textureDownscaleService; + _pairStateCache = pairStateCache; + _pairPerformanceMetricsCache = pairPerformanceMetricsCache; + } + + public IPairHandlerAdapter Create(string ident) + { + var downloadManager = _fileDownloadManagerFactory.Create(); + var dalamudUtilService = _serviceProvider.GetRequiredService(); + return new PairHandlerAdapter( + _loggerFactory.CreateLogger(), + _mediator, + _pairManager, + ident, + _gameObjectHandlerFactory, + _ipcManager, + downloadManager, + _pluginWarningNotificationManager, + dalamudUtilService, + _lifetime, + _fileCacheManager, + _playerPerformanceService, + _pairProcessingLimiter, + _serverConfigManager, + _textureDownscaleService, + _pairStateCache, + _pairPerformanceMetricsCache); + } +} diff --git a/LightlessSync/Plugin.cs b/LightlessSync/Plugin.cs index d1f3b6d..2cf8bdf 100644 --- a/LightlessSync/Plugin.cs +++ b/LightlessSync/Plugin.cs @@ -18,6 +18,7 @@ using LightlessSync.Services.ActorTracking; using LightlessSync.Services.CharaData; using LightlessSync.Services.Events; using LightlessSync.Services.Mediator; +using LightlessSync.Services.Rendering; using LightlessSync.Services.ServerConfiguration; using LightlessSync.Services.TextureCompression; using LightlessSync.UI; @@ -33,8 +34,6 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using NReco.Logging.File; -using System; -using System.IO; using System.Net.Http.Headers; using System.Reflection; using OtterTex; @@ -178,6 +177,10 @@ public sealed class Plugin : IDalamudPlugin sp.GetRequiredService(), sp.GetRequiredService())); + services.AddSingleton(sp => new PictomancyService( + sp.GetRequiredService>(), + pluginInterface)); + // Tag (Groups) UIs services.AddSingleton(); services.AddSingleton(); @@ -260,11 +263,14 @@ public sealed class Plugin : IDalamudPlugin services.AddSingleton(sp => new LightFinderPlateHandler( sp.GetRequiredService>(), - sp.GetRequiredService(), - pluginInterface, + addonLifecycle, + gameGui, sp.GetRequiredService(), + sp.GetRequiredService(), objectTable, - gameGui)); + sp.GetRequiredService(), + pluginInterface, + sp.GetRequiredService())); services.AddSingleton(sp => new LightFinderScannerService( sp.GetRequiredService>(), diff --git a/LightlessSync/Services/ActorTracking/ActorObjectService.cs b/LightlessSync/Services/ActorTracking/ActorObjectService.cs index 2305c2a..c2650ad 100644 --- a/LightlessSync/Services/ActorTracking/ActorObjectService.cs +++ b/LightlessSync/Services/ActorTracking/ActorObjectService.cs @@ -1,27 +1,20 @@ -using LightlessSync; -using LightlessObjectKind = LightlessSync.API.Data.Enum.ObjectKind; -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using Dalamud.Game; -using Dalamud.Game.ClientState; +using System.Collections.Concurrent; using Dalamud.Game.ClientState.Objects.SubKinds; -using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Hooking; using Dalamud.Plugin.Services; +using FFXIVClientStructs.Interop; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Object; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using LightlessSync.Services.Mediator; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using DalamudObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind; -using FFXIVClientStructs.Interop; -using System.Threading; +using LightlessObjectKind = LightlessSync.API.Data.Enum.ObjectKind; namespace LightlessSync.Services.ActorTracking; -public sealed unsafe class ActorObjectService : IHostedService, IDisposable +public sealed class ActorObjectService : IHostedService, IDisposable { public readonly record struct ActorDescriptor( string Name, @@ -38,25 +31,13 @@ public sealed unsafe class ActorObjectService : IHostedService, IDisposable private readonly IFramework _framework; private readonly IGameInteropProvider _interop; private readonly IObjectTable _objectTable; - private readonly IClientState _clientState; private readonly LightlessMediator _mediator; private readonly ConcurrentDictionary _activePlayers = new(); private readonly ConcurrentDictionary _actorsByHash = new(StringComparer.Ordinal); private readonly ConcurrentDictionary> _actorsByName = new(StringComparer.Ordinal); - private ActorDescriptor[] _playerCharacterSnapshot = Array.Empty(); - private nint[] _playerAddressSnapshot = Array.Empty(); - private readonly HashSet _renderedPlayers = new(); - private readonly HashSet _renderedCompanions = new(); - private readonly Dictionary _ownedObjects = new(); - private nint[] _renderedPlayerSnapshot = Array.Empty(); - private nint[] _renderedCompanionSnapshot = Array.Empty(); - private nint[] _ownedObjectSnapshot = Array.Empty(); - private IReadOnlyDictionary _ownedObjectMapSnapshot = new Dictionary(); - private nint _localPlayerAddress = nint.Zero; - private nint _localPetAddress = nint.Zero; - private nint _localMinionMountAddress = nint.Zero; - private nint _localCompanionAddress = nint.Zero; + private readonly OwnedObjectTracker _ownedTracker = new(); + private ActorSnapshot _snapshot = ActorSnapshot.Empty; private Hook? _onInitializeHook; private Hook? _onTerminateHook; @@ -80,16 +61,30 @@ public sealed unsafe class ActorObjectService : IHostedService, IDisposable _framework = framework; _interop = interop; _objectTable = objectTable; - _clientState = clientState; _mediator = mediator; } - public IReadOnlyList PlayerAddresses => Volatile.Read(ref _playerAddressSnapshot); + private ActorSnapshot Snapshot => Volatile.Read(ref _snapshot); + + public IReadOnlyList PlayerAddresses => Snapshot.PlayerAddresses; public IEnumerable PlayerDescriptors => _activePlayers.Values; - public IReadOnlyList PlayerCharacterDescriptors => Volatile.Read(ref _playerCharacterSnapshot); + public IReadOnlyList PlayerCharacterDescriptors => Snapshot.PlayerDescriptors; public bool TryGetActorByHash(string hash, out ActorDescriptor descriptor) => _actorsByHash.TryGetValue(hash, out descriptor); + public bool TryGetValidatedActorByHash(string hash, out ActorDescriptor descriptor) + { + descriptor = default; + if (!_actorsByHash.TryGetValue(hash, out var candidate)) + return false; + + if (!ValidateDescriptorThreadSafe(candidate)) + return false; + + descriptor = candidate; + return true; + } + public bool TryGetPlayerByName(string name, out ActorDescriptor descriptor) { descriptor = default; @@ -100,6 +95,9 @@ public sealed unsafe class ActorObjectService : IHostedService, IDisposable ActorDescriptor? best = null; foreach (var candidate in entries.Values) { + if (!ValidateDescriptorThreadSafe(candidate)) + continue; + if (best is null || IsBetterNameMatch(candidate, best.Value)) { best = candidate; @@ -115,23 +113,54 @@ public sealed unsafe class ActorObjectService : IHostedService, IDisposable return false; } public bool HooksActive => _hooksActive; - public IReadOnlyList RenderedPlayerAddresses => Volatile.Read(ref _renderedPlayerSnapshot); - public IReadOnlyList RenderedCompanionAddresses => Volatile.Read(ref _renderedCompanionSnapshot); - public IReadOnlyList OwnedObjectAddresses => Volatile.Read(ref _ownedObjectSnapshot); - public IReadOnlyDictionary OwnedObjects => Volatile.Read(ref _ownedObjectMapSnapshot); - public nint LocalPlayerAddress => Volatile.Read(ref _localPlayerAddress); - public nint LocalPetAddress => Volatile.Read(ref _localPetAddress); - public nint LocalMinionOrMountAddress => Volatile.Read(ref _localMinionMountAddress); - public nint LocalCompanionAddress => Volatile.Read(ref _localCompanionAddress); + public IReadOnlyList RenderedPlayerAddresses => Snapshot.OwnedObjects.RenderedPlayers; + public IReadOnlyList RenderedCompanionAddresses => Snapshot.OwnedObjects.RenderedCompanions; + public IReadOnlyList OwnedObjectAddresses => Snapshot.OwnedObjects.OwnedAddresses; + public IReadOnlyDictionary OwnedObjects => Snapshot.OwnedObjects.Map; + public nint LocalPlayerAddress => Snapshot.OwnedObjects.LocalPlayer; + public nint LocalPetAddress => Snapshot.OwnedObjects.LocalPet; + public nint LocalMinionOrMountAddress => Snapshot.OwnedObjects.LocalMinionOrMount; + public nint LocalCompanionAddress => Snapshot.OwnedObjects.LocalCompanion; + + public bool TryGetOwnedKind(nint address, out LightlessObjectKind kind) + => OwnedObjects.TryGetValue(address, out kind); + + public bool TryGetOwnedActor(LightlessObjectKind kind, out ActorDescriptor descriptor) + { + descriptor = default; + if (!TryGetOwnedObject(kind, out var address)) + return false; + return TryGetDescriptor(address, out descriptor); + } + + public bool TryGetOwnedObjectByIndex(ushort objectIndex, out LightlessObjectKind ownedKind) + { + ownedKind = default; + var ownedSnapshot = OwnedObjects; + foreach (var (address, kind) in ownedSnapshot) + { + if (!TryGetDescriptor(address, out var descriptor)) + continue; + + if (descriptor.ObjectIndex == objectIndex) + { + ownedKind = kind; + return true; + } + } + + return false; + } public bool TryGetOwnedObject(LightlessObjectKind kind, out nint address) { + var ownedSnapshot = Snapshot.OwnedObjects; address = kind switch { - LightlessObjectKind.Player => Volatile.Read(ref _localPlayerAddress), - LightlessObjectKind.Pet => Volatile.Read(ref _localPetAddress), - LightlessObjectKind.MinionOrMount => Volatile.Read(ref _localMinionMountAddress), - LightlessObjectKind.Companion => Volatile.Read(ref _localCompanionAddress), + LightlessObjectKind.Player => ownedSnapshot.LocalPlayer, + LightlessObjectKind.Pet => ownedSnapshot.LocalPet, + LightlessObjectKind.MinionOrMount => ownedSnapshot.LocalMinionOrMount, + LightlessObjectKind.Companion => ownedSnapshot.LocalCompanion, _ => nint.Zero }; @@ -158,7 +187,7 @@ public sealed unsafe class ActorObjectService : IHostedService, IDisposable public bool TryGetPlayerAddressByHash(string hash, out nint address) { - if (TryGetActorByHash(hash, out var descriptor) && descriptor.Address != nint.Zero) + if (TryGetValidatedActorByHash(hash, out var descriptor) && descriptor.Address != nint.Zero) { address = descriptor.Address; return true; @@ -168,6 +197,50 @@ public sealed unsafe class ActorObjectService : IHostedService, IDisposable return false; } + public async Task WaitForFullyLoadedAsync(nint address, CancellationToken cancellationToken = default) + { + if (address == nint.Zero) + throw new ArgumentException("Address cannot be zero.", nameof(address)); + + while (true) + { + cancellationToken.ThrowIfCancellationRequested(); + + var isLoaded = await _framework.RunOnFrameworkThread(() => IsObjectFullyLoaded(address)).ConfigureAwait(false); + if (isLoaded) + return; + + await Task.Delay(100, cancellationToken).ConfigureAwait(false); + } + } + + private bool ValidateDescriptorThreadSafe(ActorDescriptor descriptor) + { + if (_framework.IsInFrameworkUpdateThread) + return ValidateDescriptorInternal(descriptor); + + return _framework.RunOnFrameworkThread(() => ValidateDescriptorInternal(descriptor)).GetAwaiter().GetResult(); + } + + private bool ValidateDescriptorInternal(ActorDescriptor descriptor) + { + if (descriptor.Address == nint.Zero) + return false; + + if (descriptor.ObjectKind == DalamudObjectKind.Player && + !string.IsNullOrEmpty(descriptor.HashedContentId)) + { + var liveHash = DalamudUtilService.GetHashedCIDFromPlayerPointer(descriptor.Address); + if (!string.Equals(liveHash, descriptor.HashedContentId, StringComparison.Ordinal)) + { + UntrackGameObject(descriptor.Address); + return false; + } + } + + return true; + } + public void RefreshTrackedActors(bool force = false) { var now = DateTime.UtcNow; @@ -185,7 +258,7 @@ public sealed unsafe class ActorObjectService : IHostedService, IDisposable } else { - _framework.RunOnFrameworkThread(RefreshTrackedActorsInternal); + _ = _framework.RunOnFrameworkThread(RefreshTrackedActorsInternal); } } @@ -211,23 +284,12 @@ public sealed unsafe class ActorObjectService : IHostedService, IDisposable _activePlayers.Clear(); _actorsByHash.Clear(); _actorsByName.Clear(); - Volatile.Write(ref _playerCharacterSnapshot, Array.Empty()); - Volatile.Write(ref _playerAddressSnapshot, Array.Empty()); - Volatile.Write(ref _renderedPlayerSnapshot, Array.Empty()); - Volatile.Write(ref _renderedCompanionSnapshot, Array.Empty()); - Volatile.Write(ref _ownedObjectSnapshot, Array.Empty()); - Volatile.Write(ref _ownedObjectMapSnapshot, new Dictionary()); - Volatile.Write(ref _localPlayerAddress, nint.Zero); - Volatile.Write(ref _localPetAddress, nint.Zero); - Volatile.Write(ref _localMinionMountAddress, nint.Zero); - Volatile.Write(ref _localCompanionAddress, nint.Zero); - _renderedPlayers.Clear(); - _renderedCompanions.Clear(); - _ownedObjects.Clear(); + _ownedTracker.Reset(); + Volatile.Write(ref _snapshot, ActorSnapshot.Empty); return Task.CompletedTask; } - private void InitializeHooks() + private unsafe void InitializeHooks() { if (_hooksActive) return; @@ -271,7 +333,7 @@ public sealed unsafe class ActorObjectService : IHostedService, IDisposable }); } - private void OnCharacterInitialized(Character* chara) + private unsafe void OnCharacterInitialized(Character* chara) { try { @@ -285,7 +347,7 @@ public sealed unsafe class ActorObjectService : IHostedService, IDisposable QueueFrameworkUpdate(() => TrackGameObject((GameObject*)chara)); } - private void OnCharacterTerminated(Character* chara) + private unsafe void OnCharacterTerminated(Character* chara) { var address = (nint)chara; QueueFrameworkUpdate(() => UntrackGameObject(address)); @@ -299,7 +361,7 @@ public sealed unsafe class ActorObjectService : IHostedService, IDisposable } } - private GameObject* OnCharacterDisposed(Character* chara, byte freeMemory) + private unsafe GameObject* OnCharacterDisposed(Character* chara, byte freeMemory) { var address = (nint)chara; QueueFrameworkUpdate(() => UntrackGameObject(address)); @@ -314,7 +376,7 @@ public sealed unsafe class ActorObjectService : IHostedService, IDisposable } } - private void TrackGameObject(GameObject* gameObject) + private unsafe void TrackGameObject(GameObject* gameObject) { if (gameObject == null) return; @@ -332,14 +394,10 @@ public sealed unsafe class ActorObjectService : IHostedService, IDisposable if (_activePlayers.TryGetValue(descriptor.Address, out var existing)) { - RemoveDescriptorFromIndexes(existing); - RemoveDescriptorFromCollections(existing); + RemoveDescriptor(existing); } - _activePlayers[descriptor.Address] = descriptor; - IndexDescriptor(descriptor); - AddDescriptorToCollections(descriptor); - RebuildSnapshots(); + AddDescriptor(descriptor); if (_logger.IsEnabled(LogLevel.Debug)) { @@ -355,16 +413,16 @@ public sealed unsafe class ActorObjectService : IHostedService, IDisposable _mediator.Publish(new ActorTrackedMessage(descriptor)); } - private ActorDescriptor? BuildDescriptor(GameObject* gameObject, DalamudObjectKind objectKind) + private unsafe ActorDescriptor? BuildDescriptor(GameObject* gameObject, DalamudObjectKind objectKind) { if (gameObject == null) return null; var address = (nint)gameObject; string name = string.Empty; - ushort objectIndex = (ushort)gameObject->ObjectIndex; + ushort objectIndex = gameObject->ObjectIndex; bool isInGpose = objectIndex >= 200; - bool isLocal = _clientState.LocalPlayer?.Address == address; + bool isLocal = _objectTable.LocalPlayer?.Address == address; string hashedCid = string.Empty; if (_objectTable.CreateObjectReference(address) is IPlayerCharacter playerCharacter) @@ -372,7 +430,7 @@ public sealed unsafe class ActorObjectService : IHostedService, IDisposable name = playerCharacter.Name.TextValue ?? string.Empty; objectIndex = playerCharacter.ObjectIndex; isInGpose = objectIndex >= 200; - isLocal = playerCharacter.Address == _clientState.LocalPlayer?.Address; + isLocal = playerCharacter.Address == _objectTable.LocalPlayer?.Address; } else { @@ -389,7 +447,7 @@ public sealed unsafe class ActorObjectService : IHostedService, IDisposable return new ActorDescriptor(name, hashedCid, address, objectIndex, isLocal, isInGpose, objectKind, ownedKind, ownerEntityId); } - private (LightlessObjectKind? OwnedKind, uint OwnerEntityId) DetermineOwnedKind(GameObject* gameObject, DalamudObjectKind objectKind, bool isLocalPlayer) + private unsafe (LightlessObjectKind? OwnedKind, uint OwnerEntityId) DetermineOwnedKind(GameObject* gameObject, DalamudObjectKind objectKind, bool isLocalPlayer) { if (gameObject == null) return (null, 0); @@ -406,7 +464,7 @@ public sealed unsafe class ActorObjectService : IHostedService, IDisposable return (LightlessObjectKind.Player, entityId); } - if (_clientState.LocalPlayer is not { } localPlayer) + if (_objectTable.LocalPlayer is not { } localPlayer) return (null, 0); var ownerId = gameObject->OwnerId; @@ -453,9 +511,7 @@ public sealed unsafe class ActorObjectService : IHostedService, IDisposable if (_activePlayers.TryRemove(address, out var descriptor)) { - RemoveDescriptorFromIndexes(descriptor); - RemoveDescriptorFromCollections(descriptor); - RebuildSnapshots(); + RemoveDescriptor(descriptor); if (_logger.IsEnabled(LogLevel.Debug)) { _logger.LogDebug("Actor untracked: {Name} addr={Address:X} idx={Index} owned={OwnedKind}", @@ -469,7 +525,7 @@ public sealed unsafe class ActorObjectService : IHostedService, IDisposable } } - private void RefreshTrackedActorsInternal() + private unsafe void RefreshTrackedActorsInternal() { var addresses = EnumerateActiveCharacterAddresses(); HashSet seen = new(addresses.Count); @@ -524,7 +580,10 @@ public sealed unsafe class ActorObjectService : IHostedService, IDisposable return candidate.ObjectIndex < current.ObjectIndex; } - private void OnCompanionInitialized(Companion* companion) + private bool TryGetDescriptor(nint address, out ActorDescriptor descriptor) + => _activePlayers.TryGetValue(address, out descriptor); + + private unsafe void OnCompanionInitialized(Companion* companion) { try { @@ -538,7 +597,7 @@ public sealed unsafe class ActorObjectService : IHostedService, IDisposable QueueFrameworkUpdate(() => TrackGameObject((GameObject*)companion)); } - private void OnCompanionTerminated(Companion* companion) + private unsafe void OnCompanionTerminated(Companion* companion) { var address = (nint)companion; QueueFrameworkUpdate(() => UntrackGameObject(address)); @@ -559,107 +618,46 @@ public sealed unsafe class ActorObjectService : IHostedService, IDisposable _actorsByHash.TryRemove(descriptor.HashedContentId, out _); } - if (descriptor.ObjectKind == DalamudObjectKind.Player && !string.IsNullOrEmpty(descriptor.Name)) + if (descriptor.ObjectKind == DalamudObjectKind.Player + && !string.IsNullOrEmpty(descriptor.Name) + && _actorsByName.TryGetValue(descriptor.Name, out var bucket)) { - if (_actorsByName.TryGetValue(descriptor.Name, out var bucket)) + bucket.TryRemove(descriptor.Address, out _); + if (bucket.IsEmpty) { - bucket.TryRemove(descriptor.Address, out _); - if (bucket.IsEmpty) - { - _actorsByName.TryRemove(descriptor.Name, out _); - } + _actorsByName.TryRemove(descriptor.Name, out _); } } } - private void AddDescriptorToCollections(ActorDescriptor descriptor) + private void AddDescriptor(ActorDescriptor descriptor) { - if (descriptor.ObjectKind == DalamudObjectKind.Player) - { - _renderedPlayers.Add(descriptor.Address); - if (descriptor.IsLocalPlayer) - { - Volatile.Write(ref _localPlayerAddress, descriptor.Address); - } - } - else if (descriptor.ObjectKind == DalamudObjectKind.Companion) - { - _renderedCompanions.Add(descriptor.Address); - } - - if (descriptor.OwnedKind is { } ownedKind) - { - _ownedObjects[descriptor.Address] = ownedKind; - switch (ownedKind) - { - case LightlessObjectKind.Player: - Volatile.Write(ref _localPlayerAddress, descriptor.Address); - break; - case LightlessObjectKind.Pet: - Volatile.Write(ref _localPetAddress, descriptor.Address); - break; - case LightlessObjectKind.MinionOrMount: - Volatile.Write(ref _localMinionMountAddress, descriptor.Address); - break; - case LightlessObjectKind.Companion: - Volatile.Write(ref _localCompanionAddress, descriptor.Address); - break; - } - } + _activePlayers[descriptor.Address] = descriptor; + IndexDescriptor(descriptor); + _ownedTracker.OnDescriptorAdded(descriptor); + PublishSnapshot(); } - private void RemoveDescriptorFromCollections(ActorDescriptor descriptor) + private void RemoveDescriptor(ActorDescriptor descriptor) { - if (descriptor.ObjectKind == DalamudObjectKind.Player) - { - _renderedPlayers.Remove(descriptor.Address); - if (descriptor.IsLocalPlayer && Volatile.Read(ref _localPlayerAddress) == descriptor.Address) - { - Volatile.Write(ref _localPlayerAddress, nint.Zero); - } - } - else if (descriptor.ObjectKind == DalamudObjectKind.Companion) - { - _renderedCompanions.Remove(descriptor.Address); - if (Volatile.Read(ref _localCompanionAddress) == descriptor.Address) - { - Volatile.Write(ref _localCompanionAddress, nint.Zero); - } - } - - if (descriptor.OwnedKind is { } ownedKind) - { - _ownedObjects.Remove(descriptor.Address); - switch (ownedKind) - { - case LightlessObjectKind.Player when Volatile.Read(ref _localPlayerAddress) == descriptor.Address: - Volatile.Write(ref _localPlayerAddress, nint.Zero); - break; - case LightlessObjectKind.Pet when Volatile.Read(ref _localPetAddress) == descriptor.Address: - Volatile.Write(ref _localPetAddress, nint.Zero); - break; - case LightlessObjectKind.MinionOrMount when Volatile.Read(ref _localMinionMountAddress) == descriptor.Address: - Volatile.Write(ref _localMinionMountAddress, nint.Zero); - break; - case LightlessObjectKind.Companion when Volatile.Read(ref _localCompanionAddress) == descriptor.Address: - Volatile.Write(ref _localCompanionAddress, nint.Zero); - break; - } - } + RemoveDescriptorFromIndexes(descriptor); + _ownedTracker.OnDescriptorRemoved(descriptor); + PublishSnapshot(); } - private void RebuildSnapshots() + private void PublishSnapshot() { var playerDescriptors = _activePlayers.Values .Where(descriptor => descriptor.ObjectKind == DalamudObjectKind.Player) .ToArray(); + var playerAddresses = new nint[playerDescriptors.Length]; + for (var i = 0; i < playerDescriptors.Length; i++) + playerAddresses[i] = playerDescriptors[i].Address; - Volatile.Write(ref _playerCharacterSnapshot, playerDescriptors); - Volatile.Write(ref _playerAddressSnapshot, playerDescriptors.Select(d => d.Address).ToArray()); - Volatile.Write(ref _renderedPlayerSnapshot, _renderedPlayers.ToArray()); - Volatile.Write(ref _renderedCompanionSnapshot, _renderedCompanions.ToArray()); - Volatile.Write(ref _ownedObjectSnapshot, _ownedObjects.Keys.ToArray()); - Volatile.Write(ref _ownedObjectMapSnapshot, new Dictionary(_ownedObjects)); + var ownedSnapshot = _ownedTracker.CreateSnapshot(); + var nextGeneration = Snapshot.Generation + 1; + var snapshot = new ActorSnapshot(playerDescriptors, playerAddresses, ownedSnapshot, nextGeneration); + Volatile.Write(ref _snapshot, snapshot); } private void QueueFrameworkUpdate(Action action) @@ -673,7 +671,7 @@ public sealed unsafe class ActorObjectService : IHostedService, IDisposable return; } - _framework.RunOnFrameworkThread(action); + _ = _framework.RunOnFrameworkThread(action); } private void DisposeHooks() @@ -723,7 +721,7 @@ public sealed unsafe class ActorObjectService : IHostedService, IDisposable or DalamudObjectKind.Companion or DalamudObjectKind.MountType; - private static List EnumerateActiveCharacterAddresses() + private static unsafe List EnumerateActiveCharacterAddresses() { var results = new List(64); var manager = GameObjectManager.Instance(); @@ -751,4 +749,170 @@ public sealed unsafe class ActorObjectService : IHostedService, IDisposable return results; } + + private static unsafe bool IsObjectFullyLoaded(nint address) + { + if (address == nint.Zero) + return false; + + var gameObject = (GameObject*)address; + if (gameObject == null) + return false; + + var drawObject = gameObject->DrawObject; + if (drawObject == null) + return false; + + if (gameObject->RenderFlags == 2048) + return false; + + var characterBase = (CharacterBase*)drawObject; + if (characterBase == null) + return false; + + if (characterBase->HasModelInSlotLoaded != 0) + return false; + + if (characterBase->HasModelFilesInSlotLoaded != 0) + return false; + + return true; + } + + private sealed class OwnedObjectTracker + { + private readonly HashSet _renderedPlayers = new(); + private readonly HashSet _renderedCompanions = new(); + private readonly Dictionary _ownedObjects = new(); + private nint _localPlayerAddress = nint.Zero; + private nint _localPetAddress = nint.Zero; + private nint _localMinionMountAddress = nint.Zero; + private nint _localCompanionAddress = nint.Zero; + + public void OnDescriptorAdded(ActorDescriptor descriptor) + { + if (descriptor.ObjectKind == DalamudObjectKind.Player) + { + _renderedPlayers.Add(descriptor.Address); + if (descriptor.IsLocalPlayer) + _localPlayerAddress = descriptor.Address; + } + else if (descriptor.ObjectKind == DalamudObjectKind.Companion) + { + _renderedCompanions.Add(descriptor.Address); + } + + if (descriptor.OwnedKind is { } ownedKind) + { + _ownedObjects[descriptor.Address] = ownedKind; + switch (ownedKind) + { + case LightlessObjectKind.Player: + _localPlayerAddress = descriptor.Address; + break; + case LightlessObjectKind.Pet: + _localPetAddress = descriptor.Address; + break; + case LightlessObjectKind.MinionOrMount: + _localMinionMountAddress = descriptor.Address; + break; + case LightlessObjectKind.Companion: + _localCompanionAddress = descriptor.Address; + break; + } + } + } + + public void OnDescriptorRemoved(ActorDescriptor descriptor) + { + if (descriptor.ObjectKind == DalamudObjectKind.Player) + { + _renderedPlayers.Remove(descriptor.Address); + if (descriptor.IsLocalPlayer && _localPlayerAddress == descriptor.Address) + _localPlayerAddress = nint.Zero; + } + else if (descriptor.ObjectKind == DalamudObjectKind.Companion) + { + _renderedCompanions.Remove(descriptor.Address); + if (_localCompanionAddress == descriptor.Address) + _localCompanionAddress = nint.Zero; + } + + if (descriptor.OwnedKind is { } ownedKind) + { + _ownedObjects.Remove(descriptor.Address); + switch (ownedKind) + { + case LightlessObjectKind.Player when _localPlayerAddress == descriptor.Address: + _localPlayerAddress = nint.Zero; + break; + case LightlessObjectKind.Pet when _localPetAddress == descriptor.Address: + _localPetAddress = nint.Zero; + break; + case LightlessObjectKind.MinionOrMount when _localMinionMountAddress == descriptor.Address: + _localMinionMountAddress = nint.Zero; + break; + case LightlessObjectKind.Companion when _localCompanionAddress == descriptor.Address: + _localCompanionAddress = nint.Zero; + break; + } + } + } + + public OwnedObjectSnapshot CreateSnapshot() + => new( + _renderedPlayers.ToArray(), + _renderedCompanions.ToArray(), + _ownedObjects.Keys.ToArray(), + new Dictionary(_ownedObjects), + _localPlayerAddress, + _localPetAddress, + _localMinionMountAddress, + _localCompanionAddress); + + public void Reset() + { + _renderedPlayers.Clear(); + _renderedCompanions.Clear(); + _ownedObjects.Clear(); + _localPlayerAddress = nint.Zero; + _localPetAddress = nint.Zero; + _localMinionMountAddress = nint.Zero; + _localCompanionAddress = nint.Zero; + } + } + + private sealed record OwnedObjectSnapshot( + IReadOnlyList RenderedPlayers, + IReadOnlyList RenderedCompanions, + IReadOnlyList OwnedAddresses, + IReadOnlyDictionary Map, + nint LocalPlayer, + nint LocalPet, + nint LocalMinionOrMount, + nint LocalCompanion) + { + public static OwnedObjectSnapshot Empty { get; } = new( + Array.Empty(), + Array.Empty(), + Array.Empty(), + new Dictionary(), + nint.Zero, + nint.Zero, + nint.Zero, + nint.Zero); + } + + private sealed record ActorSnapshot( + IReadOnlyList PlayerDescriptors, + IReadOnlyList PlayerAddresses, + OwnedObjectSnapshot OwnedObjects, + int Generation) + { + public static ActorSnapshot Empty { get; } = new( + Array.Empty(), + Array.Empty(), + OwnedObjectSnapshot.Empty, + 0); + } } diff --git a/LightlessSync/Services/Chat/ZoneChatService.cs b/LightlessSync/Services/Chat/ZoneChatService.cs index 9126436..edb3a86 100644 --- a/LightlessSync/Services/Chat/ZoneChatService.cs +++ b/LightlessSync/Services/Chat/ZoneChatService.cs @@ -1,4 +1,3 @@ -using LightlessSync.API.Dto; using LightlessSync.API.Dto.Chat; using LightlessSync.Services.ActorTracking; using LightlessSync.Services.Mediator; @@ -21,12 +20,11 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS private const int MaxReportContextLength = 1000; private readonly ApiController _apiController; - private readonly ChatConfigService _chatConfigService; private readonly DalamudUtilService _dalamudUtilService; private readonly ActorObjectService _actorObjectService; private readonly PairUiService _pairUiService; - private readonly object _sync = new(); + private readonly Lock _sync = new(); private readonly Dictionary _channels = new(StringComparer.Ordinal); private readonly List _channelOrder = new(); @@ -55,7 +53,6 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS PairUiService pairUiService) : base(logger, mediator) { - _chatConfigService = chatConfigService; _apiController = apiController; _dalamudUtilService = dalamudUtilService; _actorObjectService = actorObjectService; @@ -63,12 +60,12 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS _isLoggedIn = _dalamudUtilService.IsLoggedIn; _isConnected = _apiController.IsConnected; - _chatEnabled = _chatConfigService.Current.AutoEnableChatOnLogin; + _chatEnabled = chatConfigService.Current.AutoEnableChatOnLogin; } public IReadOnlyList GetChannelsSnapshot() { - lock (_sync) + using (_sync.EnterScope()) { var snapshots = new List(_channelOrder.Count); foreach (var key in _channelOrder) @@ -107,7 +104,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS { get { - lock (_sync) + using (_sync.EnterScope()) { return _chatEnabled; } @@ -118,7 +115,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS { get { - lock (_sync) + using (_sync.EnterScope()) { return _chatEnabled && _isConnected; } @@ -127,7 +124,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS public void SetActiveChannel(string? key) { - lock (_sync) + using (_sync.EnterScope()) { _activeChannelKey = key; if (key is not null && _channels.TryGetValue(key, out var state)) @@ -145,7 +142,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS private async Task EnableChatAsync() { bool wasEnabled; - lock (_sync) + using (_sync.EnterScope()) { wasEnabled = _chatEnabled; if (!wasEnabled) @@ -170,7 +167,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS List groupDescriptors; ChatChannelDescriptor? zoneDescriptor; - lock (_sync) + using (_sync.EnterScope()) { wasEnabled = _chatEnabled; if (!wasEnabled) @@ -259,7 +256,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS return Task.FromResult(new ChatReportResult(false, "Please describe why you are reporting this message.")); } - lock (_sync) + using (_sync.EnterScope()) { if (!_chatEnabled) { @@ -311,8 +308,8 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS Mediator.Subscribe(this, _ => HandleLogout()); Mediator.Subscribe(this, _ => ScheduleZonePresenceUpdate()); Mediator.Subscribe(this, _ => ScheduleZonePresenceUpdate(force: true)); - Mediator.Subscribe(this, msg => HandleConnected(msg.Connection)); - Mediator.Subscribe(this, _ => HandleConnected(null)); + Mediator.Subscribe(this, _ => HandleConnected()); + Mediator.Subscribe(this, _ => HandleConnected()); Mediator.Subscribe(this, _ => HandleReconnecting()); Mediator.Subscribe(this, _ => HandleReconnecting()); Mediator.Subscribe(this, _ => RefreshGroupsFromPairManager()); @@ -371,11 +368,11 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS } } - private void HandleConnected(ConnectionDto? connection) + private void HandleConnected() { _isConnected = true; - lock (_sync) + using (_sync.EnterScope()) { _selfTokens.Clear(); _pendingSelfMessages.Clear(); @@ -410,7 +407,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS { _isConnected = false; - lock (_sync) + using (_sync.EnterScope()) { _selfTokens.Clear(); _pendingSelfMessages.Clear(); @@ -475,7 +472,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS private void UpdateChannelsForDisabledState() { - lock (_sync) + using (_sync.EnterScope()) { foreach (var state in _channels.Values) { @@ -513,7 +510,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS string? zoneKey; ZoneChannelDefinition? definition = null; - lock (_sync) + using (_sync.EnterScope()) { _territoryToZoneKey.TryGetValue(territoryId, out zoneKey); if (zoneKey is not null) @@ -538,7 +535,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS bool shouldForceSend; - lock (_sync) + using (_sync.EnterScope()) { var state = EnsureZoneStateLocked(); state.DisplayName = definition.Value.DisplayName; @@ -566,7 +563,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS ChatChannelDescriptor? descriptor = null; bool clearedHistory = false; - lock (_sync) + using (_sync.EnterScope()) { descriptor = _lastZoneDescriptor; _lastZoneDescriptor = null; @@ -590,9 +587,9 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS state.DisplayName = "Zone Chat"; } - if (_activeChannelKey == ZoneChannelKey) + if (string.Equals(_activeChannelKey, ZoneChannelKey, StringComparison.Ordinal)) { - _activeChannelKey = _channelOrder.FirstOrDefault(key => key != ZoneChannelKey); + _activeChannelKey = _channelOrder.FirstOrDefault(key => !string.Equals(key, ZoneChannelKey, StringComparison.Ordinal)); } } @@ -627,7 +624,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS { var infoList = infos ?? Array.Empty(); - lock (_sync) + using (_sync.EnterScope()) { _zoneDefinitions.Clear(); _territoryToZoneKey.Clear(); @@ -657,7 +654,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS { if (def.TerritoryNames.Contains(variant)) { - _territoryToZoneKey[(uint)kvp.Key] = def.Key; + _territoryToZoneKey[kvp.Key] = def.Key; break; } } @@ -689,7 +686,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS var descriptorsToJoin = new List(); var descriptorsToLeave = new List(); - lock (_sync) + using (_sync.EnterScope()) { var remainingGroups = new HashSet(_groupDefinitions.Keys, StringComparer.OrdinalIgnoreCase); @@ -807,7 +804,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS return; List descriptors; - lock (_sync) + using (_sync.EnterScope()) { descriptors = _channels.Values .Where(state => state.Type == ChatChannelType.Group) @@ -832,7 +829,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS var presenceKey = BuildPresenceKey(descriptor); bool stateMatches; - lock (_sync) + using (_sync.EnterScope()) { stateMatches = !force && _lastPresenceStates.TryGetValue(presenceKey, out var lastState) @@ -846,7 +843,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS { await _apiController.UpdateChatPresence(new ChatPresenceUpdateDto(descriptor, territoryId, isActive)).ConfigureAwait(false); - lock (_sync) + using (_sync.EnterScope()) { if (isActive) { @@ -870,7 +867,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS var key = normalized.Type == ChatChannelType.Zone ? ZoneChannelKey : BuildChannelKey(normalized); var pending = new PendingSelfMessage(key, message); - lock (_sync) + using (_sync.EnterScope()) { _pendingSelfMessages.Add(pending); while (_pendingSelfMessages.Count > 20) @@ -884,7 +881,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS private void RemovePendingSelfMessage(PendingSelfMessage pending) { - lock (_sync) + using (_sync.EnterScope()) { var index = _pendingSelfMessages.FindIndex(p => string.Equals(p.ChannelKey, pending.ChannelKey, StringComparison.Ordinal) && @@ -905,7 +902,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS var message = BuildMessage(dto, fromSelf); bool publishChannelList = false; - lock (_sync) + using (_sync.EnterScope()) { if (!_channels.TryGetValue(key, out var state)) { @@ -960,7 +957,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS if (publishChannelList) { - lock (_sync) + using (_sync.EnterScope()) { UpdateChannelOrderLocked(); } @@ -973,7 +970,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS { if (dto.Sender.User?.UID is { } uid && string.Equals(uid, _apiController.UID, StringComparison.Ordinal)) { - lock (_sync) + using (_sync.EnterScope()) { _selfTokens[channelKey] = dto.Sender.Token; } @@ -981,7 +978,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS return true; } - lock (_sync) + using (_sync.EnterScope()) { if (_selfTokens.TryGetValue(channelKey, out var token) && string.Equals(token, dto.Sender.Token, StringComparison.Ordinal)) @@ -1014,7 +1011,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS { var isZone = dto.Channel.Type == ChatChannelType.Zone; if (!string.IsNullOrEmpty(dto.Sender.HashedCid) && - _actorObjectService.TryGetActorByHash(dto.Sender.HashedCid, out var descriptor) && + _actorObjectService.TryGetValidatedActorByHash(dto.Sender.HashedCid, out var descriptor) && !string.IsNullOrWhiteSpace(descriptor.Name)) { return descriptor.Name; @@ -1065,7 +1062,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS { _activeChannelKey = _channelOrder[0]; } - else if (_activeChannelKey is not null && !_channelOrder.Contains(_activeChannelKey)) + else if (_activeChannelKey is not null && !_channelOrder.Contains(_activeChannelKey, StringComparer.Ordinal)) { _activeChannelKey = _channelOrder.Count > 0 ? _channelOrder[0] : null; } @@ -1108,7 +1105,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS private static bool ChannelDescriptorsMatch(ChatChannelDescriptor left, ChatChannelDescriptor right) => left.Type == right.Type - && NormalizeKey(left.CustomKey) == NormalizeKey(right.CustomKey) + && string.Equals(NormalizeKey(left.CustomKey), NormalizeKey(right.CustomKey), StringComparison.Ordinal) && left.WorldId == right.WorldId; private ChatChannelState EnsureZoneStateLocked() @@ -1180,6 +1177,3 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS private readonly record struct PendingSelfMessage(string ChannelKey, string Message); } - - - diff --git a/LightlessSync/Services/DalamudUtilService.cs b/LightlessSync/Services/DalamudUtilService.cs index 1bbef90..fdf2ec3 100644 --- a/LightlessSync/Services/DalamudUtilService.cs +++ b/LightlessSync/Services/DalamudUtilService.cs @@ -360,7 +360,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber public IntPtr GetPlayerCharacterFromCachedTableByIdent(string characterName) { - if (_actorObjectService.TryGetActorByHash(characterName, out var actor)) + if (_actorObjectService.TryGetValidatedActorByHash(characterName, out var actor)) return actor.Address; return IntPtr.Zero; } @@ -639,7 +639,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber internal (string Name, nint Address) FindPlayerByNameHash(string ident) { - if (_actorObjectService.TryGetActorByHash(ident, out var descriptor)) + if (_actorObjectService.TryGetValidatedActorByHash(ident, out var descriptor)) { return (descriptor.Name, descriptor.Address); } diff --git a/LightlessSync/Services/LightFinder/LightFinderPlateHandler.cs b/LightlessSync/Services/LightFinder/LightFinderPlateHandler.cs index 8002beb..544ada1 100644 --- a/LightlessSync/Services/LightFinder/LightFinderPlateHandler.cs +++ b/LightlessSync/Services/LightFinder/LightFinderPlateHandler.cs @@ -1,249 +1,692 @@ -using Dalamud.Bindings.ImGui; -using Dalamud.Game.ClientState.Objects.SubKinds; -using Dalamud.Game.ClientState.Objects.Types; +using Dalamud.Bindings.ImGui; +using Dalamud.Game.Addon.Lifecycle; +using Dalamud.Game.Addon.Lifecycle.AddonArgTypes; +using Dalamud.Game.ClientState.Objects.Enums; +using Dalamud.Game.Text; using Dalamud.Interface; using Dalamud.Plugin; using Dalamud.Plugin.Services; -using FFXIVClientStructs.FFXIV.Client.Game.Object; +using FFXIVClientStructs.FFXIV.Client.System.Framework; +using FFXIVClientStructs.FFXIV.Client.UI; +using FFXIVClientStructs.FFXIV.Component.GUI; using LightlessSync.LightlessConfiguration; using LightlessSync.Services.Mediator; +using LightlessSync.Services.Rendering; using LightlessSync.UI; -using Microsoft.Extensions.Hosting; +using LightlessSync.UI.Services; +using LightlessSync.Utils; +using LightlessSync.UtilsEnum.Enum; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Hosting; +using Pictomancy; using System.Collections.Immutable; +using System.Globalization; using System.Numerics; +using Task = System.Threading.Tasks.Task; -namespace LightlessSync.Services.LightFinder +namespace LightlessSync.Services.LightFinder; + +public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscriber { - public class LightFinderPlateHandler : IHostedService, IMediatorSubscriber + private readonly ILogger _logger; + private readonly IAddonLifecycle _addonLifecycle; + private readonly IGameGui _gameGui; + private readonly IObjectTable _objectTable; + private readonly LightlessConfigService _configService; + private readonly PairUiService _pairUiService; + private readonly LightlessMediator _mediator; + public LightlessMediator Mediator => _mediator; + + private readonly IUiBuilder _uiBuilder; + private bool _mEnabled; + private bool _needsLabelRefresh; + private bool _drawSubscribed; + private AddonNamePlate* _mpNameplateAddon; + private readonly object _labelLock = new(); + private readonly NameplateBuffers _buffers = new(); + private int _labelRenderCount; + + private const string DefaultLabelText = "LightFinder"; + private const SeIconChar DefaultIcon = SeIconChar.Hyadelyn; + private static readonly string DefaultIconGlyph = SeIconCharExtensions.ToIconString(DefaultIcon); + private static readonly Vector2 DefaultPivot = new(0.5f, 1f); + + private ImmutableHashSet _activeBroadcastingCids = []; + + public LightFinderPlateHandler( + ILogger logger, + IAddonLifecycle addonLifecycle, + IGameGui gameGui, + LightlessConfigService configService, + LightlessMediator mediator, + IObjectTable objectTable, + PairUiService pairUiService, + IDalamudPluginInterface pluginInterface, + PictomancyService pictomancyService) { - private readonly ILogger _logger; - private readonly LightlessConfigService _configService; - private readonly IDalamudPluginInterface _pluginInterface; - private readonly IObjectTable _gameObjects; - private readonly IGameGui _gameGui; + _logger = logger; + _addonLifecycle = addonLifecycle; + _gameGui = gameGui; + _configService = configService; + _mediator = mediator; + _objectTable = objectTable; + _pairUiService = pairUiService; + _uiBuilder = pluginInterface.UiBuilder ?? throw new ArgumentNullException(nameof(pluginInterface)); + _ = pictomancyService ?? throw new ArgumentNullException(nameof(pictomancyService)); - private const float _defaultNameplateDistance = 15.0f; - private ImmutableHashSet _activeBroadcastingCids = []; - private readonly Dictionary _smoothed = []; - private readonly float _defaultHeightOffset = 0f; + } - public LightlessMediator Mediator { get; } - - public LightFinderPlateHandler( - ILogger logger, - LightlessMediator mediator, - IDalamudPluginInterface dalamudPluginInterface, - LightlessConfigService configService, - IObjectTable gameObjects, - IGameGui gameGui) + internal void Init() + { + if (!_drawSubscribed) { - _logger = logger; - Mediator = mediator; - _pluginInterface = dalamudPluginInterface; - _configService = configService; - _gameObjects = gameObjects; - _gameGui = gameGui; + _uiBuilder.Draw += OnUiBuilderDraw; + _drawSubscribed = true; } - public Task StartAsync(CancellationToken cancellationToken) + EnableNameplate(); + _mediator.Subscribe(this, OnTick); + } + + internal void Uninit() + { + DisableNameplate(); + if (_drawSubscribed) { - _logger.LogInformation("Starting LightFinderPlateHandler..."); - - _pluginInterface.UiBuilder.Draw += OnDraw; - - _logger.LogInformation("LightFinderPlateHandler started."); - return Task.CompletedTask; + _uiBuilder.Draw -= OnUiBuilderDraw; + _drawSubscribed = false; } + ClearLabelBuffer(); + _mediator.Unsubscribe(this); + _mpNameplateAddon = null; + } - public Task StopAsync(CancellationToken cancellationToken) + internal void EnableNameplate() + { + if (!_mEnabled) { - _logger.LogInformation("Stopping LightFinderPlateHandler..."); - - _pluginInterface.UiBuilder.Draw -= OnDraw; - - _logger.LogInformation("LightFinderPlateHandler stopped."); - return Task.CompletedTask; - } - - private unsafe void OnDraw() - { - if (!_configService.Current.BroadcastEnabled) - return; - - if (_activeBroadcastingCids.Count == 0) - return; - - var drawList = ImGui.GetForegroundDrawList(); - - foreach (var obj in _gameObjects.PlayerObjects.OfType()) + try { - //Double check to be sure, should always be true due to OfType filter above - if (obj is not IPlayerCharacter player) - continue; - - if (player.Address == IntPtr.Zero) - continue; - - var hashedCID = DalamudUtilService.GetHashedCIDFromPlayerPointer(player.Address); - if (!_activeBroadcastingCids.Contains(hashedCID)) - continue; - - //Approximate check if nameplate should be visible (at short distances) - if (!ShouldApproximateNameplateVisible(player)) - continue; - - if (!TryGetApproxNameplateScreenPos(player, out var rawScreenPos)) - continue; - - var rawVector3 = new Vector3(rawScreenPos.X, rawScreenPos.Y, 0f); - - if (rawVector3 == Vector3.Zero) - { - _smoothed.Remove(obj); - continue; - } - - //Possible have to rework this. Currently just a simple distance check to avoid jitter. - Vector3 smoothedVector3; - - if (_smoothed.TryGetValue(obj, out var lastVector3)) - { - var deltaVector2 = new Vector2(rawVector3.X - lastVector3.X, rawVector3.Y - lastVector3.Y); - if (deltaVector2.Length() < 1f) - smoothedVector3 = lastVector3; - else - smoothedVector3 = rawVector3; - } - else - { - smoothedVector3 = rawVector3; - } - - _smoothed[obj] = smoothedVector3; - - var screenPos = new Vector2(smoothedVector3.X, smoothedVector3.Y); - - var radiusWorld = Math.Max(player.HitboxRadius, 0.5f); - var radiusPx = radiusWorld * 8.0f; - var offsetPx = GetScreenOffset(player); - var drawPos = new Vector2(screenPos.X, screenPos.Y - offsetPx); - - var fillColor = ImGui.GetColorU32(UiSharedService.Color(UIColors.Get("Lightfinder"))); - var outlineColor = ImGui.GetColorU32(UiSharedService.Color(UIColors.Get("LightfinderEdge"))); - - drawList.AddCircleFilled(drawPos, radiusPx, fillColor); - drawList.AddCircle(drawPos, radiusPx, outlineColor, 0, 2.0f); - - var label = "LightFinder"; - var icon = FontAwesomeIcon.Bullseye.ToIconString(); - - ImGui.PushFont(UiBuilder.IconFont); - var iconSize = ImGui.CalcTextSize(icon); - var iconPos = new Vector2(drawPos.X - iconSize.X / 2f, drawPos.Y - radiusPx - iconSize.Y - 2f); - drawList.AddText(iconPos, fillColor, icon); - ImGui.PopFont(); - - /* var scale = 1.4f; - var font = ImGui.GetFont(); - var baseFontSize = ImGui.GetFontSize(); - var fontSize = baseFontSize * scale; - - var baseTextSize = ImGui.CalcTextSize(label); - var textSize = baseTextSize * scale; - - var textPos = new Vector2( - drawPos.X - textSize.X / 2f, - drawPos.Y - radiusPx - textSize.Y - 2f - ); - - drawList.AddText(font, fontSize, textPos, fillColor, label); */ + _addonLifecycle.RegisterListener(AddonEvent.PostDraw, "NamePlate", NameplateDrawDetour); + _mEnabled = true; + } + catch (Exception e) + { + _logger.LogError(e, "Unknown error while trying to enable nameplate."); + DisableNameplate(); } } + } - // Get screen offset based on distance to local player (to scale size appropriately) - // I need to fine tune these values still - private float GetScreenOffset(IPlayerCharacter player) + internal void DisableNameplate() + { + if (_mEnabled) { - var local = _gameObjects.LocalPlayer; - if (local == null) - return 32.1f; + try + { + _addonLifecycle.UnregisterListener(NameplateDrawDetour); + } + catch (Exception e) + { + _logger.LogError(e, "Unknown error while unregistering nameplate listener."); + } - var delta = player.Position - local.Position; - var dist = MathF.Sqrt(delta.X * delta.X + delta.Z * delta.Z); + _mEnabled = false; + ClearNameplateCaches(); + } + } - const float minDist = 2.1f; - const float maxDist = 30.4f; - dist = Math.Clamp(dist, minDist, maxDist); - - var t = 1f - (dist - minDist) / (maxDist - minDist); - - const float minOffset = 24.4f; - const float maxOffset = 56.4f; - return minOffset + (maxOffset - minOffset) * t; + private void NameplateDrawDetour(AddonEvent type, AddonArgs args) + { + if (args.Addon.Address == nint.Zero) + { + if (_logger.IsEnabled(LogLevel.Warning)) + _logger.LogWarning("Nameplate draw detour received a null addon address, skipping update."); + return; } - private bool TryGetApproxNameplateScreenPos(IPlayerCharacter player, out Vector2 screenPos) + var pNameplateAddon = (AddonNamePlate*)args.Addon.Address; + + if (_mpNameplateAddon != pNameplateAddon) { - screenPos = default; + ClearNameplateCaches(); + _mpNameplateAddon = pNameplateAddon; + } - var worldPos = player.Position; + UpdateNameplateNodes(); + } - var visualHeight = GetVisualHeight(player); + private void UpdateNameplateNodes() + { + var currentHandle = _gameGui.GetAddonByName("NamePlate"); + if (currentHandle.Address == nint.Zero) + { + if (_logger.IsEnabled(LogLevel.Debug)) + _logger.LogDebug("NamePlate addon unavailable during update, skipping label refresh."); + ClearLabelBuffer(); + return; + } - worldPos.Y += (visualHeight + 1.2f) + _defaultHeightOffset; + var currentAddon = (AddonNamePlate*)currentHandle.Address; + if (_mpNameplateAddon == null || currentAddon == null || currentAddon != _mpNameplateAddon) + { + if (_mpNameplateAddon != null && _logger.IsEnabled(LogLevel.Debug)) + _logger.LogDebug("Cached NamePlate addon pointer differs from current: waiting for new hook (cached {Cached}, current {Current}).", (IntPtr)_mpNameplateAddon, (IntPtr)currentAddon); + return; + } - if (!_gameGui.WorldToScreen(worldPos, out var raw)) - return false; + var framework = Framework.Instance(); + if (framework == null) + { + if (_logger.IsEnabled(LogLevel.Debug)) + _logger.LogDebug("Framework instance unavailable during nameplate update, skipping."); + return; + } - screenPos = raw; + var uiModule = framework->GetUIModule(); + if (uiModule == null) + { + if (_logger.IsEnabled(LogLevel.Debug)) + _logger.LogDebug("UI module unavailable during nameplate update, skipping."); + return; + } + + var ui3DModule = uiModule->GetUI3DModule(); + if (ui3DModule == null) + { + if (_logger.IsEnabled(LogLevel.Debug)) + _logger.LogDebug("UI3D module unavailable during nameplate update, skipping."); + return; + } + + var vec = ui3DModule->NamePlateObjectInfoPointers; + if (vec.IsEmpty) + { + ClearLabelBuffer(); + return; + } + + var visibleUserIdsSnapshot = VisibleUserIds; + var safeCount = System.Math.Min(ui3DModule->NamePlateObjectInfoCount, vec.Length); + var currentConfig = _configService.Current; + var labelColor = UIColors.Get("Lightfinder"); + var edgeColor = UIColors.Get("LightfinderEdge"); + var scratchCount = 0; + + for (int i = 0; i < safeCount; ++i) + { + var objectInfoPtr = vec[i]; + if (objectInfoPtr == null) + continue; + + var objectInfo = objectInfoPtr.Value; + if (objectInfo == null || objectInfo->GameObject == null) + continue; + + var nameplateIndex = objectInfo->NamePlateIndex; + if (nameplateIndex < 0 || nameplateIndex >= AddonNamePlate.NumNamePlateObjects) + continue; + + var gameObject = objectInfo->GameObject; + if ((ObjectKind)gameObject->ObjectKind != ObjectKind.Player) + continue; + + // CID gating + var cid = DalamudUtilService.GetHashedCIDFromPlayerPointer((nint)gameObject); + if (cid == null || !_activeBroadcastingCids.Contains(cid)) + continue; + + var local = _objectTable.LocalPlayer; + if (!currentConfig.LightfinderLabelShowOwn && local != null && + objectInfo->GameObject->GetGameObjectId() == local.GameObjectId) + continue; + + var hidePaired = !currentConfig.LightfinderLabelShowPaired; + var goId = gameObject->GetGameObjectId(); + if (hidePaired && visibleUserIdsSnapshot.Contains(goId)) + continue; + + var nameplateObject = _mpNameplateAddon->NamePlateObjectArray[nameplateIndex]; + var root = nameplateObject.RootComponentNode; + var nameContainer = nameplateObject.NameContainer; + var nameText = nameplateObject.NameText; + var marker = nameplateObject.MarkerIcon; + + if (root == null || root->Component == null || nameContainer == null || nameText == null) + { + if (_logger.IsEnabled(LogLevel.Debug)) + _logger.LogDebug("Nameplate {Index} missing required nodes during update, skipping.", nameplateIndex); + continue; + } + + root->Component->UldManager.UpdateDrawNodeList(); + + bool isVisible = + (marker != null && marker->AtkResNode.IsVisible()) || + (nameContainer->IsVisible() && nameText->AtkResNode.IsVisible()) || + currentConfig.LightfinderLabelShowHidden; + + if (!isVisible) + continue; + + var scaleMultiplier = System.Math.Clamp(currentConfig.LightfinderLabelScale, 0.5f, 2.0f); + var baseScale = currentConfig.LightfinderLabelUseIcon ? 1.0f : 0.5f; + var effectiveScale = baseScale * scaleMultiplier; + var baseFontSize = currentConfig.LightfinderLabelUseIcon ? 36f : 24f; + var targetFontSize = (int)System.Math.Round(baseFontSize * scaleMultiplier); + var labelContent = currentConfig.LightfinderLabelUseIcon + ? NormalizeIconGlyph(currentConfig.LightfinderLabelIconGlyph) + : DefaultLabelText; + + if (!currentConfig.LightfinderLabelUseIcon && (string.IsNullOrWhiteSpace(labelContent) || string.Equals(labelContent, "-", StringComparison.Ordinal))) + labelContent = DefaultLabelText; + + var nodeWidth = (int)System.Math.Round(AtkNodeHelpers.DefaultTextNodeWidth * effectiveScale); + var nodeHeight = (int)System.Math.Round(AtkNodeHelpers.DefaultTextNodeHeight * effectiveScale); + AlignmentType alignment; + + var textScaleY = nameText->AtkResNode.ScaleY; + if (textScaleY <= 0f) + textScaleY = 1f; + + var blockHeight = ResolveCache( + _buffers.TextHeights, + nameplateIndex, + System.Math.Abs((int)nameplateObject.TextH), + () => GetScaledTextHeight(nameText), + nodeHeight); + + var containerHeight = ResolveCache( + _buffers.ContainerHeights, + nameplateIndex, + (int)nameContainer->Height, + () => + { + var computed = blockHeight + (int)System.Math.Round(8 * textScaleY); + return computed <= blockHeight ? blockHeight + 1 : computed; + }, + blockHeight + 1); + + var blockTop = containerHeight - blockHeight; + if (blockTop < 0) + blockTop = 0; + var verticalPadding = (int)System.Math.Round(4 * effectiveScale); + + var positionY = blockTop - verticalPadding; + + var rawTextWidth = (int)nameplateObject.TextW; + var textWidth = ResolveCache( + _buffers.TextWidths, + nameplateIndex, + System.Math.Abs(rawTextWidth), + () => GetScaledTextWidth(nameText), + nodeWidth); + + var textOffset = (int)System.Math.Round(nameText->AtkResNode.X); + var hasValidOffset = TryCacheTextOffset(nameplateIndex, rawTextWidth, textOffset); + + if (nameContainer == null) + { + if (_logger.IsEnabled(LogLevel.Debug)) + _logger.LogDebug("Nameplate {Index} container became unavailable during update, skipping.", nameplateIndex); + continue; + } + + float finalX; + if (currentConfig.LightfinderAutoAlign) + { + var measuredWidth = System.Math.Max(1, textWidth > 0 ? textWidth : nodeWidth); + var measuredWidthF = (float)measuredWidth; + var alignmentType = currentConfig.LabelAlignment; + + var containerScale = nameContainer->ScaleX; + if (containerScale <= 0f) + containerScale = 1f; + var containerWidthRaw = (float)nameContainer->Width; + if (containerWidthRaw <= 0f) + containerWidthRaw = measuredWidthF; + var containerWidth = containerWidthRaw * containerScale; + if (containerWidth <= 0f) + containerWidth = measuredWidthF; + + var containerLeft = nameContainer->ScreenX; + var containerRight = containerLeft + containerWidth; + var containerCenter = containerLeft + (containerWidth * 0.5f); + + var iconMargin = currentConfig.LightfinderLabelUseIcon + ? System.Math.Min(containerWidth * 0.1f, 14f * containerScale) + : 0f; + + switch (alignmentType) + { + case LabelAlignment.Left: + finalX = containerLeft + iconMargin; + alignment = AlignmentType.BottomLeft; + break; + case LabelAlignment.Right: + finalX = containerRight - iconMargin; + alignment = AlignmentType.BottomRight; + break; + default: + finalX = containerCenter; + alignment = AlignmentType.Bottom; + break; + } + + finalX += currentConfig.LightfinderLabelOffsetX; + } + else + { + var cachedTextOffset = _buffers.TextOffsets[nameplateIndex]; + var hasCachedOffset = cachedTextOffset != int.MinValue; + var baseOffsetX = (!currentConfig.LightfinderLabelUseIcon && hasValidOffset && hasCachedOffset) ? cachedTextOffset : 0; + finalX = nameContainer->ScreenX + baseOffsetX + 58 + currentConfig.LightfinderLabelOffsetX; + alignment = AlignmentType.Bottom; + } + + positionY += currentConfig.LightfinderLabelOffsetY; + alignment = (AlignmentType)System.Math.Clamp((int)alignment, 0, 8); + + var finalPosition = new Vector2(finalX, nameContainer->ScreenY + positionY); + var pivot = (currentConfig.LightfinderAutoAlign || currentConfig.LightfinderLabelUseIcon) + ? AlignmentToPivot(alignment) + : DefaultPivot; + var textColorPacked = PackColor(labelColor); + var edgeColorPacked = PackColor(edgeColor); + + _buffers.LabelScratch[scratchCount++] = new NameplateLabelInfo( + finalPosition, + labelContent, + textColorPacked, + edgeColorPacked, + targetFontSize, + pivot, + currentConfig.LightfinderLabelUseIcon); + } + + lock (_labelLock) + { + if (scratchCount == 0) + { + _labelRenderCount = 0; + } + else + { + Array.Copy(_buffers.LabelScratch, _buffers.LabelRender, scratchCount); + _labelRenderCount = scratchCount; + } + } + } + + private void OnUiBuilderDraw() + { + if (!_mEnabled) + return; + + int copyCount; + lock (_labelLock) + { + copyCount = _labelRenderCount; + if (copyCount == 0) + return; + + Array.Copy(_buffers.LabelRender, _buffers.LabelCopy, copyCount); + } + + using var drawList = PictoService.Draw(); + if (drawList == null) + return; + + for (int i = 0; i < copyCount; ++i) + { + ref var info = ref _buffers.LabelCopy[i]; + var font = default(ImFontPtr); + if (info.UseIcon) + { + var ioFonts = ImGui.GetIO().Fonts; + font = ioFonts.Fonts.Size > 1 ? new ImFontPtr(ioFonts.Fonts[1]) : ImGui.GetFont(); + } + + drawList.AddScreenText(info.ScreenPosition, info.Text, info.TextColor, info.FontSize, info.Pivot, info.EdgeColor, font); + } + } + + private static Vector2 AlignmentToPivot(AlignmentType alignment) => alignment switch + { + AlignmentType.BottomLeft => new Vector2(0f, 1f), + AlignmentType.BottomRight => new Vector2(1f, 1f), + AlignmentType.TopLeft => new Vector2(0f, 0f), + AlignmentType.TopRight => new Vector2(1f, 0f), + AlignmentType.Top => new Vector2(0.5f, 0f), + AlignmentType.Left => new Vector2(0f, 0.5f), + AlignmentType.Right => new Vector2(1f, 0.5f), + _ => DefaultPivot + }; + + private static uint PackColor(Vector4 color) + { + var r = (byte)System.Math.Clamp(color.X * 255f, 0f, 255f); + var g = (byte)System.Math.Clamp(color.Y * 255f, 0f, 255f); + var b = (byte)System.Math.Clamp(color.Z * 255f, 0f, 255f); + var a = (byte)System.Math.Clamp(color.W * 255f, 0f, 255f); + return (uint)((a << 24) | (b << 16) | (g << 8) | r); + } + + private void ClearLabelBuffer() + { + lock (_labelLock) + { + _labelRenderCount = 0; + } + } + + private static unsafe int GetScaledTextHeight(AtkTextNode* node) + { + if (node == null) + return 0; + + var resNode = &node->AtkResNode; + var rawHeight = (int)resNode->GetHeight(); + if (rawHeight <= 0 && node->LineSpacing > 0) + rawHeight = node->LineSpacing; + if (rawHeight <= 0) + rawHeight = AtkNodeHelpers.DefaultTextNodeHeight; + + var scale = resNode->ScaleY; + if (scale <= 0f) + scale = 1f; + + var computed = (int)System.Math.Round(rawHeight * scale); + return System.Math.Max(1, computed); + } + + private static unsafe int GetScaledTextWidth(AtkTextNode* node) + { + if (node == null) + return 0; + + var resNode = &node->AtkResNode; + var rawWidth = (int)resNode->GetWidth(); + if (rawWidth <= 0) + rawWidth = AtkNodeHelpers.DefaultTextNodeWidth; + + var scale = resNode->ScaleX; + if (scale <= 0f) + scale = 1f; + + var computed = (int)System.Math.Round(rawWidth * scale); + return System.Math.Max(1, computed); + } + + private static int ResolveCache( + int[] cache, + int index, + int rawValue, + Func fallback, + int fallbackWhenZero) + { + if (rawValue > 0) + { + cache[index] = rawValue; + return rawValue; + } + + var cachedValue = cache[index]; + if (cachedValue > 0) + return cachedValue; + + var computed = fallback(); + if (computed <= 0) + computed = fallbackWhenZero; + + cache[index] = computed; + return computed; + } + + private bool TryCacheTextOffset(int nameplateIndex, int measuredTextWidth, int textOffset) + { + if (System.Math.Abs(measuredTextWidth) > 0 || textOffset != 0) + { + _buffers.TextOffsets[nameplateIndex] = textOffset; return true; } - // Approximate check to see if nameplate would be visible based on distance and screen position - // Also has to be fine tuned still - private bool ShouldApproximateNameplateVisible(IPlayerCharacter player) + return false; + } + + internal static string NormalizeIconGlyph(string? rawInput) + { + if (string.IsNullOrWhiteSpace(rawInput)) + return DefaultIconGlyph; + + var trimmed = rawInput.Trim(); + + if (Enum.TryParse(trimmed, true, out var iconEnum)) + return SeIconCharExtensions.ToIconString(iconEnum); + + var hexCandidate = trimmed.StartsWith("0x", StringComparison.OrdinalIgnoreCase) + ? trimmed[2..] + : trimmed; + + if (ushort.TryParse(hexCandidate, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var hexValue)) + return char.ConvertFromUtf32(hexValue); + + var enumerator = trimmed.EnumerateRunes(); + if (enumerator.MoveNext()) + return enumerator.Current.ToString(); + + return DefaultIconGlyph; + } + + internal static string ToIconEditorString(string? rawInput) + { + var normalized = NormalizeIconGlyph(rawInput); + var runeEnumerator = normalized.EnumerateRunes(); + return runeEnumerator.MoveNext() + ? runeEnumerator.Current.Value.ToString("X4", CultureInfo.InvariantCulture) + : DefaultIconGlyph; + } + private readonly struct NameplateLabelInfo + { + public NameplateLabelInfo( + Vector2 screenPosition, + string text, + uint textColor, + uint edgeColor, + float fontSize, + Vector2 pivot, + bool useIcon) { - var local = _gameObjects.LocalPlayer; - if (local == null) - return false; - - var delta = player.Position - local.Position; - var distance2D = MathF.Sqrt(delta.X * delta.X + delta.Z * delta.Z); - if (distance2D > _defaultNameplateDistance) - return false; - - var verticalDelta = MathF.Abs(delta.Y); - if (verticalDelta > 3.4f) - return false; - - return TryGetApproxNameplateScreenPos(player, out _); + ScreenPosition = screenPosition; + Text = text; + TextColor = textColor; + EdgeColor = edgeColor; + FontSize = fontSize; + Pivot = pivot; + UseIcon = useIcon; } - private static unsafe float GetVisualHeight(IPlayerCharacter player) + public Vector2 ScreenPosition { get; } + public string Text { get; } + public uint TextColor { get; } + public uint EdgeColor { get; } + public float FontSize { get; } + public Vector2 Pivot { get; } + public bool UseIcon { get; } + } + + private HashSet VisibleUserIds + => [.. _pairUiService.GetSnapshot().PairsByUid.Values + .Where(u => u.IsVisible && u.PlayerCharacterId != uint.MaxValue) + .Select(u => (ulong)u.PlayerCharacterId)]; + + public void FlagRefresh() + { + _needsLabelRefresh = true; + } + + public void OnTick(PriorityFrameworkUpdateMessage _) + { + if (_needsLabelRefresh) { - var gameObject = (GameObject*)player.Address; - if (gameObject == null) - return Math.Max(player.HitboxRadius * 2.0f, 1.7f); // fallback - - // This should account for transformations (sitting, crouching, etc.) - var radius = gameObject->GetRadius(adjustByTransformation: true); - if (radius <= 0) - radius = Math.Max(player.HitboxRadius * 2.0f, 1.7f); - - return radius; - } - - // Update the set of active broadcasting CIDs (Same uses as in NameplateHnadler before) - public void UpdateBroadcastingCids(IEnumerable cids) - { - var newSet = cids.ToImmutableHashSet(StringComparer.Ordinal); - if (ReferenceEquals(_activeBroadcastingCids, newSet) || _activeBroadcastingCids.SetEquals(newSet)) - return; - - _activeBroadcastingCids = newSet; - if (_logger.IsEnabled(LogLevel.Information)) - _logger.LogInformation("Active broadcast CIDs: {Cids}", string.Join(',', _activeBroadcastingCids)); + UpdateNameplateNodes(); + _needsLabelRefresh = false; } } + + public void UpdateBroadcastingCids(IEnumerable cids) + { + var newSet = cids.ToImmutableHashSet(StringComparer.Ordinal); + if (ReferenceEquals(_activeBroadcastingCids, newSet) || _activeBroadcastingCids.SetEquals(newSet)) + return; + + _activeBroadcastingCids = newSet; + if (_logger.IsEnabled(LogLevel.Information)) + _logger.LogInformation("Active broadcast CIDs: {Cids}", string.Join(',', _activeBroadcastingCids)); + FlagRefresh(); + } + + public void ClearNameplateCaches() + { + _buffers.Clear(); + ClearLabelBuffer(); + } + + private sealed class NameplateBuffers + { + public NameplateBuffers() + { + TextOffsets = new int[AddonNamePlate.NumNamePlateObjects]; + System.Array.Fill(TextOffsets, int.MinValue); + } + + public int[] TextWidths { get; } = new int[AddonNamePlate.NumNamePlateObjects]; + public int[] TextHeights { get; } = new int[AddonNamePlate.NumNamePlateObjects]; + public int[] ContainerHeights { get; } = new int[AddonNamePlate.NumNamePlateObjects]; + public int[] TextOffsets { get; } + public NameplateLabelInfo[] LabelScratch { get; } = new NameplateLabelInfo[AddonNamePlate.NumNamePlateObjects]; + public NameplateLabelInfo[] LabelRender { get; } = new NameplateLabelInfo[AddonNamePlate.NumNamePlateObjects]; + public NameplateLabelInfo[] LabelCopy { get; } = new NameplateLabelInfo[AddonNamePlate.NumNamePlateObjects]; + + public void Clear() + { + System.Array.Clear(TextWidths, 0, TextWidths.Length); + System.Array.Clear(TextHeights, 0, TextHeights.Length); + System.Array.Clear(ContainerHeights, 0, ContainerHeights.Length); + System.Array.Fill(TextOffsets, int.MinValue); + } + } + + public Task StartAsync(CancellationToken cancellationToken) + { + Init(); + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) + { + Uninit(); + return Task.CompletedTask; + } + } \ No newline at end of file diff --git a/LightlessSync/Services/Rendering/PctDrawListExtensions.cs b/LightlessSync/Services/Rendering/PctDrawListExtensions.cs new file mode 100644 index 0000000..20bcb45 --- /dev/null +++ b/LightlessSync/Services/Rendering/PctDrawListExtensions.cs @@ -0,0 +1,82 @@ +using System.Numerics; +using System.Reflection; +using Dalamud.Bindings.ImGui; +using Pictomancy; + +namespace LightlessSync.Services.Rendering; + +internal static class PctDrawListExtensions +{ + private static readonly FieldInfo? DrawListField = typeof(PctDrawList).GetField("_drawList", BindingFlags.Instance | BindingFlags.NonPublic); + + private static bool TryGetImDrawList(PctDrawList drawList, out ImDrawListPtr ptr) + { + ptr = default; + if (DrawListField == null) + return false; + + if (DrawListField.GetValue(drawList) is ImDrawListPtr list) + { + ptr = list; + return true; + } + + return false; + } + + public static void AddScreenText(this PctDrawList drawList, Vector2 screenPosition, string text, uint color, float fontSize, Vector2? pivot = null, uint? outlineColor = null, ImFontPtr fontOverride = default) + { + if (drawList == null || string.IsNullOrEmpty(text)) + return; + + if (!TryGetImDrawList(drawList, out var imDrawList)) + return; + + var font = fontOverride.IsNull ? ImGui.GetFont() : fontOverride; + if (font.IsNull) + return; + + var size = MathF.Max(1f, fontSize); + var pivotValue = pivot ?? new Vector2(0.5f, 0.5f); + + Vector2 measured; + float calcFontSize; + if (!fontOverride.IsNull) + { + ImGui.PushFont(font); + measured = ImGui.CalcTextSize(text); + calcFontSize = ImGui.GetFontSize(); + ImGui.PopFont(); + } + else + { + measured = ImGui.CalcTextSize(text); + calcFontSize = ImGui.GetFontSize(); + } + + if (calcFontSize > 0f && MathF.Abs(size - calcFontSize) > 0.001f) + { + measured *= size / calcFontSize; + } + + var drawPos = screenPosition - measured * pivotValue; + if (outlineColor.HasValue) + { + var thickness = MathF.Max(1f, size / 24f); + Span offsets = stackalloc Vector2[4] + { + new Vector2(1f, 0f), + new Vector2(-1f, 0f), + new Vector2(0f, 1f), + new Vector2(0f, -1f) + }; + + foreach (var offset in offsets) + { + imDrawList.AddText(font, size, drawPos + offset * thickness, outlineColor.Value, text); + } + } + + imDrawList.AddText(font, size, drawPos, color, text); + } +} diff --git a/LightlessSync/Services/Rendering/PictomancyService.cs b/LightlessSync/Services/Rendering/PictomancyService.cs new file mode 100644 index 0000000..7d12b4c --- /dev/null +++ b/LightlessSync/Services/Rendering/PictomancyService.cs @@ -0,0 +1,47 @@ +using Dalamud.Plugin; +using Microsoft.Extensions.Logging; +using Pictomancy; + +namespace LightlessSync.Services.Rendering; + +public sealed class PictomancyService : IDisposable +{ + private readonly ILogger _logger; + private bool _initialized; + + public PictomancyService(ILogger logger, IDalamudPluginInterface pluginInterface) + { + _logger = logger; + + try + { + PictoService.Initialize(pluginInterface); + _initialized = true; + if (_logger.IsEnabled(LogLevel.Debug)) + _logger.LogDebug("Pictomancy initialized"); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to initialize Pictomancy"); + } + } + + public void Dispose() + { + if (!_initialized) + return; + + try + { + PictoService.Dispose(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to dispose Pictomancy"); + } + finally + { + _initialized = false; + } + } +} diff --git a/LightlessSync/Services/TextureCompression/TextureDownscaleService.cs b/LightlessSync/Services/TextureCompression/TextureDownscaleService.cs index b43b1b5..a7b42f5 100644 --- a/LightlessSync/Services/TextureCompression/TextureDownscaleService.cs +++ b/LightlessSync/Services/TextureCompression/TextureDownscaleService.cs @@ -1,8 +1,9 @@ -using System; using System.Collections.Concurrent; +using System.Buffers; using System.Buffers.Binary; using System.Globalization; using System.IO; +using System.Runtime.InteropServices; using OtterTex; using OtterImage = OtterTex.Image; using LightlessSync.LightlessConfiguration; @@ -11,7 +12,6 @@ using Microsoft.Extensions.Logging; using Lumina.Data.Files; using SixLabors.ImageSharp; using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; /* * OtterTex made by Ottermandias @@ -33,6 +33,7 @@ public sealed class TextureDownscaleService private readonly ConcurrentDictionary _activeJobs = new(StringComparer.OrdinalIgnoreCase); private readonly ConcurrentDictionary _downscaledPaths = new(StringComparer.OrdinalIgnoreCase); + private readonly SemaphoreSlim _downscaleSemaphore = new(4); private static readonly IReadOnlyDictionary BlockCompressedFormatMap = new Dictionary { @@ -80,7 +81,10 @@ public sealed class TextureDownscaleService if (!filePath.EndsWith(".tex", StringComparison.OrdinalIgnoreCase)) return; if (_activeJobs.ContainsKey(hash)) return; - _activeJobs[hash] = Task.Run(() => DownscaleInternalAsync(hash, filePath, mapKind), CancellationToken.None); + _activeJobs[hash] = Task.Run(async () => + { + await DownscaleInternalAsync(hash, filePath, mapKind).ConfigureAwait(false); + }, CancellationToken.None); } public string GetPreferredPath(string hash, string originalPath) @@ -108,6 +112,7 @@ public sealed class TextureDownscaleService bool onlyDownscaleUncompressed = false; bool? isIndexTexture = null; + await _downscaleSemaphore.WaitAsync().ConfigureAwait(false); try { if (!File.Exists(sourcePath)) @@ -157,6 +162,15 @@ public sealed class TextureDownscaleService return; } + if (headerInfo is { } headerValue && + headerValue.Width <= targetMaxDimension && + headerValue.Height <= targetMaxDimension) + { + _downscaledPaths[hash] = sourcePath; + _logger.LogTrace("Skipping downscale for index texture {Hash}; header dimensions {Width}x{Height} within target.", hash, headerValue.Width, headerValue.Height); + return; + } + if (onlyDownscaleUncompressed && headerInfo.HasValue && IsBlockCompressedFormat(headerInfo.Value.Format)) { _downscaledPaths[hash] = sourcePath; @@ -172,21 +186,20 @@ public sealed class TextureDownscaleService var height = rgbaInfo.Meta.Height; var requiredLength = width * height * bytesPerPixel; - var rgbaPixels = rgbaScratch.Pixels[..requiredLength].ToArray(); + var rgbaPixels = rgbaScratch.Pixels.Slice(0, requiredLength); using var originalImage = SixLabors.ImageSharp.Image.LoadPixelData(rgbaPixels, width, height); var targetSize = CalculateTargetSize(originalImage.Width, originalImage.Height, targetMaxDimension); if (targetSize.width == originalImage.Width && targetSize.height == originalImage.Height) { + _downscaledPaths[hash] = sourcePath; + _logger.LogTrace("Skipping downscale for index texture {Hash}; already within bounds.", hash); return; } using var resized = IndexDownscaler.Downscale(originalImage, targetSize.width, targetSize.height, BlockMultiple); - var resizedPixels = new byte[targetSize.width * targetSize.height * 4]; - resized.CopyPixelDataTo(resizedPixels); - - using var resizedScratch = ScratchImage.FromRGBA(resizedPixels, targetSize.width, targetSize.height, out var creationInfo).ThrowIfError(creationInfo); + using var resizedScratch = CreateScratchImage(resized, targetSize.width, targetSize.height); using var finalScratch = resizedScratch.Convert(DXGIFormat.B8G8R8A8UNorm); TexFileHelper.Save(destination, finalScratch); @@ -209,6 +222,7 @@ public sealed class TextureDownscaleService } finally { + _downscaleSemaphore.Release(); _activeJobs.TryRemove(hash, out _); } } @@ -227,6 +241,41 @@ public sealed class TextureDownscaleService return (resultWidth, resultHeight); } + private static ScratchImage CreateScratchImage(Image image, int width, int height) + { + const int BytesPerPixel = 4; + var requiredLength = width * height * BytesPerPixel; + + static ScratchImage Create(ReadOnlySpan pixels, int width, int height) + { + var scratchResult = ScratchImage.FromRGBA(pixels, width, height, out var creationInfo); + return scratchResult.ThrowIfError(creationInfo); + } + + if (image.DangerousTryGetSinglePixelMemory(out var pixelMemory)) + { + var byteSpan = MemoryMarshal.AsBytes(pixelMemory.Span); + if (byteSpan.Length < requiredLength) + { + throw new InvalidOperationException($"Image buffer shorter than expected ({byteSpan.Length} < {requiredLength})."); + } + + return Create(byteSpan.Slice(0, requiredLength), width, height); + } + + var rented = ArrayPool.Shared.Rent(requiredLength); + try + { + var rentedSpan = rented.AsSpan(0, requiredLength); + image.CopyPixelDataTo(rentedSpan); + return Create(rentedSpan, width, height); + } + finally + { + ArrayPool.Shared.Return(rented); + } + } + private static bool IsIndexMap(TextureMapKind kind) => kind is TextureMapKind.Mask or TextureMapKind.Index; @@ -420,21 +469,6 @@ public sealed class TextureDownscaleService private static int ReduceDimension(int value) => value <= 1 ? 1 : Math.Max(1, value / 2); - private static Image ReduceLinearTexture(Image source, int targetWidth, int targetHeight) - { - var clone = source.Clone(); - - while (clone.Width > targetWidth || clone.Height > targetHeight) - { - var nextWidth = Math.Max(targetWidth, Math.Max(BlockMultiple, clone.Width / 2)); - var nextHeight = Math.Max(targetHeight, Math.Max(BlockMultiple, clone.Height / 2)); - clone.Mutate(ctx => ctx.Resize(nextWidth, nextHeight, KnownResamplers.Lanczos3)); - } - - return clone; - } - - private static bool ShouldTrim(in TexMeta meta, int targetMaxDimension) { var depth = meta.Dimension == TexDimension.Tex3D ? Math.Max(1, meta.Depth) : 1; @@ -443,12 +477,7 @@ public sealed class TextureDownscaleService private static bool ShouldTrimDimensions(int width, int height, int depth, int targetMaxDimension) { - if (width <= targetMaxDimension || height <= targetMaxDimension) - { - return false; - } - - if (depth > 1 && depth <= targetMaxDimension) + if (width <= targetMaxDimension && height <= targetMaxDimension && depth <= targetMaxDimension) { return false; } diff --git a/LightlessSync/UI/Handlers/IdDisplayHandler.cs b/LightlessSync/UI/Handlers/IdDisplayHandler.cs index 28f3053..b31b145 100644 --- a/LightlessSync/UI/Handlers/IdDisplayHandler.cs +++ b/LightlessSync/UI/Handlers/IdDisplayHandler.cs @@ -51,10 +51,11 @@ public class IdDisplayHandler (bool textIsUid, string playerText) = GetGroupText(group); if (!string.Equals(_editEntry, group.GID, StringComparison.Ordinal)) { - ImGui.AlignTextToFramePadding(); - using (ImRaii.PushFont(UiBuilder.MonoFont, textIsUid)) + { + ImGui.AlignTextToFramePadding(); ImGui.TextUnformatted(playerText); + } if (ImGui.IsItemHovered()) { @@ -121,108 +122,113 @@ public class IdDisplayHandler if (!string.Equals(_editEntry, pair.UserData.UID, StringComparison.Ordinal)) { - ImGui.AlignTextToFramePadding(); - var rowStart = ImGui.GetCursorScreenPos(); - var rowWidth = MathF.Max(editBoxWidth.Invoke(), 0f); - var rowRightLimit = rowStart.X + rowWidth; - var font = textIsUid ? UiBuilder.MonoFont : ImGui.GetFont(); + var rowWidth = MathF.Max(editBoxWidth.Invoke(), 0f); + float rowRightLimit = 0f; + Vector2 nameRectMin = Vector2.Zero; + Vector2 nameRectMax = Vector2.Zero; + float rowTopForStats = 0f; + float frameHeightForStats = 0f; - Vector4? textColor = null; - Vector4? glowColor = null; - - if (pair.UserData.HasVanity) - { - if (!string.IsNullOrWhiteSpace(pair.UserData.TextColorHex)) - { - textColor = UIColors.HexToRgba(pair.UserData.TextColorHex); - } - - if (!string.IsNullOrWhiteSpace(pair.UserData.TextGlowColorHex)) - { - glowColor = UIColors.HexToRgba(pair.UserData.TextGlowColorHex); - } - } - - var useVanityColors = _lightlessConfigService.Current.useColoredUIDs && (textColor != null || glowColor != null); - var seString = useVanityColors - ? SeStringUtils.BuildFormattedPlayerName(playerText, textColor, glowColor) - : SeStringUtils.BuildPlain(playerText); - - var drawList = ImGui.GetWindowDrawList(); - bool useHighlight = false; - float highlightPadX = 0f; - float highlightPadY = 0f; - - if (useVanityColors) - { - float boost = Luminance.ComputeHighlight(textColor, glowColor); - - if (boost > 0f) - { - var style = ImGui.GetStyle(); - useHighlight = true; - highlightPadX = MathF.Max(style.FramePadding.X * 0.6f, 2f * ImGuiHelpers.GlobalScale); - highlightPadY = MathF.Max(style.FramePadding.Y * 0.55f, 1.25f * ImGuiHelpers.GlobalScale); - drawList.ChannelsSplit(2); - drawList.ChannelsSetCurrent(1); - - _highlightBoost = boost; - } - else - { - _highlightBoost = 0f; - } - } - - Vector2 itemMin; - Vector2 itemMax; using (ImRaii.PushFont(font, textIsUid)) { - SeStringUtils.RenderSeStringWithHitbox(seString, rowStart, font, pair.UserData.UID); - itemMin = ImGui.GetItemRectMin(); - itemMax = ImGui.GetItemRectMax(); - } + ImGui.AlignTextToFramePadding(); + var rowStart = ImGui.GetCursorScreenPos(); + rowRightLimit = rowStart.X + rowWidth; - if (useHighlight) + Vector4? textColor = null; + Vector4? glowColor = null; + + if (pair.UserData.HasVanity) + { + if (!string.IsNullOrWhiteSpace(pair.UserData.TextColorHex)) + { + textColor = UIColors.HexToRgba(pair.UserData.TextColorHex); + } + + if (!string.IsNullOrWhiteSpace(pair.UserData.TextGlowColorHex)) + { + glowColor = UIColors.HexToRgba(pair.UserData.TextGlowColorHex); + } + } + + var useVanityColors = _lightlessConfigService.Current.useColoredUIDs && (textColor != null || glowColor != null); + var seString = useVanityColors + ? SeStringUtils.BuildFormattedPlayerName(playerText, textColor, glowColor) + : SeStringUtils.BuildPlain(playerText); + + var drawList = ImGui.GetWindowDrawList(); + bool useHighlight = false; + float highlightPadX = 0f; + float highlightPadY = 0f; + + if (useVanityColors) + { + float boost = Luminance.ComputeHighlight(textColor, glowColor); + + if (boost > 0f) + { + var style = ImGui.GetStyle(); + useHighlight = true; + highlightPadX = MathF.Max(style.FramePadding.X * 0.6f, 2f * ImGuiHelpers.GlobalScale); + highlightPadY = MathF.Max(style.FramePadding.Y * 0.55f, 1.25f * ImGuiHelpers.GlobalScale); + drawList.ChannelsSplit(2); + drawList.ChannelsSetCurrent(1); + + _highlightBoost = boost; + } + else + { + _highlightBoost = 0f; + } + } + + SeStringUtils.RenderSeStringWithHitbox(seString, rowStart, font, pair.UserData.UID); + nameRectMin = ImGui.GetItemRectMin(); + nameRectMax = ImGui.GetItemRectMax(); + + if (useHighlight) + { + var style = ImGui.GetStyle(); + var frameHeight = ImGui.GetFrameHeight(); + var rowTop = rowStart.Y - style.FramePadding.Y; + var rowBottom = rowTop + frameHeight; + + var highlightMin = new Vector2(nameRectMin.X - highlightPadX, rowTop - highlightPadY); + var highlightMax = new Vector2(nameRectMax.X + highlightPadX, rowBottom + highlightPadY); + + var windowPos = ImGui.GetWindowPos(); + var contentMin = windowPos + ImGui.GetWindowContentRegionMin(); + var contentMax = windowPos + ImGui.GetWindowContentRegionMax(); + highlightMin.X = MathF.Max(highlightMin.X, contentMin.X); + highlightMax.X = MathF.Min(highlightMax.X, contentMax.X); + highlightMin.Y = MathF.Max(highlightMin.Y, contentMin.Y); + highlightMax.Y = MathF.Min(highlightMax.Y, contentMax.Y); + + var highlightColor = new Vector4( + 0.25f + _highlightBoost, + 0.25f + _highlightBoost, + 0.25f + _highlightBoost, + 1f + ); + + highlightColor = Luminance.BackgroundContrast(textColor, glowColor, highlightColor, ref _currentBg); + + float rounding = style.FrameRounding > 0f ? style.FrameRounding : 5f * ImGuiHelpers.GlobalScale; + drawList.ChannelsSetCurrent(0); + drawList.AddRectFilled(highlightMin, highlightMax, ImGui.GetColorU32(highlightColor), rounding); + + var borderColor = style.Colors[(int)ImGuiCol.Border]; + borderColor.W *= 0.25f; + drawList.AddRect(highlightMin, highlightMax, ImGui.GetColorU32(borderColor), rounding); + drawList.ChannelsMerge(); + } + } { var style = ImGui.GetStyle(); - var frameHeight = ImGui.GetFrameHeight(); - var rowTop = rowStart.Y - style.FramePadding.Y; - var rowBottom = rowTop + frameHeight; - - var highlightMin = new Vector2(itemMin.X - highlightPadX, rowTop - highlightPadY); - var highlightMax = new Vector2(itemMax.X + highlightPadX, rowBottom + highlightPadY); - - var windowPos = ImGui.GetWindowPos(); - var contentMin = windowPos + ImGui.GetWindowContentRegionMin(); - var contentMax = windowPos + ImGui.GetWindowContentRegionMax(); - highlightMin.X = MathF.Max(highlightMin.X, contentMin.X); - highlightMax.X = MathF.Min(highlightMax.X, contentMax.X); - highlightMin.Y = MathF.Max(highlightMin.Y, contentMin.Y); - highlightMax.Y = MathF.Min(highlightMax.Y, contentMax.Y); - - var highlightColor = new Vector4( - 0.25f + _highlightBoost, - 0.25f + _highlightBoost, - 0.25f + _highlightBoost, - 1f - ); - - highlightColor = Luminance.BackgroundContrast(textColor, glowColor, highlightColor, ref _currentBg); - - float rounding = style.FrameRounding > 0f ? style.FrameRounding : 5f * ImGuiHelpers.GlobalScale; - drawList.ChannelsSetCurrent(0); - drawList.AddRectFilled(highlightMin, highlightMax, ImGui.GetColorU32(highlightColor), rounding); - - var borderColor = style.Colors[(int)ImGuiCol.Border]; - borderColor.W *= 0.25f; - drawList.AddRect(highlightMin, highlightMax, ImGui.GetColorU32(borderColor), rounding); - drawList.ChannelsMerge(); + frameHeightForStats = ImGui.GetFrameHeight(); + rowTopForStats = nameRectMin.Y - style.FramePadding.Y; } - - var nameRectMin = ImGui.GetItemRectMin(); - var nameRectMax = ImGui.GetItemRectMax(); if (ImGui.IsItemHovered()) { if (!string.Equals(_lastMouseOverUid, id, StringComparison.Ordinal)) @@ -292,12 +298,9 @@ public class IdDisplayHandler const float compactFontScale = 0.85f; ImGui.SetWindowFontScale(compactFontScale); var compactHeight = ImGui.GetTextLineHeight(); - var nameHeight = nameRectMax.Y - nameRectMin.Y; var targetPos = ImGui.GetCursorScreenPos(); var availableWidth = MathF.Max(rowRightLimit - targetPos.X, 0f); - var centeredY = nameRectMin.Y + MathF.Max((nameHeight - compactHeight) * 0.5f, 0f); - float verticalOffset = 1f * ImGuiHelpers.GlobalScale; - centeredY += verticalOffset; + var centeredY = rowTopForStats + MathF.Max((frameHeightForStats - compactHeight) * 0.5f, 0f); ImGui.SetCursorScreenPos(new Vector2(targetPos.X, centeredY)); var performanceText = string.Empty; diff --git a/LightlessSync/UI/ZoneChatUi.cs b/LightlessSync/UI/ZoneChatUi.cs index 697f100..084b55b 100644 --- a/LightlessSync/UI/ZoneChatUi.cs +++ b/LightlessSync/UI/ZoneChatUi.cs @@ -48,7 +48,7 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase private readonly ImGuiWindowFlags _unpinnedWindowFlags; private float _currentWindowOpacity = DefaultWindowOpacity; private bool _isWindowPinned; - private bool _showRulesOverlay = true; + private bool _showRulesOverlay; private string? _selectedChannelKey; private bool _scrollToBottom = true; @@ -165,7 +165,7 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase } } - private void DrawHeader(ChatChannelSnapshot channel) + private static void DrawHeader(ChatChannelSnapshot channel) { var prefix = channel.Type == ChatChannelType.Zone ? "Zone" : "Syncshell"; Vector4 color; @@ -577,12 +577,10 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase ImGui.Separator(); ImGui.TextUnformatted("Reason (required)"); - if (ImGui.InputTextMultiline("##chat_report_reason", ref _reportReason, ReportReasonMaxLength, new Vector2(-1, 80f * ImGuiHelpers.GlobalScale))) + if (ImGui.InputTextMultiline("##chat_report_reason", ref _reportReason, ReportReasonMaxLength, new Vector2(-1, 80f * ImGuiHelpers.GlobalScale)) + && _reportReason.Length > ReportReasonMaxLength) { - if (_reportReason.Length > ReportReasonMaxLength) - { - _reportReason = _reportReason[..(int)ReportReasonMaxLength]; - } + _reportReason = _reportReason[..ReportReasonMaxLength]; } ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey3); @@ -591,12 +589,10 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase ImGui.Spacing(); ImGui.TextUnformatted("Additional context (optional)"); - if (ImGui.InputTextMultiline("##chat_report_context", ref _reportAdditionalContext, ReportContextMaxLength, new Vector2(-1, 120f * ImGuiHelpers.GlobalScale))) + if (ImGui.InputTextMultiline("##chat_report_context", ref _reportAdditionalContext, ReportContextMaxLength, new Vector2(-1, 120f * ImGuiHelpers.GlobalScale)) + && _reportAdditionalContext.Length > ReportContextMaxLength) { - if (_reportAdditionalContext.Length > ReportContextMaxLength) - { - _reportAdditionalContext = _reportAdditionalContext[..(int)ReportContextMaxLength]; - } + _reportAdditionalContext = _reportAdditionalContext[..ReportContextMaxLength]; } ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey3); @@ -768,7 +764,7 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase } } - private bool TryCreateCopyMessageAction(ChatMessageEntry message, out ChatMessageContextAction action) + private static bool TryCreateCopyMessageAction(ChatMessageEntry message, out ChatMessageContextAction action) { var text = message.Payload.Message; if (string.IsNullOrEmpty(text)) @@ -920,7 +916,7 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase private void EnsureSelectedChannel(IReadOnlyList channels) { - if (_selectedChannelKey is not null && channels.Any(channel => channel.Key == _selectedChannelKey)) + if (_selectedChannelKey is not null && channels.Any(channel => string.Equals(channel.Key, _selectedChannelKey, StringComparison.Ordinal))) return; _selectedChannelKey = channels.Count > 0 ? channels[0].Key : null; @@ -1264,11 +1260,7 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase var isSelected = string.Equals(channel.Key, _selectedChannelKey, StringComparison.Ordinal); var showBadge = !isSelected && channel.UnreadCount > 0; var isZoneChannel = channel.Type == ChatChannelType.Zone; - var badgeText = string.Empty; - var badgePadding = Vector2.Zero; - var badgeTextSize = Vector2.Zero; - float badgeWidth = 0f; - float badgeHeight = 0f; + (string Text, Vector2 TextSize, float Width, float Height)? badgeMetrics = null; var normal = isSelected ? UIColors.Get("LightlessPurpleDefault") : UIColors.Get("ButtonDefault"); var hovered = isSelected @@ -1285,15 +1277,16 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase if (showBadge) { var badgeSpacing = 4f * ImGuiHelpers.GlobalScale; - badgePadding = new Vector2(4f, 1.5f) * ImGuiHelpers.GlobalScale; - badgeText = channel.UnreadCount > MaxBadgeDisplay + var badgePadding = new Vector2(4f, 1.5f) * ImGuiHelpers.GlobalScale; + var badgeText = channel.UnreadCount > MaxBadgeDisplay ? $"{MaxBadgeDisplay}+" : channel.UnreadCount.ToString(CultureInfo.InvariantCulture); - badgeTextSize = ImGui.CalcTextSize(badgeText); - badgeWidth = badgeTextSize.X + badgePadding.X * 2f; - badgeHeight = badgeTextSize.Y + badgePadding.Y * 2f; + var badgeTextSize = ImGui.CalcTextSize(badgeText); + var badgeWidth = badgeTextSize.X + badgePadding.X * 2f; + var badgeHeight = badgeTextSize.Y + badgePadding.Y * 2f; var customPadding = new Vector2(baseFramePadding.X + badgeWidth + badgeSpacing, baseFramePadding.Y); ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, customPadding); + badgeMetrics = (badgeText, badgeTextSize, badgeWidth, badgeHeight); } var clicked = ImGui.Button($"{channel.DisplayName}##chat_channel_{channel.Key}"); @@ -1324,20 +1317,20 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase drawList.AddRect(itemMin, itemMax, borderColorU32, style.FrameRounding, ImDrawFlags.None, borderThickness); } - if (showBadge) + if (showBadge && badgeMetrics is { } metrics) { var buttonSizeY = itemMax.Y - itemMin.Y; var badgeMin = new Vector2( itemMin.X + baseFramePadding.X, - itemMin.Y + (buttonSizeY - badgeHeight) * 0.5f); - var badgeMax = badgeMin + new Vector2(badgeWidth, badgeHeight); + itemMin.Y + (buttonSizeY - metrics.Height) * 0.5f); + var badgeMax = badgeMin + new Vector2(metrics.Width, metrics.Height); var badgeColor = UIColors.Get("DimRed"); var badgeColorU32 = ImGui.ColorConvertFloat4ToU32(badgeColor); - drawList.AddRectFilled(badgeMin, badgeMax, badgeColorU32, badgeHeight * 0.5f); + drawList.AddRectFilled(badgeMin, badgeMax, badgeColorU32, metrics.Height * 0.5f); var textPos = new Vector2( - badgeMin.X + (badgeWidth - badgeTextSize.X) * 0.5f, - badgeMin.Y + (badgeHeight - badgeTextSize.Y) * 0.5f); - drawList.AddText(textPos, ImGui.ColorConvertFloat4ToU32(ImGuiColors.DalamudWhite), badgeText); + badgeMin.X + (metrics.Width - metrics.TextSize.X) * 0.5f, + badgeMin.Y + (metrics.Height - metrics.TextSize.Y) * 0.5f); + drawList.AddText(textPos, ImGui.ColorConvertFloat4ToU32(ImGuiColors.DalamudWhite), metrics.Text); } first = false; diff --git a/LightlessSync/Utils/SeStringUtils.cs b/LightlessSync/Utils/SeStringUtils.cs index c8b9a7b..c3e50fd 100644 --- a/LightlessSync/Utils/SeStringUtils.cs +++ b/LightlessSync/Utils/SeStringUtils.cs @@ -565,19 +565,30 @@ public static class SeStringUtils public static Vector2 RenderSeStringWithHitbox(DalamudSeString seString, Vector2 position, ImFontPtr? font = null, string? id = null) { var drawList = ImGui.GetWindowDrawList(); - + var usedFont = font ?? UiBuilder.MonoFont; var drawParams = new SeStringDrawParams { - Font = font ?? UiBuilder.MonoFont, + Font = usedFont, Color = 0xFFFFFFFF, WrapWidth = float.MaxValue, TargetDrawList = drawList }; - ImGui.SetCursorScreenPos(position); - ImGuiHelpers.SeStringWrapped(seString.Encode(), drawParams); - var textSize = ImGui.CalcTextSize(seString.TextValue); + if (textSize.Y <= 0f) + { + textSize.Y = usedFont.FontSize; + } + + var style = ImGui.GetStyle(); + var fontHeight = usedFont.FontSize > 0f ? usedFont.FontSize : ImGui.GetFontSize(); + var frameHeight = fontHeight + style.FramePadding.Y * 2f; + var hitboxHeight = MathF.Max(frameHeight, textSize.Y); + var verticalOffset = MathF.Max((hitboxHeight - textSize.Y) * 0.5f, 0f); + + var drawPos = new Vector2(position.X, position.Y + verticalOffset); + ImGui.SetCursorScreenPos(drawPos); + ImGuiHelpers.SeStringWrapped(seString.Encode(), drawParams); ImGui.SetCursorScreenPos(position); if (id is not null) @@ -591,30 +602,52 @@ public static class SeStringUtils try { - ImGui.InvisibleButton("##hitbox", textSize); + ImGui.InvisibleButton("##hitbox", new Vector2(textSize.X, hitboxHeight)); } finally { ImGui.PopID(); } - return textSize; + return new Vector2(textSize.X, hitboxHeight); } public static Vector2 RenderIconWithHitbox(int iconId, Vector2 position, ImFontPtr? font = null, string? id = null) { var drawList = ImGui.GetWindowDrawList(); + var usedFont = font ?? UiBuilder.MonoFont; + var iconMacro = $""; - var drawParams = new SeStringDrawParams + var measureParams = new SeStringDrawParams { - Font = font ?? UiBuilder.MonoFont, + Font = usedFont, Color = 0xFFFFFFFF, - WrapWidth = float.MaxValue, - TargetDrawList = drawList + WrapWidth = float.MaxValue }; - var iconMacro = $""; - var drawResult = ImGuiHelpers.CompileSeStringWrapped(iconMacro, drawParams); + var measureResult = ImGuiHelpers.CompileSeStringWrapped(iconMacro, measureParams); + var iconSize = measureResult.Size; + if (iconSize.Y <= 0f) + { + iconSize.Y = usedFont.FontSize > 0f ? usedFont.FontSize : ImGui.GetFontSize(); + } + + var style = ImGui.GetStyle(); + var fontHeight = usedFont.FontSize > 0f ? usedFont.FontSize : ImGui.GetFontSize(); + var frameHeight = fontHeight + style.FramePadding.Y * 2f; + var hitboxHeight = MathF.Max(frameHeight, iconSize.Y); + var verticalOffset = MathF.Max((hitboxHeight - iconSize.Y) * 0.5f, 0f); + + var drawPos = new Vector2(position.X, position.Y + verticalOffset); + var drawParams = new SeStringDrawParams + { + Font = usedFont, + Color = 0xFFFFFFFF, + WrapWidth = float.MaxValue, + TargetDrawList = drawList, + ScreenOffset = drawPos + }; + ImGuiHelpers.CompileSeStringWrapped(iconMacro, drawParams); ImGui.SetCursorScreenPos(position); if (id is not null) @@ -628,14 +661,14 @@ public static class SeStringUtils try { - ImGui.InvisibleButton("##iconHitbox", drawResult.Size); + ImGui.InvisibleButton("##iconHitbox", new Vector2(iconSize.X, hitboxHeight)); } finally { ImGui.PopID(); } - return drawResult.Size; + return new Vector2(iconSize.X, hitboxHeight); } #region Internal Payloads diff --git a/LightlessSync/Utils/VariousExtensions.cs b/LightlessSync/Utils/VariousExtensions.cs index e0fd466..3f47d98 100644 --- a/LightlessSync/Utils/VariousExtensions.cs +++ b/LightlessSync/Utils/VariousExtensions.cs @@ -177,7 +177,8 @@ public static class VariousExtensions if (objectKind != ObjectKind.Player) continue; bool manipDataDifferent = !string.Equals(oldData.ManipulationData, newData.ManipulationData, StringComparison.Ordinal); - if (manipDataDifferent || forceApplyMods) + var hasManipulationData = !string.IsNullOrEmpty(newData.ManipulationData); + if (manipDataDifferent || (forceApplyMods && hasManipulationData)) { logger.LogDebug("[BASE-{appBase}] Updating {object}/{kind} (Diff manip data) => {change}", applicationBase, cachedPlayer, objectKind, PlayerChanges.ModManip); charaDataToUpdate[objectKind].Add(PlayerChanges.ModManip); diff --git a/LightlessSync/WebAPI/Files/FileDownloadManager.cs b/LightlessSync/WebAPI/Files/FileDownloadManager.cs index 181b02a..49dd868 100644 --- a/LightlessSync/WebAPI/Files/FileDownloadManager.cs +++ b/LightlessSync/WebAPI/Files/FileDownloadManager.cs @@ -12,7 +12,6 @@ using System.Collections.Concurrent; using System.Net; using System.Net.Http.Json; using LightlessSync.LightlessConfiguration; -using LightlessSync.Services.PairProcessing; namespace LightlessSync.WebAPI.Files; @@ -22,12 +21,10 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase private readonly FileCompactor _fileCompactor; private readonly FileCacheManager _fileDbManager; private readonly FileTransferOrchestrator _orchestrator; - private readonly PairProcessingLimiter _pairProcessingLimiter; private readonly LightlessConfigService _configService; private readonly TextureDownscaleService _textureDownscaleService; private readonly TextureMetadataHelper _textureMetadataHelper; private readonly ConcurrentDictionary _activeDownloadStreams; - private static readonly TimeSpan DownloadStallTimeout = TimeSpan.FromSeconds(30); private volatile bool _disableDirectDownloads; private int _consecutiveDirectDownloadFailures; private bool _lastConfigDirectDownloadsState; @@ -38,7 +35,6 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase FileTransferOrchestrator orchestrator, FileCacheManager fileCacheManager, FileCompactor fileCompactor, - PairProcessingLimiter pairProcessingLimiter, LightlessConfigService configService, TextureDownscaleService textureDownscaleService, TextureMetadataHelper textureMetadataHelper) : base(logger, mediator) { @@ -46,7 +42,6 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase _orchestrator = orchestrator; _fileDbManager = fileCacheManager; _fileCompactor = fileCompactor; - _pairProcessingLimiter = pairProcessingLimiter; _configService = configService; _textureDownscaleService = textureDownscaleService; _textureMetadataHelper = textureMetadataHelper; @@ -282,42 +277,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase int bytesRead; try { - using var readCancellation = CancellationTokenSource.CreateLinkedTokenSource(ct); - var readTask = stream.ReadAsync(buffer.AsMemory(0, buffer.Length), readCancellation.Token).AsTask(); - while (!readTask.IsCompleted) - { - var completedTask = await Task.WhenAny(readTask, Task.Delay(DownloadStallTimeout)).ConfigureAwait(false); - if (completedTask == readTask) - { - break; - } - - ct.ThrowIfCancellationRequested(); - - var snapshot = _pairProcessingLimiter.GetSnapshot(); - if (snapshot.Waiting > 0) - { - readCancellation.Cancel(); - try - { - await readTask.ConfigureAwait(false); - } - catch (OperationCanceledException) - { - // expected when cancelling the read due to timeout - } - catch (Exception ex) - { - Logger.LogDebug(ex, "Error finishing read task after stall detection for {requestUrl}", requestUrl); - } - - throw new TimeoutException($"No data received for {DownloadStallTimeout.TotalSeconds} seconds while downloading {requestUrl} (waiting: {snapshot.Waiting})"); - } - - Logger.LogTrace("Download stalled for {requestUrl} but no queued pairs, continuing to wait", requestUrl); - } - - bytesRead = await readTask.ConfigureAwait(false); + bytesRead = await stream.ReadAsync(buffer.AsMemory(0, buffer.Length), ct).ConfigureAwait(false); } catch (OperationCanceledException ex) { @@ -340,11 +300,6 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase Logger.LogDebug("{requestUrl} downloaded to {destination}", requestUrl, destinationFilename); } } - catch (TimeoutException ex) - { - Logger.LogWarning(ex, "Detected stalled download for {requestUrl}, aborting transfer", requestUrl); - throw; - } catch (OperationCanceledException) { throw; diff --git a/LightlessSync/packages.lock.json b/LightlessSync/packages.lock.json index a109393..daa1008 100644 --- a/LightlessSync/packages.lock.json +++ b/LightlessSync/packages.lock.json @@ -521,26 +521,979 @@ "resolved": "17.6.3", "contentHash": "N0ZIanl1QCgvUumEL1laasU0a7sOE5ZwLZVTn0pAePnfhq8P7SvTjF8Axq+CnavuQkmdQpGNXQ1efZtu5kDFbA==" }, + "Microsoft.NETCore.Platforms": { + "type": "Transitive", + "resolved": "1.1.0", + "contentHash": "kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A==" + }, + "Microsoft.NETCore.Targets": { + "type": "Transitive", + "resolved": "1.1.0", + "contentHash": "aOZA3BWfz9RXjpzt0sRJJMjAscAUm3Hoa4UWAfceV9UTYxgwZ1lZt5nO2myFf+/jetYQo4uTP7zS8sJY67BBxg==" + }, + "Microsoft.Win32.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "9ZQKCWxH7Ijp9BfahvL2Zyf1cJIk8XYLF6Yjzr2yi0b2cOut/HQ31qf1ThHAgCc3WiZMdnWcfJCgN82/0UunxA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "NETStandard.Library": { + "type": "Transitive", + "resolved": "1.6.1", + "contentHash": "WcSp3+vP+yHNgS8EV5J7pZ9IRpeDuARBPN28by8zqff1wJQXm26PVU8L3/fYLBJVU7BtDyqNVWq2KlCVvSSR4A==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.Win32.Primitives": "4.3.0", + "System.AppContext": "4.3.0", + "System.Collections": "4.3.0", + "System.Collections.Concurrent": "4.3.0", + "System.Console": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.Tools": "4.3.0", + "System.Diagnostics.Tracing": "4.3.0", + "System.Globalization": "4.3.0", + "System.Globalization.Calendars": "4.3.0", + "System.IO": "4.3.0", + "System.IO.Compression": "4.3.0", + "System.IO.Compression.ZipFile": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Linq": "4.3.0", + "System.Linq.Expressions": "4.3.0", + "System.Net.Http": "4.3.0", + "System.Net.Primitives": "4.3.0", + "System.Net.Sockets": "4.3.0", + "System.ObjectModel": "4.3.0", + "System.Reflection": "4.3.0", + "System.Reflection.Extensions": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Runtime.InteropServices.RuntimeInformation": "4.3.0", + "System.Runtime.Numerics": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Security.Cryptography.X509Certificates": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Text.Encoding.Extensions": "4.3.0", + "System.Text.RegularExpressions": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "System.Threading.Timer": "4.3.0", + "System.Xml.ReaderWriter": "4.3.0", + "System.Xml.XDocument": "4.3.0" + } + }, + "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "HdSSp5MnJSsg08KMfZThpuLPJpPwE5hBXvHwoKWosyHHfe8Mh5WKT0ylEOf6yNzX6Ngjxe4Whkafh5q7Ymac4Q==" + }, + "runtime.fedora.23-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "+yH1a49wJMy8Zt4yx5RhJrxO/DBDByAiCzNwiETI+1S4mPdCu0OY4djdciC7Vssk0l22wQaDLrXxXkp+3+7bVA==" + }, + "runtime.fedora.24-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "c3YNH1GQJbfIPJeCnr4avseugSqPrxwIqzthYyZDN6EuOyNOzq+y2KSUfRcXauya1sF4foESTgwM5e1A8arAKw==" + }, + "runtime.native.System": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "c/qWt2LieNZIj1jGnVNsE2Kl23Ya2aSTBuXMD6V7k9KWr6l16Tqdwq+hJScEpWER9753NWC8h96PaVNY5Ld7Jw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0" + } + }, + "runtime.native.System.IO.Compression": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "INBPonS5QPEgn7naufQFXJEp3zX6L4bwHgJ/ZH78aBTpeNfQMtf7C6VrAFhlq2xxWBveIOWyFzQjJ8XzHMhdOQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0" + } + }, + "runtime.native.System.Net.Http": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "ZVuZJqnnegJhd2k/PtAbbIcZ3aZeITq3sj06oKfMBSfphW3HDmk/t4ObvbOk/JA/swGR0LNqMksAh/f7gpTROg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0" + } + }, + "runtime.native.System.Security.Cryptography.Apple": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "DloMk88juo0OuOWr56QG7MNchmafTLYWvABy36izkrLI5VledI0rq28KGs1i9wbpeT9NPQrx/wTf8U2vazqQ3Q==", + "dependencies": { + "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.Apple": "4.3.0" + } + }, + "runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "NS1U+700m4KFRHR5o4vo9DSlTmlCKu/u7dtE5sUHVIPB+xpXxYQvgBgA6wEIeCz6Yfn0Z52/72WYsToCEPJnrw==", + "dependencies": { + "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.fedora.23-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.fedora.24-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.opensuse.13.2-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.opensuse.42.1-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.rhel.7-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.ubuntu.14.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.ubuntu.16.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "runtime.opensuse.13.2-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "b3pthNgxxFcD+Pc0WSEoC0+md3MyhRS6aCEeenvNE3Fdw1HyJ18ZhRFVJJzIeR/O/jpxPboB805Ho0T3Ul7w8A==" + }, + "runtime.opensuse.42.1-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "KeLz4HClKf+nFS7p/6Fi/CqyLXh81FpiGzcmuS8DGi9lUqSnZ6Es23/gv2O+1XVGfrbNmviF7CckBpavkBoIFQ==" + }, + "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.Apple": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "kVXCuMTrTlxq4XOOMAysuNwsXWpYeboGddNGpIgNSZmv1b6r/s/DPk0fYMB7Q5Qo4bY68o48jt4T4y5BVecbCQ==" + }, + "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "X7IdhILzr4ROXd8mI1BUCQMSHSQwelUlBjF1JyTKCjXaOGn2fB4EKBxQbCK2VjO3WaWIdlXZL3W6TiIVnrhX4g==" + }, + "runtime.rhel.7-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "nyFNiCk/r+VOiIqreLix8yN+q3Wga9+SE8BCgkf+2BwEKiNx6DyvFjCgkfV743/grxv8jHJ8gUK4XEQw7yzRYg==" + }, + "runtime.ubuntu.14.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "ytoewC6wGorL7KoCAvRfsgoJPJbNq+64k2SqW6JcOAebWsFUvCCYgfzQMrnpvPiEl4OrblUlhF2ji+Q1+SVLrQ==" + }, + "runtime.ubuntu.16.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "I8bKw2I8k58Wx7fMKQJn2R8lamboCAiHfHeV/pS65ScKWMMI0+wJkLYlEKvgW1D/XvSl/221clBoR2q9QNNM7A==" + }, + "runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "VB5cn/7OzUfzdnC8tqAIMQciVLiq2epm2NrAm1E9OjNRyG4lVhfR61SMcLizejzQP8R8Uf/0l5qOIbUEi+RdEg==" + }, + "SharpDX": { + "type": "Transitive", + "resolved": "4.2.0", + "contentHash": "3pv0LFMvfK/dv1qISJnn8xBeeT6R/FRvr0EV4KI2DGsL84Qlv6P7isWqxGyU0LCwlSVCJN3jgHJ4Bl0KI2PJww==", + "dependencies": { + "NETStandard.Library": "1.6.1" + } + }, + "SharpDX.D3DCompiler": { + "type": "Transitive", + "resolved": "4.2.0", + "contentHash": "Rnsd6Ilp127xbXqhTit8WKFQUrXwWxqVGpglyWDNkIBCk0tWXNQEjrJpsl0KAObzyZaa33+EXAikLVt5fnd3GA==", + "dependencies": { + "NETStandard.Library": "1.6.1", + "SharpDX": "4.2.0" + } + }, + "SharpDX.Direct2D1": { + "type": "Transitive", + "resolved": "4.2.0", + "contentHash": "Qs8LzDMaQf1u3KB8ArHu9pDv6itZ++QXs99a/bVAG+nKr0Hx5NG4mcN5vsfE0mVR2TkeHfeUm4PksRah6VUPtA==", + "dependencies": { + "NETStandard.Library": "1.6.1", + "SharpDX": "4.2.0", + "SharpDX.DXGI": "4.2.0" + } + }, + "SharpDX.Direct3D11": { + "type": "Transitive", + "resolved": "4.2.0", + "contentHash": "oTm/iT5X/IIuJ8kNYP+DTC/MhBhqtRF5dbgPPFgLBdQv0BKzNTzXQQXd7SveBFjQg6hXEAJ2jGCAzNYvGFc9LA==", + "dependencies": { + "NETStandard.Library": "1.6.1", + "SharpDX": "4.2.0", + "SharpDX.DXGI": "4.2.0" + } + }, + "SharpDX.DXGI": { + "type": "Transitive", + "resolved": "4.2.0", + "contentHash": "UjKqkgWc8U+SP+j3LBzFP6OB6Ntapjih7Xo+g1rLcsGbIb5KwewBrBChaUu7sil8rWoeVU/k0EJd3SMN4VqNZw==", + "dependencies": { + "NETStandard.Library": "1.6.1", + "SharpDX": "4.2.0" + } + }, + "SharpDX.Mathematics": { + "type": "Transitive", + "resolved": "4.2.0", + "contentHash": "R2pcKLgdsP9p5WyTjHmGOZ0ka0zASAZYc6P4L6rSvjYhf6klGYbent7MiVwbkwkt9dD44p5brjy5IwAnVONWGw==", + "dependencies": { + "NETStandard.Library": "1.6.1", + "SharpDX": "4.2.0" + } + }, + "System.AppContext": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "fKC+rmaLfeIzUhagxY17Q9siv/sPrjjKcfNg1Ic8IlQkZLipo8ljcaZQu4VtI4Jqbzjc2VTjzGLF6WmsRXAEgA==", + "dependencies": { + "System.Runtime": "4.3.0" + } + }, + "System.Buffers": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "ratu44uTIHgeBeI0dE8DWvmXVBSo4u7ozRZZHOMmK/JPpYyo0dAfgSiHlpiObMQ5lEtEyIXA40sKRYg5J6A8uQ==", + "dependencies": { + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.Tracing": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Collections": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "3Dcj85/TBdVpL5Zr+gEEBUuFe2icOnLalmEh9hfck1PTYbbyWuZgh4fmm2ysCLTrqLQw6t3TgTyJ+VLp+Qb+Lw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Collections.Concurrent": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "ztl69Xp0Y/UXCL+3v3tEU+lIy+bvjKNUmopn1wep/a291pVPK7dxBd6T7WnlQqRog+d1a/hSsgRsmFnIBKTPLQ==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.Tracing": "4.3.0", + "System.Globalization": "4.3.0", + "System.Reflection": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.Console": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "DHDrIxiqk1h03m6khKWV2X8p/uvN79rgSqpilL6uzpmSfxfU5ng8VcPtW4qsDsQDHiTv6IPV9TmD5M/vElPNLg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.IO": "4.3.0", + "System.Runtime": "4.3.0", + "System.Text.Encoding": "4.3.0" + } + }, + "System.Diagnostics.Debug": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "ZUhUOdqmaG5Jk3Xdb8xi5kIyQYAA4PnTNlHx1mu9ZY3qv4ELIdKbnL/akbGaKi2RnNUWaZsAs31rvzFdewTj2g==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Diagnostics.DiagnosticSource": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "tD6kosZnTAGdrEa0tZSuFyunMbt/5KYDnHdndJYGqZoNy00XVXyACd5d6KnE1YgYv3ne2CjtAfNXo/fwEhnKUA==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Tracing": "4.3.0", + "System.Reflection": "4.3.0", + "System.Runtime": "4.3.0", + "System.Threading": "4.3.0" + } + }, "System.Diagnostics.EventLog": { "type": "Transitive", "resolved": "9.0.3", "contentHash": "0nDJBZ06DVdTG2vvCZ4XjazLVaFawdT0pnji23ISX8I8fEOlRJyzH2I0kWiAbCtFwry2Zir4qE4l/GStLATfFw==" }, + "System.Diagnostics.Tools": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "UUvkJfSYJMM6x527dJg2VyWPSRqIVB0Z7dbjHst1zmwTXz5CcXSYJFWRpuigfbO1Lf7yfZiIaEUesfnl/g5EyA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Diagnostics.Tracing": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "rswfv0f/Cqkh78rA5S8eN8Neocz234+emGCtTF3lxPY96F+mmmUen6tbn0glN6PMvlKQb9bPAY5e9u7fgPTkKw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Globalization": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "kYdVd2f2PAdFGblzFswE4hkNANJBKRmsfa2X5LG2AcWE1c7/4t0pYae1L8vfZ5xvE2nK/R9JprtToA61OSHWIg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Globalization.Calendars": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "GUlBtdOWT4LTV3I+9/PJW+56AnnChTaOqqTLFtdmype/L500M2LIyXgmtd9X2P2VOkmJd5c67H5SaC2QcL1bFA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Globalization": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Globalization.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "FhKmdR6MPG+pxow6wGtNAWdZh7noIOpdD5TwQ3CprzgIE1bBBoim0vbR1+AWsWjQmU7zXHgQo4TWSP6lCeiWcQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Globalization": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.InteropServices": "4.3.0" + } + }, + "System.IO": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "3qjaHvxQPDpSOYICjUoTsmoq5u6QJAFRUITgeT/4gqkF1bajbSmb1kwSxEA8AHlofqgcKJcM8udgieRNhaJ5Cg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.IO.Compression": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "YHndyoiV90iu4iKG115ibkhrG+S3jBm8Ap9OwoUAzO5oPDAWcr0SFwQFm0HjM8WkEZWo0zvLTyLmbvTkW1bXgg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Buffers": "4.3.0", + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "runtime.native.System": "4.3.0", + "runtime.native.System.IO.Compression": "4.3.0" + } + }, + "System.IO.Compression.ZipFile": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "G4HwjEsgIwy3JFBduZ9quBkAu+eUwjIdJleuNSgmUojbH6O3mlvEIme+GHx/cLlTAPcrnnL7GqvB9pTlWRfhOg==", + "dependencies": { + "System.Buffers": "4.3.0", + "System.IO": "4.3.0", + "System.IO.Compression": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Text.Encoding": "4.3.0" + } + }, + "System.IO.FileSystem": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "3wEMARTnuio+ulnvi+hkRNROYwa1kylvYahhcLk4HSoVdl+xxTFVeVlYOfLwrDPImGls0mDqbMhrza8qnWPTdA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.IO": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.IO.FileSystem.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "6QOb2XFLch7bEc4lIcJH49nJN2HV+OC3fHDgsLVsBVBk3Y4hFAnOBGzJ2lUu7CyDDFo9IBWkSsnbkT6IBwwiMw==", + "dependencies": { + "System.Runtime": "4.3.0" + } + }, + "System.Linq": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "5DbqIUpsDp0dFftytzuMmc0oeMdQwjcP/EWxsksIz/w1TcFRkZ3yKKz0PqiYFMmEwPSWw+qNVqD7PJ889JzHbw==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0" + } + }, + "System.Linq.Expressions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "PGKkrd2khG4CnlyJwxwwaWWiSiWFNBGlgXvJpeO0xCXrZ89ODrQ6tjEWS/kOqZ8GwEOUATtKtzp1eRgmYNfclg==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.Linq": "4.3.0", + "System.ObjectModel": "4.3.0", + "System.Reflection": "4.3.0", + "System.Reflection.Emit": "4.3.0", + "System.Reflection.Emit.ILGeneration": "4.3.0", + "System.Reflection.Emit.Lightweight": "4.3.0", + "System.Reflection.Extensions": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Reflection.TypeExtensions": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Threading": "4.3.0" + } + }, "System.Memory": { "type": "Transitive", "resolved": "4.5.5", "contentHash": "XIWiDvKPXaTveaB7HVganDlOCRoj03l+jrwNvcge/t8vhGYKvqV+dMv6G4SAX2NoNmN0wZfVPTAlFwZcZvVOUw==" }, + "System.Net.Http": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "sYg+FtILtRQuYWSIAuNOELwVuVsxVyJGWQyOnlAzhV4xvhyFnON1bAzYYC+jjRW8JREM45R0R5Dgi8MTC5sEwA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.DiagnosticSource": "4.3.0", + "System.Diagnostics.Tracing": "4.3.0", + "System.Globalization": "4.3.0", + "System.Globalization.Extensions": "4.3.0", + "System.IO": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.Net.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.OpenSsl": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Security.Cryptography.X509Certificates": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "runtime.native.System": "4.3.0", + "runtime.native.System.Net.Http": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Net.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "qOu+hDwFwoZPbzPvwut2qATe3ygjeQBDQj91xlsaqGFQUI5i4ZnZb8yyQuLGpDGivEPIt8EJkd1BVzVoP31FXA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "System.Runtime.Handles": "4.3.0" + } + }, "System.Net.ServerSentEvents": { "type": "Transitive", "resolved": "9.0.3", "contentHash": "Vs/C2V27bjtwLqYag9ATzHilcUn8VQTICre4jSBMGFUeSTxEZffTjb+xZwjcmPsVAjmSZmBI5N7Ezq8UFvqQQg==" }, + "System.Net.Sockets": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "m6icV6TqQOAdgt5N/9I5KNpjom/5NFtkmGseEH+AK/hny8XrytLH3+b5M8zL/Ycg3fhIocFpUMyl/wpFnVRvdw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.IO": "4.3.0", + "System.Net.Primitives": "4.3.0", + "System.Runtime": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.ObjectModel": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "bdX+80eKv9bN6K4N+d77OankKHGn6CH711a6fcOpMQu2Fckp/Ft4L/kW9WznHpyR0NRAvJutzOMHNNlBGvxQzQ==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Reflection": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "KMiAFoW7MfJGa9nDFNcfu+FpEdiHpWgTcS2HdMpDvt9saK3y/G4GwprPyzqjFH9NTaGPQeWNHU+iDlDILj96aQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.IO": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.Emit": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "228FG0jLcIwTVJyz8CLFKueVqQK36ANazUManGaJHkO0icjiIypKW7YLWLIWahyIkdh5M7mV2dJepllLyA1SKg==", + "dependencies": { + "System.IO": "4.3.0", + "System.Reflection": "4.3.0", + "System.Reflection.Emit.ILGeneration": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.Emit.ILGeneration": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "59tBslAk9733NXLrUJrwNZEzbMAcu8k344OYo+wfSVygcgZ9lgBdGIzH/nrg3LYhXceynyvTc8t5/GD4Ri0/ng==", + "dependencies": { + "System.Reflection": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.Emit.Lightweight": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "oadVHGSMsTmZsAF864QYN1t1QzZjIcuKU3l2S9cZOwDdDueNTrqq1yRj7koFfIGEnKpt6NjpL3rOzRhs4ryOgA==", + "dependencies": { + "System.Reflection": "4.3.0", + "System.Reflection.Emit.ILGeneration": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "rJkrJD3kBI5B712aRu4DpSIiHRtr6QlfZSQsb0hYHrDCZORXCFjQfoipo2LaMUHoT9i1B7j7MnfaEKWDFmFQNQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Reflection": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "5RXItQz5As4xN2/YUDxdpsEkMhvw3e6aNveFXUn4Hl/udNTCNhnKp8lT9fnc3MhvGKh1baak5CovpuQUXHAlIA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.TypeExtensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "7u6ulLcZbyxB5Gq0nMkQttcdBTx57ibzw+4IOXEfR+sXYQoHvjW5LTLyNr8O22UIMrqYbchJQJnos4eooYzYJA==", + "dependencies": { + "System.Reflection": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Resources.ResourceManager": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "/zrcPkkWdZmI4F92gL/TPumP98AVDu/Wxr3CSJGQQ+XN6wbRZcyfSKVoPo17ilb3iOr0cCRqJInGwNMolqhS8A==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Globalization": "4.3.0", + "System.Reflection": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Runtime": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "JufQi0vPQ0xGnAczR13AUFglDyVYt4Kqnz1AZaiKZ5+GICq0/1MH/mO/eAJHt/mHW1zjKBJd7kV26SrxddAhiw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0" + } + }, + "System.Runtime.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "guW0uK0fn5fcJJ1tJVXYd7/1h5F+pea1r7FLSOz/f8vPEqbR2ZAknuRDvTQ8PzAilDveOxNjSfr0CHfIQfFk8g==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Runtime.Handles": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "OKiSUN7DmTWeYb3l51A7EYaeNMnvxwE249YtZz7yooT4gOZhmTjIn48KgSsw2k2lYdLgTKNJw/ZIfSElwDRVgg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Runtime.InteropServices": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "uv1ynXqiMK8mp1GM3jDqPCFN66eJ5w5XNomaK2XD+TuCroNTLFGeZ+WCmBMcBDyTFKou3P6cR6J/QsaqDp7fGQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Reflection": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Handles": "4.3.0" + } + }, + "System.Runtime.InteropServices.RuntimeInformation": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "cbz4YJMqRDR7oLeMRbdYv7mYzc++17lNhScCX0goO2XpGWdvAt60CGN+FHdePUEHCe/Jy9jUlvNAiNdM+7jsOw==", + "dependencies": { + "System.Reflection": "4.3.0", + "System.Reflection.Extensions": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Threading": "4.3.0", + "runtime.native.System": "4.3.0" + } + }, + "System.Runtime.Numerics": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "yMH+MfdzHjy17l2KESnPiF2dwq7T+xLnSJar7slyimAkUh/gTrS9/UQOtv7xarskJ2/XDSNvfLGOBQPjL7PaHQ==", + "dependencies": { + "System.Globalization": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0" + } + }, + "System.Security.Cryptography.Algorithms": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "W1kd2Y8mYSCgc3ULTAZ0hOP2dSdG5YauTb1089T0/kRcN2MpSAW1izOFROrJgxSlMn3ArsgHXagigyi+ibhevg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Runtime.Numerics": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "runtime.native.System.Security.Cryptography.Apple": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Security.Cryptography.Cng": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "03idZOqFlsKRL4W+LuCpJ6dBYDUWReug6lZjBa3uJWnk5sPCUXckocevTaUA8iT/MFSrY/2HXkOt753xQ/cf8g==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0" + } + }, + "System.Security.Cryptography.Csp": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "X4s/FCkEUnRGnwR3aSfVIkldBmtURMhmexALNTwpjklzxWU7yjMk7GHLKOZTNkgnWnE0q7+BCf9N2LVRWxewaA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.IO": "4.3.0", + "System.Reflection": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Security.Cryptography.Encoding": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "1DEWjZZly9ae9C79vFwqaO5kaOlI5q+3/55ohmq/7dpDyDfc8lYe7YVxJUZ5MF/NtbkRjwFRo14yM4OEo9EmDw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.Collections.Concurrent": "4.3.0", + "System.Linq": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "h4CEgOgv5PKVF/HwaHzJRiVboL2THYCou97zpmhjghx5frc7fIvlkY1jL+lnIQyChrJDMNEXS6r7byGif8Cy4w==", + "dependencies": { + "System.Collections": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Runtime.Numerics": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Security.Cryptography.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "7bDIyVFNL/xKeFHjhobUAQqSpJq9YTOpbEs6mR233Et01STBMXNAc/V+BM6dwYGc95gVh/Zf+iVXWzj3mE8DWg==", + "dependencies": { + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.Security.Cryptography.X509Certificates": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "t2Tmu6Y2NtJ2um0RtcuhP7ZdNNxXEgUm2JeoA/0NvlMjAhKCnM1NX07TDl3244mVp3QU6LPEhT3HTtH1uF7IYw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.Globalization.Calendars": "4.3.0", + "System.IO": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Runtime.Numerics": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Cng": "4.3.0", + "System.Security.Cryptography.Csp": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.OpenSsl": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "runtime.native.System": "4.3.0", + "runtime.native.System.Net.Http": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Text.Encoding": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "BiIg+KWaSDOITze6jGQynxg64naAPtqGHBwDrLaCtixsa5bKiR8dpPOHA7ge3C0JJQizJE+sfkz1wV+BAKAYZw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Text.Encoding.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "YVMK0Bt/A43RmwizJoZ22ei2nmrhobgeiYwFzC4YAN+nue8RF6djXDMog0UCn+brerQoYVyaS+ghy9P/MUVcmw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "System.Text.Encoding": "4.3.0" + } + }, + "System.Text.RegularExpressions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "RpT2DA+L660cBt1FssIE9CAGpLFdFPuheB7pLpKpn6ZXNby7jDERe8Ua/Ne2xGiwLVG2JOqziiaVCGDon5sKFA==", + "dependencies": { + "System.Runtime": "4.3.0" + } + }, + "System.Threading": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "VkUS0kOBcUf3Wwm0TSbrevDDZ6BlM+b/HRiapRFWjM5O0NS0LviG0glKmFK+hhPDd1XFeSdU1GmlLhb2CoVpIw==", + "dependencies": { + "System.Runtime": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, "System.Threading.Channels": { "type": "Transitive", "resolved": "9.0.3", "contentHash": "Ao0iegVONKYVw0eWxJv0ArtMVfkFjgyyYKtUXru6xX5H95flSZWW3QCavD4PAgwpc0ETP38kGHaYbPzSE7sw2w==" }, + "System.Threading.Tasks": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "LbSxKEdOUhVe8BezB/9uOGGppt+nZf6e1VFyw6v3DN6lqitm0OSn2uXMOdtP0M3W4iMcqcivm2J6UgqiwwnXiA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Threading.Tasks.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "npvJkVKl5rKXrtl1Kkm6OhOUaYGEiF9wFbppFRWSMoApKzt2PiPHT2Bb8a5sAWxprvdOAtvaARS9QYMznEUtug==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Runtime": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.Threading.Timer": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "Z6YfyYTCg7lOZjJzBjONJTFKGN9/NIYKSxhU5GRd+DTwHSZyvWp1xuI5aR+dLg+ayyC5Xv57KiY4oJ0tMO89fQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Xml.ReaderWriter": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "GrprA+Z0RUXaR4N7/eW71j1rgMnEnEVlgii49GZyAjTH7uliMnrOU3HNFBr6fEDBCJCIdlVNq9hHbaDR621XBA==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Text.Encoding.Extensions": "4.3.0", + "System.Text.RegularExpressions": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "System.Threading.Tasks.Extensions": "4.3.0" + } + }, + "System.Xml.XDocument": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "5zJ0XDxAIg8iy+t4aMnQAu0MqVbqyvfoUVl1yDV61xdo3Vth45oA2FoY4pPkxYAH5f8ixpmTqXeEIya95x0aCQ==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.Tools": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.Reflection": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "System.Xml.ReaderWriter": "4.3.0" + } + }, "lightlesssync.api": { "type": "Project", "dependencies": { @@ -569,6 +1522,15 @@ }, "penumbra.string": { "type": "Project" + }, + "pictomancy": { + "type": "Project", + "dependencies": { + "SharpDX.D3DCompiler": "[4.2.0, )", + "SharpDX.Direct2D1": "[4.2.0, )", + "SharpDX.Direct3D11": "[4.2.0, )", + "SharpDX.Mathematics": "[4.2.0, )" + } } } } diff --git a/Pictomancy/Pictomancy/packages.lock.json b/Pictomancy/Pictomancy/packages.lock.json new file mode 100644 index 0000000..95a3cd4 --- /dev/null +++ b/Pictomancy/Pictomancy/packages.lock.json @@ -0,0 +1,970 @@ +{ + "version": 1, + "dependencies": { + "net9.0-windows7.0": { + "DotNet.ReproducibleBuilds": { + "type": "Direct", + "requested": "[1.2.25, )", + "resolved": "1.2.25", + "contentHash": "xCXiw7BCxHJ8pF6wPepRUddlh2dlQlbr81gXA72hdk4FLHkKXas7EH/n+fk5UCA/YfMqG1Z6XaPiUjDbUNBUzg==" + }, + "SharpDX.D3DCompiler": { + "type": "Direct", + "requested": "[4.2.0, )", + "resolved": "4.2.0", + "contentHash": "Rnsd6Ilp127xbXqhTit8WKFQUrXwWxqVGpglyWDNkIBCk0tWXNQEjrJpsl0KAObzyZaa33+EXAikLVt5fnd3GA==", + "dependencies": { + "NETStandard.Library": "1.6.1", + "SharpDX": "4.2.0" + } + }, + "SharpDX.Direct2D1": { + "type": "Direct", + "requested": "[4.2.0, )", + "resolved": "4.2.0", + "contentHash": "Qs8LzDMaQf1u3KB8ArHu9pDv6itZ++QXs99a/bVAG+nKr0Hx5NG4mcN5vsfE0mVR2TkeHfeUm4PksRah6VUPtA==", + "dependencies": { + "NETStandard.Library": "1.6.1", + "SharpDX": "4.2.0", + "SharpDX.DXGI": "4.2.0" + } + }, + "SharpDX.Direct3D11": { + "type": "Direct", + "requested": "[4.2.0, )", + "resolved": "4.2.0", + "contentHash": "oTm/iT5X/IIuJ8kNYP+DTC/MhBhqtRF5dbgPPFgLBdQv0BKzNTzXQQXd7SveBFjQg6hXEAJ2jGCAzNYvGFc9LA==", + "dependencies": { + "NETStandard.Library": "1.6.1", + "SharpDX": "4.2.0", + "SharpDX.DXGI": "4.2.0" + } + }, + "SharpDX.Mathematics": { + "type": "Direct", + "requested": "[4.2.0, )", + "resolved": "4.2.0", + "contentHash": "R2pcKLgdsP9p5WyTjHmGOZ0ka0zASAZYc6P4L6rSvjYhf6klGYbent7MiVwbkwkt9dD44p5brjy5IwAnVONWGw==", + "dependencies": { + "NETStandard.Library": "1.6.1", + "SharpDX": "4.2.0" + } + }, + "Microsoft.NETCore.Platforms": { + "type": "Transitive", + "resolved": "1.1.0", + "contentHash": "kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A==" + }, + "Microsoft.NETCore.Targets": { + "type": "Transitive", + "resolved": "1.1.0", + "contentHash": "aOZA3BWfz9RXjpzt0sRJJMjAscAUm3Hoa4UWAfceV9UTYxgwZ1lZt5nO2myFf+/jetYQo4uTP7zS8sJY67BBxg==" + }, + "Microsoft.Win32.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "9ZQKCWxH7Ijp9BfahvL2Zyf1cJIk8XYLF6Yjzr2yi0b2cOut/HQ31qf1ThHAgCc3WiZMdnWcfJCgN82/0UunxA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "NETStandard.Library": { + "type": "Transitive", + "resolved": "1.6.1", + "contentHash": "WcSp3+vP+yHNgS8EV5J7pZ9IRpeDuARBPN28by8zqff1wJQXm26PVU8L3/fYLBJVU7BtDyqNVWq2KlCVvSSR4A==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.Win32.Primitives": "4.3.0", + "System.AppContext": "4.3.0", + "System.Collections": "4.3.0", + "System.Collections.Concurrent": "4.3.0", + "System.Console": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.Tools": "4.3.0", + "System.Diagnostics.Tracing": "4.3.0", + "System.Globalization": "4.3.0", + "System.Globalization.Calendars": "4.3.0", + "System.IO": "4.3.0", + "System.IO.Compression": "4.3.0", + "System.IO.Compression.ZipFile": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Linq": "4.3.0", + "System.Linq.Expressions": "4.3.0", + "System.Net.Http": "4.3.0", + "System.Net.Primitives": "4.3.0", + "System.Net.Sockets": "4.3.0", + "System.ObjectModel": "4.3.0", + "System.Reflection": "4.3.0", + "System.Reflection.Extensions": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Runtime.InteropServices.RuntimeInformation": "4.3.0", + "System.Runtime.Numerics": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Security.Cryptography.X509Certificates": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Text.Encoding.Extensions": "4.3.0", + "System.Text.RegularExpressions": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "System.Threading.Timer": "4.3.0", + "System.Xml.ReaderWriter": "4.3.0", + "System.Xml.XDocument": "4.3.0" + } + }, + "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "HdSSp5MnJSsg08KMfZThpuLPJpPwE5hBXvHwoKWosyHHfe8Mh5WKT0ylEOf6yNzX6Ngjxe4Whkafh5q7Ymac4Q==" + }, + "runtime.fedora.23-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "+yH1a49wJMy8Zt4yx5RhJrxO/DBDByAiCzNwiETI+1S4mPdCu0OY4djdciC7Vssk0l22wQaDLrXxXkp+3+7bVA==" + }, + "runtime.fedora.24-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "c3YNH1GQJbfIPJeCnr4avseugSqPrxwIqzthYyZDN6EuOyNOzq+y2KSUfRcXauya1sF4foESTgwM5e1A8arAKw==" + }, + "runtime.native.System": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "c/qWt2LieNZIj1jGnVNsE2Kl23Ya2aSTBuXMD6V7k9KWr6l16Tqdwq+hJScEpWER9753NWC8h96PaVNY5Ld7Jw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0" + } + }, + "runtime.native.System.IO.Compression": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "INBPonS5QPEgn7naufQFXJEp3zX6L4bwHgJ/ZH78aBTpeNfQMtf7C6VrAFhlq2xxWBveIOWyFzQjJ8XzHMhdOQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0" + } + }, + "runtime.native.System.Net.Http": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "ZVuZJqnnegJhd2k/PtAbbIcZ3aZeITq3sj06oKfMBSfphW3HDmk/t4ObvbOk/JA/swGR0LNqMksAh/f7gpTROg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0" + } + }, + "runtime.native.System.Security.Cryptography.Apple": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "DloMk88juo0OuOWr56QG7MNchmafTLYWvABy36izkrLI5VledI0rq28KGs1i9wbpeT9NPQrx/wTf8U2vazqQ3Q==", + "dependencies": { + "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.Apple": "4.3.0" + } + }, + "runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "NS1U+700m4KFRHR5o4vo9DSlTmlCKu/u7dtE5sUHVIPB+xpXxYQvgBgA6wEIeCz6Yfn0Z52/72WYsToCEPJnrw==", + "dependencies": { + "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.fedora.23-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.fedora.24-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.opensuse.13.2-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.opensuse.42.1-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.rhel.7-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.ubuntu.14.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.ubuntu.16.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "runtime.opensuse.13.2-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "b3pthNgxxFcD+Pc0WSEoC0+md3MyhRS6aCEeenvNE3Fdw1HyJ18ZhRFVJJzIeR/O/jpxPboB805Ho0T3Ul7w8A==" + }, + "runtime.opensuse.42.1-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "KeLz4HClKf+nFS7p/6Fi/CqyLXh81FpiGzcmuS8DGi9lUqSnZ6Es23/gv2O+1XVGfrbNmviF7CckBpavkBoIFQ==" + }, + "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.Apple": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "kVXCuMTrTlxq4XOOMAysuNwsXWpYeboGddNGpIgNSZmv1b6r/s/DPk0fYMB7Q5Qo4bY68o48jt4T4y5BVecbCQ==" + }, + "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "X7IdhILzr4ROXd8mI1BUCQMSHSQwelUlBjF1JyTKCjXaOGn2fB4EKBxQbCK2VjO3WaWIdlXZL3W6TiIVnrhX4g==" + }, + "runtime.rhel.7-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "nyFNiCk/r+VOiIqreLix8yN+q3Wga9+SE8BCgkf+2BwEKiNx6DyvFjCgkfV743/grxv8jHJ8gUK4XEQw7yzRYg==" + }, + "runtime.ubuntu.14.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "ytoewC6wGorL7KoCAvRfsgoJPJbNq+64k2SqW6JcOAebWsFUvCCYgfzQMrnpvPiEl4OrblUlhF2ji+Q1+SVLrQ==" + }, + "runtime.ubuntu.16.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "I8bKw2I8k58Wx7fMKQJn2R8lamboCAiHfHeV/pS65ScKWMMI0+wJkLYlEKvgW1D/XvSl/221clBoR2q9QNNM7A==" + }, + "runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "VB5cn/7OzUfzdnC8tqAIMQciVLiq2epm2NrAm1E9OjNRyG4lVhfR61SMcLizejzQP8R8Uf/0l5qOIbUEi+RdEg==" + }, + "SharpDX": { + "type": "Transitive", + "resolved": "4.2.0", + "contentHash": "3pv0LFMvfK/dv1qISJnn8xBeeT6R/FRvr0EV4KI2DGsL84Qlv6P7isWqxGyU0LCwlSVCJN3jgHJ4Bl0KI2PJww==", + "dependencies": { + "NETStandard.Library": "1.6.1" + } + }, + "SharpDX.DXGI": { + "type": "Transitive", + "resolved": "4.2.0", + "contentHash": "UjKqkgWc8U+SP+j3LBzFP6OB6Ntapjih7Xo+g1rLcsGbIb5KwewBrBChaUu7sil8rWoeVU/k0EJd3SMN4VqNZw==", + "dependencies": { + "NETStandard.Library": "1.6.1", + "SharpDX": "4.2.0" + } + }, + "System.AppContext": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "fKC+rmaLfeIzUhagxY17Q9siv/sPrjjKcfNg1Ic8IlQkZLipo8ljcaZQu4VtI4Jqbzjc2VTjzGLF6WmsRXAEgA==", + "dependencies": { + "System.Runtime": "4.3.0" + } + }, + "System.Buffers": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "ratu44uTIHgeBeI0dE8DWvmXVBSo4u7ozRZZHOMmK/JPpYyo0dAfgSiHlpiObMQ5lEtEyIXA40sKRYg5J6A8uQ==", + "dependencies": { + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.Tracing": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Collections": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "3Dcj85/TBdVpL5Zr+gEEBUuFe2icOnLalmEh9hfck1PTYbbyWuZgh4fmm2ysCLTrqLQw6t3TgTyJ+VLp+Qb+Lw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Collections.Concurrent": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "ztl69Xp0Y/UXCL+3v3tEU+lIy+bvjKNUmopn1wep/a291pVPK7dxBd6T7WnlQqRog+d1a/hSsgRsmFnIBKTPLQ==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.Tracing": "4.3.0", + "System.Globalization": "4.3.0", + "System.Reflection": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.Console": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "DHDrIxiqk1h03m6khKWV2X8p/uvN79rgSqpilL6uzpmSfxfU5ng8VcPtW4qsDsQDHiTv6IPV9TmD5M/vElPNLg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.IO": "4.3.0", + "System.Runtime": "4.3.0", + "System.Text.Encoding": "4.3.0" + } + }, + "System.Diagnostics.Debug": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "ZUhUOdqmaG5Jk3Xdb8xi5kIyQYAA4PnTNlHx1mu9ZY3qv4ELIdKbnL/akbGaKi2RnNUWaZsAs31rvzFdewTj2g==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Diagnostics.DiagnosticSource": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "tD6kosZnTAGdrEa0tZSuFyunMbt/5KYDnHdndJYGqZoNy00XVXyACd5d6KnE1YgYv3ne2CjtAfNXo/fwEhnKUA==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Tracing": "4.3.0", + "System.Reflection": "4.3.0", + "System.Runtime": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Diagnostics.Tools": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "UUvkJfSYJMM6x527dJg2VyWPSRqIVB0Z7dbjHst1zmwTXz5CcXSYJFWRpuigfbO1Lf7yfZiIaEUesfnl/g5EyA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Diagnostics.Tracing": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "rswfv0f/Cqkh78rA5S8eN8Neocz234+emGCtTF3lxPY96F+mmmUen6tbn0glN6PMvlKQb9bPAY5e9u7fgPTkKw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Globalization": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "kYdVd2f2PAdFGblzFswE4hkNANJBKRmsfa2X5LG2AcWE1c7/4t0pYae1L8vfZ5xvE2nK/R9JprtToA61OSHWIg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Globalization.Calendars": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "GUlBtdOWT4LTV3I+9/PJW+56AnnChTaOqqTLFtdmype/L500M2LIyXgmtd9X2P2VOkmJd5c67H5SaC2QcL1bFA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Globalization": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Globalization.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "FhKmdR6MPG+pxow6wGtNAWdZh7noIOpdD5TwQ3CprzgIE1bBBoim0vbR1+AWsWjQmU7zXHgQo4TWSP6lCeiWcQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Globalization": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.InteropServices": "4.3.0" + } + }, + "System.IO": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "3qjaHvxQPDpSOYICjUoTsmoq5u6QJAFRUITgeT/4gqkF1bajbSmb1kwSxEA8AHlofqgcKJcM8udgieRNhaJ5Cg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.IO.Compression": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "YHndyoiV90iu4iKG115ibkhrG+S3jBm8Ap9OwoUAzO5oPDAWcr0SFwQFm0HjM8WkEZWo0zvLTyLmbvTkW1bXgg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Buffers": "4.3.0", + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "runtime.native.System": "4.3.0", + "runtime.native.System.IO.Compression": "4.3.0" + } + }, + "System.IO.Compression.ZipFile": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "G4HwjEsgIwy3JFBduZ9quBkAu+eUwjIdJleuNSgmUojbH6O3mlvEIme+GHx/cLlTAPcrnnL7GqvB9pTlWRfhOg==", + "dependencies": { + "System.Buffers": "4.3.0", + "System.IO": "4.3.0", + "System.IO.Compression": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Text.Encoding": "4.3.0" + } + }, + "System.IO.FileSystem": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "3wEMARTnuio+ulnvi+hkRNROYwa1kylvYahhcLk4HSoVdl+xxTFVeVlYOfLwrDPImGls0mDqbMhrza8qnWPTdA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.IO": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.IO.FileSystem.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "6QOb2XFLch7bEc4lIcJH49nJN2HV+OC3fHDgsLVsBVBk3Y4hFAnOBGzJ2lUu7CyDDFo9IBWkSsnbkT6IBwwiMw==", + "dependencies": { + "System.Runtime": "4.3.0" + } + }, + "System.Linq": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "5DbqIUpsDp0dFftytzuMmc0oeMdQwjcP/EWxsksIz/w1TcFRkZ3yKKz0PqiYFMmEwPSWw+qNVqD7PJ889JzHbw==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0" + } + }, + "System.Linq.Expressions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "PGKkrd2khG4CnlyJwxwwaWWiSiWFNBGlgXvJpeO0xCXrZ89ODrQ6tjEWS/kOqZ8GwEOUATtKtzp1eRgmYNfclg==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.Linq": "4.3.0", + "System.ObjectModel": "4.3.0", + "System.Reflection": "4.3.0", + "System.Reflection.Emit": "4.3.0", + "System.Reflection.Emit.ILGeneration": "4.3.0", + "System.Reflection.Emit.Lightweight": "4.3.0", + "System.Reflection.Extensions": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Reflection.TypeExtensions": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Net.Http": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "sYg+FtILtRQuYWSIAuNOELwVuVsxVyJGWQyOnlAzhV4xvhyFnON1bAzYYC+jjRW8JREM45R0R5Dgi8MTC5sEwA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.DiagnosticSource": "4.3.0", + "System.Diagnostics.Tracing": "4.3.0", + "System.Globalization": "4.3.0", + "System.Globalization.Extensions": "4.3.0", + "System.IO": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.Net.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.OpenSsl": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Security.Cryptography.X509Certificates": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "runtime.native.System": "4.3.0", + "runtime.native.System.Net.Http": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Net.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "qOu+hDwFwoZPbzPvwut2qATe3ygjeQBDQj91xlsaqGFQUI5i4ZnZb8yyQuLGpDGivEPIt8EJkd1BVzVoP31FXA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "System.Runtime.Handles": "4.3.0" + } + }, + "System.Net.Sockets": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "m6icV6TqQOAdgt5N/9I5KNpjom/5NFtkmGseEH+AK/hny8XrytLH3+b5M8zL/Ycg3fhIocFpUMyl/wpFnVRvdw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.IO": "4.3.0", + "System.Net.Primitives": "4.3.0", + "System.Runtime": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.ObjectModel": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "bdX+80eKv9bN6K4N+d77OankKHGn6CH711a6fcOpMQu2Fckp/Ft4L/kW9WznHpyR0NRAvJutzOMHNNlBGvxQzQ==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Reflection": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "KMiAFoW7MfJGa9nDFNcfu+FpEdiHpWgTcS2HdMpDvt9saK3y/G4GwprPyzqjFH9NTaGPQeWNHU+iDlDILj96aQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.IO": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.Emit": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "228FG0jLcIwTVJyz8CLFKueVqQK36ANazUManGaJHkO0icjiIypKW7YLWLIWahyIkdh5M7mV2dJepllLyA1SKg==", + "dependencies": { + "System.IO": "4.3.0", + "System.Reflection": "4.3.0", + "System.Reflection.Emit.ILGeneration": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.Emit.ILGeneration": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "59tBslAk9733NXLrUJrwNZEzbMAcu8k344OYo+wfSVygcgZ9lgBdGIzH/nrg3LYhXceynyvTc8t5/GD4Ri0/ng==", + "dependencies": { + "System.Reflection": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.Emit.Lightweight": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "oadVHGSMsTmZsAF864QYN1t1QzZjIcuKU3l2S9cZOwDdDueNTrqq1yRj7koFfIGEnKpt6NjpL3rOzRhs4ryOgA==", + "dependencies": { + "System.Reflection": "4.3.0", + "System.Reflection.Emit.ILGeneration": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "rJkrJD3kBI5B712aRu4DpSIiHRtr6QlfZSQsb0hYHrDCZORXCFjQfoipo2LaMUHoT9i1B7j7MnfaEKWDFmFQNQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Reflection": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "5RXItQz5As4xN2/YUDxdpsEkMhvw3e6aNveFXUn4Hl/udNTCNhnKp8lT9fnc3MhvGKh1baak5CovpuQUXHAlIA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.TypeExtensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "7u6ulLcZbyxB5Gq0nMkQttcdBTx57ibzw+4IOXEfR+sXYQoHvjW5LTLyNr8O22UIMrqYbchJQJnos4eooYzYJA==", + "dependencies": { + "System.Reflection": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Resources.ResourceManager": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "/zrcPkkWdZmI4F92gL/TPumP98AVDu/Wxr3CSJGQQ+XN6wbRZcyfSKVoPo17ilb3iOr0cCRqJInGwNMolqhS8A==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Globalization": "4.3.0", + "System.Reflection": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Runtime": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "JufQi0vPQ0xGnAczR13AUFglDyVYt4Kqnz1AZaiKZ5+GICq0/1MH/mO/eAJHt/mHW1zjKBJd7kV26SrxddAhiw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0" + } + }, + "System.Runtime.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "guW0uK0fn5fcJJ1tJVXYd7/1h5F+pea1r7FLSOz/f8vPEqbR2ZAknuRDvTQ8PzAilDveOxNjSfr0CHfIQfFk8g==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Runtime.Handles": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "OKiSUN7DmTWeYb3l51A7EYaeNMnvxwE249YtZz7yooT4gOZhmTjIn48KgSsw2k2lYdLgTKNJw/ZIfSElwDRVgg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Runtime.InteropServices": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "uv1ynXqiMK8mp1GM3jDqPCFN66eJ5w5XNomaK2XD+TuCroNTLFGeZ+WCmBMcBDyTFKou3P6cR6J/QsaqDp7fGQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Reflection": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Handles": "4.3.0" + } + }, + "System.Runtime.InteropServices.RuntimeInformation": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "cbz4YJMqRDR7oLeMRbdYv7mYzc++17lNhScCX0goO2XpGWdvAt60CGN+FHdePUEHCe/Jy9jUlvNAiNdM+7jsOw==", + "dependencies": { + "System.Reflection": "4.3.0", + "System.Reflection.Extensions": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Threading": "4.3.0", + "runtime.native.System": "4.3.0" + } + }, + "System.Runtime.Numerics": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "yMH+MfdzHjy17l2KESnPiF2dwq7T+xLnSJar7slyimAkUh/gTrS9/UQOtv7xarskJ2/XDSNvfLGOBQPjL7PaHQ==", + "dependencies": { + "System.Globalization": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0" + } + }, + "System.Security.Cryptography.Algorithms": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "W1kd2Y8mYSCgc3ULTAZ0hOP2dSdG5YauTb1089T0/kRcN2MpSAW1izOFROrJgxSlMn3ArsgHXagigyi+ibhevg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Runtime.Numerics": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "runtime.native.System.Security.Cryptography.Apple": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Security.Cryptography.Cng": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "03idZOqFlsKRL4W+LuCpJ6dBYDUWReug6lZjBa3uJWnk5sPCUXckocevTaUA8iT/MFSrY/2HXkOt753xQ/cf8g==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0" + } + }, + "System.Security.Cryptography.Csp": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "X4s/FCkEUnRGnwR3aSfVIkldBmtURMhmexALNTwpjklzxWU7yjMk7GHLKOZTNkgnWnE0q7+BCf9N2LVRWxewaA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.IO": "4.3.0", + "System.Reflection": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Security.Cryptography.Encoding": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "1DEWjZZly9ae9C79vFwqaO5kaOlI5q+3/55ohmq/7dpDyDfc8lYe7YVxJUZ5MF/NtbkRjwFRo14yM4OEo9EmDw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.Collections.Concurrent": "4.3.0", + "System.Linq": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "h4CEgOgv5PKVF/HwaHzJRiVboL2THYCou97zpmhjghx5frc7fIvlkY1jL+lnIQyChrJDMNEXS6r7byGif8Cy4w==", + "dependencies": { + "System.Collections": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Runtime.Numerics": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Security.Cryptography.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "7bDIyVFNL/xKeFHjhobUAQqSpJq9YTOpbEs6mR233Et01STBMXNAc/V+BM6dwYGc95gVh/Zf+iVXWzj3mE8DWg==", + "dependencies": { + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.Security.Cryptography.X509Certificates": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "t2Tmu6Y2NtJ2um0RtcuhP7ZdNNxXEgUm2JeoA/0NvlMjAhKCnM1NX07TDl3244mVp3QU6LPEhT3HTtH1uF7IYw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.Globalization.Calendars": "4.3.0", + "System.IO": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Runtime.Numerics": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Cng": "4.3.0", + "System.Security.Cryptography.Csp": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.OpenSsl": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "runtime.native.System": "4.3.0", + "runtime.native.System.Net.Http": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Text.Encoding": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "BiIg+KWaSDOITze6jGQynxg64naAPtqGHBwDrLaCtixsa5bKiR8dpPOHA7ge3C0JJQizJE+sfkz1wV+BAKAYZw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Text.Encoding.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "YVMK0Bt/A43RmwizJoZ22ei2nmrhobgeiYwFzC4YAN+nue8RF6djXDMog0UCn+brerQoYVyaS+ghy9P/MUVcmw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "System.Text.Encoding": "4.3.0" + } + }, + "System.Text.RegularExpressions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "RpT2DA+L660cBt1FssIE9CAGpLFdFPuheB7pLpKpn6ZXNby7jDERe8Ua/Ne2xGiwLVG2JOqziiaVCGDon5sKFA==", + "dependencies": { + "System.Runtime": "4.3.0" + } + }, + "System.Threading": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "VkUS0kOBcUf3Wwm0TSbrevDDZ6BlM+b/HRiapRFWjM5O0NS0LviG0glKmFK+hhPDd1XFeSdU1GmlLhb2CoVpIw==", + "dependencies": { + "System.Runtime": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.Threading.Tasks": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "LbSxKEdOUhVe8BezB/9uOGGppt+nZf6e1VFyw6v3DN6lqitm0OSn2uXMOdtP0M3W4iMcqcivm2J6UgqiwwnXiA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Threading.Tasks.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "npvJkVKl5rKXrtl1Kkm6OhOUaYGEiF9wFbppFRWSMoApKzt2PiPHT2Bb8a5sAWxprvdOAtvaARS9QYMznEUtug==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Runtime": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.Threading.Timer": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "Z6YfyYTCg7lOZjJzBjONJTFKGN9/NIYKSxhU5GRd+DTwHSZyvWp1xuI5aR+dLg+ayyC5Xv57KiY4oJ0tMO89fQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Xml.ReaderWriter": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "GrprA+Z0RUXaR4N7/eW71j1rgMnEnEVlgii49GZyAjTH7uliMnrOU3HNFBr6fEDBCJCIdlVNq9hHbaDR621XBA==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Text.Encoding.Extensions": "4.3.0", + "System.Text.RegularExpressions": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "System.Threading.Tasks.Extensions": "4.3.0" + } + }, + "System.Xml.XDocument": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "5zJ0XDxAIg8iy+t4aMnQAu0MqVbqyvfoUVl1yDV61xdo3Vth45oA2FoY4pPkxYAH5f8ixpmTqXeEIya95x0aCQ==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.Tools": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.Reflection": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "System.Xml.ReaderWriter": "4.3.0" + } + } + } + } +} \ No newline at end of file diff --git a/ffxiv_pictomancy b/ffxiv_pictomancy new file mode 160000 index 0000000..788bc33 --- /dev/null +++ b/ffxiv_pictomancy @@ -0,0 +1 @@ +Subproject commit 788bc339a67e7a3db01a47a954034a83b9c3b61b