From e5dc2080e24f104dc94b851a8cbf9642ab97e5e9 Mon Sep 17 00:00:00 2001 From: Cazzar Date: Thu, 5 Oct 2023 10:08:35 +1100 Subject: [PATCH] Auto Backup/Save poses (#114) * Add ability to automatically save poses on a set interval This is disabled by default, but allows you set an interval and location for saving. Some refactors were needed to make the exporting and importing features usable from multiple locations. * Fix formatting * Add the auto save path to the custom sidebar items if it is enabled. This allows easier access to autosaves. * Format the interval with '%d s' to indicate seconds * Formatting fixes * Set default for `ClearAutoSavesOnExit` to `false`. * Refactor `PluginLog` => `Logger` * Refactor `ActorsList.GetSelectorList` => `SavedObjects` * Run `PoseAutoSave.Save` from Framework thread * Introduce mutex lock for `PoseAutoSave.Disable` --------- Co-authored-by: chirp <72366111+chirpxiv@users.noreply.github.com> --- Ktisis/Configuration.cs | 16 ++- Ktisis/Helpers/PoseHelpers.cs | 107 ++++++++++++++++++ Ktisis/Interface/Components/ActorsList.cs | 4 +- Ktisis/Interface/KtisisGui.cs | 9 ++ Ktisis/Interface/Windows/ConfigGui.cs | 55 ++++++--- .../Windows/Workspace/Tabs/PoseTab.cs | 98 +--------------- Ktisis/Interop/Hooks/PoseHooks.cs | 5 + Ktisis/Locale/i18n/English.json | 5 + Ktisis/Util/PoseAutoSave.cs | 101 +++++++++++++++++ 9 files changed, 286 insertions(+), 114 deletions(-) create mode 100644 Ktisis/Helpers/PoseHelpers.cs create mode 100644 Ktisis/Util/PoseAutoSave.cs diff --git a/Ktisis/Configuration.cs b/Ktisis/Configuration.cs index 5b59c7c35..f5c37b58b 100644 --- a/Ktisis/Configuration.cs +++ b/Ktisis/Configuration.cs @@ -2,6 +2,7 @@ using System.Linq; using System.Numerics; using System.Collections.Generic; +using System.IO; using ImGuizmoNET; @@ -65,6 +66,13 @@ public class Configuration : IPluginConfiguration { public float SkeletonLineOpacityWhileUsing { get; set; } = 0.15F; public float SkeletonDotRadius { get; set; } = 3.0F; + //AutoSave + public bool EnableAutoSave { get; set; } = false; + public int AutoSaveInterval { get; set; } = 60; + public int AutoSaveCount { get; set; } = 5; + public string AutoSavePath { get; set; } = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "Ktisis", "PoseAutoBackup"); + public bool ClearAutoSavesOnExit { get; set; } = false; + // References // The reference Key creates a uniqueness constraint for imgui window IDs for each reference. public Dictionary References { get; set; } = new(); @@ -138,15 +146,15 @@ public bool IsBoneCategoryVisible(Category category) { public bool EnableParenting { get; set; } = true; public bool LinkedGaze { get; set; } = true; - + public bool ShowToolbar { get; set; } = false; public Dictionary SavedDirPaths { get; set; } = new(); - + // Camera public float FreecamMoveSpeed { get; set; } = 0.1f; - + public float FreecamShiftMuli { get; set; } = 2.5f; public float FreecamCtrlMuli { get; set; } = 0.25f; public float FreecamUpDownMuli { get; set; } = 1f; @@ -159,7 +167,7 @@ public bool IsBoneCategoryVisible(Category category) { public Keybind FreecamRight { get; set; } = new(VirtualKey.D); public Keybind FreecamUp { get; set; } = new(VirtualKey.SPACE); public Keybind FreecamDown { get; set; } = new(VirtualKey.Q); - + public Keybind FreecamFast { get; set; } = new(VirtualKey.SHIFT); public Keybind FreecamSlow { get; set; } = new(VirtualKey.CONTROL); diff --git a/Ktisis/Helpers/PoseHelpers.cs b/Ktisis/Helpers/PoseHelpers.cs new file mode 100644 index 000000000..a058babe4 --- /dev/null +++ b/Ktisis/Helpers/PoseHelpers.cs @@ -0,0 +1,107 @@ +using System.IO; +using System.Collections.Generic; + +using Ktisis.Data.Files; +using Ktisis.Data.Serialization; +using Ktisis.Structs.Actor; +using Ktisis.Structs.Poses; +using Ktisis.Interop.Hooks; + +namespace Ktisis.Helpers { + internal class PoseHelpers { + public unsafe static void ExportPose(Actor* actor, string path, PoseMode modes) { + var model = actor->Model; + if (model == null) return; + + var skeleton = model->Skeleton; + if (skeleton == null) return; + + var pose = new PoseFile { + Position = model->Position, + Rotation = model->Rotation, + Scale = model->Scale, + Bones = new () + }; + + pose.Bones.Store(skeleton); + + if (modes.HasFlag(PoseMode.Weapons)) { + var main = actor->GetWeaponSkeleton(WeaponSlot.MainHand); + if (main != null) { + pose.MainHand = new (); + pose.MainHand.Store(main); + } + + var off = actor->GetWeaponSkeleton(WeaponSlot.OffHand); + if (off != null) { + pose.OffHand = new (); + pose.OffHand.Store(off); + } + + var prop = actor->GetWeaponSkeleton(WeaponSlot.Prop); + if (prop != null) { + pose.Prop = new (); + pose.Prop.Store(prop); + } + } + + var json = JsonParser.Serialize(pose); + + using var file = new StreamWriter(path); + file.Write(json); + } + + public unsafe static void ImportPose(Actor* actor, List path, PoseMode modes) { + var content = File.ReadAllText(path[0]); + var pose = JsonParser.Deserialize(content); + if (pose == null) return; + + if (actor->Model == null) return; + + var skeleton = actor->Model->Skeleton; + if (skeleton == null) return; + + pose.ConvertLegacyBones(); + + // Ensure posing is enabled. + if (!PoseHooks.PosingEnabled && !PoseHooks.AnamPosingEnabled) + PoseHooks.EnablePosing(); + + if (pose.Bones != null) { + for (var p = 0; p < skeleton->PartialSkeletonCount; p++) { + switch (p) { + case 0: + if (!modes.HasFlag(PoseMode.Body)) continue; + break; + case 1: + if (!modes.HasFlag(PoseMode.Face)) continue; + break; + } + + pose.Bones.ApplyToPartial(skeleton, p, Ktisis.Configuration.PoseTransforms); + } + } + + if (modes.HasFlag(PoseMode.Weapons)) { + var wepTrans = Ktisis.Configuration.PoseTransforms; + if (Ktisis.Configuration.PositionWeapons) + wepTrans |= PoseTransforms.Position; + + if (pose.MainHand != null) { + var skele = actor->GetWeaponSkeleton(WeaponSlot.MainHand); + if (skele != null) pose.MainHand.Apply(skele, wepTrans); + } + + if (pose.OffHand != null) { + var skele = actor->GetWeaponSkeleton(WeaponSlot.OffHand); + if (skele != null) pose.OffHand.Apply(skele, wepTrans); + } + + if (pose.Prop != null) { + var skele = actor->GetWeaponSkeleton(WeaponSlot.Prop); + if (skele != null) pose.Prop.Apply(skele, wepTrans); + } + } + } + } +} diff --git a/Ktisis/Interface/Components/ActorsList.cs b/Ktisis/Interface/Components/ActorsList.cs index 72bdc675b..4a0473ff0 100644 --- a/Ktisis/Interface/Components/ActorsList.cs +++ b/Ktisis/Interface/Components/ActorsList.cs @@ -14,8 +14,7 @@ namespace Ktisis.Interface.Components { internal static class ActorsList { - - private static List SavedObjects = new(); + internal static List SavedObjects = new(); private static List? SelectorList = null; private static string Search = ""; private static readonly HashSet WhitelistObjectKinds = new(){ @@ -29,7 +28,6 @@ internal static class ActorsList { // TODO to clear the list on gpose leave public static void Clear() => SavedObjects.Clear(); - // Draw public unsafe static void Draw() { diff --git a/Ktisis/Interface/KtisisGui.cs b/Ktisis/Interface/KtisisGui.cs index 1293cb6fc..a47dc8b42 100644 --- a/Ktisis/Interface/KtisisGui.cs +++ b/Ktisis/Interface/KtisisGui.cs @@ -19,6 +19,15 @@ static KtisisGui() { FontAwesomeIcon.None, 0 )); + + if (Ktisis.Configuration.EnableAutoSave) { + FileDialogManager.CustomSideBarItems.Add(( + "AutoSave", + Ktisis.Configuration.AutoSavePath, + FontAwesomeIcon.Bookmark, + -1 + )); + } } public static void Draw() { diff --git a/Ktisis/Interface/Windows/ConfigGui.cs b/Ktisis/Interface/Windows/ConfigGui.cs index 1ee68ea59..044f117f5 100644 --- a/Ktisis/Interface/Windows/ConfigGui.cs +++ b/Ktisis/Interface/Windows/ConfigGui.cs @@ -1,8 +1,8 @@ using System; -using System.Collections.Generic; using System.Linq; -using System.Numerics; using System.Text; +using System.Numerics; +using System.Collections.Generic; using ImGuiNET; @@ -75,6 +75,8 @@ public static void Draw() { DrawInputTab(cfg); if (ImGui.BeginTabItem(Locale.GetString("Camera"))) DrawCameraTab(cfg); + if (ImGui.BeginTabItem(Locale.GetString("AutoSave"))) + DrawAutoSaveTab(cfg); if (ImGui.BeginTabItem(Locale.GetString("References"))) DrawReferencesTab(cfg); if (ImGui.BeginTabItem(Locale.GetString("Language"))) @@ -150,7 +152,7 @@ public static void DrawInterfaceTab(Configuration cfg) { var displayMultiplierInputs = cfg.TransformTableDisplayMultiplierInputs; if (ImGui.Checkbox(Locale.GetString("Show_speed_multipler_inputs"), ref displayMultiplierInputs)) cfg.TransformTableDisplayMultiplierInputs = displayMultiplierInputs; - + var showToolbar = cfg.ShowToolbar; if (ImGui.Checkbox("Show Experimental Toolbar", ref showToolbar)) cfg.ShowToolbar = showToolbar; @@ -174,13 +176,13 @@ public static void DrawInterfaceTab(Configuration cfg) { public static void DrawOverlayTab(Configuration cfg) { ImGui.Spacing(); - + var order = cfg.OrderBoneListByDistance; if (ImGui.Checkbox("Order bone list by distance from camera", ref order)) cfg.OrderBoneListByDistance = order; ImGui.Spacing(); - + if (ImGui.CollapsingHeader(Locale.GetString("Skeleton_lines_and_dots"), ImGuiTreeNodeFlags.DefaultOpen)) { ImGui.Separator(); var drawLines = cfg.DrawLinesOnSkeleton; @@ -202,11 +204,11 @@ public static void DrawOverlayTab(Configuration cfg) { var lineThickness = cfg.SkeletonLineThickness; if (ImGui.SliderFloat(Locale.GetString("Lines_thickness"), ref lineThickness, 0.01F, 15F, "%.1f")) cfg.SkeletonLineThickness = lineThickness; - + var lineOpacity = cfg.SkeletonLineOpacity; if (ImGui.SliderFloat(Locale.GetString("Lines_opacity"), ref lineOpacity, 0.01F, 1F, "%.2f")) cfg.SkeletonLineOpacity = lineOpacity; - + var lineOpacityWhileUsing = cfg.SkeletonLineOpacityWhileUsing; if (ImGui.SliderFloat(Locale.GetString("Lines_opacity_while_using"), ref lineOpacityWhileUsing, 0.01F, 1F, "%.2f")) cfg.SkeletonLineOpacityWhileUsing = lineOpacityWhileUsing; @@ -270,6 +272,33 @@ public static void DrawGizmoTab(Configuration cfg) { ImGui.EndTabItem(); } + // AutoSave + public static void DrawAutoSaveTab(Configuration cfg) { + var enableAutoSave = cfg.EnableAutoSave; + if (ImGui.Checkbox(Locale.GetString("Enable_auto_save"), ref enableAutoSave)) + cfg.EnableAutoSave = enableAutoSave; + + var clearOnExit = cfg.ClearAutoSavesOnExit; + if (ImGui.Checkbox(Locale.GetString("Clear_auto_saves_on_exit"), ref clearOnExit)) + cfg.ClearAutoSavesOnExit = clearOnExit; + + ImGui.Spacing(); + + var autoSaveInterval = cfg.AutoSaveInterval; + if (ImGui.SliderInt(Locale.GetString("Auto_save_interval"), ref autoSaveInterval, 10, 600, "%d s")) + cfg.AutoSaveInterval = autoSaveInterval; + + var autoSaveCount = cfg.AutoSaveCount; + if (ImGui.SliderInt(Locale.GetString("Auto_save_count"), ref autoSaveCount, 1, 20)) + cfg.AutoSaveCount = autoSaveCount; + + var autoSavePath = cfg.AutoSavePath; + if (ImGui.InputText(Locale.GetString("Auto_save_path"), ref autoSavePath, 256)) + cfg.AutoSavePath = autoSavePath; + + ImGui.EndTabItem(); + } + // Language public static void DrawLanguageTab(Configuration cfg) { @@ -421,11 +450,11 @@ private static void DrawCameraTab(Configuration cfg) { var shiftMuli = cfg.FreecamShiftMuli; if (ImGui.DragFloat("Fast speed multiplier", ref shiftMuli, 0.001f, 0, 10)) cfg.FreecamShiftMuli = shiftMuli; - + var ctrlMuli = cfg.FreecamCtrlMuli; if (ImGui.DragFloat("Slow speed multiplier", ref ctrlMuli, 0.001f, 0, 10)) cfg.FreecamCtrlMuli = ctrlMuli; - + var upDownMuli = cfg.FreecamUpDownMuli; if (ImGui.DragFloat("Up/down speed multiplier", ref upDownMuli, 0.001f, 0, 10)) cfg.FreecamUpDownMuli = upDownMuli; @@ -437,7 +466,7 @@ private static void DrawCameraTab(Configuration cfg) { cfg.FreecamSensitivity = camSens; ImGui.Spacing(); - + ImGui.PushItemWidth(ImGui.GetFontSize() * 8); ImGui.Text("Work camera keybinds"); @@ -450,17 +479,17 @@ private static void DrawCameraTab(Configuration cfg) { KeybindEdit.Draw("Down##WCDown", cfg.FreecamDown); ImGui.Spacing(); - + KeybindEdit.Draw("Fast speed modifier##WCUp", cfg.FreecamFast); KeybindEdit.Draw("Slow speed modifier##WCUp", cfg.FreecamSlow); ImGui.PopItemWidth(); - + ImGui.EndTabItem(); } // Data - + public static void DrawDataTab(Configuration cfg) { ImGui.Spacing(); var validGlamPlatesFound = GlamourDresser.CountValid(); diff --git a/Ktisis/Interface/Windows/Workspace/Tabs/PoseTab.cs b/Ktisis/Interface/Windows/Workspace/Tabs/PoseTab.cs index 75e37a0f4..d0788004d 100644 --- a/Ktisis/Interface/Windows/Workspace/Tabs/PoseTab.cs +++ b/Ktisis/Interface/Windows/Workspace/Tabs/PoseTab.cs @@ -1,5 +1,3 @@ -using System.IO; - using ImGuiNET; using Dalamud.Interface; @@ -7,12 +5,10 @@ using Ktisis.Util; using Ktisis.Overlay; +using Ktisis.Helpers; using Ktisis.Structs.Actor; using Ktisis.Structs.Poses; -using Ktisis.Data.Files; -using Ktisis.Data.Serialization; using Ktisis.Interface.Components; -using Ktisis.Interop.Hooks; namespace Ktisis.Interface.Windows.Workspace.Tabs { public static class PoseTab { @@ -180,56 +176,7 @@ public unsafe static void ImportExportPose(Actor* actor) { (success, path) => { if (!success) return; - var content = File.ReadAllText(path[0]); - var pose = JsonParser.Deserialize(content); - if (pose == null) return; - - if (actor->Model == null) return; - - var skeleton = actor->Model->Skeleton; - if (skeleton == null) return; - - pose.ConvertLegacyBones(); - - // Ensure posing is enabled. - if (!PoseHooks.PosingEnabled && !PoseHooks.AnamPosingEnabled) - PoseHooks.EnablePosing(); - - if (pose.Bones != null) { - for (var p = 0; p < skeleton->PartialSkeletonCount; p++) { - switch (p) { - case 0: - if (!body) continue; - break; - case 1: - if (!face) continue; - break; - } - - pose.Bones.ApplyToPartial(skeleton, p, trans); - } - } - - if (modes.HasFlag(PoseMode.Weapons)) { - var wepTrans = trans; - if (Ktisis.Configuration.PositionWeapons) - wepTrans |= PoseTransforms.Position; - - if (pose.MainHand != null) { - var skele = actor->GetWeaponSkeleton(WeaponSlot.MainHand); - if (skele != null) pose.MainHand.Apply(skele, wepTrans); - } - - if (pose.OffHand != null) { - var skele = actor->GetWeaponSkeleton(WeaponSlot.OffHand); - if (skele != null) pose.OffHand.Apply(skele, wepTrans); - } - - if (pose.Prop != null) { - var skele = actor->GetWeaponSkeleton(WeaponSlot.Prop); - if (skele != null) pose.Prop.Apply(skele, wepTrans); - } - } + PoseHelpers.ImportPose(actor, path, Ktisis.Configuration.PoseMode); }, 1, null @@ -246,44 +193,7 @@ public unsafe static void ImportExportPose(Actor* actor) { (success, path) => { if (!success) return; - var model = actor->Model; - if (model == null) return; - - var skeleton = model->Skeleton; - if (skeleton == null) return; - - var pose = new PoseFile(); - - pose.Position = model->Position; - pose.Rotation = model->Rotation; - pose.Scale = model->Scale; - - pose.Bones = new PoseContainer(); - pose.Bones.Store(skeleton); - - if (modes.HasFlag(PoseMode.Weapons)) { - var main = actor->GetWeaponSkeleton(WeaponSlot.MainHand); - if (main != null) { - pose.MainHand = new PoseContainer(); - pose.MainHand.Store(main); - } - - var off = actor->GetWeaponSkeleton(WeaponSlot.OffHand); - if (off != null) { - pose.OffHand = new PoseContainer(); - pose.OffHand.Store(off); - } - - var prop = actor->GetWeaponSkeleton(WeaponSlot.Prop); - if (prop != null) { - pose.Prop = new PoseContainer(); - pose.Prop.Store(prop); - } - } - - var json = JsonParser.Serialize(pose); - using (var file = new StreamWriter(path)) - file.Write(json); + PoseHelpers.ExportPose(actor, path, Ktisis.Configuration.PoseMode); } ); } @@ -291,4 +201,4 @@ public unsafe static void ImportExportPose(Actor* actor) { ImGui.Spacing(); } } -} \ No newline at end of file +} diff --git a/Ktisis/Interop/Hooks/PoseHooks.cs b/Ktisis/Interop/Hooks/PoseHooks.cs index 00fd0751e..f7b0e4ade 100644 --- a/Ktisis/Interop/Hooks/PoseHooks.cs +++ b/Ktisis/Interop/Hooks/PoseHooks.cs @@ -10,6 +10,7 @@ using Ktisis.Structs; using Ktisis.Structs.Actor; using Ktisis.Structs.Poses; +using Ktisis.Util; namespace Ktisis.Interop.Hooks { public static class PoseHooks { @@ -42,6 +43,8 @@ public static class PoseHooks { internal static Dictionary PreservedPoses = new(); + internal static PoseAutoSave AutoSave = new(); + internal static unsafe void Init() { var setBoneModelSpaceFfxiv = Services.SigScanner.ScanText("48 8B C4 48 89 58 18 55 56 57 41 54 41 55 41 56 41 57 48 81 EC ?? ?? ?? ?? 0F 29 70 B8 0F 29 78 A8 44 0F 29 40 ?? 44 0F 29 48 ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 84 24 ?? ?? ?? ?? 48 8B B1"); SetBoneModelSpaceFfxivHook = Services.Hooking.HookFromAddress(setBoneModelSpaceFfxiv, SetBoneModelSpaceFfxivDetour); @@ -78,6 +81,7 @@ internal static void DisablePosing() { UpdatePosHook?.Disable(); AnimFrozenHook?.Disable(); BustHook?.Disable(); + AutoSave?.Disable(); PosingEnabled = false; } @@ -89,6 +93,7 @@ internal static void EnablePosing() { UpdatePosHook?.Enable(); AnimFrozenHook?.Enable(); BustHook?.Enable(); + AutoSave?.Enable(); PosingEnabled = true; } diff --git a/Ktisis/Locale/i18n/English.json b/Ktisis/Locale/i18n/English.json index a3e7af750..c6b3031f2 100644 --- a/Ktisis/Locale/i18n/English.json +++ b/Ktisis/Locale/i18n/English.json @@ -62,6 +62,11 @@ "Keyboard_shortcuts": "Keyboard Shortcuts", "Pressing_keys": "Pressing Keys", "Edit_bone_positions": "Edit bone positions", + "Enable_auto_save": "Enable auto save", + "Auto_save_interval": "Auto Save Interval", + "Auto_save_count": "Auto Save Count", + "Auto_save_path": "Auto Save Path", + "Clear_auto_saves_on_exit": "Clear auto saves on exit", // Display keyboard configs from enum defined in Input.cs "Input_Generic_Toggle": "Toggle", "Input_Generic_Hold": "Hold", diff --git a/Ktisis/Util/PoseAutoSave.cs b/Ktisis/Util/PoseAutoSave.cs new file mode 100644 index 000000000..52e103533 --- /dev/null +++ b/Ktisis/Util/PoseAutoSave.cs @@ -0,0 +1,101 @@ +using System; +using System.IO; +using System.Timers; +using System.Collections.Generic; + +using Ktisis.Helpers; +using Ktisis.Structs.Actor; +using Ktisis.Structs.Poses; +using Ktisis.Interface.Components; + +namespace Ktisis.Util { + internal class PoseAutoSave { + private Queue prefixes = new(); + private Timer? _timer = null; + private string SaveFolder => Ktisis.Configuration.AutoSavePath; + + public void Enable() { + if (!Ktisis.Configuration.EnableAutoSave) + return; + + if (!Directory.Exists(SaveFolder)) + Directory.CreateDirectory(SaveFolder); + + _timer = new Timer(TimeSpan.FromSeconds(Ktisis.Configuration.AutoSaveInterval)); + _timer.Elapsed += OnElapsed; + _timer.AutoReset = true; + + _timer.Start(); + prefixes.Clear(); + } + + public void Disable() { + lock (this) { + _timer?.Stop(); + + if (!Ktisis.Configuration.ClearAutoSavesOnExit || Ktisis.Configuration.AutoSaveCount <= 0) + return; + + while (prefixes.Count > 0) { + DeleteOldest(); + } + } + } + + private void OnElapsed(object? sender, ElapsedEventArgs e) { + Services.Framework.RunOnFrameworkThread(Save); + } + + private void Save() { + if (!Ktisis.IsInGPose) { + Disable(); + return; + } + + var actors = ActorsList.SavedObjects; + + Logger.Information($"Saving {actors.Count} actors"); + + var prefix = $"AutoSave - {DateTime.Now:HH-mm-ss}"; + var folder = Path.Combine(SaveFolder, prefix); + prefixes.Enqueue(prefix); + + if (!Directory.Exists(folder)) + Directory.CreateDirectory(folder); + + unsafe { + foreach (var actorPtr in actors) { + var actor = (Actor*)actorPtr; + var filename = $"{actor->GetNameOrId()}.pose"; + Logger.Information($"Saving {filename}"); + + var path = Path.Combine(folder, filename); + Logger.Verbose($"Saving to {path}"); + + PoseHelpers.ExportPose(actor, path, PoseMode.All); + } + } + + Logger.Verbose($"Prefix count: {prefixes.Count} max: {Ktisis.Configuration.AutoSaveCount}"); + + //Clear old saves + while (prefixes.Count > Ktisis.Configuration.AutoSaveCount) { + DeleteOldest(); + } + + //Dynamically update the interval. + + if (_timer != null && Math.Abs(_timer.Interval - TimeSpan.FromSeconds(Ktisis.Configuration.AutoSaveInterval).TotalMilliseconds) > 0.01) + _timer.Interval = TimeSpan.FromSeconds(Ktisis.Configuration.AutoSaveInterval).TotalMilliseconds; + } + + private void DeleteOldest() { + var oldest = prefixes.Dequeue(); + var folder = Path.Combine(SaveFolder, oldest); + if (Directory.Exists(folder)) { + Logger.Verbose($"Deleting {folder}"); + Directory.Delete(folder, true); + } + } + } +}