diff --git a/MareSynchronos/.editorconfig b/MareSynchronos/.editorconfig index f6abd9f..441acd7 100644 --- a/MareSynchronos/.editorconfig +++ b/MareSynchronos/.editorconfig @@ -102,10 +102,13 @@ dotnet_diagnostic.MA0075.severity = silent dotnet_diagnostic.S3358.severity = suggestion # S6678: Use PascalCase for named placeholders -dotnet_diagnostic.S6678.severity = suggestion +dotnet_diagnostic.S6678.severity = none # S6605: Collection-specific "Exists" method should be used instead of the "Any" extension -dotnet_diagnostic.S6605.severity = suggestion +dotnet_diagnostic.S6605.severity = none # S6667: Logging in a catch clause should pass the caught exception as a parameter. dotnet_diagnostic.S6667.severity = suggestion + +# IDE0290: Use primary constructor +csharp_style_prefer_primary_constructors = false diff --git a/MareSynchronos/FileCache/CacheMonitor.cs b/MareSynchronos/FileCache/CacheMonitor.cs index af2e59f..7d9321d 100644 --- a/MareSynchronos/FileCache/CacheMonitor.cs +++ b/MareSynchronos/FileCache/CacheMonitor.cs @@ -5,7 +5,7 @@ using MareSynchronos.Services.Mediator; using MareSynchronos.Utils; using Microsoft.Extensions.Logging; using System.Collections.Concurrent; -using System.Diagnostics; +using System.Collections.Immutable; namespace MareSynchronos.FileCache; @@ -20,13 +20,12 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase private long _currentFileProgress = 0; private CancellationTokenSource _scanCancellationTokenSource = new(); private readonly CancellationTokenSource _periodicCalculationTokenSource = new(); - private readonly string[] _allowedExtensions = [".mdl", ".tex", ".mtrl", ".tmb", ".pap", ".avfx", ".atex", ".sklb", ".eid", ".phyb", ".pbd", ".scd", ".skp", ".shpk"]; + public static readonly IImmutableList AllowedFileExtensions = [".mdl", ".tex", ".mtrl", ".tmb", ".pap", ".avfx", ".atex", ".sklb", ".eid", ".phyb", ".pbd", ".scd", ".skp", ".shpk"]; public CacheMonitor(ILogger logger, IpcManager ipcManager, MareConfigService configService, FileCacheManager fileDbManager, MareMediator mediator, PerformanceCollectorService performanceCollector, DalamudUtilService dalamudUtil, FileCompactor fileCompactor) : base(logger, mediator) { - Logger.LogInformation("Creating CacheMonitor from {trace}", Environment.StackTrace); _ipcManager = ipcManager; _configService = configService; _fileDbManager = fileDbManager; @@ -153,7 +152,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase { Logger.LogTrace("Mare FSW: FileChanged: {change} => {path}", e.ChangeType, e.FullPath); - if (!_allowedExtensions.Any(ext => e.FullPath.EndsWith(ext, StringComparison.OrdinalIgnoreCase))) return; + if (!AllowedFileExtensions.Any(ext => e.FullPath.EndsWith(ext, StringComparison.OrdinalIgnoreCase))) return; lock (_watcherChanges) { @@ -197,7 +196,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase private void Fs_Changed(object sender, FileSystemEventArgs e) { if (Directory.Exists(e.FullPath)) return; - if (!_allowedExtensions.Any(ext => e.FullPath.EndsWith(ext, StringComparison.OrdinalIgnoreCase))) return; + if (!AllowedFileExtensions.Any(ext => e.FullPath.EndsWith(ext, StringComparison.OrdinalIgnoreCase))) return; if (e.ChangeType is not (WatcherChangeTypes.Changed or WatcherChangeTypes.Deleted or WatcherChangeTypes.Created)) return; @@ -221,7 +220,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase { foreach (var file in directoryFiles) { - if (!_allowedExtensions.Any(ext => file.EndsWith(ext, StringComparison.OrdinalIgnoreCase))) continue; + if (!AllowedFileExtensions.Any(ext => file.EndsWith(ext, StringComparison.OrdinalIgnoreCase))) continue; var oldPath = file.Replace(e.FullPath, e.OldFullPath, StringComparison.OrdinalIgnoreCase); _watcherChanges.Remove(oldPath); @@ -233,7 +232,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase } else { - if (!_allowedExtensions.Any(ext => e.FullPath.EndsWith(ext, StringComparison.OrdinalIgnoreCase))) return; + if (!AllowedFileExtensions.Any(ext => e.FullPath.EndsWith(ext, StringComparison.OrdinalIgnoreCase))) return; lock (_watcherChanges) { @@ -499,7 +498,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase [ .. Directory.GetFiles(folder, "*.*", SearchOption.AllDirectories) .AsParallel() - .Where(f => _allowedExtensions.Any(e => f.EndsWith(e, StringComparison.OrdinalIgnoreCase)) + .Where(f => AllowedFileExtensions.Any(e => f.EndsWith(e, StringComparison.OrdinalIgnoreCase)) && !f.Contains(@"\bg\", StringComparison.OrdinalIgnoreCase) && !f.Contains(@"\bgcommon\", StringComparison.OrdinalIgnoreCase) && !f.Contains(@"\ui\", StringComparison.OrdinalIgnoreCase)), diff --git a/MareSynchronos/FileCache/FileCacheManager.cs b/MareSynchronos/FileCache/FileCacheManager.cs index 48cc052..e920155 100644 --- a/MareSynchronos/FileCache/FileCacheManager.cs +++ b/MareSynchronos/FileCache/FileCacheManager.cs @@ -139,18 +139,7 @@ public sealed class FileCacheManager : IHostedService return Path.Combine(_configService.Current.CacheFolder, hash + "." + extension); } - public async Task GetCompressedFileLength(string fileHash, CancellationToken uploadToken) - { - var fileCache = GetFileCacheByHash(fileHash)!.ResolvedFilepath; - using var fs = File.OpenRead(fileCache); - var cs = new CountedStream(Stream.Null); - using var encstream = LZ4Stream.Encode(cs, new LZ4EncoderSettings(){CompressionLevel=K4os.Compression.LZ4.LZ4Level.L09_HC}); - await fs.CopyToAsync(encstream, uploadToken).ConfigureAwait(false); - encstream.Close(); - return uploadToken.IsCancellationRequested ? 0 : cs.BytesWritten; - } - - public async Task GetCompressedFileData(string fileHash, CancellationToken uploadToken) + public async Task<(string, byte[])> GetCompressedFileData(string fileHash, CancellationToken uploadToken) { var fileCache = GetFileCacheByHash(fileHash)!.ResolvedFilepath; using var fs = File.OpenRead(fileCache); @@ -158,7 +147,7 @@ public sealed class FileCacheManager : IHostedService using var encstream = LZ4Stream.Encode(ms, new LZ4EncoderSettings(){CompressionLevel=K4os.Compression.LZ4.LZ4Level.L09_HC}); await fs.CopyToAsync(encstream, uploadToken).ConfigureAwait(false); encstream.Close(); - return ms.ToArray(); + return (fileHash, ms.ToArray()); } public FileCacheEntity? GetFileCacheByHash(string hash) @@ -428,6 +417,13 @@ public sealed class FileCacheManager : IHostedService if (File.Exists(_csvPath)) { + if (!_ipcManager.Penumbra.APIAvailable || string.IsNullOrEmpty(_ipcManager.Penumbra.ModDirectory)) + { + _mareMediator.Publish(new NotificationMessage("Penumbra not connected", + "Could not load local file cache data. Penumbra is not connected or not properly set up. Please enable and/or configure Penumbra properly to use Loporrit. After, reload Loporrit in the Plugin installer.", + MareConfiguration.Models.NotificationType.Error)); + } + _logger.LogInformation("{csvPath} found, parsing", _csvPath); bool success = false; diff --git a/MareSynchronos/FileCache/TransientResourceManager.cs b/MareSynchronos/FileCache/TransientResourceManager.cs index 0328ce1..fff95b9 100644 --- a/MareSynchronos/FileCache/TransientResourceManager.cs +++ b/MareSynchronos/FileCache/TransientResourceManager.cs @@ -11,13 +11,13 @@ namespace MareSynchronos.FileCache; public sealed class TransientResourceManager : DisposableMediatorSubscriberBase { + private readonly object _cacheAdditionLock = new(); private readonly HashSet _cachedHandledPaths = new(StringComparer.Ordinal); private readonly TransientConfigService _configurationService; private readonly DalamudUtilService _dalamudUtil; private readonly string[] _fileTypesToHandle = ["tmb", "pap", "avfx", "atex", "sklb", "eid", "phyb", "scd", "skp", "shpk"]; private readonly HashSet _playerRelatedPointers = []; - private HashSet _cachedFrameAddresses = []; - private readonly object _cacheAdditionLock = new(); + private ConcurrentDictionary _cachedFrameAddresses = []; public TransientResourceManager(ILogger logger, TransientConfigService configurationService, DalamudUtilService dalamudUtil, MareMediator mediator) : base(logger, mediator) @@ -162,7 +162,13 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase { if (TransientResources.TryGetValue(ptr, out var set)) { - set.RemoveWhere(p => list.Contains(p, StringComparer.OrdinalIgnoreCase)); + foreach (var file in set.Where(p => list.Contains(p, StringComparer.OrdinalIgnoreCase))) + { + Logger.LogTrace("Removing From Transient: {file}", file); + } + + int removed = set.RemoveWhere(p => list.Contains(p, StringComparer.OrdinalIgnoreCase)); + Logger.LogInformation("Removed {removed} previously existing transient paths", removed); } } @@ -189,7 +195,7 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase private void DalamudUtil_FrameworkUpdate() { - _cachedFrameAddresses = _playerRelatedPointers.Select(c => c.CurrentAddress()).ToHashSet(); + _cachedFrameAddresses = _cachedFrameAddresses = new ConcurrentDictionary(_playerRelatedPointers.Where(k => k.Address != nint.Zero).ToDictionary(c => c.CurrentAddress(), c => c.ObjectKind)); lock (_cacheAdditionLock) { _cachedHandledPaths.Clear(); @@ -250,7 +256,7 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase } // ignore files not belonging to anything player related - if (!_cachedFrameAddresses.Contains(gameObject)) + if (!_cachedFrameAddresses.TryGetValue(gameObject, out var objectKind)) { lock (_cacheAdditionLock) { @@ -272,9 +278,18 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase } else { + var thing = _playerRelatedPointers.FirstOrDefault(f => f.Address == gameObject); value.Add(replacedGamePath); - Logger.LogDebug("Adding {replacedGamePath} for {gameObject} ({filePath})", replacedGamePath, gameObject.ToString("X"), filePath); - Mediator.Publish(new TransientResourceChangedMessage(gameObject)); + Logger.LogDebug("Adding {replacedGamePath} for {gameObject} ({filePath})", replacedGamePath, thing?.ToString() ?? gameObject.ToString("X"), filePath); + _ = Task.Run(async () => + { + _sendTransientCts?.Cancel(); + _sendTransientCts?.Dispose(); + _sendTransientCts = new(); + var token = _sendTransientCts.Token; + await Task.Delay(TimeSpan.FromSeconds(2), token).ConfigureAwait(false); + Mediator.Publish(new TransientResourceChangedMessage(gameObject)); + }); } } @@ -287,4 +302,6 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase _configurationService.Save(); } } + + private CancellationTokenSource _sendTransientCts = new(); } \ No newline at end of file diff --git a/MareSynchronos/Interop/GameModel/MdlFile.cs b/MareSynchronos/Interop/GameModel/MdlFile.cs new file mode 100644 index 0000000..bd98064 --- /dev/null +++ b/MareSynchronos/Interop/GameModel/MdlFile.cs @@ -0,0 +1,257 @@ +using Lumina.Data; +using Lumina.Extensions; +using System.Text; +using static Lumina.Data.Parsing.MdlStructs; + +namespace MareSynchronos.Interop.GameModel; + +#pragma warning disable S1104 // Fields should not have public accessibility + +// This code is completely and shamelessly borrowed from Penumbra to load V5 and V6 model files. +// Original Source: https://github.com/Ottermandias/Penumbra.GameData/blob/main/Files/MdlFile.cs +public class MdlFile +{ + public const int V5 = 0x01000005; + public const int V6 = 0x01000006; + public const uint NumVertices = 17; + public const uint FileHeaderSize = 0x44; + + // Raw data to write back. + public uint Version = 0x01000005; + public float Radius; + public float ModelClipOutDistance; + public float ShadowClipOutDistance; + public byte BgChangeMaterialIndex; + public byte BgCrestChangeMaterialIndex; + public ushort CullingGridCount; + public byte Flags3; + public byte Unknown6; + public ushort Unknown8; + public ushort Unknown9; + + // Offsets are stored relative to RuntimeSize instead of file start. + public uint[] VertexOffset = [0, 0, 0]; + public uint[] IndexOffset = [0, 0, 0]; + + public uint[] VertexBufferSize = [0, 0, 0]; + public uint[] IndexBufferSize = [0, 0, 0]; + public byte LodCount; + public bool EnableIndexBufferStreaming; + public bool EnableEdgeGeometry; + + public ModelFlags1 Flags1; + public ModelFlags2 Flags2; + + public VertexDeclarationStruct[] VertexDeclarations = []; + public ElementIdStruct[] ElementIds = []; + public MeshStruct[] Meshes = []; + public BoundingBoxStruct[] BoneBoundingBoxes = []; + public LodStruct[] Lods = []; + public ExtraLodStruct[] ExtraLods = []; + + public MdlFile(string filePath) + { + using var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read); + using var r = new LuminaBinaryReader(stream); + + var header = LoadModelFileHeader(r); + LodCount = header.LodCount; + VertexBufferSize = header.VertexBufferSize; + IndexBufferSize = header.IndexBufferSize; + VertexOffset = header.VertexOffset; + IndexOffset = header.IndexOffset; + + var dataOffset = FileHeaderSize + header.RuntimeSize + header.StackSize; + for (var i = 0; i < LodCount; ++i) + { + VertexOffset[i] -= dataOffset; + IndexOffset[i] -= dataOffset; + } + + VertexDeclarations = new VertexDeclarationStruct[header.VertexDeclarationCount]; + for (var i = 0; i < header.VertexDeclarationCount; ++i) + VertexDeclarations[i] = VertexDeclarationStruct.Read(r); + + _ = LoadStrings(r); + + var modelHeader = LoadModelHeader(r); + ElementIds = new ElementIdStruct[modelHeader.ElementIdCount]; + for (var i = 0; i < modelHeader.ElementIdCount; i++) + ElementIds[i] = ElementIdStruct.Read(r); + + Lods = new LodStruct[3]; + for (var i = 0; i < 3; i++) + { + var lod = r.ReadStructure(); + if (i < LodCount) + { + lod.VertexDataOffset -= dataOffset; + lod.IndexDataOffset -= dataOffset; + } + + Lods[i] = lod; + } + + ExtraLods = (modelHeader.Flags2 & ModelFlags2.ExtraLodEnabled) != 0 + ? r.ReadStructuresAsArray(3) + : []; + + Meshes = new MeshStruct[modelHeader.MeshCount]; + for (var i = 0; i < modelHeader.MeshCount; i++) + Meshes[i] = MeshStruct.Read(r); + } + + private ModelFileHeader LoadModelFileHeader(LuminaBinaryReader r) + { + var header = ModelFileHeader.Read(r); + Version = header.Version; + EnableIndexBufferStreaming = header.EnableIndexBufferStreaming; + EnableEdgeGeometry = header.EnableEdgeGeometry; + return header; + } + + private ModelHeader LoadModelHeader(BinaryReader r) + { + var modelHeader = r.ReadStructure(); + Radius = modelHeader.Radius; + Flags1 = modelHeader.Flags1; + Flags2 = modelHeader.Flags2; + ModelClipOutDistance = modelHeader.ModelClipOutDistance; + ShadowClipOutDistance = modelHeader.ShadowClipOutDistance; + CullingGridCount = modelHeader.CullingGridCount; + Flags3 = modelHeader.Flags3; + Unknown6 = modelHeader.Unknown6; + Unknown8 = modelHeader.Unknown8; + Unknown9 = modelHeader.Unknown9; + BgChangeMaterialIndex = modelHeader.BGChangeMaterialIndex; + BgCrestChangeMaterialIndex = modelHeader.BGCrestChangeMaterialIndex; + + return modelHeader; + } + + private static (uint[], string[]) LoadStrings(BinaryReader r) + { + var stringCount = r.ReadUInt16(); + r.ReadUInt16(); + var stringSize = (int)r.ReadUInt32(); + var stringData = r.ReadBytes(stringSize); + var start = 0; + var strings = new string[stringCount]; + var offsets = new uint[stringCount]; + for (var i = 0; i < stringCount; ++i) + { + var span = stringData.AsSpan(start); + var idx = span.IndexOf((byte)'\0'); + strings[i] = Encoding.UTF8.GetString(span[..idx]); + offsets[i] = (uint)start; + start = start + idx + 1; + } + + return (offsets, strings); + } + + public unsafe struct ModelHeader + { + // MeshHeader + public float Radius; + public ushort MeshCount; + public ushort AttributeCount; + public ushort SubmeshCount; + public ushort MaterialCount; + public ushort BoneCount; + public ushort BoneTableCount; + public ushort ShapeCount; + public ushort ShapeMeshCount; + public ushort ShapeValueCount; + public byte LodCount; + public ModelFlags1 Flags1; + public ushort ElementIdCount; + public byte TerrainShadowMeshCount; + public ModelFlags2 Flags2; + public float ModelClipOutDistance; + public float ShadowClipOutDistance; + public ushort CullingGridCount; + public ushort TerrainShadowSubmeshCount; + public byte Flags3; + public byte BGChangeMaterialIndex; + public byte BGCrestChangeMaterialIndex; + public byte Unknown6; + public ushort BoneTableArrayCountTotal; + public ushort Unknown8; + public ushort Unknown9; + private fixed byte _padding[6]; + } + + public struct ShapeStruct + { + public uint StringOffset; + public ushort[] ShapeMeshStartIndex; + public ushort[] ShapeMeshCount; + + public static ShapeStruct Read(LuminaBinaryReader br) + { + ShapeStruct ret = new ShapeStruct(); + ret.StringOffset = br.ReadUInt32(); + ret.ShapeMeshStartIndex = br.ReadUInt16Array(3); + ret.ShapeMeshCount = br.ReadUInt16Array(3); + return ret; + } + } + + [Flags] + public enum ModelFlags1 : byte + { + DustOcclusionEnabled = 0x80, + SnowOcclusionEnabled = 0x40, + RainOcclusionEnabled = 0x20, + Unknown1 = 0x10, + LightingReflectionEnabled = 0x08, + WavingAnimationDisabled = 0x04, + LightShadowDisabled = 0x02, + ShadowDisabled = 0x01, + } + + [Flags] + public enum ModelFlags2 : byte + { + Unknown2 = 0x80, + BgUvScrollEnabled = 0x40, + EnableForceNonResident = 0x20, + ExtraLodEnabled = 0x10, + ShadowMaskEnabled = 0x08, + ForceLodRangeEnabled = 0x04, + EdgeGeometryEnabled = 0x02, + Unknown3 = 0x01 + } + + public struct VertexDeclarationStruct + { + // There are always 17, but stop when stream = -1 + public VertexElement[] VertexElements; + + public static VertexDeclarationStruct Read(LuminaBinaryReader br) + { + VertexDeclarationStruct ret = new VertexDeclarationStruct(); + + var elems = new List(); + + // Read the vertex elements that we need + var thisElem = br.ReadStructure(); + do + { + elems.Add(thisElem); + thisElem = br.ReadStructure(); + } while (thisElem.Stream != 255); + + // Skip the number of bytes that we don't need to read + // We skip elems.Count * 9 because we had to read the invalid element + int toSeek = 17 * 8 - (elems.Count + 1) * 8; + br.Seek(br.BaseStream.Position + toSeek); + + ret.VertexElements = elems.ToArray(); + + return ret; + } + } +} +#pragma warning restore S1104 // Fields should not have public accessibility \ No newline at end of file diff --git a/MareSynchronos/Interop/Ipc/IpcCallerCustomize.cs b/MareSynchronos/Interop/Ipc/IpcCallerCustomize.cs index a0dea63..5029a14 100644 --- a/MareSynchronos/Interop/Ipc/IpcCallerCustomize.cs +++ b/MareSynchronos/Interop/Ipc/IpcCallerCustomize.cs @@ -129,7 +129,7 @@ public sealed class IpcCallerCustomize : IIpcCaller private void OnCustomizePlusScaleChange(ushort c, Guid g) { var obj = _dalamudUtil.GetCharacterFromObjectTableByIndex(c); - _mareMediator.Publish(new CustomizePlusMessage(obj?.Name.ToString() ?? string.Empty)); + _mareMediator.Publish(new CustomizePlusMessage(obj?.Address ?? null)); } public void Dispose() diff --git a/MareSynchronos/Interop/Ipc/IpcCallerGlamourer.cs b/MareSynchronos/Interop/Ipc/IpcCallerGlamourer.cs index e22b7bf..ccd73a4 100644 --- a/MareSynchronos/Interop/Ipc/IpcCallerGlamourer.cs +++ b/MareSynchronos/Interop/Ipc/IpcCallerGlamourer.cs @@ -10,7 +10,7 @@ using Microsoft.Extensions.Logging; namespace MareSynchronos.Interop.Ipc; -public sealed class IpcCallerGlamourer : IIpcCaller +public sealed class IpcCallerGlamourer : DisposableMediatorSubscriberBase, IIpcCaller { private readonly ILogger _logger; private readonly IDalamudPluginInterface _pi; @@ -31,15 +31,15 @@ public sealed class IpcCallerGlamourer : IIpcCaller private readonly uint LockCode = 0x626E7579; public IpcCallerGlamourer(ILogger logger, IDalamudPluginInterface pi, DalamudUtilService dalamudUtil, MareMediator mareMediator, - RedrawManager redrawManager) + RedrawManager redrawManager) : base(logger, mareMediator) { - _glamourerApiVersions = new(pi); - _glamourerGetAllCustomization = new(pi); - _glamourerApplyAll = new(pi); - _glamourerRevert = new(pi); - _glamourerRevertByName = new(pi); - _glamourerUnlock = new(pi); - _glamourerUnlockByName = new(pi); + _glamourerApiVersions = new ApiVersion(pi); + _glamourerGetAllCustomization = new GetStateBase64(pi); + _glamourerApplyAll = new ApplyState(pi); + _glamourerRevert = new RevertState(pi); + _glamourerRevertByName = new RevertStateName(pi); + _glamourerUnlock = new UnlockState(pi); + _glamourerUnlockByName = new UnlockStateName(pi); _logger = logger; _pi = pi; @@ -50,6 +50,16 @@ public sealed class IpcCallerGlamourer : IIpcCaller _glamourerStateChanged = StateChanged.Subscriber(pi, GlamourerChanged); _glamourerStateChanged.Enable(); + + Mediator.Subscribe(this, s => _shownGlamourerUnavailable = false); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + _redrawManager.Cancel(); + _glamourerStateChanged?.Dispose(); } public bool APIAvailable { get; private set; } @@ -93,11 +103,6 @@ public sealed class IpcCallerGlamourer : IIpcCaller } } - public void Dispose() - { - _glamourerStateChanged?.Dispose(); - } - public async Task ApplyAllAsync(ILogger logger, GameObjectHandler handler, string? customization, Guid applicationId, CancellationToken token, bool fireAndForget = false) { if (!APIAvailable || string.IsNullOrEmpty(customization) || _dalamudUtil.IsZoning) return; diff --git a/MareSynchronos/Interop/Ipc/IpcCallerHeels.cs b/MareSynchronos/Interop/Ipc/IpcCallerHeels.cs index 0836770..994b73c 100644 --- a/MareSynchronos/Interop/Ipc/IpcCallerHeels.cs +++ b/MareSynchronos/Interop/Ipc/IpcCallerHeels.cs @@ -1,5 +1,4 @@ -using Dalamud.Game.ClientState.Objects.Types; -using Dalamud.Plugin; +using Dalamud.Plugin; using Dalamud.Plugin.Ipc; using MareSynchronos.Services; using MareSynchronos.Services.Mediator; diff --git a/MareSynchronos/Interop/Ipc/IpcCallerPenumbra.cs b/MareSynchronos/Interop/Ipc/IpcCallerPenumbra.cs index 4dcda07..3bedf2a 100644 --- a/MareSynchronos/Interop/Ipc/IpcCallerPenumbra.cs +++ b/MareSynchronos/Interop/Ipc/IpcCallerPenumbra.cs @@ -1,4 +1,4 @@ -using Dalamud.Plugin; +using Dalamud.Plugin; using MareSynchronos.MareConfiguration.Models; using MareSynchronos.PlayerData.Handlers; using MareSynchronos.Services; @@ -90,6 +90,8 @@ public sealed class IpcCallerPenumbra : DisposableMediatorSubscriberBase, IIpcCa { _penumbraRedraw.Invoke(msg.Character.ObjectIndex, RedrawType.AfterGPose); }); + + Mediator.Subscribe(this, (msg) => _shownPenumbraUnavailable = false); } public bool APIAvailable { get; private set; } = false; @@ -103,7 +105,14 @@ public sealed class IpcCallerPenumbra : DisposableMediatorSubscriberBase, IIpcCa .FirstOrDefault(p => string.Equals(p.InternalName, "Penumbra", StringComparison.OrdinalIgnoreCase)) ?.Version ?? new Version(0, 0, 0, 0)); penumbraAvailable = penumbraVersion >= new Version(1, 0, 1, 0); - penumbraAvailable &= _penumbraEnabled.Invoke(); + try + { + penumbraAvailable &= _penumbraEnabled.Invoke(); + } + catch + { + penumbraAvailable = false; + } _shownPenumbraUnavailable = _shownPenumbraUnavailable && !penumbraAvailable; APIAvailable = penumbraAvailable; } @@ -196,7 +205,7 @@ public sealed class IpcCallerPenumbra : DisposableMediatorSubscriberBase, IIpcCa await _dalamudUtil.RunOnFrameworkThread(async () => { var gameObject = await _dalamudUtil.CreateGameObjectAsync(await _dalamudUtil.GetPlayerPointerAsync().ConfigureAwait(false)).ConfigureAwait(false); - _penumbraRedraw.Invoke(gameObject!.ObjectIndex, RedrawType.Redraw); + _penumbraRedraw.Invoke(gameObject!.ObjectIndex, setting: RedrawType.Redraw); }).ConfigureAwait(false); } @@ -210,6 +219,7 @@ public sealed class IpcCallerPenumbra : DisposableMediatorSubscriberBase, IIpcCa var collId = _penumbraCreateNamedTemporaryCollection.Invoke(collName); logger.LogTrace("Creating Temp Collection {collName}, GUID: {collId}", collName, collId); return collId; + }).ConfigureAwait(false); } @@ -330,4 +340,4 @@ public sealed class IpcCallerPenumbra : DisposableMediatorSubscriberBase, IIpcCa _mareMediator.Publish(new PenumbraInitializedMessage()); _penumbraRedraw!.Invoke(0, setting: RedrawType.Redraw); } -} \ No newline at end of file +} diff --git a/MareSynchronos/MareConfiguration/ConfigurationMigrator.cs b/MareSynchronos/MareConfiguration/ConfigurationMigrator.cs index 3de116e..80c5993 100644 --- a/MareSynchronos/MareConfiguration/ConfigurationMigrator.cs +++ b/MareSynchronos/MareConfiguration/ConfigurationMigrator.cs @@ -1,22 +1,22 @@ -using Dalamud.Plugin; -using MareSynchronos.MareConfiguration.Configurations; +using MareSynchronos.WebAPI; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using Newtonsoft.Json; namespace MareSynchronos.MareConfiguration; -public class ConfigurationMigrator(ILogger logger, IDalamudPluginInterface pi, +public class ConfigurationMigrator(ILogger logger, NotesConfigService notesConfig) : IHostedService { + private readonly ILogger _logger = logger; + public void Migrate() { - var oldUri = MareSynchronos.WebAPI.ApiController.LoporritServiceUriOld; - var newUri = MareSynchronos.WebAPI.ApiController.LoporritServiceUri; + var oldUri = ApiController.LoporritServiceUriOld; + var newUri = ApiController.LoporritServiceUri; if (notesConfig.Current.ServerNotes.TryGetValue(oldUri, out var old)) { - logger.LogDebug("Migrating server notes {old} => {new}", oldUri, newUri); + _logger.LogDebug("Migrating server notes {old} => {new}", oldUri, newUri); notesConfig.Current.ServerNotes.TryAdd(newUri, new()); var merged = notesConfig.Current.ServerNotes.GetValueOrDefault(newUri, new()); foreach (var (k, v) in old.GidServerComments) @@ -38,11 +38,4 @@ public class ConfigurationMigrator(ILogger logger, IDalam { return Task.CompletedTask; } - - private static void SaveConfig(IMareConfiguration config, string path) - { - File.WriteAllText(path, JsonConvert.SerializeObject(config, Formatting.Indented)); - } - - private string ConfigurationPath(string configName) => Path.Combine(pi.ConfigDirectory.FullName, configName); } diff --git a/MareSynchronos/MarePlugin.cs b/MareSynchronos/MarePlugin.cs index 03cb0c2..280960a 100644 --- a/MareSynchronos/MarePlugin.cs +++ b/MareSynchronos/MarePlugin.cs @@ -159,7 +159,7 @@ public class MarePlugin : MediatorSubscriberBase, IHostedService { Mediator.Publish(new NotificationMessage("Abnormal Log Level", $"Your log level is set to '{_mareConfigService.Current.LogLevel}' which is not recommended for normal usage. Set it to '{LogLevel.Information}' in \"Loporrit Settings -> Debug\" unless instructed otherwise.", - NotificationType.Error, TimeSpan.FromSeconds(15000))); + MareConfiguration.Models.NotificationType.Error, TimeSpan.FromSeconds(15000))); } #endif } diff --git a/MareSynchronos/PlayerData/Export/MareCharaFileManager.cs b/MareSynchronos/PlayerData/Export/MareCharaFileManager.cs index b03e397..0eed3c0 100644 --- a/MareSynchronos/PlayerData/Export/MareCharaFileManager.cs +++ b/MareSynchronos/PlayerData/Export/MareCharaFileManager.cs @@ -64,7 +64,7 @@ public class MareCharaFileManager : DisposableMediatorSubscriberBase } foreach (var id in _gposeCustomizeObjects.Where(d => d != null)) { - await _ipcManager.CustomizePlus.RevertByIdAsync(id).ConfigureAwait(false); + await _ipcManager.CustomizePlus.RevertByIdAsync(id.Value); } _gposeGameObjects.Clear(); }); @@ -100,7 +100,7 @@ public class MareCharaFileManager : DisposableMediatorSubscriberBase } var applicationId = Guid.NewGuid(); var coll = await _ipcManager.Penumbra.CreateTemporaryCollectionAsync(_logger, charaTarget.Name.TextValue).ConfigureAwait(false); - await _ipcManager.Penumbra.AssignTemporaryCollectionAsync(_logger, coll, charaTarget.ObjectTableIndex()!.Value).ConfigureAwait(false); + await _ipcManager.Penumbra.AssignTemporaryCollectionAsync(_logger, coll, charaTarget.ObjectIndex).ConfigureAwait(false); await _ipcManager.Penumbra.SetTemporaryModsAsync(_logger, applicationId, coll, extractedFiles.Union(fileSwaps).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal)).ConfigureAwait(false); await _ipcManager.Penumbra.SetManipulationDataAsync(_logger, applicationId, coll, LoadedCharaFile.CharaFileData.ManipulationData).ConfigureAwait(false); diff --git a/MareSynchronos/PlayerData/Factories/PairHandlerFactory.cs b/MareSynchronos/PlayerData/Factories/PairHandlerFactory.cs index 6a150c7..000a4b7 100644 --- a/MareSynchronos/PlayerData/Factories/PairHandlerFactory.cs +++ b/MareSynchronos/PlayerData/Factories/PairHandlerFactory.cs @@ -1,11 +1,11 @@ -using MareSynchronos.API.Dto.User; -using MareSynchronos.FileCache; +using MareSynchronos.FileCache; using MareSynchronos.Interop.Ipc; using MareSynchronos.PlayerData.Handlers; using MareSynchronos.PlayerData.Pairs; using MareSynchronos.Services; using MareSynchronos.Services.Mediator; using MareSynchronos.Services.ServerConfiguration; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; namespace MareSynchronos.PlayerData.Factories; @@ -16,18 +16,19 @@ public class PairHandlerFactory private readonly FileCacheManager _fileCacheManager; private readonly FileDownloadManagerFactory _fileDownloadManagerFactory; private readonly GameObjectHandlerFactory _gameObjectHandlerFactory; - private readonly CancellationToken _dalamudLifetimeToken; + private readonly IHostApplicationLifetime _hostApplicationLifetime; private readonly IpcManager _ipcManager; private readonly ILoggerFactory _loggerFactory; private readonly MareMediator _mareMediator; - private readonly XivDataAnalyzer _xivDataAnalyzer; + private readonly PlayerPerformanceService _playerPerformanceService; + private readonly ServerConfigurationManager _serverConfigManager; private readonly PluginWarningNotificationService _pluginWarningNotificationManager; - private readonly ServerConfigurationManager _serverConfigurationManager; public PairHandlerFactory(ILoggerFactory loggerFactory, GameObjectHandlerFactory gameObjectHandlerFactory, IpcManager ipcManager, FileDownloadManagerFactory fileDownloadManagerFactory, DalamudUtilService dalamudUtilService, - PluginWarningNotificationService pluginWarningNotificationManager, ServerConfigurationManager serverConfigurationManager, - CancellationToken dalamudLifetime, FileCacheManager fileCacheManager, MareMediator mareMediator, XivDataAnalyzer modelAnalyzer) + PluginWarningNotificationService pluginWarningNotificationManager, IHostApplicationLifetime hostApplicationLifetime, + FileCacheManager fileCacheManager, MareMediator mareMediator, PlayerPerformanceService playerPerformanceService, + ServerConfigurationManager serverConfigManager) { _loggerFactory = loggerFactory; _gameObjectHandlerFactory = gameObjectHandlerFactory; @@ -35,17 +36,17 @@ public class PairHandlerFactory _fileDownloadManagerFactory = fileDownloadManagerFactory; _dalamudUtilService = dalamudUtilService; _pluginWarningNotificationManager = pluginWarningNotificationManager; - _serverConfigurationManager = serverConfigurationManager; - _dalamudLifetimeToken = dalamudLifetime; + _hostApplicationLifetime = hostApplicationLifetime; _fileCacheManager = fileCacheManager; _mareMediator = mareMediator; - _xivDataAnalyzer = modelAnalyzer; + _playerPerformanceService = playerPerformanceService; + _serverConfigManager = serverConfigManager; } - public PairHandler Create(OnlineUserIdentDto onlineUserIdentDto) + public PairHandler Create(Pair pair) { - return new PairHandler(_loggerFactory.CreateLogger(), onlineUserIdentDto, _gameObjectHandlerFactory, - _ipcManager, _fileDownloadManagerFactory.Create(), _pluginWarningNotificationManager, _serverConfigurationManager, _dalamudUtilService, - _dalamudLifetimeToken, _fileCacheManager, _mareMediator, _xivDataAnalyzer); + return new PairHandler(_loggerFactory.CreateLogger(), pair, _gameObjectHandlerFactory, + _ipcManager, _fileDownloadManagerFactory.Create(), _pluginWarningNotificationManager, _dalamudUtilService, _hostApplicationLifetime, + _fileCacheManager, _mareMediator, _playerPerformanceService, _serverConfigManager); } } \ No newline at end of file diff --git a/MareSynchronos/PlayerData/Factories/PlayerDataFactory.cs b/MareSynchronos/PlayerData/Factories/PlayerDataFactory.cs index 01ef727..ca92d8d 100644 --- a/MareSynchronos/PlayerData/Factories/PlayerDataFactory.cs +++ b/MareSynchronos/PlayerData/Factories/PlayerDataFactory.cs @@ -116,7 +116,6 @@ public class PlayerDataFactory private async Task CreateCharacterData(CharacterData previousData, GameObjectHandler playerRelatedObject, CancellationToken token) { var objectKind = playerRelatedObject.ObjectKind; - var charaPointer = playerRelatedObject.Address; _logger.LogDebug("Building character data for {obj}", playerRelatedObject); @@ -178,10 +177,10 @@ public class PlayerDataFactory _logger.LogDebug("Handling transient update for {obj}", playerRelatedObject); // remove all potentially gathered paths from the transient resource manager that are resolved through static resolving - _transientResourceManager.ClearTransientPaths(charaPointer, previousData.FileReplacements[objectKind].SelectMany(c => c.GamePaths).ToList()); + _transientResourceManager.ClearTransientPaths(playerRelatedObject.Address, previousData.FileReplacements[objectKind].SelectMany(c => c.GamePaths).ToList()); // get all remaining paths and resolve them - var transientPaths = ManageSemiTransientData(objectKind, charaPointer); + var transientPaths = ManageSemiTransientData(objectKind, playerRelatedObject.Address); var resolvedTransientPaths = await GetFileReplacementsFromPaths(transientPaths, new HashSet(StringComparer.Ordinal)).ConfigureAwait(false); _logger.LogDebug("== Transient Replacements =="); @@ -217,7 +216,7 @@ public class PlayerDataFactory if (objectKind == ObjectKind.Player) { previousData.PetNamesData = _ipcManager.PetNames.GetLocalNames(); - _logger.LogDebug("Pet Nicknames is now: {moodles}", previousData.PetNamesData); + _logger.LogDebug("Pet Nicknames is now: {petnames}", previousData.PetNamesData); } if (previousData.FileReplacements.TryGetValue(objectKind, out HashSet? fileReplacements)) diff --git a/MareSynchronos/PlayerData/Handlers/GameObjectHandler.cs b/MareSynchronos/PlayerData/Handlers/GameObjectHandler.cs index 1af8031..912d151 100644 --- a/MareSynchronos/PlayerData/Handlers/GameObjectHandler.cs +++ b/MareSynchronos/PlayerData/Handlers/GameObjectHandler.cs @@ -1,13 +1,10 @@ -using Dalamud.Game.ClientState.Objects.Types; -using Dalamud.Memory; -using FFXIVClientStructs.FFXIV.Client.Game.Character; -using FFXIVClientStructs.FFXIV.Client.Game.Object; +using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using MareSynchronos.Services; using MareSynchronos.Services.Mediator; using MareSynchronos.Utils; using Microsoft.Extensions.Logging; -using System.Runtime.InteropServices; +using System.Runtime.CompilerServices; using static FFXIVClientStructs.FFXIV.Client.Game.Character.DrawDataContainer; using ObjectKind = MareSynchronos.API.Data.Enum.ObjectKind; @@ -28,7 +25,7 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase private CancellationTokenSource _zoningCts = new(); public GameObjectHandler(ILogger logger, PerformanceCollectorService performanceCollector, - MareMediator mediator, DalamudUtilService dalamudUtil, ObjectKind objectKind, Func getAddress, bool watchedObject = true) : base(logger, mediator) + MareMediator mediator, DalamudUtilService dalamudUtil, ObjectKind objectKind, Func getAddress, bool ownedObject = true) : base(logger, mediator) { _performanceCollector = performanceCollector; ObjectKind = objectKind; @@ -38,10 +35,10 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase _dalamudUtil.EnsureIsOnFramework(); return getAddress.Invoke(); }; - _isOwnedObject = watchedObject; + _isOwnedObject = ownedObject; Name = string.Empty; - if (watchedObject) + if (ownedObject) { Mediator.Subscribe(this, (msg) => { @@ -114,13 +111,13 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase private ushort[] MainHandData { get; set; } = new ushort[3]; private ushort[] OffHandData { get; set; } = new ushort[3]; - public async Task ActOnFrameworkAfterEnsureNoDrawAsync(Action act, CancellationToken token) + public async Task ActOnFrameworkAfterEnsureNoDrawAsync(Action act, CancellationToken token) { while (await _dalamudUtil.RunOnFrameworkThread(() => { if (IsBeingDrawn()) return true; var gameObj = _dalamudUtil.CreateGameObject(Address); - if (gameObj is ICharacter chara) + if (gameObj is Dalamud.Game.ClientState.Objects.Types.ICharacter chara) { act.Invoke(chara); } @@ -149,7 +146,7 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase return _getAddress.Invoke(); } - public IGameObject? GetGameObject() + public Dalamud.Game.ClientState.Objects.Types.IGameObject? GetGameObject() { return _dalamudUtil.CreateGameObject(Address); } @@ -189,7 +186,7 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase if (Address != IntPtr.Zero) { _ptrNullCounter = 0; - var drawObjAddr = (IntPtr)((GameObject*)Address)->DrawObject; + var drawObjAddr = (IntPtr)((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)Address)->DrawObject; DrawObjectAddress = drawObjAddr; } else @@ -211,9 +208,7 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase _clearCts = null; } var chara = (Character*)Address; - string name; - fixed (byte* nameData = chara->GameObject.Name) - MemoryHelper.ReadStringNullTerminated((nint)nameData, out name); + var name = chara->GameObject.NameString; bool nameChange = !string.Equals(name, Name, StringComparison.Ordinal); if (nameChange) { @@ -244,8 +239,7 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase } else { - fixed (EquipmentModelId* equipmentData = chara->DrawData.EquipmentModelIds) - equipDiff = CompareAndUpdateEquipByteData((byte*)equipmentData); + equipDiff = CompareAndUpdateEquipByteData((byte*)Unsafe.AsPointer(ref chara->DrawData.EquipmentModelIds[0])); if (equipDiff) Logger.LogTrace("Checking [{this}] equip data from game obj, result: {diff}", this, equipDiff); } @@ -275,15 +269,13 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase TribeId = tribeId; } - fixed (byte* customizeData = ((Human*)DrawObjectAddress)->Customize.Data) - customizeDiff = CompareAndUpdateCustomizeData(customizeData); + customizeDiff = CompareAndUpdateCustomizeData(((Human*)DrawObjectAddress)->Customize.Data); if (customizeDiff) Logger.LogTrace("Checking [{this}] customize data as human from draw obj, result: {diff}", this, customizeDiff); } else { - fixed (byte* customizeData = ((Human*)DrawObjectAddress)->Customize.Data) - customizeDiff = CompareAndUpdateCustomizeData(customizeData); + customizeDiff = CompareAndUpdateCustomizeData(chara->DrawData.CustomizeData.Data); if (customizeDiff) Logger.LogTrace("Checking [{this}] customize data from game obj, result: {diff}", this, equipDiff); } @@ -316,13 +308,13 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase _clearCts = null; } - private unsafe bool CompareAndUpdateCustomizeData(byte* customizeData) + private unsafe bool CompareAndUpdateCustomizeData(Span customizeData) { bool hasChanges = false; - for (int i = 0; i < CustomizeData.Length; i++) + for (int i = 0; i < customizeData.Length; i++) { - var data = Marshal.ReadByte((IntPtr)customizeData, i); + var data = customizeData[i]; if (CustomizeData[i] != data) { CustomizeData[i] = data; @@ -338,7 +330,7 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase bool hasChanges = false; for (int i = 0; i < EquipSlotData.Length; i++) { - var data = Marshal.ReadByte((IntPtr)equipSlotData, i); + var data = equipSlotData[i]; if (EquipSlotData[i] != data) { EquipSlotData[i] = data; @@ -392,7 +384,7 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase private unsafe IntPtr GetDrawObjUnsafe(nint curPtr) { - return (IntPtr)((GameObject*)curPtr)->DrawObject; + return (IntPtr)((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)curPtr)->DrawObject; } private bool IsBeingDrawn() @@ -433,7 +425,7 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase { var drawObjZero = drawObj == IntPtr.Zero; if (drawObjZero) return DrawCondition.DrawObjectZero; - var renderFlags = ((GameObject*)curPtr)->RenderFlags != 0x0; + var renderFlags = (((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)curPtr)->RenderFlags) != 0x0; if (renderFlags) return DrawCondition.RenderFlags; if (ObjectKind == ObjectKind.Player) @@ -459,6 +451,10 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase { _zoningCts?.CancelAfter(2500); } + catch (ObjectDisposedException) + { + // ignore + } catch (Exception ex) { Logger.LogWarning(ex, "Zoning CTS cancel issue"); diff --git a/MareSynchronos/PlayerData/Handlers/PairHandler.cs b/MareSynchronos/PlayerData/Handlers/PairHandler.cs index 64577c5..1bff501 100644 --- a/MareSynchronos/PlayerData/Handlers/PairHandler.cs +++ b/MareSynchronos/PlayerData/Handlers/PairHandler.cs @@ -1,5 +1,4 @@ using MareSynchronos.API.Data; -using MareSynchronos.API.Dto.User; using MareSynchronos.FileCache; using MareSynchronos.Interop.Ipc; using MareSynchronos.PlayerData.Factories; @@ -10,6 +9,7 @@ using MareSynchronos.Services.Mediator; using MareSynchronos.Services.ServerConfiguration; using MareSynchronos.Utils; using MareSynchronos.WebAPI.Files; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using System.Collections.Concurrent; using System.Diagnostics; @@ -24,45 +24,44 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase private readonly DalamudUtilService _dalamudUtil; private readonly FileDownloadManager _downloadManager; private readonly FileCacheManager _fileDbManager; - private readonly XivDataAnalyzer _xivDataAnalyzer; private readonly GameObjectHandlerFactory _gameObjectHandlerFactory; private readonly IpcManager _ipcManager; - private readonly CancellationToken _lifetime; + private readonly IHostApplicationLifetime _lifetime; + private readonly PlayerPerformanceService _playerPerformanceService; + private readonly ServerConfigurationManager _serverConfigManager; private readonly PluginWarningNotificationService _pluginWarningNotificationManager; - private readonly ServerConfigurationManager _serverConfigurationManager; private CancellationTokenSource? _applicationCancellationTokenSource = new(); 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 bool _forceApplyMods = false; private bool _isVisible; private Guid _penumbraCollection = Guid.Empty; - private Dictionary _customizeIds = []; private bool _redrawOnNextApplication = false; - private CombatData? _dataReceivedInCombat; - public long LastAppliedDataSize { get; private set; } - public long LastAppliedDataTris { get; private set; } - public PairHandler(ILogger logger, OnlineUserIdentDto onlineUser, + public PairHandler(ILogger logger, Pair pair, GameObjectHandlerFactory gameObjectHandlerFactory, IpcManager ipcManager, FileDownloadManager transferManager, - PluginWarningNotificationService pluginWarningNotificationManager, ServerConfigurationManager serverConfigurationManager, - DalamudUtilService dalamudUtil, CancellationToken lifetime, + PluginWarningNotificationService pluginWarningNotificationManager, + DalamudUtilService dalamudUtil, IHostApplicationLifetime lifetime, FileCacheManager fileDbManager, MareMediator mediator, - XivDataAnalyzer modelAnalyzer) : base(logger, mediator) + PlayerPerformanceService playerPerformanceService, + ServerConfigurationManager serverConfigManager) : base(logger, mediator) { - OnlineUser = onlineUser; + Pair = pair; _gameObjectHandlerFactory = gameObjectHandlerFactory; _ipcManager = ipcManager; _downloadManager = transferManager; _pluginWarningNotificationManager = pluginWarningNotificationManager; - _serverConfigurationManager = serverConfigurationManager; _dalamudUtil = dalamudUtil; _lifetime = lifetime; _fileDbManager = fileDbManager; - _xivDataAnalyzer = modelAnalyzer; + _playerPerformanceService = playerPerformanceService; + _serverConfigManager = serverConfigManager; Mediator.Subscribe(this, (_) => FrameworkUpdate()); Mediator.Subscribe(this, (_) => @@ -73,6 +72,7 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase }); Mediator.Subscribe(this, (_) => { + _penumbraCollection = Guid.Empty; if (!IsVisible && _charaHandler != null) { PlayerName = string.Empty; @@ -87,24 +87,23 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase _redrawOnNextApplication = true; } }); - Mediator.Subscribe(this, (msg) => + Mediator.Subscribe(this, (msg) => { - if (IsVisible && _dataReceivedInCombat != null) + if (IsVisible && _dataReceivedInDowntime != null) { - ApplyCharacterData(_dataReceivedInCombat.ApplicationId, - _dataReceivedInCombat.CharacterData, _dataReceivedInCombat.Forced); - _dataReceivedInCombat = null; + ApplyCharacterData(_dataReceivedInDowntime.ApplicationId, + _dataReceivedInDowntime.CharacterData, _dataReceivedInDowntime.Forced); + _dataReceivedInDowntime = null; } }); - Mediator.Subscribe(this, _ => + Mediator.Subscribe(this, _ => { - _dataReceivedInCombat = null; + _dataReceivedInDowntime = null; _downloadCancellationTokenSource = _downloadCancellationTokenSource?.CancelRecreate(); _applicationCancellationTokenSource = _applicationCancellationTokenSource?.CancelRecreate(); }); - LastAppliedDataSize = -1; - LastAppliedDataTris = -1; + LastAppliedDataBytes = -1; } public bool IsVisible @@ -116,34 +115,36 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase { _isVisible = value; string text = "User Visibility Changed, now: " + (_isVisible ? "Is Visible" : "Is not Visible"); - Mediator.Publish(new EventMessage(new Event(PlayerName, OnlineUser.User, nameof(PairHandler), + Mediator.Publish(new EventMessage(new Event(PlayerName, Pair.UserData, nameof(PairHandler), EventSeverity.Informational, text))); } } } - public OnlineUserIdentDto OnlineUser { get; private set; } + + public long LastAppliedDataBytes { get; private set; } + public Pair Pair { get; private set; } 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 string? PlayerName { get; private set; } - public string PlayerNameHash => OnlineUser.Ident; + public string PlayerNameHash => Pair.Ident; public void ApplyCharacterData(Guid applicationBase, CharacterData characterData, bool forceApplyCustomization = false) { - if (_dalamudUtil.IsInCombat) + if (_dalamudUtil.IsInCombatOrPerforming) { - Mediator.Publish(new EventMessage(new Event(PlayerName, OnlineUser.User, nameof(PairHandler), EventSeverity.Warning, - "Cannot apply character data: you are in combat, deferring application"))); - Logger.LogDebug("[BASE-{appBase}] Received data but player is in combat", applicationBase); - _dataReceivedInCombat = new(applicationBase, characterData, forceApplyCustomization); + Mediator.Publish(new EventMessage(new Event(PlayerName, Pair.UserData, nameof(PairHandler), EventSeverity.Warning, + "Cannot apply character data: you are in combat or performing music, deferring application"))); + Logger.LogDebug("[BASE-{appBase}] Received data but player is in combat or performing", applicationBase); + _dataReceivedInDowntime = new(applicationBase, characterData, forceApplyCustomization); SetUploading(isUploading: false); return; } if (_charaHandler == null || (PlayerCharacter == IntPtr.Zero)) { - Mediator.Publish(new EventMessage(new Event(PlayerName, OnlineUser.User, nameof(PairHandler), EventSeverity.Warning, + Mediator.Publish(new EventMessage(new Event(PlayerName, Pair.UserData, nameof(PairHandler), EventSeverity.Warning, "Cannot apply character data: Receiving Player is in an invalid state, deferring application"))); Logger.LogDebug("[BASE-{appBase}] Received data but player was in invalid state, charaHandlerIsNull: {charaIsNull}, playerPointerIsNull: {ptrIsNull}", applicationBase, _charaHandler == null, PlayerCharacter == IntPtr.Zero); @@ -165,13 +166,13 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase if (_dalamudUtil.IsInCutscene || _dalamudUtil.IsInGpose || !_ipcManager.Penumbra.APIAvailable || !_ipcManager.Glamourer.APIAvailable) { - Mediator.Publish(new EventMessage(new Event(PlayerName, OnlineUser.User, nameof(PairHandler), EventSeverity.Warning, + Mediator.Publish(new EventMessage(new Event(PlayerName, Pair.UserData, nameof(PairHandler), EventSeverity.Warning, "Cannot apply character data: you are in GPose, a Cutscene or Penumbra/Glamourer is not available"))); Logger.LogInformation("[BASE-{appbase}] Application of data for {player} while in cutscene/gpose or Penumbra/Glamourer unavailable, returning", applicationBase, this); return; } - Mediator.Publish(new EventMessage(new Event(PlayerName, OnlineUser.User, nameof(PairHandler), EventSeverity.Informational, + Mediator.Publish(new EventMessage(new Event(PlayerName, Pair.UserData, nameof(PairHandler), EventSeverity.Informational, "Applying Character Data"))); _forceApplyMods |= forceApplyCustomization; @@ -191,7 +192,7 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase if (charaDataToUpdate.TryGetValue(ObjectKind.Player, out var playerChanges)) { - _pluginWarningNotificationManager.NotifyForMissingPlugins(OnlineUser.User, PlayerName!, playerChanges); + _pluginWarningNotificationManager.NotifyForMissingPlugins(Pair.UserData, PlayerName!, playerChanges); } Logger.LogDebug("[BASE-{appbase}] Downloading and applying character for {name}", applicationBase, this); @@ -201,9 +202,9 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase public override string ToString() { - return OnlineUser == null + return Pair == null ? base.ToString() ?? string.Empty - : OnlineUser.User.AliasOrUID + ":" + PlayerName + ":" + (PlayerCharacter != nint.Zero ? "HasChar" : "NoChar"); + : Pair.UserData.AliasOrUID + ":" + PlayerName + ":" + (PlayerCharacter != nint.Zero ? "HasChar" : "NoChar"); } internal void SetUploading(bool isUploading = true) @@ -222,7 +223,7 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase SetUploading(isUploading: false); _downloadManager.Dispose(); var name = PlayerName; - Logger.LogDebug("Disposing {name} ({user})", name, OnlineUser); + Logger.LogDebug("Disposing {name} ({user})", name, Pair); try { Guid applicationId = Guid.NewGuid(); @@ -235,25 +236,24 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase if (!string.IsNullOrEmpty(name)) { - Mediator.Publish(new EventMessage(new Event(name, OnlineUser.User, nameof(PairHandler), EventSeverity.Informational, "Disposing User"))); + Mediator.Publish(new EventMessage(new Event(name, Pair.UserData, nameof(PairHandler), EventSeverity.Informational, "Disposing User"))); } - if (_lifetime.IsCancellationRequested) return; - - Logger.LogDebug("[{applicationId}] Removing Temp Collection for {name} ({user})", applicationId, name, OnlineUser); - + Logger.LogDebug("[{applicationId}] Removing Temp Collection for {name} ({user})", applicationId, name, Pair.UserPair); if (_penumbraCollection != Guid.Empty) { _ipcManager.Penumbra.RemoveTemporaryCollectionAsync(Logger, applicationId, _penumbraCollection).GetAwaiter().GetResult(); _penumbraCollection = Guid.Empty; } + if (_lifetime.ApplicationStopping.IsCancellationRequested) return; + if (_dalamudUtil is { IsZoning: false, IsInCutscene: false } && !string.IsNullOrEmpty(name)) { - Logger.LogTrace("[{applicationId}] Restoring state for {name} ({OnlineUser})", applicationId, name, OnlineUser); + Logger.LogTrace("[{applicationId}] Restoring state for {name} ({OnlineUser})", applicationId, name, Pair.UserPair); if (!IsVisible) { - Logger.LogDebug("[{applicationId}] Restoring Glamourer for {name} ({user})", applicationId, name, OnlineUser); + Logger.LogDebug("[{applicationId}] Restoring Glamourer for {name} ({user})", applicationId, name, Pair.UserPair); _ipcManager.Glamourer.RevertByNameAsync(Logger, name, applicationId).GetAwaiter().GetResult(); } else @@ -261,6 +261,8 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase var cts = new CancellationTokenSource(); cts.CancelAfter(TimeSpan.FromSeconds(60)); + Logger.LogInformation("[{applicationId}] CachedData is null {isNull}, contains things: {contains}", applicationId, _cachedData == null, _cachedData?.FileReplacements.Any() ?? false); + foreach (KeyValuePair> item in _cachedData?.FileReplacements ?? []) { try @@ -380,136 +382,151 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase _downloadCancellationTokenSource = _downloadCancellationTokenSource?.CancelRecreate() ?? new CancellationTokenSource(); var downloadToken = _downloadCancellationTokenSource.Token; - _ = Task.Run(async () => + _ = DownloadAndApplyCharacterAsync(applicationBase, charaData, updatedData, updateModdedPaths, updateManip, downloadToken).ConfigureAwait(false); + } + + private async Task DownloadAndApplyCharacterAsync(Guid applicationBase, CharacterData charaData, Dictionary> updatedData, + bool updateModdedPaths, bool updateManip, CancellationToken downloadToken) + { + Dictionary<(string GamePath, string? Hash), string> moddedPaths = []; + + if (updateModdedPaths) { - Dictionary<(string GamePath, string? Hash), string> moddedPaths = new(); + int attempts = 0; + List toDownloadReplacements = TryCalculateModdedDictionary(applicationBase, charaData, out moddedPaths, downloadToken); + + while (toDownloadReplacements.Count > 0 && attempts++ <= 10 && !downloadToken.IsCancellationRequested) + { + _downloadManager.CancelDownload(); + Logger.LogDebug("[BASE-{appBase}] Downloading missing files for player {name}, {kind}", applicationBase, PlayerName, updatedData); + + Mediator.Publish(new EventMessage(new Event(PlayerName, Pair.UserData, nameof(PairHandler), 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.CancelDownload(); + return; + } + + await _downloadManager.DownloadFiles(_charaHandler!, toDownloadReplacements, downloadToken).ConfigureAwait(false); + _downloadManager.CancelDownload(); + + if (downloadToken.IsCancellationRequested) + { + Logger.LogTrace("[BASE-{appBase}] Detected cancellation", applicationBase); + _downloadManager.CancelDownload(); + 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)).ConfigureAwait(false); + } + + if (!await _playerPerformanceService.CheckBothThresholds(this, charaData).ConfigureAwait(false)) + return; + } + + downloadToken.ThrowIfCancellationRequested(); + + var appToken = _applicationCancellationTokenSource?.Token; + while ((!_applicationTask?.IsCompleted ?? false) + && !downloadToken.IsCancellationRequested + && (!appToken?.IsCancellationRequested ?? false)) + { + // block until current application is done + 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)) return; + + _applicationCancellationTokenSource = _applicationCancellationTokenSource.CancelRecreate() ?? new CancellationTokenSource(); + var token = _applicationCancellationTokenSource.Token; + + _applicationTask = ApplyCharacterDataAsync(applicationBase, charaData, updatedData, updateModdedPaths, updateManip, moddedPaths, token); + } + + private async Task ApplyCharacterDataAsync(Guid applicationBase, CharacterData charaData, Dictionary> updatedData, bool updateModdedPaths, bool updateManip, + Dictionary<(string GamePath, string? Hash), string> moddedPaths, CancellationToken token) + { + try + { + _applicationId = Guid.NewGuid(); + Logger.LogDebug("[BASE-{applicationId}] Starting application task for {this}: {appId}", applicationBase, this, _applicationId); + + Logger.LogDebug("[{applicationId}] Waiting for initial draw for for {handler}", _applicationId, _charaHandler); + await _dalamudUtil.WaitWhileCharacterIsDrawing(Logger, _charaHandler!, _applicationId, 30000, token).ConfigureAwait(false); + + token.ThrowIfCancellationRequested(); if (updateModdedPaths) { - int attempts = 0; - List toDownloadReplacements = TryCalculateModdedDictionary(applicationBase, charaData, out moddedPaths, downloadToken); + // ensure collection is set + var objIndex = await _dalamudUtil.RunOnFrameworkThread(() => _charaHandler!.GetGameObject()!.ObjectIndex).ConfigureAwait(false); + await _ipcManager.Penumbra.AssignTemporaryCollectionAsync(Logger, _penumbraCollection, objIndex).ConfigureAwait(false); - while (toDownloadReplacements.Count > 0 && attempts++ <= 10 && !downloadToken.IsCancellationRequested) + await _ipcManager.Penumbra.SetTemporaryModsAsync(Logger, _applicationId, _penumbraCollection, + moddedPaths.ToDictionary(k => k.Key.GamePath, k => k.Value, StringComparer.Ordinal)).ConfigureAwait(false); + LastAppliedDataBytes = -1; + foreach (var path in moddedPaths.Values.Distinct(StringComparer.OrdinalIgnoreCase).Select(v => new FileInfo(v)).Where(p => p.Exists)) { - _downloadManager.CancelDownload(); - Logger.LogDebug("[BASE-{appBase}] Downloading missing files for player {name}, {kind}", applicationBase, PlayerName, updatedData); - if (toDownloadReplacements.Any()) - { - Mediator.Publish(new EventMessage(new Event(PlayerName, OnlineUser.User, nameof(PairHandler), EventSeverity.Informational, - $"Starting download for {toDownloadReplacements.Count} files"))); - await _downloadManager.DownloadFiles(_charaHandler!, toDownloadReplacements, downloadToken).ConfigureAwait(false); - _downloadManager.CancelDownload(); - } + if (LastAppliedDataBytes == -1) LastAppliedDataBytes = 0; - if (downloadToken.IsCancellationRequested) - { - Logger.LogTrace("[BASE-{appBase}] Detected cancellation", applicationBase); - _downloadManager.CancelDownload(); - 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)).ConfigureAwait(false); + LastAppliedDataBytes += path.Length; } } - downloadToken.ThrowIfCancellationRequested(); - - var appToken = _applicationCancellationTokenSource?.Token; - while ((!_applicationTask?.IsCompleted ?? false) - && !downloadToken.IsCancellationRequested - && (!appToken?.IsCancellationRequested ?? false)) + if (updateManip) { - // block until current application is done - 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); + await _ipcManager.Penumbra.SetManipulationDataAsync(Logger, _applicationId, _penumbraCollection, charaData.ManipulationData).ConfigureAwait(false); } - if (downloadToken.IsCancellationRequested || (appToken?.IsCancellationRequested ?? false)) return; + token.ThrowIfCancellationRequested(); - _applicationCancellationTokenSource = _applicationCancellationTokenSource.CancelRecreate() ?? new CancellationTokenSource(); - var token = _applicationCancellationTokenSource.Token; - _applicationTask = Task.Run(async () => + foreach (var kind in updatedData) { - try - { - _applicationId = Guid.NewGuid(); - Logger.LogDebug("[BASE-{applicationId}] Starting application task for {this}: {appId}", applicationBase, this, _applicationId); + await ApplyCustomizationDataAsync(_applicationId, kind, charaData, token).ConfigureAwait(false); + token.ThrowIfCancellationRequested(); + } - Logger.LogDebug("[{applicationId}] Waiting for initial draw for for {handler}", _applicationId, _charaHandler); - await _dalamudUtil.WaitWhileCharacterIsDrawing(Logger, _charaHandler!, _applicationId, 30000, token).ConfigureAwait(false); + _cachedData = charaData; - token.ThrowIfCancellationRequested(); - - if (updateModdedPaths) - { - await _ipcManager.Penumbra.SetTemporaryModsAsync(Logger, _applicationId, _penumbraCollection, - moddedPaths.ToDictionary(k => k.Key.GamePath, k => k.Value, StringComparer.Ordinal)).ConfigureAwait(false); - LastAppliedDataSize = -1; - LastAppliedDataTris = -1; - foreach (var path in moddedPaths.Values.Distinct(StringComparer.OrdinalIgnoreCase).Select(v => new FileInfo(v)).Where(p => p.Exists)) - { - if (LastAppliedDataSize == -1) LastAppliedDataSize = 0; - LastAppliedDataSize += path.Length; - } - foreach (var key in moddedPaths.Keys.Where(k => !string.IsNullOrEmpty(k.Hash))) - { - if (LastAppliedDataTris == -1) LastAppliedDataTris = 0; - LastAppliedDataTris += await _xivDataAnalyzer.GetTrianglesByHash(key.Hash!).ConfigureAwait(false); - } - } - - if (updateManip) - { - await _ipcManager.Penumbra.SetManipulationDataAsync(Logger, _applicationId, _penumbraCollection, charaData.ManipulationData).ConfigureAwait(false); - } - - token.ThrowIfCancellationRequested(); - - foreach (var kind in updatedData) - { - await ApplyCustomizationDataAsync(_applicationId, kind, charaData, token).ConfigureAwait(false); - token.ThrowIfCancellationRequested(); - } - - _cachedData = charaData; - - Logger.LogDebug("[{applicationId}] Application finished", _applicationId); - } - catch (Exception ex) - { - if (ex is AggregateException aggr && aggr.InnerExceptions.Any(e => e is ArgumentNullException)) - { - IsVisible = false; - _forceApplyMods = true; - _cachedData = charaData; - Logger.LogDebug("[{applicationId}] Cancelled, player turned null during application", _applicationId); - } - else - { - Logger.LogWarning(ex, "[{applicationId}] Cancelled", _applicationId); - } - } - }, token); - }, downloadToken); + Logger.LogDebug("[{applicationId}] Application finished", _applicationId); + } + catch (Exception ex) + { + if (ex is AggregateException aggr && aggr.InnerExceptions.Any(e => e is ArgumentNullException)) + { + IsVisible = false; + _forceApplyMods = true; + _cachedData = charaData; + Logger.LogDebug("[{applicationId}] Cancelled, player turned null during application", _applicationId); + } + else + { + Logger.LogWarning(ex, "[{applicationId}] Cancelled", _applicationId); + } + } } private void FrameworkUpdate() { if (string.IsNullOrEmpty(PlayerName)) { - var pc = _dalamudUtil.FindPlayerByNameHash(OnlineUser.Ident); + var pc = _dalamudUtil.FindPlayerByNameHash(Pair.Ident); if (pc.ObjectId == 0) return; Logger.LogDebug("One-Time Initializing {this}", this); Initialize(pc.Name); Logger.LogDebug("One-Time Initialized {this}", this); - Mediator.Publish(new EventMessage(new Event(PlayerName, OnlineUser.User, nameof(PairHandler), EventSeverity.Informational, + Mediator.Publish(new EventMessage(new Event(PlayerName, Pair.UserData, nameof(PairHandler), EventSeverity.Informational, $"Initializing User For Character {pc.Name}"))); } @@ -545,7 +562,7 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase private void Initialize(string name) { PlayerName = name; - _charaHandler = _gameObjectHandlerFactory.Create(ObjectKind.Player, () => _dalamudUtil.GetPlayerCharacterFromCachedTableByIdent(OnlineUser.Ident), isWatched: false).GetAwaiter().GetResult(); + _charaHandler = _gameObjectHandlerFactory.Create(ObjectKind.Player, () => _dalamudUtil.GetPlayerCharacterFromCachedTableByIdent(Pair.Ident), isWatched: false).GetAwaiter().GetResult(); Mediator.Subscribe(this, async (_) => { @@ -562,17 +579,16 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase }); if (_penumbraCollection == Guid.Empty) - _penumbraCollection = _ipcManager.Penumbra.CreateTemporaryCollectionAsync(Logger, OnlineUser.User.UID).GetAwaiter().GetResult(); + _penumbraCollection = _ipcManager.Penumbra.CreateTemporaryCollectionAsync(Logger, Pair.UserData.UID).GetAwaiter().GetResult(); _ipcManager.Penumbra.AssignTemporaryCollectionAsync(Logger, _penumbraCollection, _charaHandler.GetGameObject()!.ObjectIndex).GetAwaiter().GetResult(); - _serverConfigurationManager.SetNameForUid(OnlineUser.User.UID, name); } private async Task RevertCustomizationDataAsync(ObjectKind objectKind, string name, Guid applicationId, CancellationToken cancelToken) { - nint address = _dalamudUtil.GetPlayerCharacterFromCachedTableByIdent(OnlineUser.Ident); + nint address = _dalamudUtil.GetPlayerCharacterFromCachedTableByIdent(Pair.Ident); if (address == nint.Zero) return; - Logger.LogDebug("[{applicationId}] Reverting all Customization for {alias}/{name} {objectKind}", applicationId, OnlineUser.User.AliasOrUID, name, objectKind); + Logger.LogDebug("[{applicationId}] Reverting all Customization for {alias}/{name} {objectKind}", applicationId, Pair.UserData.AliasOrUID, name, objectKind); if (_customizeIds.TryGetValue(objectKind, out var customizeId)) { @@ -583,18 +599,18 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase { using GameObjectHandler tempHandler = await _gameObjectHandlerFactory.Create(ObjectKind.Player, () => address, isWatched: false).ConfigureAwait(false); tempHandler.CompareNameAndThrow(name); - Logger.LogDebug("[{applicationId}] Restoring Customization and Equipment for {alias}/{name}", applicationId, OnlineUser.User.AliasOrUID, name); + Logger.LogDebug("[{applicationId}] Restoring Customization and Equipment for {alias}/{name}", applicationId, Pair.UserData.AliasOrUID, name); await _ipcManager.Glamourer.RevertAsync(Logger, tempHandler, applicationId, cancelToken).ConfigureAwait(false); tempHandler.CompareNameAndThrow(name); - Logger.LogDebug("[{applicationId}] Restoring Heels for {alias}/{name}", applicationId, OnlineUser.User.AliasOrUID, name); + Logger.LogDebug("[{applicationId}] Restoring Heels for {alias}/{name}", applicationId, Pair.UserData.AliasOrUID, name); await _ipcManager.Heels.RestoreOffsetForPlayerAsync(address).ConfigureAwait(false); tempHandler.CompareNameAndThrow(name); - Logger.LogDebug("[{applicationId}] Restoring C+ for {alias}/{name}", applicationId, OnlineUser.User.AliasOrUID, name); + Logger.LogDebug("[{applicationId}] Restoring C+ for {alias}/{name}", applicationId, Pair.UserData.AliasOrUID, name); await _ipcManager.CustomizePlus.RevertByIdAsync(customizeId).ConfigureAwait(false); tempHandler.CompareNameAndThrow(name); - Logger.LogDebug("[{applicationId}] Restoring Honorific for {alias}/{name}", applicationId, OnlineUser.User.AliasOrUID, name); + Logger.LogDebug("[{applicationId}] Restoring Honorific for {alias}/{name}", applicationId, Pair.UserData.AliasOrUID, name); await _ipcManager.Honorific.ClearTitleAsync(address).ConfigureAwait(false); - Logger.LogDebug("[{applicationId}] Restoring Pet Nicknames for {alias}/{name}", applicationId, OnlineUser.User.AliasOrUID, name); + Logger.LogDebug("[{applicationId}] Restoring Pet Nicknames for {alias}/{name}", applicationId, Pair.UserData.AliasOrUID, name); await _ipcManager.PetNames.ClearPlayerData(address).ConfigureAwait(false); } else if (objectKind == ObjectKind.MinionOrMount) @@ -636,7 +652,7 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase { Stopwatch st = Stopwatch.StartNew(); ConcurrentBag missingFiles = []; - moddedDictionary = new Dictionary<(string GamePath, string? Hash), string>(); + moddedDictionary = []; ConcurrentDictionary<(string GamePath, string? Hash), string> outputDict = new(); bool hasMigrationChanges = false; @@ -692,4 +708,4 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase Logger.LogDebug("[BASE-{appBase}] ModdedPaths calculated in {time}ms, missing files: {count}, total files: {total}", applicationBase, st.ElapsedMilliseconds, missingFiles.Count, moddedDictionary.Keys.Count); return [.. missingFiles]; } -} +} \ No newline at end of file diff --git a/MareSynchronos/PlayerData/Pairs/OnlinePlayerManager.cs b/MareSynchronos/PlayerData/Pairs/OnlinePlayerManager.cs index 197eaef..7655b72 100644 --- a/MareSynchronos/PlayerData/Pairs/OnlinePlayerManager.cs +++ b/MareSynchronos/PlayerData/Pairs/OnlinePlayerManager.cs @@ -53,7 +53,7 @@ public class OnlinePlayerManager : DisposableMediatorSubscriberBase var newVisiblePlayers = _newVisiblePlayers.ToList(); _newVisiblePlayers.Clear(); Logger.LogTrace("Has new visible players, pushing character data"); - PushCharacterData(newVisiblePlayers.Select(c => c.OnlineUser.User).ToList()); + PushCharacterData(newVisiblePlayers.Select(c => c.Pair.UserData).ToList()); } private void PlayerManagerOnPlayerHasChanged() diff --git a/MareSynchronos/PlayerData/Pairs/Pair.cs b/MareSynchronos/PlayerData/Pairs/Pair.cs index 8349228..8385de9 100644 --- a/MareSynchronos/PlayerData/Pairs/Pair.cs +++ b/MareSynchronos/PlayerData/Pairs/Pair.cs @@ -1,5 +1,4 @@ using Dalamud.Game.Gui.ContextMenu; -using Dalamud.Game.Text.SeStringHandling; using MareSynchronos.API.Data; using MareSynchronos.API.Data.Comparer; using MareSynchronos.API.Data.Enum; @@ -24,9 +23,8 @@ public class Pair private readonly MareMediator _mediator; private readonly MareConfigService _mareConfig; private readonly ServerConfigurationManager _serverConfigurationManager; - private CancellationTokenSource _applicationCts = new CancellationTokenSource(); + private CancellationTokenSource _applicationCts = new(); private OnlineUserIdentDto? _onlineUserIdentDto = null; - private string? _playerName = null; public Pair(ILogger logger, PairHandlerFactory cachedPlayerFactory, MareMediator mediator, MareConfigService mareConfig, ServerConfigurationManager serverConfigurationManager) @@ -49,8 +47,10 @@ public class Pair public CharacterData? LastReceivedCharacterData { get; set; } public string? PlayerName => GetPlayerName(); public uint PlayerCharacterId => GetPlayerCharacterId(); - public long LastAppliedDataSize => CachedPlayer?.LastAppliedDataSize ?? -1; - public long LastAppliedDataTris => CachedPlayer?.LastAppliedDataTris ?? -1; + public long LastAppliedDataBytes => CachedPlayer?.LastAppliedDataBytes ?? -1; + public long LastAppliedDataTris { get; set; } = -1; + public long LastAppliedApproximateVRAMBytes { get; set; } = -1; + public string Ident => _onlineUserIdentDto?.Ident ?? string.Empty; public UserData UserData => UserPair?.User ?? GroupPair.First().Value.User; @@ -154,7 +154,7 @@ public class Pair } CachedPlayer?.Dispose(); - CachedPlayer = _cachedPlayerFactory.Create(new OnlineUserIdentDto(UserData, _onlineUserIdentDto!.Ident)); + CachedPlayer = _cachedPlayerFactory.Create(this); } finally { @@ -207,11 +207,11 @@ public class Pair { if (wait) _creationSemaphore.Wait(); - _onlineUserIdentDto = null; LastReceivedCharacterData = null; var player = CachedPlayer; CachedPlayer = null; player?.Dispose(); + _onlineUserIdentDto = null; } finally { diff --git a/MareSynchronos/PlayerData/Pairs/PairManager.cs b/MareSynchronos/PlayerData/Pairs/PairManager.cs index e30af46..3ec2f59 100644 --- a/MareSynchronos/PlayerData/Pairs/PairManager.cs +++ b/MareSynchronos/PlayerData/Pairs/PairManager.cs @@ -1,5 +1,4 @@ -using Dalamud.Game.Gui.ContextMenu; -using Dalamud.Plugin.Services; +using Dalamud.Plugin.Services; using MareSynchronos.API.Data; using MareSynchronos.API.Data.Comparer; using MareSynchronos.API.Data.Extensions; @@ -37,7 +36,7 @@ public sealed class PairManager : DisposableMediatorSubscriberBase _directPairsInternal = DirectPairsLazy(); _groupPairsInternal = GroupPairsLazy(); - _dalamudContextMenu.OnMenuOpened += DalamudContextMenuOnMenuOpened; + _dalamudContextMenu.OnMenuOpened += DalamudContextMenuOnOnOpenGameObjectContextMenu; } public List DirectPairs => _directPairsInternal.Value; @@ -230,7 +229,8 @@ public sealed class PairManager : DisposableMediatorSubscriberBase pair.UserPair.OtherPermissions.IsDisableSounds(), pair.UserPair.OtherPermissions.IsDisableVFX()); - pair.ApplyLastReceivedData(); + if (!pair.IsPaused) + pair.ApplyLastReceivedData(); RecreateLazy(); } @@ -258,7 +258,8 @@ public sealed class PairManager : DisposableMediatorSubscriberBase pair.UserPair.OwnPermissions.IsDisableSounds(), pair.UserPair.OwnPermissions.IsDisableVFX()); - pair.ApplyLastReceivedData(); + if (!pair.IsPaused) + pair.ApplyLastReceivedData(); RecreateLazy(); } @@ -332,14 +333,14 @@ public sealed class PairManager : DisposableMediatorSubscriberBase { base.Dispose(disposing); - _dalamudContextMenu.OnMenuOpened -= DalamudContextMenuOnMenuOpened; + _dalamudContextMenu.OnMenuOpened -= DalamudContextMenuOnOnOpenGameObjectContextMenu; DisposePairs(); } - private void DalamudContextMenuOnMenuOpened(IMenuOpenedArgs args) + private void DalamudContextMenuOnOnOpenGameObjectContextMenu(Dalamud.Game.Gui.ContextMenu.IMenuOpenedArgs args) { - /* TODO: Check empty target */ + if (args.MenuType == Dalamud.Game.Gui.ContextMenu.ContextMenuType.Inventory) return; if (!_configurationService.Current.EnableRightClickMenus) return; foreach (var pair in _allClientPairs.Where((p => p.Value.IsVisible))) diff --git a/MareSynchronos/PlayerData/Services/CacheCreationService.cs b/MareSynchronos/PlayerData/Services/CacheCreationService.cs index 7f57aa4..6b9b47f 100644 --- a/MareSynchronos/PlayerData/Services/CacheCreationService.cs +++ b/MareSynchronos/PlayerData/Services/CacheCreationService.cs @@ -74,24 +74,28 @@ public sealed class CacheCreationService : DisposableMediatorSubscriberBase }); }); - Mediator.Subscribe(this, async (msg) => + Mediator.Subscribe(this, (msg) => { if (_isZoning) return; - foreach (var item in _playerRelatedObjects - .Where(item => string.IsNullOrEmpty(msg.ProfileName) - || string.Equals(item.Value.Name, msg.ProfileName, StringComparison.Ordinal)).Select(k => k.Key)) + _ = Task.Run(async () => { - Logger.LogDebug("Received CustomizePlus change, updating {obj}", item); - await AddPlayerCacheToCreate(item).ConfigureAwait(false); - } + + foreach (var item in _playerRelatedObjects + .Where(item => msg.Address == null + || item.Value.Address == msg.Address).Select(k => k.Key)) + { + Logger.LogDebug("Received CustomizePlus change, updating {obj}", item); + await AddPlayerCacheToCreate(item).ConfigureAwait(false); + } + }); }); - Mediator.Subscribe(this, async (_) => + Mediator.Subscribe(this, (msg) => { if (_isZoning) return; Logger.LogDebug("Received Heels Offset change, updating player"); - await AddPlayerCacheToCreate().ConfigureAwait(false); + _ = AddPlayerCacheToCreate(); }); - Mediator.Subscribe(this, async (msg) => + Mediator.Subscribe(this, (msg) => { if (_isZoning) return; var changedType = _playerRelatedObjects.FirstOrDefault(f => f.Value.Address == msg.Address); @@ -118,10 +122,10 @@ public sealed class CacheCreationService : DisposableMediatorSubscriberBase PetNicknamesChanged(); } }); - Mediator.Subscribe(this, async (msg) => + Mediator.Subscribe(this, (msg) => { Logger.LogDebug("Received Penumbra Mod settings change, updating player"); - await AddPlayerCacheToCreate().ConfigureAwait(false); + _ = AddPlayerCacheToCreate(); }); Mediator.Subscribe(this, (msg) => ProcessCacheCreation()); diff --git a/MareSynchronos/Plugin.cs b/MareSynchronos/Plugin.cs index f5f15a8..229478c 100644 --- a/MareSynchronos/Plugin.cs +++ b/MareSynchronos/Plugin.cs @@ -35,20 +35,19 @@ public sealed class Plugin : IDalamudPlugin private readonly IHost _host; public static Plugin Self; - public Action _realOnFrameworkUpdate = null; + public Action? _realOnFrameworkUpdate { get; set; } // Proxy function in the LoporritSync namespace to avoid confusion in /xlstats public void OnFrameworkUpdate(IFramework framework) { - if (_realOnFrameworkUpdate != null) - _realOnFrameworkUpdate(framework); + _realOnFrameworkUpdate?.Invoke(framework); } public Plugin(IDalamudPluginInterface pluginInterface, ICommandManager commandManager, IDataManager gameData, IFramework framework, IObjectTable objectTable, IClientState clientState, ICondition condition, IChatGui chatGui, - IGameGui gameGui, IDtrBar dtrBar, IToastGui toastGui, IPluginLog pluginLog, ITargetManager targetManager, IGameLifecycle addonLifecycle, - INotificationManager notificationManager, ITextureProvider textureProvider, IContextMenu contextMenu, IGameInteropProvider gameInteropProvider, - INamePlateGui namePlateGui) + IGameGui gameGui, IDtrBar dtrBar, IToastGui toastGui, IPluginLog pluginLog, ITargetManager targetManager, INotificationManager notificationManager, + ITextureProvider textureProvider, IContextMenu contextMenu, + IGameInteropProvider gameInteropProvider, INamePlateGui namePlateGui) { Plugin.Self = this; _host = new HostBuilder() @@ -61,7 +60,7 @@ public sealed class Plugin : IDalamudPlugin }) .ConfigureServices(collection => { - collection.AddSingleton(new WindowSystem("LoporritSync")); + collection.AddSingleton(new WindowSystem("MareSynchronos")); collection.AddSingleton(); collection.AddSingleton(new Dalamud.Localization("MareSynchronos.Localization.", "", useEmbedded: true)); @@ -69,8 +68,6 @@ public sealed class Plugin : IDalamudPlugin collection.AddSingleton(); collection.AddSingleton(); collection.AddSingleton(); - collection.AddSingleton((s) => new PairManager(s.GetRequiredService>(), s.GetRequiredService(), - s.GetRequiredService(), s.GetRequiredService(), contextMenu)); collection.AddSingleton(); collection.AddSingleton(); collection.AddSingleton(); @@ -81,27 +78,25 @@ public sealed class Plugin : IDalamudPlugin collection.AddSingleton(); collection.AddSingleton(); collection.AddSingleton(); - collection.AddSingleton((s) => new PairHandlerFactory(s.GetRequiredService(), s.GetRequiredService(), - s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), - s.GetRequiredService(), s.GetRequiredService(), - CancellationTokenSource.CreateLinkedTokenSource(addonLifecycle.GameShuttingDownToken, addonLifecycle.DalamudUnloadingToken).Token, - s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService())); + collection.AddSingleton(); collection.AddSingleton(); - collection.AddSingleton(s => new(s.GetRequiredService>(), s.GetRequiredService(), - s.GetRequiredService(), gameData)); + collection.AddSingleton(); collection.AddSingleton(); collection.AddSingleton(); collection.AddSingleton(); collection.AddSingleton(); collection.AddSingleton(); collection.AddSingleton(); - collection.AddSingleton(); + collection.AddSingleton(); + collection.AddSingleton((s) => new EventAggregator(s.GetRequiredService(), + s.GetRequiredService>(), s.GetRequiredService())); collection.AddSingleton((s) => new DalamudUtilService(s.GetRequiredService>(), clientState, objectTable, framework, gameGui, toastGui, condition, gameData, targetManager, s.GetRequiredService(), s.GetRequiredService())); collection.AddSingleton((s) => new DtrEntry(s.GetRequiredService>(), dtrBar, s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService())); - + collection.AddSingleton(s => new PairManager(s.GetRequiredService>(), s.GetRequiredService(), + s.GetRequiredService(), s.GetRequiredService(), contextMenu)); collection.AddSingleton(); collection.AddSingleton((s) => new IpcCallerPenumbra(s.GetRequiredService>(), pluginInterface, s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService())); @@ -119,7 +114,9 @@ public sealed class Plugin : IDalamudPlugin s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService())); - + collection.AddSingleton((s) => new NotificationService(s.GetRequiredService>(), + s.GetRequiredService(), s.GetRequiredService(), + notificationManager, chatGui, s.GetRequiredService())); collection.AddSingleton((s) => new MareConfigService(pluginInterface.ConfigDirectory.FullName)); collection.AddSingleton((s) => new ServerConfigService(pluginInterface.ConfigDirectory.FullName)); collection.AddSingleton((s) => new NotesConfigService(pluginInterface.ConfigDirectory.FullName)); @@ -127,7 +124,7 @@ public sealed class Plugin : IDalamudPlugin collection.AddSingleton((s) => new SyncshellConfigService(pluginInterface.ConfigDirectory.FullName)); collection.AddSingleton((s) => new TransientConfigService(pluginInterface.ConfigDirectory.FullName)); collection.AddSingleton((s) => new XivDataStorageService(pluginInterface.ConfigDirectory.FullName)); - collection.AddSingleton((s) => new ConfigurationMigrator(s.GetRequiredService>(), pluginInterface, s.GetRequiredService())); + collection.AddSingleton((s) => new ConfigurationMigrator(s.GetRequiredService>(), s.GetRequiredService())); collection.AddSingleton(); // add scoped services @@ -143,8 +140,8 @@ public sealed class Plugin : IDalamudPlugin collection.AddScoped(); collection.AddScoped((s) => new EditProfileUi(s.GetRequiredService>(), - s.GetRequiredService(), s.GetRequiredService(), pluginInterface.UiBuilder, s.GetRequiredService(), - s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService())); + s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), + s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService())); collection.AddScoped(); collection.AddScoped(); collection.AddScoped(); @@ -157,10 +154,8 @@ public sealed class Plugin : IDalamudPlugin s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService())); collection.AddScoped((s) => new CommandManagerService(commandManager, s.GetRequiredService(), - s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), - s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService())); - collection.AddScoped((s) => new NotificationService(s.GetRequiredService>(), - s.GetRequiredService(), notificationManager, chatGui, s.GetRequiredService())); + s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), + s.GetRequiredService(), s.GetRequiredService())); collection.AddScoped((s) => new UiSharedService(s.GetRequiredService>(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), pluginInterface, textureProvider, s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService())); @@ -171,14 +166,15 @@ public sealed class Plugin : IDalamudPlugin collection.AddScoped((s) => new GuiHookService(s.GetRequiredService>(), s.GetRequiredService(), s.GetRequiredService(), namePlateGui, s.GetRequiredService())); - collection.AddHostedService(p => p.GetRequiredService()); collection.AddHostedService(p => p.GetRequiredService()); + collection.AddHostedService(p => p.GetRequiredService()); + collection.AddHostedService(p => p.GetRequiredService()); collection.AddHostedService(p => p.GetRequiredService()); collection.AddHostedService(p => p.GetRequiredService()); collection.AddHostedService(p => p.GetRequiredService()); collection.AddHostedService(p => p.GetRequiredService()); - collection.AddHostedService(p => p.GetRequiredService()); collection.AddHostedService(p => p.GetRequiredService()); + collection.AddHostedService(p => p.GetRequiredService()); }) .Build(); @@ -190,4 +186,4 @@ public sealed class Plugin : IDalamudPlugin _host.StopAsync().GetAwaiter().GetResult(); _host.Dispose(); } -} +} \ No newline at end of file diff --git a/MareSynchronos/Services/CharacterAnalyzer.cs b/MareSynchronos/Services/CharacterAnalyzer.cs index b408f7a..7a26f98 100644 --- a/MareSynchronos/Services/CharacterAnalyzer.cs +++ b/MareSynchronos/Services/CharacterAnalyzer.cs @@ -193,16 +193,16 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable public bool IsComputed => OriginalSize > 0 && CompressedSize > 0; public async Task ComputeSizes(FileCacheManager fileCacheManager, CancellationToken token) { - var compressedsize = await fileCacheManager.GetCompressedFileLength(Hash, token).ConfigureAwait(false); + var compressedsize = await fileCacheManager.GetCompressedFileData(Hash, token).ConfigureAwait(false); var normalSize = new FileInfo(FilePaths[0]).Length; var entries = fileCacheManager.GetAllFileCachesByHash(Hash, ignoreCacheEntries: true, validate: false); foreach (var entry in entries) { entry.Size = normalSize; - entry.CompressedSize = compressedsize; + entry.CompressedSize = compressedsize.Item2.LongLength; } OriginalSize = normalSize; - CompressedSize = compressedsize; + CompressedSize = compressedsize.Item2.LongLength; } public long OriginalSize { get; private set; } = OriginalSize; public long CompressedSize { get; private set; } = CompressedSize; diff --git a/MareSynchronos/Services/DalamudUtilService.cs b/MareSynchronos/Services/DalamudUtilService.cs index a5b00fe..7b8548f 100644 --- a/MareSynchronos/Services/DalamudUtilService.cs +++ b/MareSynchronos/Services/DalamudUtilService.cs @@ -36,6 +36,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber private readonly List _classJobIdsIgnoredForPets = [30]; private readonly IClientState _clientState; private readonly ICondition _condition; + private readonly IDataManager _gameData; private readonly IFramework _framework; private readonly IGameGui _gameGui; private readonly IToastGui _toastGui; @@ -63,6 +64,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber _gameGui = gameGui; _toastGui = toastGui; _condition = condition; + _gameData = gameData; Mediator = mediator; _performanceCollector = performanceCollector; WorldData = new(() => @@ -98,27 +100,28 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber public bool IsWine { get; init; } public unsafe GameObject* GposeTarget => TargetSystem.Instance()->GPoseTarget; - public unsafe DalamudGameObject? GposeTargetGameObject => GposeTarget == null ? null : _objectTable[GposeTarget->ObjectIndex]; + public unsafe Dalamud.Game.ClientState.Objects.Types.IGameObject? GposeTargetGameObject => GposeTarget == null ? null : _objectTable[GposeTarget->ObjectIndex]; public bool IsAnythingDrawing { get; private set; } = false; public bool IsInCutscene { get; private set; } = false; public bool IsInGpose { get; private set; } = false; public bool IsLoggedIn { get; private set; } public bool IsOnFrameworkThread => _framework.IsInFrameworkUpdateThread; public bool IsZoning => _condition[ConditionFlag.BetweenAreas] || _condition[ConditionFlag.BetweenAreas51]; - public bool IsInCombat { get; private set; } = false; + public bool IsInCombatOrPerforming { get; private set; } = false; + public bool HasModifiedGameFiles => _gameData.HasModifiedGameDataFiles; public Lazy> WorldData { get; private set; } public Lazy> UiColors { get; private set; } public MareMediator Mediator { get; } - public DalamudGameObject? CreateGameObject(IntPtr reference) + public Dalamud.Game.ClientState.Objects.Types.IGameObject? CreateGameObject(IntPtr reference) { EnsureIsOnFramework(); return _objectTable.CreateObjectReference(reference); } - public async Task CreateGameObjectAsync(IntPtr reference) + public async Task CreateGameObjectAsync(IntPtr reference) { return await RunOnFrameworkThread(() => _objectTable.CreateObjectReference(reference)).ConfigureAwait(false); } @@ -532,19 +535,19 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber Mediator.Publish(new GposeEndMessage()); } - if (_condition[ConditionFlag.InCombat] && !IsInCombat) + if ((_condition[ConditionFlag.Performing] || _condition[ConditionFlag.InCombat]) && !IsInCombatOrPerforming) { - _logger.LogDebug("Combat start"); - IsInCombat = true; - Mediator.Publish(new CombatStartMessage()); - Mediator.Publish(new HaltScanMessage(nameof(IsInCombat))); + _logger.LogDebug("Combat/Performance start"); + IsInCombatOrPerforming = true; + Mediator.Publish(new CombatOrPerformanceStartMessage()); + Mediator.Publish(new HaltScanMessage(nameof(IsInCombatOrPerforming))); } - else if (!_condition[ConditionFlag.InCombat] && IsInCombat) + else if ((!_condition[ConditionFlag.Performing] && !_condition[ConditionFlag.InCombat]) && IsInCombatOrPerforming) { - _logger.LogDebug("Combat end"); - IsInCombat = false; - Mediator.Publish(new CombatEndMessage()); - Mediator.Publish(new ResumeScanMessage(nameof(IsInCombat))); + _logger.LogDebug("Combat/Performance end"); + IsInCombatOrPerforming = false; + Mediator.Publish(new CombatOrPerformanceEndMessage()); + Mediator.Publish(new ResumeScanMessage(nameof(IsInCombatOrPerforming))); } if (_condition[ConditionFlag.WatchingCutscene] && !IsInCutscene) @@ -595,7 +598,13 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber Mediator.Publish(new ResumeScanMessage(nameof(ConditionFlag.BetweenAreas))); } - if (!IsInCombat) + var localPlayer = _clientState.LocalPlayer; + if (localPlayer != null) + { + _classJobId = localPlayer.ClassJob.RowId; + } + + if (!IsInCombatOrPerforming) Mediator.Publish(new FrameworkUpdateMessage()); Mediator.Publish(new PriorityFrameworkUpdateMessage()); @@ -603,8 +612,6 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber if (isNormalFrameworkUpdate) return; - var localPlayer = _clientState.LocalPlayer; - if (localPlayer != null && !IsLoggedIn) { _logger.LogDebug("Logged in"); @@ -619,7 +626,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber Mediator.Publish(new DalamudLogoutMessage()); } - if (IsInCombat) + if (IsInCombatOrPerforming) Mediator.Publish(new FrameworkUpdateMessage()); Mediator.Publish(new DelayedFrameworkUpdateMessage()); diff --git a/MareSynchronos/Services/Events/EventAggregator.cs b/MareSynchronos/Services/Events/EventAggregator.cs index b2b4ed2..482ef93 100644 --- a/MareSynchronos/Services/Events/EventAggregator.cs +++ b/MareSynchronos/Services/Events/EventAggregator.cs @@ -1,4 +1,4 @@ -using MareSynchronos.MareConfiguration; +using MareSynchronos.MareConfiguration; using MareSynchronos.Services.Mediator; using MareSynchronos.Utils; using Microsoft.Extensions.Hosting; @@ -21,17 +21,12 @@ public class EventAggregator : MediatorSubscriberBase, IHostedService public EventAggregator(MareConfigService configService, ILogger logger, MareMediator mareMediator) : base(logger, mareMediator) { - Logger.LogInformation("Starting EventAggregatorService"); - Logger.LogInformation("Started EventAggregatorService"); - _configDirectory = configService.ConfigurationDirectory; - _logger = logger; - _currentTime = DateTime.UnixEpoch; - Mediator.Subscribe(this, (msg) => { _lock.Wait(); try { + Logger.LogTrace("Received Event: {evt}", msg.Event.ToString()); _events.Add(msg.Event); if (configService.Current.LogEvents) WriteToFile(msg.Event); @@ -45,6 +40,9 @@ public class EventAggregator : MediatorSubscriberBase, IHostedService }); EventList = CreateEventLazy(); + _configDirectory = configService.ConfigurationDirectory; + _logger = logger; + _currentTime = DateTime.Now - TimeSpan.FromDays(1); } private void RecreateLazy() @@ -103,6 +101,8 @@ public class EventAggregator : MediatorSubscriberBase, IHostedService public Task StartAsync(CancellationToken cancellationToken) { + Logger.LogInformation("Starting EventAggregatorService"); + Logger.LogInformation("Started EventAggregatorService"); return Task.CompletedTask; } diff --git a/MareSynchronos/Services/Mediator/Messages.cs b/MareSynchronos/Services/Mediator/Messages.cs index ce6afff..f913eb1 100644 --- a/MareSynchronos/Services/Mediator/Messages.cs +++ b/MareSynchronos/Services/Mediator/Messages.cs @@ -38,7 +38,7 @@ public record PenumbraRedrawMessage(IntPtr Address, int ObjTblIdx, bool WasReque public record GlamourerChangedMessage(IntPtr Address) : MessageBase; public record HeelsOffsetMessage : MessageBase; public record PenumbraResourceLoadMessage(IntPtr GameObject, string GamePath, string FilePath) : SameThreadMessage; -public record CustomizePlusMessage(string ProfileName) : MessageBase; +public record CustomizePlusMessage(nint? Address) : MessageBase; public record HonorificMessage(string NewHonorificTitle) : MessageBase; public record PetNamesReadyMessage : MessageBase; public record PetNamesMessage(string PetNicknamesData) : MessageBase; @@ -68,6 +68,7 @@ public record UiToggleMessage(Type UiType) : MessageBase; public record PlayerUploadingMessage(GameObjectHandler Handler, bool IsUploading) : MessageBase; public record ClearProfileDataMessage(UserData? UserData = null) : MessageBase; public record CyclePauseMessage(UserData UserData) : MessageBase; +public record PauseMessage(UserData UserData) : MessageBase; public record ProfilePopoutToggle(Pair? Pair) : MessageBase; public record CompactUiChange(Vector2 Size, Vector2 Position) : MessageBase; public record ProfileOpenStandaloneMessage(Pair Pair) : MessageBase; @@ -80,8 +81,8 @@ public record OpenPermissionWindow(Pair Pair) : MessageBase; public record DownloadLimitChangedMessage() : SameThreadMessage; public record CensusUpdateMessage(byte Gender, byte RaceId, byte TribeId) : MessageBase; public record TargetPairMessage(Pair Pair) : MessageBase; -public record CombatStartMessage : MessageBase; -public record CombatEndMessage : MessageBase; +public record CombatOrPerformanceStartMessage : MessageBase; +public record CombatOrPerformanceEndMessage : MessageBase; public record EventMessage(Event Event) : MessageBase; public record PenumbraDirectoryChangedMessage(string? ModDirectory) : MessageBase; public record PenumbraRedrawCharacterMessage(ICharacter Character) : SameThreadMessage; diff --git a/MareSynchronos/Services/NotificationService.cs b/MareSynchronos/Services/NotificationService.cs index f50664d..57099b8 100644 --- a/MareSynchronos/Services/NotificationService.cs +++ b/MareSynchronos/Services/NotificationService.cs @@ -4,24 +4,39 @@ using Dalamud.Plugin.Services; using MareSynchronos.MareConfiguration; using MareSynchronos.MareConfiguration.Models; using MareSynchronos.Services.Mediator; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using NotificationType = MareSynchronos.MareConfiguration.Models.NotificationType; namespace MareSynchronos.Services; -public class NotificationService : DisposableMediatorSubscriberBase +public class NotificationService : DisposableMediatorSubscriberBase, IHostedService { + private readonly DalamudUtilService _dalamudUtilService; private readonly INotificationManager _notificationManager; private readonly IChatGui _chatGui; private readonly MareConfigService _configurationService; - public NotificationService(ILogger logger, MareMediator mediator, INotificationManager notificationManager, IChatGui chatGui, MareConfigService configurationService) : base(logger, mediator) + public NotificationService(ILogger logger, MareMediator mediator, + DalamudUtilService dalamudUtilService, + INotificationManager notificationManager, + IChatGui chatGui, MareConfigService configurationService) : base(logger, mediator) { + _dalamudUtilService = dalamudUtilService; _notificationManager = notificationManager; _chatGui = chatGui; _configurationService = configurationService; + } + public Task StartAsync(CancellationToken cancellationToken) + { Mediator.Subscribe(this, ShowNotification); + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; } private void PrintErrorChat(string? message) @@ -64,6 +79,8 @@ public class NotificationService : DisposableMediatorSubscriberBase { Logger.LogInformation("{msg}", msg.ToString()); + if (!_dalamudUtilService.IsLoggedIn) return; + switch (msg.Type) { case NotificationType.Info: diff --git a/MareSynchronos/Services/PlayerPerformanceService.cs b/MareSynchronos/Services/PlayerPerformanceService.cs new file mode 100644 index 0000000..3c0fd34 --- /dev/null +++ b/MareSynchronos/Services/PlayerPerformanceService.cs @@ -0,0 +1,115 @@ +using MareSynchronos.API.Data; +using MareSynchronos.FileCache; +using MareSynchronos.PlayerData.Handlers; +using MareSynchronos.Services.Mediator; +using MareSynchronos.WebAPI.Files.Models; +using Microsoft.Extensions.Logging; + +namespace MareSynchronos.Services; + +public class PlayerPerformanceService +{ + private readonly FileCacheManager _fileCacheManager; + private readonly XivDataAnalyzer _xivDataAnalyzer; + private readonly ILogger _logger; + private readonly MareMediator _mediator; + + public PlayerPerformanceService(ILogger logger, MareMediator mediator, + FileCacheManager fileCacheManager, + XivDataAnalyzer xivDataAnalyzer) + { + _logger = logger; + _mediator = mediator; + _fileCacheManager = fileCacheManager; + _xivDataAnalyzer = xivDataAnalyzer; + } + + public async Task CheckBothThresholds(PairHandler pairHandler, CharacterData charaData) + { + bool notPausedAfterVram = ComputeAndAutoPauseOnVRAMUsageThresholds(pairHandler, charaData, []); + if (!notPausedAfterVram) return false; + bool notPausedAfterTris = await CheckTriangleUsageThresholds(pairHandler, charaData).ConfigureAwait(false); + if (!notPausedAfterTris) return false; + + return true; + } + + public async Task CheckTriangleUsageThresholds(PairHandler pairHandler, CharacterData charaData) + { + var pair = pairHandler.Pair; + + long triUsage = 0; + + if (!charaData.FileReplacements.TryGetValue(API.Data.Enum.ObjectKind.Player, out List? playerReplacements)) + { + pair.LastAppliedDataTris = 0; + return true; + } + + var moddedModelHashes = playerReplacements.Where(p => string.IsNullOrEmpty(p.FileSwapPath) && p.GamePaths.Any(g => g.EndsWith("mdl", StringComparison.OrdinalIgnoreCase))) + .Select(p => p.Hash) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); + + foreach (var hash in moddedModelHashes) + { + triUsage += await _xivDataAnalyzer.GetTrianglesByHash(hash).ConfigureAwait(false); + } + + pair.LastAppliedDataTris = triUsage; + + _logger.LogDebug("Calculated VRAM usage for {p}", pairHandler); + + return true; + } + + public bool ComputeAndAutoPauseOnVRAMUsageThresholds(PairHandler pairHandler, CharacterData charaData, List toDownloadFiles) + { + var pair = pairHandler.Pair; + + long vramUsage = 0; + + if (!charaData.FileReplacements.TryGetValue(API.Data.Enum.ObjectKind.Player, out List? playerReplacements)) + { + pair.LastAppliedApproximateVRAMBytes = 0; + return true; + } + + var moddedTextureHashes = playerReplacements.Where(p => string.IsNullOrEmpty(p.FileSwapPath) && p.GamePaths.Any(g => g.EndsWith(".tex", StringComparison.OrdinalIgnoreCase))) + .Select(p => p.Hash) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); + + foreach (var hash in moddedTextureHashes) + { + long fileSize = 0; + + var download = toDownloadFiles.Find(f => string.Equals(hash, f.Hash, StringComparison.OrdinalIgnoreCase)); + if (download != null) + { + fileSize = download.TotalRaw; + } + else + { + var fileEntry = _fileCacheManager.GetFileCacheByHash(hash); + if (fileEntry == null) continue; + + if (fileEntry.Size == null) + { + fileEntry.Size = new FileInfo(fileEntry.ResolvedFilepath).Length; + _fileCacheManager.UpdateHashedFile(fileEntry, computeProperties: true); + } + + fileSize = fileEntry.Size.Value; + } + + vramUsage += fileSize; + } + + pair.LastAppliedApproximateVRAMBytes = vramUsage; + + _logger.LogDebug("Calculated VRAM usage for {p}", pairHandler); + + return true; + } +} \ No newline at end of file diff --git a/MareSynchronos/Services/ServerConfiguration/ServerConfigurationManager.cs b/MareSynchronos/Services/ServerConfiguration/ServerConfigurationManager.cs index 4a75e73..fa11f50 100644 --- a/MareSynchronos/Services/ServerConfiguration/ServerConfigurationManager.cs +++ b/MareSynchronos/Services/ServerConfiguration/ServerConfigurationManager.cs @@ -147,15 +147,19 @@ public class ServerConfigurationManager Save(); } - internal void AddCurrentCharacterToServer(int serverSelectionIndex = -1, bool addLastSecretKey = false) + internal void AddCurrentCharacterToServer(int serverSelectionIndex = -1) { if (serverSelectionIndex == -1) serverSelectionIndex = CurrentServerIndex; var server = GetServerByIndex(serverSelectionIndex); + if (server.Authentications.Any(c => string.Equals(c.CharacterName, _dalamudUtil.GetPlayerNameAsync().GetAwaiter().GetResult(), StringComparison.Ordinal) + && c.WorldId == _dalamudUtil.GetHomeWorldIdAsync().GetAwaiter().GetResult())) + return; + server.Authentications.Add(new Authentication() { CharacterName = _dalamudUtil.GetPlayerNameAsync().GetAwaiter().GetResult(), WorldId = _dalamudUtil.GetHomeWorldIdAsync().GetAwaiter().GetResult(), - SecretKeyIdx = addLastSecretKey ? server.SecretKeys.Last().Key : -1, + SecretKeyIdx = server.SecretKeys.Last().Key, }); Save(); } @@ -163,7 +167,10 @@ public class ServerConfigurationManager internal void AddEmptyCharacterToServer(int serverSelectionIndex) { var server = GetServerByIndex(serverSelectionIndex); - server.Authentications.Add(new Authentication()); + server.Authentications.Add(new Authentication() + { + SecretKeyIdx = server.SecretKeys.Any() ? server.SecretKeys.First().Key : -1, + }); Save(); } diff --git a/MareSynchronos/Services/XivDataAnalyzer.cs b/MareSynchronos/Services/XivDataAnalyzer.cs index 14ff8ae..60e0db8 100644 --- a/MareSynchronos/Services/XivDataAnalyzer.cs +++ b/MareSynchronos/Services/XivDataAnalyzer.cs @@ -1,12 +1,10 @@ -using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.Havok.Animation; using FFXIVClientStructs.Havok.Common.Base.Types; using FFXIVClientStructs.Havok.Common.Serialize.Util; -using Lumina; -using Lumina.Data.Files; using MareSynchronos.FileCache; +using MareSynchronos.Interop.GameModel; using MareSynchronos.MareConfiguration; using MareSynchronos.PlayerData.Handlers; using Microsoft.Extensions.Logging; @@ -19,15 +17,14 @@ public sealed class XivDataAnalyzer private readonly ILogger _logger; private readonly FileCacheManager _fileCacheManager; private readonly XivDataStorageService _configService; - private readonly GameData _luminaGameData; + private readonly List _failedCalculatedTris = []; public XivDataAnalyzer(ILogger logger, FileCacheManager fileCacheManager, - XivDataStorageService configService, IDataManager gameData) + XivDataStorageService configService) { _logger = logger; _fileCacheManager = fileCacheManager; _configService = configService; - _luminaGameData = new GameData(gameData.GameData.DataPath.FullName); } public unsafe Dictionary>? GetSkeletonBoneIndices(GameObjectHandler handler) @@ -153,50 +150,65 @@ public sealed class XivDataAnalyzer return output; } - public Task GetTrianglesFromGamePath(string gamePath) + public async Task GetTrianglesByHash(string hash) { - if (_configService.Current.TriangleDictionary.TryGetValue(gamePath, out var cachedTris)) - return Task.FromResult(cachedTris); + if (_configService.Current.TriangleDictionary.TryGetValue(hash, out var cachedTris) && cachedTris > 0) + return cachedTris; - _logger.LogDebug("Detected Model File {path}, calculating Tris", gamePath); - var file = _luminaGameData.GetFile(gamePath); - if (file == null) - return Task.FromResult((long)0); - - if (file.FileHeader.LodCount <= 0) - return Task.FromResult((long)0); - var meshIdx = file.Lods[0].MeshIndex; - var meshCnt = file.Lods[0].MeshCount; - var tris = file.Meshes.Skip(meshIdx).Take(meshCnt).Sum(p => p.IndexCount) / 3; - - _logger.LogDebug("{filePath} => {tris} triangles", gamePath, tris); - _configService.Current.TriangleDictionary[gamePath] = tris; - _configService.Save(); - return Task.FromResult(tris); - } - - public Task GetTrianglesByHash(string hash) - { - if (_configService.Current.TriangleDictionary.TryGetValue(hash, out var cachedTris)) - return Task.FromResult(cachedTris); + if (_failedCalculatedTris.Contains(hash, StringComparer.Ordinal)) + return 0; var path = _fileCacheManager.GetFileCacheByHash(hash); if (path == null || !path.ResolvedFilepath.EndsWith(".mdl", StringComparison.OrdinalIgnoreCase)) - return Task.FromResult((long)0); + return 0; var filePath = path.ResolvedFilepath; - _logger.LogDebug("Detected Model File {path}, calculating Tris", filePath); - var file = _luminaGameData.GetFileFromDisk(filePath); - if (file.FileHeader.LodCount <= 0) - return Task.FromResult((long)0); - var meshIdx = file.Lods[0].MeshIndex; - var meshCnt = file.Lods[0].MeshCount; - var tris = file.Meshes.Skip(meshIdx).Take(meshCnt).Sum(p => p.IndexCount) / 3; + try + { + _logger.LogDebug("Detected Model File {path}, calculating Tris", filePath); + var file = new MdlFile(filePath); + if (file.LodCount <= 0) + { + _failedCalculatedTris.Add(hash); + _configService.Current.TriangleDictionary[hash] = 0; + _configService.Save(); + return 0; + } - _logger.LogDebug("{filePath} => {tris} triangles", filePath, tris); - _configService.Current.TriangleDictionary[hash] = tris; - _configService.Save(); - return Task.FromResult(tris); + long tris = 0; + for (int i = 0; i < file.LodCount; i++) + { + try + { + var meshIdx = file.Lods[i].MeshIndex; + var meshCnt = file.Lods[i].MeshCount; + tris = file.Meshes.Skip(meshIdx).Take(meshCnt).Sum(p => p.IndexCount) / 3; + } + catch (Exception ex) + { + _logger.LogDebug(ex, "Could not load lod mesh {mesh} from path {path}", i, filePath); + continue; + } + + if (tris > 0) + { + _logger.LogDebug("TriAnalysis: {filePath} => {tris} triangles", filePath, tris); + _configService.Current.TriangleDictionary[hash] = tris; + _configService.Save(); + break; + } + } + + return tris; + } + catch (Exception e) + { + _failedCalculatedTris.Add(hash); + _configService.Current.TriangleDictionary[hash] = 0; + _configService.Save(); + _logger.LogWarning(e, "Could not parse file {file}", filePath); + return 0; + } } } diff --git a/MareSynchronos/UI/Components/DrawGroupPair.cs b/MareSynchronos/UI/Components/DrawGroupPair.cs index 8b559df..1b764cc 100644 --- a/MareSynchronos/UI/Components/DrawGroupPair.cs +++ b/MareSynchronos/UI/Components/DrawGroupPair.cs @@ -74,11 +74,15 @@ public class DrawGroupPair : DrawPairBase { _mediator.Publish(new TargetPairMessage(_pair)); } - if (_pair.LastAppliedDataSize >= 0) + if (_pair.LastAppliedDataBytes >= 0) { presenceText += UiSharedService.TooltipSeparator; presenceText += ((!_pair.IsVisible) ? "(Last) " : string.Empty) + "Mods Info" + Environment.NewLine; - presenceText += "Files Size: " + UiSharedService.ByteToString(_pair.LastAppliedDataSize, true); + presenceText += "Files Size: " + UiSharedService.ByteToString(_pair.LastAppliedDataBytes, true); + if (_pair.LastAppliedApproximateVRAMBytes >= 0) + { + presenceText += Environment.NewLine + "Approx. VRAM Usage: " + UiSharedService.ByteToString(_pair.LastAppliedApproximateVRAMBytes, true); + } if (_pair.LastAppliedDataTris >= 0) { presenceText += Environment.NewLine + "Triangle Count (excl. Vanilla): " diff --git a/MareSynchronos/UI/Components/DrawUserPair.cs b/MareSynchronos/UI/Components/DrawUserPair.cs index d548745..48f8ab1 100644 --- a/MareSynchronos/UI/Components/DrawUserPair.cs +++ b/MareSynchronos/UI/Components/DrawUserPair.cs @@ -72,16 +72,20 @@ public class DrawUserPair : DrawPairBase } ImGui.PopFont(); var visibleTooltip = _pair.UserData.AliasOrUID + " is visible: " + _pair.PlayerName! + Environment.NewLine + "Click to target this player"; - if (_pair.LastAppliedDataSize >= 0) + if (_pair.LastAppliedDataBytes >= 0) { visibleTooltip += UiSharedService.TooltipSeparator; visibleTooltip += ((!_pair.IsVisible) ? "(Last) " : string.Empty) + "Mods Info" + Environment.NewLine; - visibleTooltip += "Files Size: " + UiSharedService.ByteToString(_pair.LastAppliedDataSize, true); + visibleTooltip += "Files Size: " + UiSharedService.ByteToString(_pair.LastAppliedDataBytes, true); + if (_pair.LastAppliedApproximateVRAMBytes >= 0) + { + visibleTooltip += Environment.NewLine + "Approx. VRAM Usage: " + UiSharedService.ByteToString(_pair.LastAppliedApproximateVRAMBytes, true); + } if (_pair.LastAppliedDataTris >= 0) { visibleTooltip += Environment.NewLine + "Triangle Count (excl. Vanilla): " + (_pair.LastAppliedDataTris > 1000 ? (_pair.LastAppliedDataTris / 1000d).ToString("0.0'k'") : _pair.LastAppliedDataTris); - } + } } UiSharedService.AttachToolTip(visibleTooltip); @@ -273,16 +277,5 @@ public class DrawUserPair : DrawPairBase _ = _apiController.UserRemovePair(new(entry.UserData)); } UiSharedService.AttachToolTip("Hold CTRL and click to unpair permanently from " + entryUID); - - ImGui.Separator(); - if (!entry.IsPaused) - { - if (UiSharedService.NormalizedIconTextButton(FontAwesomeIcon.ExclamationTriangle, "Report Profile")) - { - ImGui.CloseCurrentPopup(); - _mediator.Publish(new OpenReportPopupMessage(_pair)); - } - UiSharedService.AttachToolTip("Report this users profile to the administrative team"); - } } } \ No newline at end of file diff --git a/MareSynchronos/UI/DataAnalysisUi.cs b/MareSynchronos/UI/DataAnalysisUi.cs index bc960ac..ab3b58f 100644 --- a/MareSynchronos/UI/DataAnalysisUi.cs +++ b/MareSynchronos/UI/DataAnalysisUi.cs @@ -31,7 +31,9 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase private ObjectKind _selectedObjectTab; private bool _showModal = false; - public DataAnalysisUi(ILogger logger, MareMediator mediator, CharacterAnalyzer characterAnalyzer, IpcManager ipcManager, PerformanceCollectorService performanceCollectorService) + public DataAnalysisUi(ILogger logger, MareMediator mediator, + CharacterAnalyzer characterAnalyzer, IpcManager ipcManager, + PerformanceCollectorService performanceCollectorService) : base(logger, mediator, "Character Data Analysis", performanceCollectorService) { _characterAnalyzer = characterAnalyzer; @@ -152,10 +154,10 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase + ", compressed: " + UiSharedService.ByteToString(f.Sum(v => v.CompressedSize)))); ImGui.SetTooltip(text); } - ImGui.TextUnformatted("Total size (uncompressed):"); + ImGui.TextUnformatted("Total size (actual):"); ImGui.SameLine(); ImGui.TextUnformatted(UiSharedService.ByteToString(_cachedAnalysis!.Sum(c => c.Value.Sum(c => c.Value.OriginalSize)))); - ImGui.TextUnformatted("Total size (compressed):"); + ImGui.TextUnformatted("Total size (compressed for up/download only):"); ImGui.SameLine(); ImGui.TextUnformatted(UiSharedService.ByteToString(_cachedAnalysis!.Sum(c => c.Value.Sum(c => c.Value.CompressedSize)))); ImGui.TextUnformatted($"Total modded model triangles: {_cachedAnalysis.Sum(c => c.Value.Sum(f => f.Value.Triangles))}"); @@ -170,7 +172,8 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase using var tab = ImRaii.TabItem(tabText + "###" + kvp.Key.ToString()); if (tab.Success) { - var groupedfiles = kvp.Value.Select(v => v.Value).GroupBy(f => f.FileType, StringComparer.Ordinal).OrderBy(k => k.Key, StringComparer.Ordinal).ToList(); + var groupedfiles = kvp.Value.Select(v => v.Value).GroupBy(f => f.FileType, StringComparer.Ordinal) + .OrderBy(k => k.Key, StringComparer.Ordinal).ToList(); ImGui.TextUnformatted("Files for " + kvp.Key); ImGui.SameLine(); @@ -189,12 +192,19 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase + ", compressed: " + UiSharedService.ByteToString(f.Sum(v => v.CompressedSize)))); ImGui.SetTooltip(text); } - ImGui.TextUnformatted($"{kvp.Key} size (uncompressed):"); + ImGui.TextUnformatted($"{kvp.Key} size (actual):"); ImGui.SameLine(); ImGui.TextUnformatted(UiSharedService.ByteToString(kvp.Value.Sum(c => c.Value.OriginalSize))); - ImGui.TextUnformatted($"{kvp.Key} size (compressed):"); + ImGui.TextUnformatted($"{kvp.Key} size (compressed for up/download only):"); ImGui.SameLine(); ImGui.TextUnformatted(UiSharedService.ByteToString(kvp.Value.Sum(c => c.Value.CompressedSize))); + ImGui.TextUnformatted($"{kvp.Key} VRAM usage:"); + ImGui.SameLine(); + var vramUsage = groupedfiles.SingleOrDefault(v => string.Equals(v.Key, "tex", StringComparison.Ordinal)); + if (vramUsage != null) + { + ImGui.TextUnformatted(UiSharedService.ByteToString(vramUsage.Sum(f => f.OriginalSize))); + } ImGui.TextUnformatted($"{kvp.Key} modded model triangles: {kvp.Value.Sum(f => f.Value.Triangles)}"); ImGui.Separator(); @@ -239,11 +249,11 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase ImGui.SameLine(); ImGui.TextUnformatted(fileGroup.Count().ToString()); - ImGui.TextUnformatted($"{fileGroup.Key} files size (uncompressed):"); + ImGui.TextUnformatted($"{fileGroup.Key} files size (actual):"); ImGui.SameLine(); ImGui.TextUnformatted(UiSharedService.ByteToString(fileGroup.Sum(c => c.OriginalSize))); - ImGui.TextUnformatted($"{fileGroup.Key} files size (compressed):"); + ImGui.TextUnformatted($"{fileGroup.Key} files size (compressed for up/download only):"); ImGui.SameLine(); ImGui.TextUnformatted(UiSharedService.ByteToString(fileGroup.Sum(c => c.CompressedSize))); @@ -383,10 +393,14 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase _cachedAnalysis![_selectedObjectTab] = _cachedAnalysis[_selectedObjectTab].OrderBy(k => k.Value.CompressedSize).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal); if (idx == 4 && sortSpecs.Specs.SortDirection == ImGuiSortDirection.Descending) _cachedAnalysis![_selectedObjectTab] = _cachedAnalysis[_selectedObjectTab].OrderByDescending(k => k.Value.CompressedSize).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal); + if (string.Equals(fileGroup.Key, "mdl", StringComparison.Ordinal) && idx == 5 && sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending) + _cachedAnalysis![_selectedObjectTab] = _cachedAnalysis[_selectedObjectTab].OrderBy(k => k.Value.Triangles).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal); + if (string.Equals(fileGroup.Key, "mdl", StringComparison.Ordinal) && idx == 5 && sortSpecs.Specs.SortDirection == ImGuiSortDirection.Descending) + _cachedAnalysis![_selectedObjectTab] = _cachedAnalysis[_selectedObjectTab].OrderByDescending(k => k.Value.Triangles).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal); if (string.Equals(fileGroup.Key, "tex", StringComparison.Ordinal) && idx == 5 && sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending) - _cachedAnalysis![_selectedObjectTab] = _cachedAnalysis[_selectedObjectTab].OrderBy(k => k.Value.Format).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal); + _cachedAnalysis![_selectedObjectTab] = _cachedAnalysis[_selectedObjectTab].OrderBy(k => k.Value.Format.Value, StringComparer.Ordinal).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal); if (string.Equals(fileGroup.Key, "tex", StringComparison.Ordinal) && idx == 5 && sortSpecs.Specs.SortDirection == ImGuiSortDirection.Descending) - _cachedAnalysis![_selectedObjectTab] = _cachedAnalysis[_selectedObjectTab].OrderByDescending(k => k.Value.Format).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal); + _cachedAnalysis![_selectedObjectTab] = _cachedAnalysis[_selectedObjectTab].OrderByDescending(k => k.Value.Format.Value, StringComparer.Ordinal).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal); sortSpecs.SpecsDirty = false; } diff --git a/MareSynchronos/UI/EditProfileUi.cs b/MareSynchronos/UI/EditProfileUi.cs index 529a3a9..459140c 100644 --- a/MareSynchronos/UI/EditProfileUi.cs +++ b/MareSynchronos/UI/EditProfileUi.cs @@ -21,7 +21,6 @@ public class EditProfileUi : WindowMediatorSubscriberBase private readonly ApiController _apiController; private readonly FileDialogManager _fileDialogManager; private readonly MareProfileManager _mareProfileManager; - private readonly IUiBuilder _uiBuilder; private readonly UiSharedService _uiSharedService; private readonly ServerConfigurationManager _serverConfigurationManager; private bool _adjustedForScollBarsLocalProfile = false; @@ -34,8 +33,8 @@ public class EditProfileUi : WindowMediatorSubscriberBase private bool _wasOpen; public EditProfileUi(ILogger logger, MareMediator mediator, - ApiController apiController, IUiBuilder uiBuilder, UiSharedService uiSharedService, - FileDialogManager fileDialogManager, ServerConfigurationManager serverConfigurationManager, + ApiController apiController, UiSharedService uiSharedService, FileDialogManager fileDialogManager, + ServerConfigurationManager serverConfigurationManager, MareProfileManager mareProfileManager, PerformanceCollectorService performanceCollectorService) : base(logger, mediator, "Loporrit Edit Profile###LoporritSyncEditProfileUI", performanceCollectorService) { @@ -46,7 +45,6 @@ public class EditProfileUi : WindowMediatorSubscriberBase MaximumSize = new(768, 2000) }; _apiController = apiController; - _uiBuilder = uiBuilder; _uiSharedService = uiSharedService; _fileDialogManager = fileDialogManager; _serverConfigurationManager = serverConfigurationManager; diff --git a/MareSynchronos/UI/IntroUI.cs b/MareSynchronos/UI/IntroUI.cs index 43b88ea..fdd8f39 100644 --- a/MareSynchronos/UI/IntroUI.cs +++ b/MareSynchronos/UI/IntroUI.cs @@ -18,10 +18,11 @@ using System.Net.Http.Headers; using System.Net.Http.Json; using System.Numerics; using System.Reflection; +using System.Text.RegularExpressions; namespace MareSynchronos.UI; -public class IntroUi : WindowMediatorSubscriberBase +public partial class IntroUi : WindowMediatorSubscriberBase { private readonly ApiController _apiController; private readonly MareConfigService _configService; @@ -42,13 +43,12 @@ public class IntroUi : WindowMediatorSubscriberBase private string? _registrationMessage; private RegisterReplyDto? _registrationReply; - public IntroUi(ILogger logger, UiSharedService uiShared, MareConfigService configService, ApiController apiController, + public IntroUi(ILogger logger, UiSharedService uiShared, MareConfigService configService, CacheMonitor fileCacheManager, ServerConfigurationManager serverConfigurationManager, MareMediator mareMediator, PerformanceCollectorService performanceCollectorService, DalamudUtilService dalamudUtilService) : base(logger, mareMediator, "Loporrit Setup", performanceCollectorService) { _uiShared = uiShared; _configService = configService; - _apiController = apiController; _cacheMonitor = fileCacheManager; _serverConfigurationManager = serverConfigurationManager; _dalamudUtilService = dalamudUtilService; @@ -87,11 +87,12 @@ public class IntroUi : WindowMediatorSubscriberBase UiSharedService.ColorTextWrapped("Note: Any modifications you have applied through anything but Penumbra cannot be shared and your character state on other clients " + "might look broken because of this or others players mods might not apply on your end altogether. " + "If you want to use this plugin you will have to move your mods to Penumbra.", ImGuiColors.DalamudYellow); - if (!_uiShared.DrawOtherPluginState(true)) return; + if (!_uiShared.DrawOtherPluginState(intro: true)) return; ImGui.Separator(); if (ImGui.Button("Next##toAgreement")) { _readFirstPage = true; +#if !DEBUG _timeoutTask = Task.Run(async () => { for (int i = 10; i > 0; i--) @@ -100,6 +101,9 @@ public class IntroUi : WindowMediatorSubscriberBase await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false); } }); +#else + _timeoutTask = Task.CompletedTask; +#endif } } else if (!_configService.Current.AcceptedAgreement && _readFirstPage) @@ -229,7 +233,11 @@ public class IntroUi : WindowMediatorSubscriberBase ImGui.InputText("", ref _secretKey, 64); if (_secretKey.Length > 0 && _secretKey.Length != 64) { - UiSharedService.ColorTextWrapped("Your secret key must be exactly 64 characters long. Don't enter your Lodestone auth here.", ImGuiColors.DalamudRed); + UiSharedService.ColorTextWrapped("Your secret key must be exactly 64 characters long.", ImGuiColors.DalamudRed); + } + else if (_secretKey.Length == 64 && !HexRegex().IsMatch(_secretKey)) + { + UiSharedService.ColorTextWrapped("Your secret key can only contain ABCDEF and the numbers 0-9.", ImGuiColors.DalamudRed); } else if (_secretKey.Length == 64) { @@ -238,7 +246,7 @@ public class IntroUi : WindowMediatorSubscriberBase { string keyName; if (_serverConfigurationManager.CurrentServer == null) _serverConfigurationManager.SelectServer(0); - if (_registrationReply != null && _secretKey == _registrationReply.SecretKey) + if (_registrationReply != null && _secretKey.Equals(_registrationReply.SecretKey, StringComparison.Ordinal)) keyName = _registrationReply.UID + $" (registered {DateTime.Now:yyyy-MM-dd})"; else keyName = $"Secret Key added on Setup ({DateTime.Now:yyyy-MM-dd})"; @@ -247,13 +255,13 @@ public class IntroUi : WindowMediatorSubscriberBase FriendlyName = keyName, Key = _secretKey, }); - _serverConfigurationManager.AddCurrentCharacterToServer(addLastSecretKey: true); + _serverConfigurationManager.AddCurrentCharacterToServer(); _secretKey = string.Empty; _ = Task.Run(() => _uiShared.ApiController.CreateConnections()); } } - if (_serverConfigurationManager.CurrentApiUrl == ApiController.LoporritServiceUri) + if (_serverConfigurationManager.CurrentApiUrl.Equals(ApiController.LoporritServiceUri, StringComparison.Ordinal)) { ImGui.BeginDisabled(_registrationInProgress || _registrationSuccess || _secretKey.Length > 0); ImGui.Separator(); @@ -270,13 +278,13 @@ public class IntroUi : WindowMediatorSubscriberBase var postUri = MareAuth.AuthRegisterFullPath(new Uri(_serverConfigurationManager.CurrentApiUrl .Replace("wss://", "https://", StringComparison.OrdinalIgnoreCase) .Replace("ws://", "http://", StringComparison.OrdinalIgnoreCase))); - _logger.LogInformation("Registering new account: " + postUri.ToString()); + _logger.LogInformation("Registering new account: {uri}", postUri.ToString()); var result = await httpClient.PostAsync(postUri, null).ConfigureAwait(false); result.EnsureSuccessStatusCode(); var reply = await result.Content.ReadFromJsonAsync().ConfigureAwait(false) ?? new(); if (!reply.Success) { - _logger.LogWarning("Registration failed: " + reply.ErrorMessage); + _logger.LogWarning("Registration failed: {err}", reply.ErrorMessage); _registrationMessage = reply.ErrorMessage; if (_registrationMessage.IsNullOrEmpty()) _registrationMessage = "An unknown error occured. Please try again later."; @@ -331,4 +339,7 @@ public class IntroUi : WindowMediatorSubscriberBase _tosParagraphs = [Strings.ToS.Paragraph1, Strings.ToS.Paragraph2, Strings.ToS.Paragraph3, Strings.ToS.Paragraph4, Strings.ToS.Paragraph5, Strings.ToS.Paragraph6]; } + + [GeneratedRegex("^([A-F0-9]{2})+")] + private static partial Regex HexRegex(); } diff --git a/MareSynchronos/UI/SettingsUi.cs b/MareSynchronos/UI/SettingsUi.cs index 1e4565f..34d5a05 100644 --- a/MareSynchronos/UI/SettingsUi.cs +++ b/MareSynchronos/UI/SettingsUi.cs @@ -77,12 +77,12 @@ public class SettingsUi : WindowMediatorSubscriberBase MareCharaFileManager mareCharaFileManager, PairManager pairManager, ChatService chatService, GuiHookService guiHookService, ServerConfigurationManager serverConfigurationManager, MareMediator mediator, PerformanceCollectorService performanceCollector, - DalamudUtilService dalamudUtilService, FileUploadManager fileTransferManager, FileTransferOrchestrator fileTransferOrchestrator, FileCacheManager fileCacheManager, FileCompactor fileCompactor, ApiController apiController, - IpcManager ipcManager, CacheMonitor cacheMonitor) : base(logger, mediator, "Loporrit Settings", performanceCollector) + IpcManager ipcManager, CacheMonitor cacheMonitor, + DalamudUtilService dalamudUtilService) : base(logger, mediator, "Loporrit Settings", performanceCollector) { _configService = configService; _mareCharaFileManager = mareCharaFileManager; @@ -91,7 +91,6 @@ public class SettingsUi : WindowMediatorSubscriberBase _guiHookService = guiHookService; _serverConfigurationManager = serverConfigurationManager; _performanceCollector = performanceCollector; - _dalamudUtilService = dalamudUtilService; _fileTransferManager = fileTransferManager; _fileTransferOrchestrator = fileTransferOrchestrator; _fileCacheManager = fileCacheManager; @@ -133,6 +132,7 @@ public class SettingsUi : WindowMediatorSubscriberBase public override void OnClose() { _uiShared.EditTrackerPosition = false; + base.OnClose(); } @@ -191,7 +191,7 @@ public class SettingsUi : WindowMediatorSubscriberBase } ImGui.SameLine(); ImGui.SetNextItemWidth(100 * ImGuiHelpers.GlobalScale); - _uiShared.DrawCombo("###speed", new[] { DownloadSpeeds.Bps, DownloadSpeeds.KBps, DownloadSpeeds.MBps }, + _uiShared.DrawCombo("###speed", [DownloadSpeeds.Bps, DownloadSpeeds.KBps, DownloadSpeeds.MBps], (s) => s switch { DownloadSpeeds.Bps => "Byte/s", diff --git a/MareSynchronos/UI/UISharedService.cs b/MareSynchronos/UI/UISharedService.cs index fa92a14..d8c476f 100644 --- a/MareSynchronos/UI/UISharedService.cs +++ b/MareSynchronos/UI/UISharedService.cs @@ -82,7 +82,8 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase public UiSharedService(ILogger logger, IpcManager ipcManager, ApiController apiController, CacheMonitor cacheMonitor, FileDialogManager fileDialogManager, MareConfigService configService, DalamudUtilService dalamudUtil, IDalamudPluginInterface pluginInterface, - ITextureProvider textureProvider, Dalamud.Localization localization, + ITextureProvider textureProvider, + Dalamud.Localization localization, ServerConfigurationManager serverManager, MareMediator mediator) : base(logger, mediator) { _ipcManager = ipcManager; diff --git a/MareSynchronos/WebAPI/Files/FileDownloadManager.cs b/MareSynchronos/WebAPI/Files/FileDownloadManager.cs index 437ed4a..4a9b3ce 100644 --- a/MareSynchronos/WebAPI/Files/FileDownloadManager.cs +++ b/MareSynchronos/WebAPI/Files/FileDownloadManager.cs @@ -201,7 +201,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase } } - private async Task DownloadFilesInternal(GameObjectHandler gameObjectHandler, List fileReplacement, CancellationToken ct) + public async Task> InitiateDownloadList(GameObjectHandler gameObjectHandler, List fileReplacement, CancellationToken ct) { Logger.LogDebug("Download start: {id}", gameObjectHandler.Name); @@ -212,9 +212,6 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase Logger.LogDebug("Files with size 0 or less: {files}", string.Join(", ", downloadFileInfoFromService.Where(f => f.Size <= 0).Select(f => f.Hash))); - CurrentDownloads = downloadFileInfoFromService.Distinct().Select(d => new DownloadFileTransfer(d)) - .Where(d => d.CanBeTransferred).ToList(); - foreach (var dto in downloadFileInfoFromService.Where(c => c.IsForbidden)) { if (!_orchestrator.ForbiddenTransfers.Exists(f => string.Equals(f.Hash, dto.Hash, StringComparison.Ordinal))) @@ -223,7 +220,15 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase } } - var downloadGroups = CurrentDownloads.Where(f => f.CanBeTransferred).GroupBy(f => f.DownloadUri.Host + ":" + f.DownloadUri.Port, StringComparer.Ordinal); + CurrentDownloads = downloadFileInfoFromService.Distinct().Select(d => new DownloadFileTransfer(d)) + .Where(d => d.CanBeTransferred).ToList(); + + return CurrentDownloads; + } + + private async Task DownloadFilesInternal(GameObjectHandler gameObjectHandler, List fileReplacement, CancellationToken ct) + { + var downloadGroups = CurrentDownloads.GroupBy(f => f.DownloadUri.Host + ":" + f.DownloadUri.Port, StringComparer.Ordinal); foreach (var downloadGroup in downloadGroups) { diff --git a/MareSynchronos/WebAPI/Files/FileUploadManager.cs b/MareSynchronos/WebAPI/Files/FileUploadManager.cs index 7d371e4..b20f000 100644 --- a/MareSynchronos/WebAPI/Files/FileUploadManager.cs +++ b/MareSynchronos/WebAPI/Files/FileUploadManager.cs @@ -232,10 +232,10 @@ public sealed class FileUploadManager : DisposableMediatorSubscriberBase { Logger.LogDebug("[{hash}] Compressing", file); var data = await _fileDbManager.GetCompressedFileData(file.Hash, uploadToken).ConfigureAwait(false); - CurrentUploads.Single(e => string.Equals(e.Hash, file.Hash, StringComparison.Ordinal)).Total = data.Length; + CurrentUploads.Single(e => string.Equals(e.Hash, file.Hash, StringComparison.Ordinal)).Total = data.Item2.Length; Logger.LogDebug("[{hash}] Starting upload for {filePath}", file.Hash, _fileDbManager.GetFileCacheByHash(file.Hash)!.ResolvedFilepath); await uploadTask.ConfigureAwait(false); - uploadTask = UploadFile(data, file.Hash, uploadToken); + uploadTask = UploadFile(data.Item2, file.Hash, uploadToken); uploadToken.ThrowIfCancellationRequested(); } diff --git a/MareSynchronos/WebAPI/Files/Models/DownloadFileTransfer.cs b/MareSynchronos/WebAPI/Files/Models/DownloadFileTransfer.cs index 0665b49..92f357a 100644 --- a/MareSynchronos/WebAPI/Files/Models/DownloadFileTransfer.cs +++ b/MareSynchronos/WebAPI/Files/Models/DownloadFileTransfer.cs @@ -18,5 +18,7 @@ public class DownloadFileTransfer : FileTransfer } get => Dto.Size; } + + public long TotalRaw => 0; // XXX private DownloadFileDto Dto => (DownloadFileDto)TransferDto; } \ No newline at end of file diff --git a/MareSynchronos/WebAPI/SignalR/ApiController.cs b/MareSynchronos/WebAPI/SignalR/ApiController.cs index 829fbac..f9064d5 100644 --- a/MareSynchronos/WebAPI/SignalR/ApiController.cs +++ b/MareSynchronos/WebAPI/SignalR/ApiController.cs @@ -4,6 +4,7 @@ using MareSynchronos.API.Data.Extensions; using MareSynchronos.API.Dto; using MareSynchronos.API.Dto.User; using MareSynchronos.API.SignalR; +using MareSynchronos.MareConfiguration; using MareSynchronos.MareConfiguration.Models; using MareSynchronos.PlayerData.Pairs; using MareSynchronos.Services; @@ -58,6 +59,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IM Mediator.Subscribe(this, (msg) => MareHubOnReconnecting(msg.Exception)); Mediator.Subscribe(this, (msg) => _ = CyclePause(msg.UserData)); Mediator.Subscribe(this, (msg) => _lastCensus = msg); + Mediator.Subscribe(this, (msg) => _ = Pause(msg.UserData)); ServerState = ServerState.Offline; @@ -208,6 +210,17 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IM NotificationType.Warning, TimeSpan.FromSeconds(15))); } + if (_dalamudUtil.HasModifiedGameFiles) + { + Logger.LogError("Detected modified game files on connection"); + if (false) + Mediator.Publish(new NotificationMessage("Modified Game Files detected", + "Dalamud has reported modified game files in your FFXIV installation. " + + "You will be able to connect, but the synchronization functionality might be (partially) broken. " + + "Exit the game and repair it through XIVLauncher to get rid of this message.", + NotificationType.Error, TimeSpan.FromSeconds(15))); + } + await LoadIninitialPairs().ConfigureAwait(false); await LoadOnlinePairs().ConfigureAwait(false); } @@ -255,7 +268,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IM var pair = _pairManager.GetOnlineUserPairs().Single(p => p.UserPair != null && p.UserData == userData); var perm = pair.UserPair!.OwnPermissions; perm.SetPaused(paused: true); - await UserSetPairPermissions(new API.Dto.User.UserPermissionsDto(userData, perm)).ConfigureAwait(false); + await UserSetPairPermissions(new UserPermissionsDto(userData, perm)).ConfigureAwait(false); // wait until it's changed while (pair.UserPair!.OwnPermissions != perm) { @@ -263,12 +276,20 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IM Logger.LogTrace("Waiting for permissions change for {data}", userData); } perm.SetPaused(paused: false); - await UserSetPairPermissions(new API.Dto.User.UserPermissionsDto(userData, perm)).ConfigureAwait(false); + await UserSetPairPermissions(new UserPermissionsDto(userData, perm)).ConfigureAwait(false); }, cts.Token).ContinueWith((t) => cts.Dispose()); return Task.CompletedTask; } + public async Task Pause(UserData userData) + { + var pair = _pairManager.GetOnlineUserPairs().Single(p => p.UserPair != null && p.UserData == userData); + var perm = pair.UserPair!.OwnPermissions; + perm.SetPaused(paused: true); + await UserSetPairPermissions(new UserPermissionsDto(userData, perm)).ConfigureAwait(false); + } + public Task GetConnectionDto() => GetConnectionDto(true); public async Task GetConnectionDto(bool publishConnected = true) diff --git a/MareSynchronos/WebAPI/SignalR/TokenProvider.cs b/MareSynchronos/WebAPI/SignalR/TokenProvider.cs index 1c8701f..632ad74 100644 --- a/MareSynchronos/WebAPI/SignalR/TokenProvider.cs +++ b/MareSynchronos/WebAPI/SignalR/TokenProvider.cs @@ -92,7 +92,6 @@ public sealed class TokenProvider : IDisposable, IMediatorSubscriber throw; } - _logger.LogTrace("GetNewToken: JWT {token}", response); return response; }