diff --git a/MareSynchronos/FileCache/FileCacheManager.cs b/MareSynchronos/FileCache/FileCacheManager.cs index 6d56416..853cc05 100644 --- a/MareSynchronos/FileCache/FileCacheManager.cs +++ b/MareSynchronos/FileCache/FileCacheManager.cs @@ -1,6 +1,7 @@ using LZ4; using MareSynchronos.Interop; using MareSynchronos.MareConfiguration; +using MareSynchronos.Services.Mediator; using MareSynchronos.Utils; using Microsoft.Extensions.Logging; using System.Collections.Concurrent; @@ -15,17 +16,19 @@ public sealed class FileCacheManager : IDisposable public const string CsvSplit = "|"; public const string PenumbraPrefix = "{penumbra}"; private readonly MareConfigService _configService; + private readonly MareMediator _mareMediator; private readonly string _csvPath; private readonly ConcurrentDictionary> _fileCaches = new(StringComparer.Ordinal); private readonly object _fileWriteLock = new(); private readonly IpcManager _ipcManager; private readonly ILogger _logger; - public FileCacheManager(ILogger logger, IpcManager ipcManager, MareConfigService configService) + public FileCacheManager(ILogger logger, IpcManager ipcManager, MareConfigService configService, MareMediator mareMediator) { _logger = logger; _ipcManager = ipcManager; _configService = configService; + _mareMediator = mareMediator; _csvPath = Path.Combine(configService.ConfigurationDirectory, "FileCache.csv"); lock (_fileWriteLock) @@ -172,6 +175,47 @@ public sealed class FileCacheManager : IDisposable return output; } + public Task> ValidateLocalIntegrity(IProgress<(int, int, FileCacheEntity)> progress, CancellationToken cancellationToken) + { + _mareMediator.Publish(new HaltScanMessage(nameof(ValidateLocalIntegrity))); + _logger.LogInformation("Validating local storage"); + var cacheEntries = _fileCaches.SelectMany(v => v.Value).Where(v => v.IsCacheEntry).ToList(); + List brokenEntities = new(); + int i = 0; + foreach (var fileCache in cacheEntries) + { + _logger.LogInformation("Validating {file}", fileCache.ResolvedFilepath); + + progress.Report((i, cacheEntries.Count, fileCache)); + i++; + var computedHash = Crypto.GetFileHash(fileCache.ResolvedFilepath); + if (!string.Equals(computedHash, fileCache.Hash, StringComparison.Ordinal)) + { + _logger.LogInformation("Failed to validate {file}, got hash {hash}, expected hash {hash}", fileCache.ResolvedFilepath, computedHash, fileCache.Hash); + brokenEntities.Add(fileCache); + } + + if (cancellationToken.IsCancellationRequested) break; + } + + foreach (var brokenEntity in brokenEntities) + { + RemoveHashedFile(brokenEntity.Hash, brokenEntity.PrefixedFilePath); + + try + { + File.Delete(brokenEntity.ResolvedFilepath); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Could not delete {file}", brokenEntity.ResolvedFilepath); + } + } + + _mareMediator.Publish(new ResumeScanMessage(nameof(ValidateLocalIntegrity))); + return Task.FromResult(brokenEntities); + } + public string GetCacheFilePath(string hash, string extension) { return Path.Combine(_configService.Current.CacheFolder, hash + "." + extension); diff --git a/MareSynchronos/FileCache/TransientResourceManager.cs b/MareSynchronos/FileCache/TransientResourceManager.cs index 00329f6..e794413 100644 --- a/MareSynchronos/FileCache/TransientResourceManager.cs +++ b/MareSynchronos/FileCache/TransientResourceManager.cs @@ -17,6 +17,7 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase 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(); public TransientResourceManager(ILogger logger, TransientConfigService configurationService, DalamudUtilService dalamudUtil, MareMediator mediator) : base(logger, mediator) @@ -29,7 +30,7 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase Mediator.Subscribe(this, (_) => DalamudUtil_FrameworkUpdate()); Mediator.Subscribe(this, (msg) => { - if (_playerRelatedPointers.Contains(msg.gameObjectHandler)) + if (_playerRelatedPointers.Contains(msg.GameObjectHandler)) { DalamudUtil_ClassJobChanged(); } @@ -189,7 +190,10 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase private void DalamudUtil_FrameworkUpdate() { _cachedFrameAddresses = _playerRelatedPointers.Select(c => c.CurrentAddress()).ToHashSet(); - _cachedHandledPaths.Clear(); + lock (_cacheAdditionLock) + { + _cachedHandledPaths.Clear(); + } foreach (var item in TransientResources.Where(item => !_dalamudUtil.IsGameObjectPresent(item.Key)).Select(i => i.Key).ToList()) { Logger.LogDebug("Object not present anymore: {addr}", item.ToString("X")); @@ -218,7 +222,10 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase // ignore files already processed this frame if (_cachedHandledPaths.Contains(gamePath)) return; - _cachedHandledPaths.Add(gamePath); + lock (_cacheAdditionLock) + { + _cachedHandledPaths.Add(gamePath); + } // replace individual mtrl stuff if (filePath.StartsWith("|", StringComparison.OrdinalIgnoreCase)) @@ -235,14 +242,20 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase // ignore files to not handle if (!_fileTypesToHandle.Any(type => gamePath.EndsWith(type, StringComparison.OrdinalIgnoreCase))) { - _cachedHandledPaths.Add(gamePath); + lock (_cacheAdditionLock) + { + _cachedHandledPaths.Add(gamePath); + } return; } // ignore files not belonging to anything player related if (!_cachedFrameAddresses.Contains(gameObject)) { - _cachedHandledPaths.Add(gamePath); + lock (_cacheAdditionLock) + { + _cachedHandledPaths.Add(gamePath); + } return; } diff --git a/MareSynchronos/Interop/IpcManager.cs b/MareSynchronos/Interop/IpcManager.cs index 9dbad06..91604f1 100644 --- a/MareSynchronos/Interop/IpcManager.cs +++ b/MareSynchronos/Interop/IpcManager.cs @@ -277,12 +277,7 @@ public sealed class IpcManager : DisposableMediatorSubscriberBase var gameObj = _dalamudUtil.CreateGameObject(character); if (gameObj is Character c) { - var glamourerString = _glamourerGetAllCustomization!.InvokeFunc(c); - byte[] bytes = Convert.FromBase64String(glamourerString); - // ignore transparency - bytes[88] = 128; - bytes[89] = 63; - return Convert.ToBase64String(bytes); + return _glamourerGetAllCustomization!.InvokeFunc(c); } return string.Empty; }).ConfigureAwait(false); @@ -497,7 +492,7 @@ public sealed class IpcManager : DisposableMediatorSubscriberBase { if (!CheckPenumbraApi()) return; - Mediator.Publish(new HaltScanMessage("TextureConversion")); + Mediator.Publish(new HaltScanMessage(nameof(PenumbraConvertTextureFiles))); int currentTexture = 0; foreach (var texture in textures) { @@ -524,7 +519,7 @@ public sealed class IpcManager : DisposableMediatorSubscriberBase } } } - Mediator.Publish(new ResumeScanMessage("TextureConversion")); + Mediator.Publish(new ResumeScanMessage(nameof(PenumbraConvertTextureFiles))); await _dalamudUtil.RunOnFrameworkThread(async () => { @@ -667,7 +662,10 @@ public sealed class IpcManager : DisposableMediatorSubscriberBase try { var version = _glamourerApiVersions.InvokeFunc(); - if (version.Item1 == 0 && version.Item2 >= 1) + bool versionValid = (_pi.InstalledPlugins + .FirstOrDefault(p => string.Equals(p.InternalName, "Glamourer", StringComparison.OrdinalIgnoreCase)) + ?.Version ?? new Version(0, 0, 0, 0)) >= new Version(1, 0, 6, 1); + if (version.Item1 == 0 && version.Item2 >= 1 && versionValid) { apiAvailable = true; } diff --git a/MareSynchronos/MareConfiguration/Configurations/MareConfig.cs b/MareSynchronos/MareConfiguration/Configurations/MareConfig.cs index 8392327..46a8adc 100644 --- a/MareSynchronos/MareConfiguration/Configurations/MareConfig.cs +++ b/MareSynchronos/MareConfiguration/Configurations/MareConfig.cs @@ -50,6 +50,7 @@ public class MareConfig : IMareConfiguration public int TransferBarsWidth { get; set; } = 250; public bool UseAlternativeFileUpload { get; set; } = false; public bool UseCompactor { get; set; } = false; + public bool ExperimentalUsePenumbraResourceTree { get; set; } = false; public int Version { get; set; } = 1; public NotificationLocation WarningNotification { get; set; } = NotificationLocation.Both; } \ No newline at end of file diff --git a/MareSynchronos/PlayerData/Export/MareCharaFileManager.cs b/MareSynchronos/PlayerData/Export/MareCharaFileManager.cs index b5bdd82..6090831 100644 --- a/MareSynchronos/PlayerData/Export/MareCharaFileManager.cs +++ b/MareSynchronos/PlayerData/Export/MareCharaFileManager.cs @@ -65,7 +65,7 @@ public class MareCharaFileManager : DisposableMediatorSubscriberBase public bool CurrentlyWorking { get; private set; } = false; public MareCharaFileHeader? LoadedCharaFile { get; private set; } - public async Task ApplyMareCharaFile(GameObject? charaTarget) + public async Task ApplyMareCharaFile(GameObject? charaTarget, long expectedLength) { if (charaTarget == null) return; Dictionary extractedFiles = new(StringComparer.Ordinal); @@ -80,8 +80,8 @@ public class MareCharaFileManager : DisposableMediatorSubscriberBase using var lz4Stream = new LZ4Stream(unwrapped, LZ4StreamMode.Decompress, LZ4StreamFlags.HighCompression); using var reader = new BinaryReader(lz4Stream); MareCharaFileHeader.AdvanceReaderToData(reader); - _logger.LogDebug("Applying to {chara}", charaTarget.Name.TextValue); - extractedFiles = ExtractFilesFromCharaFile(LoadedCharaFile, reader); + _logger.LogDebug("Applying to {chara}, expected length of contents: {exp}", charaTarget.Name.TextValue, expectedLength); + extractedFiles = ExtractFilesFromCharaFile(LoadedCharaFile, reader, expectedLength); Dictionary fileSwaps = new(StringComparer.Ordinal); foreach (var fileSwap in LoadedCharaFile.CharaFileData.FileSwaps) { @@ -125,14 +125,19 @@ public class MareCharaFileManager : DisposableMediatorSubscriberBase } } } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failure to read MCDF"); + throw; + } finally { CurrentlyWorking = false; _logger.LogDebug("Clearing local files"); - foreach (var file in extractedFiles) + foreach (var file in Directory.EnumerateFiles(_configService.Current.CacheFolder, "*.tmp")) { - File.Delete(file.Value); + File.Delete(file); } } } @@ -142,7 +147,7 @@ public class MareCharaFileManager : DisposableMediatorSubscriberBase LoadedCharaFile = null; } - public void LoadMareCharaFile(string filePath) + public long LoadMareCharaFile(string filePath) { CurrentlyWorking = true; try @@ -179,6 +184,7 @@ public class MareCharaFileManager : DisposableMediatorSubscriberBase _logger.LogInformation("Expected length: {expected}", expectedLength); } + return expectedLength; } finally { CurrentlyWorking = false; } } @@ -214,34 +220,38 @@ public class MareCharaFileManager : DisposableMediatorSubscriberBase } } } + catch (Exception ex) + { + _logger.LogError(ex, "Failure Saving Mare Chara File, deleting output"); + File.Delete(filePath); + } finally { CurrentlyWorking = false; } } - private Dictionary ExtractFilesFromCharaFile(MareCharaFileHeader charaFileHeader, BinaryReader reader) + private Dictionary ExtractFilesFromCharaFile(MareCharaFileHeader charaFileHeader, BinaryReader reader, long expectedLength) { + long totalRead = 0; Dictionary gamePathToFilePath = new(StringComparer.Ordinal); foreach (var fileData in charaFileHeader.CharaFileData.Files) { var fileName = Path.Combine(_configService.Current.CacheFolder, "mare_" + _globalFileCounter++ + ".tmp"); - var length = fileData.Length; - var bufferSize = 4 * 1024 * 1024; + var length = (int)fileData.Length; + var bufferSize = length; using var fs = File.OpenWrite(fileName); using var wr = new BinaryWriter(fs); - int chunk = 0; - while (length > 0) - { - if (length < bufferSize) bufferSize = (int)length; - _logger.LogTrace("Reading chunk {chunk} {bufferSize}/{length} of {fileName}", chunk++, bufferSize, length, fileName); - var buffer = reader.ReadBytes(bufferSize); - wr.Write(length > bufferSize ? buffer : buffer.Take((int)length).ToArray()); - length -= bufferSize; - } + _logger.LogTrace("Reading {length} of {fileName}", length, fileName); + var buffer = reader.ReadBytes(bufferSize); + wr.Write(buffer); wr.Flush(); + wr.Close(); + if (buffer.Length == 0) throw new EndOfStreamException("Unexpected EOF"); foreach (var path in fileData.GamePaths) { gamePathToFilePath[path] = fileName; _logger.LogTrace("{path} => {fileName}", path, fileName); } + totalRead += length; + _logger.LogTrace("Read {read}/{expected} bytes", totalRead, expectedLength); } return gamePathToFilePath; diff --git a/MareSynchronos/PlayerData/Factories/PairHandlerFactory.cs b/MareSynchronos/PlayerData/Factories/PairHandlerFactory.cs index 73bad33..f9ee5f6 100644 --- a/MareSynchronos/PlayerData/Factories/PairHandlerFactory.cs +++ b/MareSynchronos/PlayerData/Factories/PairHandlerFactory.cs @@ -1,7 +1,6 @@ using MareSynchronos.API.Dto.User; using MareSynchronos.FileCache; using MareSynchronos.Interop; -using MareSynchronos.MareConfiguration; using MareSynchronos.PlayerData.Handlers; using MareSynchronos.PlayerData.Pairs; using MareSynchronos.Services; @@ -21,13 +20,12 @@ public class PairHandlerFactory private readonly IpcManager _ipcManager; private readonly ILoggerFactory _loggerFactory; private readonly MareMediator _mareMediator; - private readonly MareConfigService _mareConfigService; private readonly PluginWarningNotificationService _pluginWarningNotificationManager; public PairHandlerFactory(ILoggerFactory loggerFactory, GameObjectHandlerFactory gameObjectHandlerFactory, IpcManager ipcManager, FileDownloadManagerFactory fileDownloadManagerFactory, DalamudUtilService dalamudUtilService, PluginWarningNotificationService pluginWarningNotificationManager, IHostApplicationLifetime hostApplicationLifetime, - FileCacheManager fileCacheManager, MareMediator mareMediator, MareConfigService mareConfigService) + FileCacheManager fileCacheManager, MareMediator mareMediator) { _loggerFactory = loggerFactory; _gameObjectHandlerFactory = gameObjectHandlerFactory; @@ -38,13 +36,12 @@ public class PairHandlerFactory _hostApplicationLifetime = hostApplicationLifetime; _fileCacheManager = fileCacheManager; _mareMediator = mareMediator; - _mareConfigService = mareConfigService; } public PairHandler Create(OnlineUserIdentDto onlineUserIdentDto) { return new PairHandler(_loggerFactory.CreateLogger(), onlineUserIdentDto, _gameObjectHandlerFactory, _ipcManager, _fileDownloadManagerFactory.Create(), _pluginWarningNotificationManager, _dalamudUtilService, _hostApplicationLifetime, - _fileCacheManager, _mareMediator, _mareConfigService); + _fileCacheManager, _mareMediator); } } \ No newline at end of file diff --git a/MareSynchronos/PlayerData/Factories/PlayerDataFactory.cs b/MareSynchronos/PlayerData/Factories/PlayerDataFactory.cs index 30700fb..3f594b3 100644 --- a/MareSynchronos/PlayerData/Factories/PlayerDataFactory.cs +++ b/MareSynchronos/PlayerData/Factories/PlayerDataFactory.cs @@ -5,6 +5,7 @@ using FFXIVClientStructs.FFXIV.Client.System.Resource; using MareSynchronos.API.Data.Enum; using MareSynchronos.FileCache; using MareSynchronos.Interop; +using MareSynchronos.MareConfiguration; using MareSynchronos.PlayerData.Data; using MareSynchronos.PlayerData.Handlers; using MareSynchronos.Services; @@ -25,11 +26,12 @@ public class PlayerDataFactory private readonly IpcManager _ipcManager; private readonly ILogger _logger; private readonly PerformanceCollectorService _performanceCollector; + private readonly MareConfigService _mareConfigService; private readonly TransientResourceManager _transientResourceManager; public PlayerDataFactory(ILogger logger, DalamudUtilService dalamudUtil, IpcManager ipcManager, TransientResourceManager transientResourceManager, FileCacheManager fileReplacementFactory, - PerformanceCollectorService performanceCollector) + PerformanceCollectorService performanceCollector, MareConfigService mareConfigService) { _logger = logger; _dalamudUtil = dalamudUtil; @@ -37,7 +39,7 @@ public class PlayerDataFactory _transientResourceManager = transientResourceManager; _fileCacheManager = fileReplacementFactory; _performanceCollector = performanceCollector; - + _mareConfigService = mareConfigService; _logger.LogTrace("Creating " + nameof(PlayerDataFactory)); } @@ -281,7 +283,7 @@ public class PlayerDataFactory AddReplacementsFromRenderModel(mdl, forwardResolve, reverseResolve); } - if (objectKind == ObjectKind.Player) + if (objectKind == ObjectKind.Player && human->CharacterBase.GetModelType() == CharacterBase.ModelType.Human) { AddPlayerSpecificReplacements(human, forwardResolve, reverseResolve); } @@ -330,12 +332,18 @@ public class PlayerDataFactory Stopwatch st = Stopwatch.StartNew(); // penumbra call, it's currently broken - // var data = (await _ipcManager.PenumbraGetCharacterData(_logger, playerRelatedObject).ConfigureAwait(false))![0]; - // if (data == null) throw new InvalidOperationException("Penumbra returned null data"); - - // gather static replacements from render model - var (forwardResolve, reverseResolve) = await _dalamudUtil.RunOnFrameworkThread(() => BuildDataFromModel(objectKind, charaPointer, token)).ConfigureAwait(false); - Dictionary> resolvedPaths = await GetFileReplacementsFromPaths(forwardResolve, reverseResolve).ConfigureAwait(false); + IReadOnlyDictionary? resolvedPaths; + if (_mareConfigService.Current.ExperimentalUsePenumbraResourceTree) + { + resolvedPaths = (await _ipcManager.PenumbraGetCharacterData(_logger, playerRelatedObject).ConfigureAwait(false))![0]; + if (resolvedPaths == null) throw new InvalidOperationException("Penumbra returned null data"); + } + else + { + // gather static replacements from render model + var (forwardResolve, reverseResolve) = await _dalamudUtil.RunOnFrameworkThread(() => BuildDataFromModel(objectKind, charaPointer, token)).ConfigureAwait(false); + resolvedPaths = await GetFileReplacementsFromPaths(forwardResolve, reverseResolve).ConfigureAwait(false); + } previousData.FileReplacements[objectKind] = new HashSet(resolvedPaths.Select(c => new FileReplacement([.. c.Value], c.Key)), FileReplacementComparer.Instance) .Where(p => p.HasFileReplacement).ToHashSet(); @@ -353,6 +361,7 @@ public class PlayerDataFactory { foreach (var item in previousData.FileReplacements[objectKind].Where(i => i.HasFileReplacement).SelectMany(p => p.GamePaths)) { + _logger.LogDebug("Persisting {item}", item); _transientResourceManager.AddSemiTransientResource(objectKind, item); } } @@ -425,7 +434,7 @@ public class PlayerDataFactory return previousData; } - private async Task>> GetFileReplacementsFromPaths(HashSet forwardResolve, HashSet reverseResolve) + private async Task> GetFileReplacementsFromPaths(HashSet forwardResolve, HashSet reverseResolve) { var forwardPaths = forwardResolve.ToArray(); var reversePaths = reverseResolve.ToArray(); @@ -457,7 +466,7 @@ public class PlayerDataFactory } } - return resolvedPaths; + return resolvedPaths.ToDictionary(k => k.Key, k => k.Value.ToArray(), StringComparer.OrdinalIgnoreCase).AsReadOnly(); } private HashSet ManageSemiTransientData(ObjectKind objectKind, IntPtr charaPointer) diff --git a/MareSynchronos/PlayerData/Handlers/PairHandler.cs b/MareSynchronos/PlayerData/Handlers/PairHandler.cs index 917dad8..c9d66ec 100644 --- a/MareSynchronos/PlayerData/Handlers/PairHandler.cs +++ b/MareSynchronos/PlayerData/Handlers/PairHandler.cs @@ -2,7 +2,6 @@ using MareSynchronos.API.Dto.User; using MareSynchronos.FileCache; using MareSynchronos.Interop; -using MareSynchronos.MareConfiguration; using MareSynchronos.PlayerData.Factories; using MareSynchronos.PlayerData.Pairs; using MareSynchronos.Services; @@ -19,10 +18,11 @@ namespace MareSynchronos.PlayerData.Handlers; public sealed class PairHandler : DisposableMediatorSubscriberBase { + private sealed record CombatData(Guid ApplicationId, CharacterData CharacterData, bool Forced); + private readonly DalamudUtilService _dalamudUtil; private readonly FileDownloadManager _downloadManager; private readonly FileCacheManager _fileDbManager; - private readonly MareConfigService _mareConfigService; private readonly GameObjectHandlerFactory _gameObjectHandlerFactory; private readonly IpcManager _ipcManager; private readonly IHostApplicationLifetime _lifetime; @@ -37,6 +37,7 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase private bool _isVisible; private string _penumbraCollection; private bool _redrawOnNextApplication = false; + private CombatData? _dataReceivedInCombat; public long LastAppliedDataSize { get; private set; } public PairHandler(ILogger logger, OnlineUserIdentDto onlineUser, @@ -44,8 +45,7 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase IpcManager ipcManager, FileDownloadManager transferManager, PluginWarningNotificationService pluginWarningNotificationManager, DalamudUtilService dalamudUtil, IHostApplicationLifetime lifetime, - FileCacheManager fileDbManager, MareMediator mediator, - MareConfigService mareConfigService) : base(logger, mediator) + FileCacheManager fileDbManager, MareMediator mediator) : base(logger, mediator) { OnlineUser = onlineUser; _gameObjectHandlerFactory = gameObjectHandlerFactory; @@ -55,7 +55,6 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase _dalamudUtil = dalamudUtil; _lifetime = lifetime; _fileDbManager = fileDbManager; - _mareConfigService = mareConfigService; _penumbraCollection = _ipcManager.PenumbraCreateTemporaryCollectionAsync(logger, OnlineUser.User.UID).ConfigureAwait(false).GetAwaiter().GetResult(); Mediator.Subscribe(this, (_) => FrameworkUpdate()); @@ -77,11 +76,26 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase }); Mediator.Subscribe(this, (msg) => { - if (msg.gameObjectHandler == _charaHandler) + if (msg.GameObjectHandler == _charaHandler) { _redrawOnNextApplication = true; } }); + Mediator.Subscribe(this, (msg) => + { + if (IsVisible && _dataReceivedInCombat != null) + { + ApplyCharacterData(_dataReceivedInCombat.ApplicationId, + _dataReceivedInCombat.CharacterData, _dataReceivedInCombat.Forced); + _dataReceivedInCombat = null; + } + }); + Mediator.Subscribe(this, _ => + { + _dataReceivedInCombat = null; + _downloadCancellationTokenSource = _downloadCancellationTokenSource?.CancelRecreate(); + _applicationCancellationTokenSource = _applicationCancellationTokenSource?.CancelRecreate(); + }); LastAppliedDataSize = -1; } @@ -107,12 +121,21 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase public void ApplyCharacterData(Guid applicationBase, CharacterData characterData, bool forceApplyCustomization = false) { + if (_dalamudUtil.IsInCombat) + { + Logger.LogDebug("[BASE-{appBase}] Received data but player is in combat", applicationBase); + _dataReceivedInCombat = new(applicationBase, characterData, forceApplyCustomization); + SetUploading(isUploading: false); + return; + } + if (_charaHandler == null || (PlayerCharacter == IntPtr.Zero)) { Logger.LogDebug("[BASE-{appBase}] Received data but player was in invalid state, charaHandlerIsNull: {charaIsNull}, playerPointerIsNull: {ptrIsNull}", applicationBase, _charaHandler == null, PlayerCharacter == IntPtr.Zero); var hasDiffMods = characterData.CheckUpdatedData(applicationBase, _cachedData, Logger, - this, forceApplyCustomization, forceApplyMods: false).Any(p => p.Value.Contains(PlayerChanges.ModManip) || p.Value.Contains(PlayerChanges.ModFiles)); + this, forceApplyCustomization, forceApplyMods: false) + .Any(p => p.Value.Contains(PlayerChanges.ModManip) || p.Value.Contains(PlayerChanges.ModFiles)); _forceApplyMods = hasDiffMods || _forceApplyMods || (PlayerCharacter == IntPtr.Zero && _cachedData == null); _cachedData = characterData; Logger.LogDebug("[BASE-{appBase}] Setting data: {hash}, forceApplyMods: {force}", applicationBase, _cachedData.DataHash.Value, _forceApplyMods); @@ -395,7 +418,7 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase { await _ipcManager.PenumbraSetTemporaryModsAsync(Logger, _applicationId, _penumbraCollection, moddedPaths).ConfigureAwait(false); LastAppliedDataSize = -1; - foreach (var path in moddedPaths.Select(v => new FileInfo(v.Value)).Where(p => p.Exists)) + foreach (var path in moddedPaths.Values.Distinct(StringComparer.OrdinalIgnoreCase).Select(v => new FileInfo(v)).Where(p => p.Exists)) { LastAppliedDataSize += path.Length; } diff --git a/MareSynchronos/PlayerData/Services/CacheCreationService.cs b/MareSynchronos/PlayerData/Services/CacheCreationService.cs index 9a80d18..3790bf6 100644 --- a/MareSynchronos/PlayerData/Services/CacheCreationService.cs +++ b/MareSynchronos/PlayerData/Services/CacheCreationService.cs @@ -48,8 +48,21 @@ public sealed class CacheCreationService : DisposableMediatorSubscriberBase _playerRelatedObjects[ObjectKind.Companion] = gameObjectHandlerFactory.Create(ObjectKind.Companion, () => dalamudUtil.GetCompanion(), isWatched: true) .GetAwaiter().GetResult(); + Mediator.Subscribe(this, (msg) => + { + if (msg.GameObjectHandler != _playerRelatedObjects[ObjectKind.Player]) return; + + Logger.LogTrace("Removing pet data for {obj}", msg.GameObjectHandler); + _playerData.FileReplacements.Remove(ObjectKind.Pet); + _playerData.GlamourerString.Remove(ObjectKind.Pet); + _playerData.CustomizePlusScale.Remove(ObjectKind.Pet); + Mediator.Publish(new CharacterDataCreatedMessage(_playerData.ToAPI())); + }); + Mediator.Subscribe(this, (msg) => { + // ignore pets + if (msg.ObjectToCreateFor == _playerRelatedObjects[ObjectKind.Pet]) return; _ = Task.Run(() => { Logger.LogTrace("Clearing cache for {obj}", msg.ObjectToCreateFor); diff --git a/MareSynchronos/Plugin.cs b/MareSynchronos/Plugin.cs index 2951485..8b396fb 100644 --- a/MareSynchronos/Plugin.cs +++ b/MareSynchronos/Plugin.cs @@ -1,4 +1,5 @@ using Dalamud.ContextMenu; +using Dalamud.Game.ClientState.Objects; using Dalamud.Interface.ImGuiFileDialog; using Dalamud.Interface.Windowing; using Dalamud.Plugin; @@ -33,7 +34,7 @@ public sealed class Plugin : IDalamudPlugin public Plugin(DalamudPluginInterface pluginInterface, ICommandManager commandManager, IDataManager gameData, IFramework framework, IObjectTable objectTable, IClientState clientState, ICondition condition, IChatGui chatGui, - IGameGui gameGui, IDtrBar dtrBar, IPluginLog pluginLog) + IGameGui gameGui, IDtrBar dtrBar, IPluginLog pluginLog, ITargetManager targetManager) { _hostBuilderRunTask = new HostBuilder() .UseContentRoot(pluginInterface.ConfigDirectory.FullName) @@ -74,7 +75,7 @@ public sealed class Plugin : IDalamudPlugin collection.AddSingleton(); collection.AddSingleton((s) => new DalamudContextMenu(pluginInterface)); collection.AddSingleton((s) => new DalamudUtilService(s.GetRequiredService>(), - clientState, objectTable, framework, gameGui, condition, gameData, + clientState, objectTable, framework, gameGui, condition, gameData, targetManager, s.GetRequiredService(), s.GetRequiredService())); collection.AddSingleton((s) => new DtrEntry(s.GetRequiredService>(), dtrBar, s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService())); diff --git a/MareSynchronos/Services/CharacterAnalyzer.cs b/MareSynchronos/Services/CharacterAnalyzer.cs index 6ae1b25..29cd776 100644 --- a/MareSynchronos/Services/CharacterAnalyzer.cs +++ b/MareSynchronos/Services/CharacterAnalyzer.cs @@ -51,18 +51,27 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable CurrentFile = 1; Logger.LogDebug("=== Computing {amount} remaining files ===", remaining.Count); - Mediator.Publish(new HaltScanMessage("CharacterAnalyzer")); - - foreach (var file in remaining) + Mediator.Publish(new HaltScanMessage(nameof(CharacterAnalyzer))); + try { - Logger.LogDebug("Computing file {file}", file.FilePaths[0]); - await file.ComputeSizes(_fileCacheManager, cancelToken).ConfigureAwait(false); - CurrentFile++; + foreach (var file in remaining) + { + Logger.LogDebug("Computing file {file}", file.FilePaths[0]); + await file.ComputeSizes(_fileCacheManager, cancelToken).ConfigureAwait(false); + CurrentFile++; + } + + _fileCacheManager.WriteOutFullCsv(); + + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Failed to analyze files"); + } + finally + { + Mediator.Publish(new ResumeScanMessage(nameof(CharacterAnalyzer))); } - - _fileCacheManager.WriteOutFullCsv(); - - Mediator.Publish(new ResumeScanMessage("CharacterAnalzyer")); } Mediator.Publish(new CharacterDataAnalyzedMessage()); diff --git a/MareSynchronos/Services/DalamudUtilService.cs b/MareSynchronos/Services/DalamudUtilService.cs index 0306e1c..4f5c812 100644 --- a/MareSynchronos/Services/DalamudUtilService.cs +++ b/MareSynchronos/Services/DalamudUtilService.cs @@ -1,4 +1,5 @@ using Dalamud.Game.ClientState.Conditions; +using Dalamud.Game.ClientState.Objects; using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game.Character; @@ -15,7 +16,7 @@ using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject; namespace MareSynchronos.Services; -public class DalamudUtilService : IHostedService +public class DalamudUtilService : IHostedService, IMediatorSubscriber { private readonly List _classJobIdsIgnoredForPets = [30]; private readonly IClientState _clientState; @@ -23,7 +24,6 @@ public class DalamudUtilService : IHostedService private readonly IFramework _framework; private readonly IGameGui _gameGui; private readonly ILogger _logger; - private readonly MareMediator _mediator; private readonly IObjectTable _objectTable; private readonly PerformanceCollectorService _performanceCollector; private uint? _classJobId = 0; @@ -35,7 +35,8 @@ public class DalamudUtilService : IHostedService private bool _sentBetweenAreas = false; public DalamudUtilService(ILogger logger, IClientState clientState, IObjectTable objectTable, IFramework framework, - IGameGui gameGui, ICondition condition, IDataManager gameData, MareMediator mediator, PerformanceCollectorService performanceCollector) + IGameGui gameGui, ICondition condition, IDataManager gameData, ITargetManager targetManager, + MareMediator mediator, PerformanceCollectorService performanceCollector) { _logger = logger; _clientState = clientState; @@ -43,7 +44,7 @@ public class DalamudUtilService : IHostedService _framework = framework; _gameGui = gameGui; _condition = condition; - _mediator = mediator; + Mediator = mediator; _performanceCollector = performanceCollector; WorldData = new(() => { @@ -51,6 +52,18 @@ public class DalamudUtilService : IHostedService .Where(w => w.IsPublic && !w.Name.RawData.IsEmpty) .ToDictionary(w => (ushort)w.RowId, w => w.Name.ToString()); }); + mediator.Subscribe(this, async (msg) => + { + if (clientState.IsPvP) return; + var name = msg.Pair.PlayerName; + if (string.IsNullOrEmpty(name)) return; + var addr = _playerCharas.FirstOrDefault(f => string.Equals(f.Value.Name, name, StringComparison.Ordinal)).Value.Address; + if (addr == nint.Zero) return; + await RunOnFrameworkThread(() => + { + targetManager.Target = CreateGameObject(addr); + }).ConfigureAwait(false); + }); } public unsafe GameObject* GposeTarget => TargetSystem.Instance()->GPoseTarget; @@ -61,9 +74,12 @@ public class DalamudUtilService : IHostedService 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 Lazy> WorldData { get; private set; } + public MareMediator Mediator { get; } + public Dalamud.Game.ClientState.Objects.Types.GameObject? CreateGameObject(IntPtr reference) { EnsureIsOnFramework(); @@ -279,6 +295,7 @@ public class DalamudUtilService : IHostedService { _logger.LogTrace("Stopping {type}", GetType()); + Mediator.UnsubscribeAll(this); _framework.Update -= FrameworkOnUpdate; return Task.CompletedTask; } @@ -433,31 +450,46 @@ public class DalamudUtilService : IHostedService { _logger.LogDebug("Gpose start"); IsInGpose = true; - _mediator.Publish(new GposeStartMessage()); + Mediator.Publish(new GposeStartMessage()); } else if (GposeTarget == null && IsInGpose) { _logger.LogDebug("Gpose end"); IsInGpose = false; - _mediator.Publish(new GposeEndMessage()); + Mediator.Publish(new GposeEndMessage()); + } + + if (_condition[ConditionFlag.InCombat] && !IsInCombat) + { + _logger.LogDebug("Combat start"); + IsInCombat = true; + Mediator.Publish(new CombatStartMessage()); + Mediator.Publish(new HaltScanMessage(nameof(IsInCombat))); + } + else if (!_condition[ConditionFlag.InCombat] && IsInCombat) + { + _logger.LogDebug("Combat end"); + IsInCombat = false; + Mediator.Publish(new CombatEndMessage()); + Mediator.Publish(new ResumeScanMessage(nameof(IsInCombat))); } if (_condition[ConditionFlag.WatchingCutscene] && !IsInCutscene) { _logger.LogDebug("Cutscene start"); IsInCutscene = true; - _mediator.Publish(new CutsceneStartMessage()); - _mediator.Publish(new HaltScanMessage("Cutscene")); + Mediator.Publish(new CutsceneStartMessage()); + Mediator.Publish(new HaltScanMessage(nameof(IsInCutscene))); } else if (!_condition[ConditionFlag.WatchingCutscene] && IsInCutscene) { _logger.LogDebug("Cutscene end"); IsInCutscene = false; - _mediator.Publish(new CutsceneEndMessage()); - _mediator.Publish(new ResumeScanMessage("Cutscene")); + Mediator.Publish(new CutsceneEndMessage()); + Mediator.Publish(new ResumeScanMessage(nameof(IsInCutscene))); } - if (IsInCutscene) { _mediator.Publish(new CutsceneFrameworkUpdateMessage()); return; } + if (IsInCutscene) { Mediator.Publish(new CutsceneFrameworkUpdateMessage()); return; } if (_condition[ConditionFlag.BetweenAreas] || _condition[ConditionFlag.BetweenAreas51]) { @@ -469,8 +501,8 @@ public class DalamudUtilService : IHostedService { _logger.LogDebug("Zone switch/Gpose start"); _sentBetweenAreas = true; - _mediator.Publish(new ZoneSwitchStartMessage()); - _mediator.Publish(new HaltScanMessage("Zone switch")); + Mediator.Publish(new ZoneSwitchStartMessage()); + Mediator.Publish(new HaltScanMessage(nameof(ConditionFlag.BetweenAreas))); } } @@ -481,11 +513,12 @@ public class DalamudUtilService : IHostedService { _logger.LogDebug("Zone switch/Gpose end"); _sentBetweenAreas = false; - _mediator.Publish(new ZoneSwitchEndMessage()); - _mediator.Publish(new ResumeScanMessage("Zone switch")); + Mediator.Publish(new ZoneSwitchEndMessage()); + Mediator.Publish(new ResumeScanMessage(nameof(ConditionFlag.BetweenAreas))); } - _mediator.Publish(new FrameworkUpdateMessage()); + if (!IsInCombat) + Mediator.Publish(new FrameworkUpdateMessage()); if (DateTime.Now < _delayedFrameworkUpdateCheck.AddSeconds(1)) return; @@ -496,16 +529,19 @@ public class DalamudUtilService : IHostedService _logger.LogDebug("Logged in"); IsLoggedIn = true; _lastZone = _clientState.TerritoryType; - _mediator.Publish(new DalamudLoginMessage()); + Mediator.Publish(new DalamudLoginMessage()); } else if (localPlayer == null && IsLoggedIn) { _logger.LogDebug("Logged out"); IsLoggedIn = false; - _mediator.Publish(new DalamudLogoutMessage()); + Mediator.Publish(new DalamudLogoutMessage()); } - _mediator.Publish(new DelayedFrameworkUpdateMessage()); + if (IsInCombat) + Mediator.Publish(new FrameworkUpdateMessage()); + + Mediator.Publish(new DelayedFrameworkUpdateMessage()); _delayedFrameworkUpdateCheck = DateTime.Now; } diff --git a/MareSynchronos/Services/Mediator/Messages.cs b/MareSynchronos/Services/Mediator/Messages.cs index 5c795d1..9e17c51 100644 --- a/MareSynchronos/Services/Mediator/Messages.cs +++ b/MareSynchronos/Services/Mediator/Messages.cs @@ -18,7 +18,7 @@ public record OpenSettingsUiMessage : MessageBase; public record DalamudLoginMessage : MessageBase; public record DalamudLogoutMessage : MessageBase; public record FrameworkUpdateMessage : SameThreadMessage; -public record ClassJobChangedMessage(GameObjectHandler gameObjectHandler) : MessageBase; +public record ClassJobChangedMessage(GameObjectHandler GameObjectHandler) : MessageBase; public record DelayedFrameworkUpdateMessage : SameThreadMessage; public record ZoneSwitchStartMessage : MessageBase; public record ZoneSwitchEndMessage : MessageBase; @@ -75,6 +75,9 @@ public record OpenSyncshellAdminPanel(GroupFullInfoDto GroupInfo) : MessageBase; 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; #pragma warning restore S2094 #pragma warning restore MA0048 // File name must match type name \ No newline at end of file diff --git a/MareSynchronos/Services/PerformanceCollectorService.cs b/MareSynchronos/Services/PerformanceCollectorService.cs index 8646628..5dc209d 100644 --- a/MareSynchronos/Services/PerformanceCollectorService.cs +++ b/MareSynchronos/Services/PerformanceCollectorService.cs @@ -173,7 +173,7 @@ public sealed class PerformanceCollectorService : IHostedService { try { - var last = entries.Value.Last(); + var last = entries.Value.ToList().Last(); if (last.Item1.AddMinutes(10) < TimeOnly.FromDateTime(DateTime.Now) && !_performanceCounters.TryRemove(entries.Key, out _)) { _logger.LogDebug("Could not remove performance counter {counter}", entries.Key); diff --git a/MareSynchronos/UI/Components/DrawUserPair.cs b/MareSynchronos/UI/Components/DrawUserPair.cs index 4322e68..4f52819 100644 --- a/MareSynchronos/UI/Components/DrawUserPair.cs +++ b/MareSynchronos/UI/Components/DrawUserPair.cs @@ -66,13 +66,18 @@ public class DrawUserPair : DrawPairBase ImGui.SetCursorPosY(textPosY); ImGui.PushFont(UiBuilder.IconFont); UiSharedService.ColorText(FontAwesomeIcon.Eye.ToIconString(), ImGuiColors.ParsedGreen); + if (ImGui.IsItemClicked()) + { + _mediator.Publish(new TargetPairMessage(_pair)); + } ImGui.PopFont(); - var visibleTooltip = _pair.UserData.AliasOrUID + " is visible: " + _pair.PlayerName!; + var visibleTooltip = _pair.UserData.AliasOrUID + " is visible: " + _pair.PlayerName! + Environment.NewLine + "Click to target this player"; if (_pair.LastAppliedDataSize >= 0) { visibleTooltip += UiSharedService.TooltipSeparator + "Loaded Mods Size: " + UiSharedService.ByteToString(_pair.LastAppliedDataSize, true); } + UiSharedService.AttachToolTip(visibleTooltip); } } diff --git a/MareSynchronos/UI/Components/Popup/BanUserPopupHandler.cs b/MareSynchronos/UI/Components/Popup/BanUserPopupHandler.cs index 8a2f69f..1803e78 100644 --- a/MareSynchronos/UI/Components/Popup/BanUserPopupHandler.cs +++ b/MareSynchronos/UI/Components/Popup/BanUserPopupHandler.cs @@ -22,6 +22,8 @@ public class BanUserPopupHandler : IPopupHandler public Vector2 PopupSize => new(500, 250); + public bool ShowClose => true; + public void DrawContent() { UiSharedService.TextWrapped("User " + (_reportedPair.UserData.AliasOrUID) + " will be banned and removed from this Syncshell."); @@ -37,10 +39,6 @@ public class BanUserPopupHandler : IPopupHandler UiSharedService.TextWrapped("The reason will be displayed in the banlist. The current server-side alias if present (Vanity ID) will automatically be attached to the reason."); } - public void OnClose() - { - } - public void Open(OpenBanUserPopupMessage message) { _reportedPair = message.PairToBan; diff --git a/MareSynchronos/UI/Components/Popup/IPopupHandler.cs b/MareSynchronos/UI/Components/Popup/IPopupHandler.cs index aa649b4..21b99f9 100644 --- a/MareSynchronos/UI/Components/Popup/IPopupHandler.cs +++ b/MareSynchronos/UI/Components/Popup/IPopupHandler.cs @@ -5,8 +5,7 @@ namespace MareSynchronos.UI.Components.Popup; public interface IPopupHandler { Vector2 PopupSize { get; } + bool ShowClose { get; } void DrawContent(); - - void OnClose(); } \ No newline at end of file diff --git a/MareSynchronos/UI/Components/Popup/PopupHandler.cs b/MareSynchronos/UI/Components/Popup/PopupHandler.cs index 46aa6e1..fede15b 100644 --- a/MareSynchronos/UI/Components/Popup/PopupHandler.cs +++ b/MareSynchronos/UI/Components/Popup/PopupHandler.cs @@ -62,17 +62,13 @@ public class PopupHandler : WindowMediatorSubscriberBase using var popup = ImRaii.Popup(WindowName, ImGuiWindowFlags.Modal); if (!popup) return; _currentHandler.DrawContent(); - ImGui.Separator(); - if (UiSharedService.NormalizedIconTextButton(FontAwesomeIcon.Times, "Close")) + if (_currentHandler.ShowClose) { - ImGui.CloseCurrentPopup(); - _currentHandler.OnClose(); + ImGui.Separator(); + if (UiSharedService.NormalizedIconTextButton(FontAwesomeIcon.Times, "Close")) + { + ImGui.CloseCurrentPopup(); + } } } - - public override void OnClose() - { - base.OnClose(); - _currentHandler?.OnClose(); - } } \ No newline at end of file diff --git a/MareSynchronos/UI/Components/Popup/ReportPopupHandler.cs b/MareSynchronos/UI/Components/Popup/ReportPopupHandler.cs index 3f58dd2..a93bfd9 100644 --- a/MareSynchronos/UI/Components/Popup/ReportPopupHandler.cs +++ b/MareSynchronos/UI/Components/Popup/ReportPopupHandler.cs @@ -24,6 +24,8 @@ internal class ReportPopupHandler : IPopupHandler public Vector2 PopupSize => new(500, 500); + public bool ShowClose => true; + public void DrawContent() { using (ImRaii.PushFont(_uiSharedService.UidFont)) @@ -49,10 +51,6 @@ internal class ReportPopupHandler : IPopupHandler } } - public void OnClose() - { - } - public void Open(OpenReportPopupMessage msg) { _reportedPair = msg.PairToReport; diff --git a/MareSynchronos/UI/GposeUi.cs b/MareSynchronos/UI/GposeUi.cs index e90fedf..9c92b52 100644 --- a/MareSynchronos/UI/GposeUi.cs +++ b/MareSynchronos/UI/GposeUi.cs @@ -15,6 +15,8 @@ public class GposeUi : WindowMediatorSubscriberBase private readonly DalamudUtilService _dalamudUtil; private readonly FileDialogManager _fileDialogManager; private readonly MareCharaFileManager _mareCharaFileManager; + private Task? _expectedLength; + private Task? _applicationTask; public GposeUi(ILogger logger, MareCharaFileManager mareCharaFileManager, DalamudUtilService dalamudUtil, FileDialogManager fileDialogManager, MareConfigService configService, @@ -51,21 +53,28 @@ public class GposeUi : WindowMediatorSubscriberBase _configService.Current.ExportFolder = Path.GetDirectoryName(path) ?? string.Empty; _configService.Save(); - _ = Task.Run(() => _mareCharaFileManager.LoadMareCharaFile(path)); + _expectedLength = Task.Run(() => _mareCharaFileManager.LoadMareCharaFile(path)); }, 1, Directory.Exists(_configService.Current.ExportFolder) ? _configService.Current.ExportFolder : null); } UiSharedService.AttachToolTip("Applies it to the currently selected GPose actor"); - if (_mareCharaFileManager.LoadedCharaFile != null) + if (_mareCharaFileManager.LoadedCharaFile != null && _expectedLength != null) { UiSharedService.TextWrapped("Loaded file: " + _mareCharaFileManager.LoadedCharaFile.FilePath); UiSharedService.TextWrapped("File Description: " + _mareCharaFileManager.LoadedCharaFile.CharaFileData.Description); if (UiSharedService.NormalizedIconTextButton(FontAwesomeIcon.Check, "Apply loaded MCDF")) { - _ = Task.Run(async () => await _mareCharaFileManager.ApplyMareCharaFile(_dalamudUtil.GposeTargetGameObject).ConfigureAwait(false)); + _applicationTask = Task.Run(async () => await _mareCharaFileManager.ApplyMareCharaFile(_dalamudUtil.GposeTargetGameObject, _expectedLength!.GetAwaiter().GetResult()).ConfigureAwait(false)); } UiSharedService.AttachToolTip("Applies it to the currently selected GPose actor"); UiSharedService.ColorTextWrapped("Warning: redrawing or changing the character will revert all applied mods.", ImGuiColors.DalamudYellow); } + if (_applicationTask?.IsFaulted ?? false) + { + UiSharedService.ColorTextWrapped("Failure to read MCDF file. MCDF file is possibly corrupt. Re-export the MCDF file and try again.", + ImGuiColors.DalamudRed); + UiSharedService.ColorTextWrapped("Note: if this is your MCDF, try redrawing yourself, wait and re-export the file. " + + "If you received it from someone else have them do the same.", ImGuiColors.DalamudYellow); + } } else { @@ -77,6 +86,8 @@ public class GposeUi : WindowMediatorSubscriberBase private void EndGpose() { IsOpen = false; + _applicationTask = null; + _expectedLength = null; _mareCharaFileManager.ClearMareCharaFile(); } diff --git a/MareSynchronos/UI/SettingsUi.cs b/MareSynchronos/UI/SettingsUi.cs index 67325eb..3af95d2 100644 --- a/MareSynchronos/UI/SettingsUi.cs +++ b/MareSynchronos/UI/SettingsUi.cs @@ -35,6 +35,7 @@ public class SettingsUi : WindowMediatorSubscriberBase private readonly FileCompactor _fileCompactor; private readonly FileUploadManager _fileTransferManager; private readonly FileTransferOrchestrator _fileTransferOrchestrator; + private readonly FileCacheManager _fileCacheManager; private readonly MareCharaFileManager _mareCharaFileManager; private readonly PairManager _pairManager; private readonly PerformanceCollectorService _performanceCollector; @@ -49,6 +50,11 @@ public class SettingsUi : WindowMediatorSubscriberBase private bool _readClearCache = false; private bool _readExport = false; private bool _wasOpen = false; + private readonly IProgress<(int, int, FileCacheEntity)> _validationProgress; + private Task>? _validationTask; + private CancellationTokenSource? _validationCts; + private (int, int, FileCacheEntity) _currentProgress; + private Task? _exportTask; public SettingsUi(ILogger logger, UiSharedService uiShared, MareConfigService configService, @@ -57,6 +63,7 @@ public class SettingsUi : WindowMediatorSubscriberBase MareMediator mediator, PerformanceCollectorService performanceCollector, FileUploadManager fileTransferManager, FileTransferOrchestrator fileTransferOrchestrator, + FileCacheManager fileCacheManager, FileCompactor fileCompactor, ApiController apiController) : base(logger, mediator, "Mare Synchronos Settings") { _configService = configService; @@ -66,11 +73,13 @@ public class SettingsUi : WindowMediatorSubscriberBase _performanceCollector = performanceCollector; _fileTransferManager = fileTransferManager; _fileTransferOrchestrator = fileTransferOrchestrator; + _fileCacheManager = fileCacheManager; _apiController = apiController; _fileCompactor = fileCompactor; _uiShared = uiShared; AllowClickthrough = false; AllowPinning = false; + _validationProgress = new Progress<(int, int, FileCacheEntity)>(v => _currentProgress = v); SizeConstraints = new WindowSizeConstraints() { @@ -431,17 +440,11 @@ public class SettingsUi : WindowMediatorSubscriberBase _configService.Current.ExportFolder = Path.GetDirectoryName(path) ?? string.Empty; _configService.Save(); - _ = Task.Run(() => + _exportTask = Task.Run(() => { - try - { - _mareCharaFileManager.SaveMareCharaFile(LastCreatedCharacterData, _exportDescription, path); - _exportDescription = string.Empty; - } - catch (Exception ex) - { - _logger.LogCritical(ex, "Error saving data"); - } + var desc = _exportDescription; + _exportDescription = string.Empty; + _mareCharaFileManager.SaveMareCharaFile(LastCreatedCharacterData, desc, path); }); }, Directory.Exists(_configService.Current.ExportFolder) ? _configService.Current.ExportFolder : null); } @@ -453,6 +456,11 @@ public class SettingsUi : WindowMediatorSubscriberBase UiSharedService.ColorTextWrapped("Export in progress", ImGuiColors.DalamudYellow); } + if (_exportTask?.IsFaulted ?? false) + { + UiSharedService.ColorTextWrapped("Export failed, check /xllog for more details.", ImGuiColors.DalamudRed); + } + ImGui.Unindent(); } bool openInGpose = _configService.Current.OpenGposeImportOnGposeStart; @@ -509,6 +517,49 @@ public class SettingsUi : WindowMediatorSubscriberBase ImGui.EndDisabled(); ImGui.TextUnformatted("The file compactor is only available on Windows."); } + ImGuiHelpers.ScaledDummy(new Vector2(10, 10)); + + ImGui.Separator(); + UiSharedService.TextWrapped("File Storage validation can make sure that all files in your local Mare Storage are valid. " + + "Run the validation before you clear the Storage for no reason. " + Environment.NewLine + + "This operation, depending on how many files you have in your storage, can take a while and will be CPU and drive intensive."); + using (ImRaii.Disabled(_validationTask != null && !_validationTask.IsCompleted)) + { + if (UiSharedService.NormalizedIconTextButton(FontAwesomeIcon.Check, "Start File Storage Validation")) + { + _validationCts?.Cancel(); + _validationCts?.Dispose(); + _validationCts = new(); + var token = _validationCts.Token; + _validationTask = Task.Run(() => _fileCacheManager.ValidateLocalIntegrity(_validationProgress, token)); + } + } + if (_validationTask != null && !_validationTask.IsCompleted) + { + ImGui.SameLine(); + if (UiSharedService.NormalizedIconTextButton(FontAwesomeIcon.Times, "Cancel")) + { + _validationCts?.Cancel(); + } + } + + if (_validationTask != null) + { + using (ImRaii.PushIndent(20f)) + { + if (_validationTask.IsCompleted) + { + UiSharedService.TextWrapped($"The storage validation has completed and removed {_validationTask.Result.Count} invalid files from storage."); + } + else + { + + UiSharedService.TextWrapped($"Storage validation is running: {_currentProgress.Item1}/{_currentProgress.Item2}"); + UiSharedService.TextWrapped($"Current item: {_currentProgress.Item3.ResolvedFilepath}"); + } + } + } + ImGui.Separator(); ImGuiHelpers.ScaledDummy(new Vector2(10, 10)); ImGui.TextUnformatted("To clear the local storage accept the following disclaimer"); @@ -549,6 +600,16 @@ public class SettingsUi : WindowMediatorSubscriberBase } _lastTab = "General"; + UiSharedService.FontText("Experimental", _uiShared.UidFont); + var usePenumbraResolve = _configService.Current.ExperimentalUsePenumbraResourceTree; + if (ImGui.Checkbox("Use Penumbra to resolve character", ref usePenumbraResolve)) + { + _configService.Current.ExperimentalUsePenumbraResourceTree = usePenumbraResolve; + _configService.Save(); + } + UiSharedService.DrawHelpText("Requires Penumbra version greater equal to 0.8.2.1 - please report issues with that feature to the Penumbra Discord"); + ImGui.Separator(); + UiSharedService.FontText("Notes", _uiShared.UidFont); if (UiSharedService.NormalizedIconTextButton(FontAwesomeIcon.StickyNote, "Export all your user notes to clipboard")) { diff --git a/MareSynchronos/WebAPI/Files/FileDownloadManager.cs b/MareSynchronos/WebAPI/Files/FileDownloadManager.cs index 936a190..de99f3f 100644 --- a/MareSynchronos/WebAPI/Files/FileDownloadManager.cs +++ b/MareSynchronos/WebAPI/Files/FileDownloadManager.cs @@ -65,7 +65,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase public async Task DownloadFiles(GameObjectHandler gameObject, List fileReplacementDto, CancellationToken ct) { - Mediator.Publish(new HaltScanMessage("Download")); + Mediator.Publish(new HaltScanMessage(nameof(DownloadFiles))); try { await DownloadFilesInternal(gameObject, fileReplacementDto, ct).ConfigureAwait(false); @@ -77,7 +77,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase finally { Mediator.Publish(new DownloadFinishedMessage(gameObject)); - Mediator.Publish(new ResumeScanMessage("Download")); + Mediator.Publish(new ResumeScanMessage(nameof(DownloadFiles))); } } diff --git a/MareSynchronos/WebAPI/Files/ThrottledStream.cs b/MareSynchronos/WebAPI/Files/ThrottledStream.cs index 0e50a68..0a083f7 100644 --- a/MareSynchronos/WebAPI/Files/ThrottledStream.cs +++ b/MareSynchronos/WebAPI/Files/ThrottledStream.cs @@ -1,6 +1,4 @@ -using Microsoft.Extensions.Logging; - -namespace MareSynchronos.WebAPI.Files +namespace MareSynchronos.WebAPI.Files { /// /// Class for streaming data with throttling support.