From 8a197e6387690210f987eab936a53b9bd426f151 Mon Sep 17 00:00:00 2001 From: rootdarkarchon Date: Thu, 7 Dec 2023 09:43:43 +0100 Subject: [PATCH] add more resilience to MCDF export and loading --- .../PlayerData/Export/MareCharaFileManager.cs | 46 +++++++++++-------- MareSynchronos/UI/GposeUi.cs | 17 +++++-- MareSynchronos/UI/SettingsUi.cs | 20 ++++---- 3 files changed, 52 insertions(+), 31 deletions(-) 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/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 714101e..106d90b 100644 --- a/MareSynchronos/UI/SettingsUi.cs +++ b/MareSynchronos/UI/SettingsUi.cs @@ -54,6 +54,7 @@ public class SettingsUi : WindowMediatorSubscriberBase private Task>? _validationTask; private CancellationTokenSource? _validationCts; private (int, int, FileCacheEntity) _currentProgress; + private Task? _exportTask; public SettingsUi(ILogger logger, UiSharedService uiShared, MareConfigService configService, @@ -439,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); } @@ -461,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;