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); + } + } + } +}