diff --git a/Glamourer.Api b/Glamourer.Api index 663702b..4bad56d 160000 --- a/Glamourer.Api +++ b/Glamourer.Api @@ -1 +1 @@ -Subproject commit 663702b57303368b3a897e9c6eae4b34b4339534 +Subproject commit 4bad56d610132b419335b89896e1f387b9ba2039 diff --git a/Glamourer/Api/StateApi.cs b/Glamourer/Api/StateApi.cs index 203f1b0..385cf3e 100644 --- a/Glamourer/Api/StateApi.cs +++ b/Glamourer/Api/StateApi.cs @@ -2,6 +2,7 @@ using Glamourer.Api.Enums; using Glamourer.Automation; using Glamourer.Designs; +using Glamourer.Designs.History; using Glamourer.Events; using Glamourer.Interop.Structs; using Glamourer.State; @@ -41,13 +42,13 @@ public sealed class StateApi : IGlamourerApiState, IApiService, IDisposable _objects = objects; _stateChanged = stateChanged; _gPose = gPose; - _stateChanged.Subscribe(OnStateChange, Events.StateChanged.Priority.GlamourerIpc); + _stateChanged.Subscribe(OnStateChanged, Events.StateChanged.Priority.GlamourerIpc); _gPose.Subscribe(OnGPoseChange, GPoseService.Priority.GlamourerIpc); } public void Dispose() { - _stateChanged.Unsubscribe(OnStateChange); + _stateChanged.Unsubscribe(OnStateChanged); _gPose.Unsubscribe(OnGPoseChange); } @@ -330,7 +331,7 @@ public sealed class StateApi : IGlamourerApiState, IApiService, IDisposable private void OnGPoseChange(bool gPose) => GPoseChanged?.Invoke(gPose); - private void OnStateChange(StateChangeType type, StateSource _2, ActorState _3, ActorData actors, object? _5) + private void OnStateChanged(StateChangeType type, StateSource _2, ActorState _3, ActorData actors, ITransaction? _5) { if (StateChanged != null) foreach (var actor in actors.Objects) diff --git a/Glamourer/Automation/AutoDesignManager.cs b/Glamourer/Automation/AutoDesignManager.cs index da9b014..474afdd 100644 --- a/Glamourer/Automation/AutoDesignManager.cs +++ b/Glamourer/Automation/AutoDesignManager.cs @@ -1,6 +1,7 @@ using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Interface.ImGuiNotification; using Glamourer.Designs; +using Glamourer.Designs.History; using Glamourer.Designs.Special; using Glamourer.Events; using Glamourer.Interop; @@ -620,7 +621,7 @@ public class AutoDesignManager : ISavable, IReadOnlyList, IDispos } } - private void OnDesignChange(DesignChanged.Type type, Design design, object? data) + private void OnDesignChange(DesignChanged.Type type, Design design, ITransaction? _) { if (type is not DesignChanged.Type.Deleted) return; diff --git a/Glamourer/Designs/DesignEditor.cs b/Glamourer/Designs/DesignEditor.cs index f68424c..df0e9ed 100644 --- a/Glamourer/Designs/DesignEditor.cs +++ b/Glamourer/Designs/DesignEditor.cs @@ -1,3 +1,4 @@ +using Glamourer.Designs.History; using Glamourer.Designs.Links; using Glamourer.Events; using Glamourer.GameData; @@ -72,7 +73,7 @@ public class DesignEditor( design.LastEdit = DateTimeOffset.UtcNow; Glamourer.Log.Debug($"Changed customize {idx.ToDefaultName()} in design {design.Identifier} from {oldValue.Value} to {value.Value}."); SaveService.QueueSave(design); - DesignChanged.Invoke(DesignChanged.Type.Customize, design, (oldValue, value, idx)); + DesignChanged.Invoke(DesignChanged.Type.Customize, design, new CustomizeTransaction(idx, oldValue, value)); } /// @@ -88,7 +89,7 @@ public class DesignEditor( design.LastEdit = DateTimeOffset.UtcNow; Glamourer.Log.Debug($"Changed entire customize with resulting flags {applied} and {changed}."); SaveService.QueueSave(design); - DesignChanged.Invoke(DesignChanged.Type.EntireCustomize, design, (oldCustomize, applied, changed)); + DesignChanged.Invoke(DesignChanged.Type.EntireCustomize, design, new EntireCustomizeTransaction(changed, oldCustomize, newCustomize)); } /// @@ -103,7 +104,7 @@ public class DesignEditor( design.LastEdit = DateTimeOffset.UtcNow; Glamourer.Log.Debug($"Set customize parameter {flag} in design {design.Identifier} from {old} to {@new}."); SaveService.QueueSave(design); - DesignChanged.Invoke(DesignChanged.Type.Parameter, design, (old, @new, flag)); + DesignChanged.Invoke(DesignChanged.Type.Parameter, design, new ParameterTransaction(flag, old, @new)); } /// @@ -122,11 +123,14 @@ public class DesignEditor( if (!ChangeMainhandPeriphery(design, currentMain, currentOff, item, out var newOff, out var newGauntlets)) return; + var currentGauntlets = design.DesignData.Item(EquipSlot.Hands); design.LastEdit = DateTimeOffset.UtcNow; SaveService.QueueSave(design); Glamourer.Log.Debug( $"Set {EquipSlot.MainHand.ToName()} weapon in design {design.Identifier} from {currentMain.Name} ({currentMain.ItemId}) to {item.Name} ({item.ItemId})."); - DesignChanged.Invoke(DesignChanged.Type.Weapon, design, (currentMain, currentOff, item, newOff, newGauntlets)); + DesignChanged.Invoke(DesignChanged.Type.Weapon, design, + new WeaponTransaction(currentMain, currentOff, currentGauntlets, item, newOff ?? currentOff, + newGauntlets ?? currentGauntlets)); return; } case EquipSlot.OffHand: @@ -139,11 +143,13 @@ public class DesignEditor( if (!design.GetDesignDataRef().SetItem(EquipSlot.OffHand, item)) return; + var currentGauntlets = design.DesignData.Item(EquipSlot.Hands); design.LastEdit = DateTimeOffset.UtcNow; SaveService.QueueSave(design); Glamourer.Log.Debug( $"Set {EquipSlot.OffHand.ToName()} weapon in design {design.Identifier} from {currentOff.Name} ({currentOff.ItemId}) to {item.Name} ({item.ItemId})."); - DesignChanged.Invoke(DesignChanged.Type.Weapon, design, (currentMain, currentOff, currentMain, item)); + DesignChanged.Invoke(DesignChanged.Type.Weapon, design, + new WeaponTransaction(currentMain, currentOff, currentGauntlets, currentMain, item, currentGauntlets)); return; } default: @@ -159,7 +165,7 @@ public class DesignEditor( Glamourer.Log.Debug( $"Set {slot.ToName()} equipment piece in design {design.Identifier} from {old.Name} ({old.ItemId}) to {item.Name} ({item.ItemId})."); SaveService.QueueSave(design); - DesignChanged.Invoke(DesignChanged.Type.Equip, design, (old, item, slot)); + DesignChanged.Invoke(DesignChanged.Type.Equip, design, new EquipTransaction(slot, old, item)); return; } } @@ -179,7 +185,7 @@ public class DesignEditor( design.LastEdit = DateTimeOffset.UtcNow; SaveService.QueueSave(design); Glamourer.Log.Debug($"Set {slot} bonus item to {item}."); - DesignChanged.Invoke(DesignChanged.Type.BonusItem, design, (oldItem, item, slot)); + DesignChanged.Invoke(DesignChanged.Type.BonusItem, design, new BonusItemTransaction(slot, oldItem, item)); } /// @@ -196,7 +202,7 @@ public class DesignEditor( design.LastEdit = DateTimeOffset.UtcNow; SaveService.QueueSave(design); Glamourer.Log.Debug($"Set stain of {slot} equipment piece to {stains}."); - DesignChanged.Invoke(DesignChanged.Type.Stains, design, (oldStain, stains, slot)); + DesignChanged.Invoke(DesignChanged.Type.Stains, design, new StainTransaction(slot, oldStain, stains)); } /// @@ -219,7 +225,7 @@ public class DesignEditor( design.LastEdit = DateTimeOffset.UtcNow; SaveService.QueueSave(design); Glamourer.Log.Debug($"Set crest visibility of {slot} equipment piece to {crest}."); - DesignChanged.Invoke(DesignChanged.Type.Crest, design, (oldCrest, crest, slot)); + DesignChanged.Invoke(DesignChanged.Type.Crest, design, new CrestTransaction(slot, oldCrest, crest)); } /// @@ -232,7 +238,7 @@ public class DesignEditor( design.LastEdit = DateTimeOffset.UtcNow; SaveService.QueueSave(design); Glamourer.Log.Debug($"Set value of {metaIndex} to {value}."); - DesignChanged.Invoke(DesignChanged.Type.Other, design, (metaIndex, false, value)); + DesignChanged.Invoke(DesignChanged.Type.Other, design, new MetaTransaction(metaIndex, !value, value)); } public void ChangeMaterialRevert(Design design, MaterialValueIndex index, bool revert) @@ -245,7 +251,7 @@ public class DesignEditor( Glamourer.Log.Debug($"Changed advanced dye value for {index} to {(revert ? "Revert." : "no longer Revert.")}"); design.LastEdit = DateTimeOffset.UtcNow; SaveService.QueueSave(design); - DesignChanged.Invoke(DesignChanged.Type.MaterialRevert, design, index); + DesignChanged.Invoke(DesignChanged.Type.MaterialRevert, design, new MaterialRevertTransaction(index, !revert, revert)); } public void ChangeMaterialValue(Design design, MaterialValueIndex index, ColorRow? row) @@ -280,7 +286,7 @@ public class DesignEditor( design.LastEdit = DateTimeOffset.UtcNow; SaveService.DelaySave(design); - DesignChanged.Invoke(DesignChanged.Type.Material, design, (oldValue, row, index)); + DesignChanged.Invoke(DesignChanged.Type.Material, design, new MaterialTransaction(index, oldValue.Value, row)); } public void ChangeApplyMaterialValue(Design design, MaterialValueIndex index, bool value) @@ -293,7 +299,7 @@ public class DesignEditor( Glamourer.Log.Debug($"Changed application of advanced dye for {index} to {value}."); design.LastEdit = DateTimeOffset.UtcNow; SaveService.QueueSave(design); - DesignChanged.Invoke(DesignChanged.Type.ApplyMaterial, design, index); + DesignChanged.Invoke(DesignChanged.Type.ApplyMaterial, design, new ApplicationTransaction(index, !value, value)); } diff --git a/Glamourer/Designs/DesignFileSystem.cs b/Glamourer/Designs/DesignFileSystem.cs index f154684..e985e32 100644 --- a/Glamourer/Designs/DesignFileSystem.cs +++ b/Glamourer/Designs/DesignFileSystem.cs @@ -1,4 +1,5 @@ using Dalamud.Interface.ImGuiNotification; +using Glamourer.Designs.History; using Glamourer.Events; using Glamourer.Services; using Newtonsoft.Json; @@ -92,13 +93,13 @@ public sealed class DesignFileSystem : FileSystem, IDisposable, ISavable _saveService.QueueSave(this); } - private void OnDesignChange(DesignChanged.Type type, Design design, object? data) + private void OnDesignChange(DesignChanged.Type type, Design design, ITransaction? data) { switch (type) { case DesignChanged.Type.Created: var parent = Root; - if (data is string path) + if ((data as CreationTransaction?)?.Path is { } path) try { parent = FindOrCreateAllFolders(path); @@ -119,7 +120,7 @@ public sealed class DesignFileSystem : FileSystem, IDisposable, ISavable case DesignChanged.Type.ReloadedAll: Reload(); return; - case DesignChanged.Type.Renamed when data is string oldName: + case DesignChanged.Type.Renamed when (data as RenameTransaction?)?.Old is { } oldName: if (!FindLeaf(design, out var leaf2)) return; diff --git a/Glamourer/Designs/DesignManager.cs b/Glamourer/Designs/DesignManager.cs index 97058a9..7eaad9d 100644 --- a/Glamourer/Designs/DesignManager.cs +++ b/Glamourer/Designs/DesignManager.cs @@ -1,4 +1,5 @@ using Dalamud.Utility; +using Glamourer.Designs.History; using Glamourer.Designs.Links; using Glamourer.Events; using Glamourer.GameData; @@ -107,7 +108,7 @@ public sealed class DesignManager : DesignEditor Designs.Add(design); Glamourer.Log.Debug($"Added new design {design.Identifier}."); SaveService.ImmediateSave(design); - DesignChanged.Invoke(DesignChanged.Type.Created, design, path); + DesignChanged.Invoke(DesignChanged.Type.Created, design, new CreationTransaction(actualName, path)); return design; } @@ -127,7 +128,7 @@ public sealed class DesignManager : DesignEditor Designs.Add(design); Glamourer.Log.Debug($"Added new design {design.Identifier} by cloning Temporary Design."); SaveService.ImmediateSave(design); - DesignChanged.Invoke(DesignChanged.Type.Created, design, path); + DesignChanged.Invoke(DesignChanged.Type.Created, design, new CreationTransaction(actualName, path)); return design; } @@ -147,7 +148,7 @@ public sealed class DesignManager : DesignEditor Glamourer.Log.Debug( $"Added new design {design.Identifier} by cloning {clone.Identifier.ToString()}."); SaveService.ImmediateSave(design); - DesignChanged.Invoke(DesignChanged.Type.Created, design, path); + DesignChanged.Invoke(DesignChanged.Type.Created, design, new CreationTransaction(actualName, path)); return design; } @@ -176,7 +177,7 @@ public sealed class DesignManager : DesignEditor design.LastEdit = DateTimeOffset.UtcNow; SaveService.QueueSave(design); Glamourer.Log.Debug($"Renamed design {design.Identifier}."); - DesignChanged.Invoke(DesignChanged.Type.Renamed, design, oldName); + DesignChanged.Invoke(DesignChanged.Type.Renamed, design, new RenameTransaction(oldName, newName)); } /// Change the description of a design. @@ -190,7 +191,7 @@ public sealed class DesignManager : DesignEditor design.LastEdit = DateTimeOffset.UtcNow; SaveService.QueueSave(design); Glamourer.Log.Debug($"Changed description of design {design.Identifier}."); - DesignChanged.Invoke(DesignChanged.Type.ChangedDescription, design, oldDescription); + DesignChanged.Invoke(DesignChanged.Type.ChangedDescription, design, new DescriptionTransaction(oldDescription, description)); } /// Change the associated color of a design. @@ -204,7 +205,7 @@ public sealed class DesignManager : DesignEditor design.LastEdit = DateTimeOffset.UtcNow; SaveService.QueueSave(design); Glamourer.Log.Debug($"Changed color of design {design.Identifier}."); - DesignChanged.Invoke(DesignChanged.Type.ChangedColor, design, oldColor); + DesignChanged.Invoke(DesignChanged.Type.ChangedColor, design, new DesignColorTransaction(oldColor, newColor)); } /// Add a new tag to a design. The tags remain sorted. @@ -218,7 +219,7 @@ public sealed class DesignManager : DesignEditor var idx = design.Tags.IndexOf(tag); SaveService.QueueSave(design); Glamourer.Log.Debug($"Added tag {tag} at {idx} to design {design.Identifier}."); - DesignChanged.Invoke(DesignChanged.Type.AddedTag, design, (tag, idx)); + DesignChanged.Invoke(DesignChanged.Type.AddedTag, design, new TagAddedTransaction(tag, idx)); } /// Remove a tag from a design by its index. @@ -232,7 +233,7 @@ public sealed class DesignManager : DesignEditor design.LastEdit = DateTimeOffset.UtcNow; SaveService.QueueSave(design); Glamourer.Log.Debug($"Removed tag {oldTag} at {tagIdx} from design {design.Identifier}."); - DesignChanged.Invoke(DesignChanged.Type.RemovedTag, design, (oldTag, tagIdx)); + DesignChanged.Invoke(DesignChanged.Type.RemovedTag, design, new TagRemovedTransaction(oldTag, tagIdx)); } /// Rename a tag from a design by its index. The tags stay sorted. @@ -247,7 +248,7 @@ public sealed class DesignManager : DesignEditor design.LastEdit = DateTimeOffset.UtcNow; SaveService.QueueSave(design); Glamourer.Log.Debug($"Renamed tag {oldTag} at {tagIdx} to {newTag} in design {design.Identifier} and reordered tags."); - DesignChanged.Invoke(DesignChanged.Type.ChangedTag, design, (oldTag, newTag, tagIdx)); + DesignChanged.Invoke(DesignChanged.Type.ChangedTag, design, new TagChangedTransaction(oldTag, newTag, tagIdx, design.Tags.IndexOf(newTag))); } /// Add an associated mod to a design. @@ -259,7 +260,7 @@ public sealed class DesignManager : DesignEditor design.LastEdit = DateTimeOffset.UtcNow; SaveService.QueueSave(design); Glamourer.Log.Debug($"Added associated mod {mod.DirectoryName} to design {design.Identifier}."); - DesignChanged.Invoke(DesignChanged.Type.AddedMod, design, (mod, settings)); + DesignChanged.Invoke(DesignChanged.Type.AddedMod, design, new ModAddedTransaction(mod, settings)); } /// Remove an associated mod from a design. @@ -271,17 +272,18 @@ public sealed class DesignManager : DesignEditor design.LastEdit = DateTimeOffset.UtcNow; SaveService.QueueSave(design); Glamourer.Log.Debug($"Removed associated mod {mod.DirectoryName} from design {design.Identifier}."); - DesignChanged.Invoke(DesignChanged.Type.RemovedMod, design, (mod, settings)); + DesignChanged.Invoke(DesignChanged.Type.RemovedMod, design, new ModRemovedTransaction(mod, settings)); } /// Add or update an associated mod to a design. public void UpdateMod(Design design, Mod mod, ModSettings settings) { + var oldSettings = design.AssociatedMods[mod]; design.AssociatedMods[mod] = settings; design.LastEdit = DateTimeOffset.UtcNow; SaveService.QueueSave(design); Glamourer.Log.Debug($"Updated (or added) associated mod {mod.DirectoryName} from design {design.Identifier}."); - DesignChanged.Invoke(DesignChanged.Type.AddedMod, design, (mod, settings)); + DesignChanged.Invoke(DesignChanged.Type.UpdatedMod, design, new ModUpdatedTransaction(mod, oldSettings, settings)); } /// Set the write protection status of a design. @@ -292,7 +294,7 @@ public sealed class DesignManager : DesignEditor SaveService.QueueSave(design); Glamourer.Log.Debug($"Set design {design.Identifier} to {(value ? "no longer be " : string.Empty)} write-protected."); - DesignChanged.Invoke(DesignChanged.Type.WriteProtection, design, value); + DesignChanged.Invoke(DesignChanged.Type.WriteProtection, design, null); } /// Set the quick design bar display status of a design. @@ -305,7 +307,7 @@ public sealed class DesignManager : DesignEditor SaveService.QueueSave(design); Glamourer.Log.Debug( $"Set design {design.Identifier} to {(!value ? "no longer be " : string.Empty)} displayed in the quick design bar."); - DesignChanged.Invoke(DesignChanged.Type.QuickDesignBar, design, value); + DesignChanged.Invoke(DesignChanged.Type.QuickDesignBar, design, null); } #endregion @@ -332,7 +334,7 @@ public sealed class DesignManager : DesignEditor design.LastEdit = DateTimeOffset.UtcNow; SaveService.QueueSave(design); Glamourer.Log.Debug($"Set applying of customization {idx.ToDefaultName()} to {value}."); - DesignChanged.Invoke(DesignChanged.Type.ApplyCustomize, design, idx); + DesignChanged.Invoke(DesignChanged.Type.ApplyCustomize, design, new ApplicationTransaction(idx, !value, value)); } /// Change whether to apply a specific equipment piece. @@ -344,7 +346,7 @@ public sealed class DesignManager : DesignEditor design.LastEdit = DateTimeOffset.UtcNow; SaveService.QueueSave(design); Glamourer.Log.Debug($"Set applying of {slot} equipment piece to {value}."); - DesignChanged.Invoke(DesignChanged.Type.ApplyEquip, design, slot); + DesignChanged.Invoke(DesignChanged.Type.ApplyEquip, design, new ApplicationTransaction((slot, false), !value, value)); } /// Change whether to apply a specific equipment piece. @@ -356,11 +358,11 @@ public sealed class DesignManager : DesignEditor design.LastEdit = DateTimeOffset.UtcNow; SaveService.QueueSave(design); Glamourer.Log.Debug($"Set applying of {slot} bonus item to {value}."); - DesignChanged.Invoke(DesignChanged.Type.ApplyBonusItem, design, slot); + DesignChanged.Invoke(DesignChanged.Type.ApplyBonusItem, design, new ApplicationTransaction(slot, !value, value)); } /// Change whether to apply a specific stain. - public void ChangeApplyStain(Design design, EquipSlot slot, bool value) + public void ChangeApplyStains(Design design, EquipSlot slot, bool value) { if (!design.SetApplyStain(slot, value)) return; @@ -368,7 +370,7 @@ public sealed class DesignManager : DesignEditor design.LastEdit = DateTimeOffset.UtcNow; SaveService.QueueSave(design); Glamourer.Log.Debug($"Set applying of stain of {slot} equipment piece to {value}."); - DesignChanged.Invoke(DesignChanged.Type.ApplyStain, design, slot); + DesignChanged.Invoke(DesignChanged.Type.ApplyStain, design, new ApplicationTransaction((slot, true), !value, value)); } /// Change whether to apply a specific crest visibility. @@ -380,7 +382,7 @@ public sealed class DesignManager : DesignEditor design.LastEdit = DateTimeOffset.UtcNow; SaveService.QueueSave(design); Glamourer.Log.Debug($"Set applying of crest visibility of {slot} equipment piece to {value}."); - DesignChanged.Invoke(DesignChanged.Type.ApplyCrest, design, slot); + DesignChanged.Invoke(DesignChanged.Type.ApplyCrest, design, new ApplicationTransaction(slot, !value, value)); } /// Change the application value of one of the meta flags. @@ -392,7 +394,7 @@ public sealed class DesignManager : DesignEditor design.LastEdit = DateTimeOffset.UtcNow; SaveService.QueueSave(design); Glamourer.Log.Debug($"Set applying of {metaIndex} to {value}."); - DesignChanged.Invoke(DesignChanged.Type.Other, design, (metaIndex, true, value)); + DesignChanged.Invoke(DesignChanged.Type.Other, design, new ApplicationTransaction(metaIndex, !value, value)); } /// Change the application value of a customize parameter. @@ -404,7 +406,7 @@ public sealed class DesignManager : DesignEditor design.LastEdit = DateTimeOffset.UtcNow; SaveService.QueueSave(design); Glamourer.Log.Debug($"Set applying of parameter {flag} to {value}."); - DesignChanged.Invoke(DesignChanged.Type.ApplyParameter, design, flag); + DesignChanged.Invoke(DesignChanged.Type.ApplyParameter, design, new ApplicationTransaction(flag, !value, value)); } #endregion diff --git a/Glamourer/Designs/History/DesignTransaction.cs b/Glamourer/Designs/History/DesignTransaction.cs new file mode 100644 index 0000000..98e3eec --- /dev/null +++ b/Glamourer/Designs/History/DesignTransaction.cs @@ -0,0 +1,181 @@ +using Glamourer.GameData; +using Glamourer.Interop.Material; +using Glamourer.Interop.Penumbra; +using Penumbra.GameData.Enums; + +namespace Glamourer.Designs.History; + +/// Only Designs. Can not be reverted. +public readonly record struct CreationTransaction(string Name, string? Path) + : ITransaction +{ + public ITransaction? Merge(ITransaction other) + => null; + + public void Revert(IDesignEditor editor, object data) + { } +} + +/// Only Designs. +public readonly record struct RenameTransaction(string Old, string New) + : ITransaction +{ + public ITransaction? Merge(ITransaction older) + => older is RenameTransaction other ? new RenameTransaction(other.Old, New) : null; + + public void Revert(IDesignEditor editor, object data) + => ((DesignManager)editor).Rename((Design)data, Old); +} + +/// Only Designs. +public readonly record struct DescriptionTransaction(string Old, string New) + : ITransaction +{ + public ITransaction? Merge(ITransaction older) + => older is DescriptionTransaction other ? new DescriptionTransaction(other.Old, New) : null; + + public void Revert(IDesignEditor editor, object data) + => ((DesignManager)editor).ChangeDescription((Design)data, Old); +} + +/// Only Designs. +public readonly record struct DesignColorTransaction(string Old, string New) + : ITransaction +{ + public ITransaction? Merge(ITransaction older) + => older is DesignColorTransaction other ? new DesignColorTransaction(other.Old, New) : null; + + public void Revert(IDesignEditor editor, object data) + => ((DesignManager)editor).ChangeColor((Design)data, Old); +} + +/// Only Designs. +public readonly record struct TagAddedTransaction(string New, int Index) + : ITransaction +{ + public ITransaction? Merge(ITransaction other) + => null; + + public void Revert(IDesignEditor editor, object data) + => ((DesignManager)editor).RemoveTag((Design)data, Index); +} + +/// Only Designs. +public readonly record struct TagRemovedTransaction(string Old, int Index) + : ITransaction +{ + public ITransaction? Merge(ITransaction other) + => null; + + public void Revert(IDesignEditor editor, object data) + => ((DesignManager)editor).AddTag((Design)data, Old); +} + +/// Only Designs. +public readonly record struct TagChangedTransaction(string Old, string New, int IndexOld, int IndexNew) + : ITransaction +{ + public ITransaction? Merge(ITransaction older) + => older is TagChangedTransaction other && other.IndexNew == IndexOld + ? new TagChangedTransaction(other.Old, New, other.IndexOld, IndexNew) + : null; + + public void Revert(IDesignEditor editor, object data) + => ((DesignManager)editor).RenameTag((Design)data, IndexNew, Old); +} + +/// Only Designs. +public readonly record struct ModAddedTransaction(Mod Mod, ModSettings Settings) + : ITransaction +{ + public ITransaction? Merge(ITransaction other) + => null; + + public void Revert(IDesignEditor editor, object data) + => ((DesignManager)editor).RemoveMod((Design)data, Mod); +} + +/// Only Designs. +public readonly record struct ModRemovedTransaction(Mod Mod, ModSettings Settings) + : ITransaction +{ + public ITransaction? Merge(ITransaction other) + => null; + + public void Revert(IDesignEditor editor, object data) + => ((DesignManager)editor).AddMod((Design)data, Mod, Settings); +} + +/// Only Designs. +public readonly record struct ModUpdatedTransaction(Mod Mod, ModSettings Old, ModSettings New) + : ITransaction +{ + public ITransaction? Merge(ITransaction older) + => older is ModUpdatedTransaction other && Mod == other.Mod ? new ModUpdatedTransaction(Mod, other.Old, New) : null; + + public void Revert(IDesignEditor editor, object data) + => ((DesignManager)editor).UpdateMod((Design)data, Mod, Old); +} + +/// Only Designs. +public readonly record struct MaterialTransaction(MaterialValueIndex Index, ColorRow? Old, ColorRow? New) + : ITransaction +{ + public ITransaction? Merge(ITransaction older) + => older is MaterialTransaction other && Index == other.Index ? new MaterialTransaction(Index, other.Old, New) : null; + + public void Revert(IDesignEditor editor, object data) + => ((DesignManager)editor).ChangeMaterialValue((Design)data, Index, Old); +} + +/// Only Designs. +public readonly record struct MaterialRevertTransaction(MaterialValueIndex Index, bool Old, bool New) + : ITransaction +{ + public ITransaction? Merge(ITransaction other) + => null; + + public void Revert(IDesignEditor editor, object data) + => ((DesignManager)editor).ChangeMaterialRevert((Design)data, Index, Old); +} + +/// Only Designs. +public readonly record struct ApplicationTransaction(object Index, bool Old, bool New) + : ITransaction +{ + public ITransaction? Merge(ITransaction other) + => null; + + public void Revert(IDesignEditor editor, object data) + { + var manager = (DesignManager)editor; + var design = (Design)data; + switch (Index) + { + case CustomizeIndex idx: + manager.ChangeApplyCustomize(design, idx, Old); + break; + case (EquipSlot slot, true): + manager.ChangeApplyStains(design, slot, Old); + break; + case (EquipSlot slot, _): + manager.ChangeApplyItem(design, slot, Old); + break; + case BonusItemFlag slot: + manager.ChangeApplyBonusItem(design, slot, Old); + break; + case CrestFlag slot: + manager.ChangeApplyCrest(design, slot, Old); + break; + case MetaIndex slot: + manager.ChangeApplyMeta(design, slot, Old); + break; + case CustomizeParameterFlag slot: + manager.ChangeApplyParameter(design, slot, Old); + break; + case MaterialValueIndex slot: + manager.ChangeApplyMaterialValue(design, slot, Old); + break; + } + } +} diff --git a/Glamourer/Designs/History/EditorHistory.cs b/Glamourer/Designs/History/EditorHistory.cs new file mode 100644 index 0000000..6382b94 --- /dev/null +++ b/Glamourer/Designs/History/EditorHistory.cs @@ -0,0 +1,191 @@ +using Glamourer.Api.Enums; +using Glamourer.Events; +using Glamourer.Interop.Structs; +using Glamourer.State; +using OtterGui.Services; + +namespace Glamourer.Designs.History; + +public class EditorHistory : IDisposable, IService +{ + public const int MaxUndo = 16; + + private sealed class Queue : IReadOnlyList + { + private DateTime _lastAdd = DateTime.UtcNow; + + private readonly ITransaction[] _data = new ITransaction[MaxUndo]; + public int Offset { get; private set; } + public int Count { get; private set; } + + public void Add(ITransaction transaction) + { + if (!TryMerge(transaction)) + { + if (Count == MaxUndo) + { + _data[Offset] = transaction; + Offset = (Offset + 1) % MaxUndo; + } + else + { + if (Offset > 0) + { + _data[(Count + Offset) % MaxUndo] = transaction; + ++Count; + } + else + { + _data[Count] = transaction; + ++Count; + } + } + } + + _lastAdd = DateTime.UtcNow; + } + + private bool TryMerge(ITransaction newTransaction) + { + if (Count == 0) + return false; + + var time = DateTime.UtcNow; + if (time - _lastAdd > TimeSpan.FromMilliseconds(250)) + return false; + + var lastIdx = (Offset + Count - 1) % MaxUndo; + if (newTransaction.Merge(_data[lastIdx]) is not { } transaction) + return false; + + _data[lastIdx] = transaction; + return true; + } + + public ITransaction? RemoveLast() + { + if (Count == 0) + return null; + + --Count; + var idx = (Offset + Count) % MaxUndo; + return _data[idx]; + } + + public IEnumerator GetEnumerator() + { + var end = Offset + (Offset + Count) % MaxUndo; + for (var i = Offset; i < end; ++i) + yield return _data[i]; + + end = Count - end; + for (var i = 0; i < end; ++i) + yield return _data[i]; + } + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + public ITransaction this[int index] + => index < 0 || index >= Count + ? throw new IndexOutOfRangeException() + : _data[(Offset + index) % MaxUndo]; + } + + private readonly DesignEditor _designEditor; + private readonly StateEditor _stateEditor; + private readonly DesignChanged _designChanged; + private readonly StateChanged _stateChanged; + + private readonly Dictionary _stateEntries = []; + private readonly Dictionary _designEntries = []; + + private bool _undoMode; + + public EditorHistory(DesignManager designEditor, StateManager stateEditor, DesignChanged designChanged, StateChanged stateChanged) + { + _designEditor = designEditor; + _stateEditor = stateEditor; + _designChanged = designChanged; + _stateChanged = stateChanged; + + _designChanged.Subscribe(OnDesignChanged, DesignChanged.Priority.EditorHistory); + _stateChanged.Subscribe(OnStateChanged, StateChanged.Priority.EditorHistory); + } + + public void Dispose() + { + _designChanged.Unsubscribe(OnDesignChanged); + _stateChanged.Unsubscribe(OnStateChanged); + } + + public bool CanUndo(ActorState state) + => _stateEntries.TryGetValue(state, out var list) && list.Count > 0; + + public bool CanUndo(Design design) + => _designEntries.TryGetValue(design, out var list) && list.Count > 0; + + public bool Undo(ActorState state) + { + if (!_stateEntries.TryGetValue(state, out var list) || list.Count == 0) + return false; + + _undoMode = true; + list.RemoveLast()!.Revert(_stateEditor, state); + _undoMode = false; + return true; + } + + public bool Undo(Design design) + { + if (!_designEntries.TryGetValue(design, out var list) || list.Count == 0) + return false; + + _undoMode = true; + list.RemoveLast()!.Revert(_designEditor, design); + _undoMode = false; + return true; + } + + + private void AddStateTransaction(ActorState state, ITransaction transaction) + { + if (!_stateEntries.TryGetValue(state, out var list)) + { + list = new Queue(); + _stateEntries.Add(state, list); + } + + list.Add(transaction); + } + + private void AddDesignTransaction(Design design, ITransaction transaction) + { + if (!_designEntries.TryGetValue(design, out var list)) + { + list = new Queue(); + _designEntries.Add(design, list); + } + + list.Add(transaction); + } + + + private void OnStateChanged(StateChangeType type, StateSource source, ActorState state, ActorData actors, ITransaction? data) + { + if (_undoMode || source is not StateSource.Manual) + return; + + if (data is not null) + AddStateTransaction(state, data); + } + + private void OnDesignChanged(DesignChanged.Type type, Design design, ITransaction? data) + { + if (_undoMode) + return; + + if (data is not null) + AddDesignTransaction(design, data); + } +} diff --git a/Glamourer/Designs/History/Transaction.cs b/Glamourer/Designs/History/Transaction.cs new file mode 100644 index 0000000..ac310b0 --- /dev/null +++ b/Glamourer/Designs/History/Transaction.cs @@ -0,0 +1,113 @@ +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; +using Glamourer.GameData; + +namespace Glamourer.Designs.History; + +public interface ITransaction +{ + public ITransaction? Merge(ITransaction other); + public void Revert(IDesignEditor editor, object data); +} + +public readonly record struct CustomizeTransaction(CustomizeIndex Slot, CustomizeValue Old, CustomizeValue New) + : ITransaction +{ + public ITransaction? Merge(ITransaction older) + => older is CustomizeTransaction other && Slot == other.Slot ? new CustomizeTransaction(Slot, other.Old, New) : null; + + public void Revert(IDesignEditor editor, object data) + => editor.ChangeCustomize(data, Slot, Old, ApplySettings.Manual); +} + +public readonly record struct EntireCustomizeTransaction(CustomizeFlag Apply, CustomizeArray Old, CustomizeArray New) + : ITransaction +{ + public ITransaction? Merge(ITransaction older) + => older is EntireCustomizeTransaction other ? new EntireCustomizeTransaction(Apply | other.Apply, other.Old, New) : null; + + public void Revert(IDesignEditor editor, object data) + => editor.ChangeEntireCustomize(data, Old, Apply, ApplySettings.Manual); +} + +public readonly record struct EquipTransaction(EquipSlot Slot, EquipItem Old, EquipItem New) + : ITransaction +{ + public ITransaction? Merge(ITransaction older) + => older is EquipTransaction other && Slot == other.Slot ? new EquipTransaction(Slot, other.Old, New) : null; + + public void Revert(IDesignEditor editor, object data) + => editor.ChangeItem(data, Slot, Old, ApplySettings.Manual); +} + +public readonly record struct BonusItemTransaction(BonusItemFlag Slot, BonusItem Old, BonusItem New) + : ITransaction +{ + public ITransaction? Merge(ITransaction older) + => older is BonusItemTransaction other && Slot == other.Slot ? new BonusItemTransaction(Slot, other.Old, New) : null; + + public void Revert(IDesignEditor editor, object data) + => editor.ChangeBonusItem(data, Slot, Old, ApplySettings.Manual); +} + +public readonly record struct WeaponTransaction( + EquipItem OldMain, + EquipItem OldOff, + EquipItem OldGauntlets, + EquipItem NewMain, + EquipItem NewOff, + EquipItem NewGauntlets) + : ITransaction +{ + public ITransaction? Merge(ITransaction older) + => older is WeaponTransaction other + ? new WeaponTransaction(other.OldMain, other.OldOff, other.OldGauntlets, NewMain, NewOff, NewGauntlets) + : null; + + public void Revert(IDesignEditor editor, object data) + { + editor.ChangeItem(data, EquipSlot.MainHand, OldMain, ApplySettings.Manual); + editor.ChangeItem(data, EquipSlot.OffHand, OldOff, ApplySettings.Manual); + editor.ChangeItem(data, EquipSlot.Hands, OldGauntlets, ApplySettings.Manual); + } +} + +public readonly record struct StainTransaction(EquipSlot Slot, StainIds Old, StainIds New) + : ITransaction +{ + public ITransaction? Merge(ITransaction older) + => older is StainTransaction other && Slot == other.Slot ? new StainTransaction(Slot, other.Old, New) : null; + + public void Revert(IDesignEditor editor, object data) + => editor.ChangeStains(data, Slot, Old, ApplySettings.Manual); +} + +public readonly record struct CrestTransaction(CrestFlag Slot, bool Old, bool New) + : ITransaction +{ + public ITransaction? Merge(ITransaction older) + => older is CrestTransaction other && Slot == other.Slot ? new CrestTransaction(Slot, other.Old, New) : null; + + public void Revert(IDesignEditor editor, object data) + => editor.ChangeCrest(data, Slot, Old, ApplySettings.Manual); +} + +public readonly record struct ParameterTransaction(CustomizeParameterFlag Slot, CustomizeParameterValue Old, CustomizeParameterValue New) + : ITransaction +{ + public ITransaction? Merge(ITransaction older) + => older is ParameterTransaction other && Slot == other.Slot ? new ParameterTransaction(Slot, other.Old, New) : null; + + public void Revert(IDesignEditor editor, object data) + => editor.ChangeCustomizeParameter(data, Slot, Old, ApplySettings.Manual); +} + +public readonly record struct MetaTransaction(MetaIndex Slot, bool Old, bool New) + : ITransaction +{ + public ITransaction? Merge(ITransaction older) + => null; + + public void Revert(IDesignEditor editor, object data) + => editor.ChangeMetaState(data, Slot, Old, ApplySettings.Manual); +} diff --git a/Glamourer/Designs/Links/DesignLinkManager.cs b/Glamourer/Designs/Links/DesignLinkManager.cs index 76d9c9a..df1f147 100644 --- a/Glamourer/Designs/Links/DesignLinkManager.cs +++ b/Glamourer/Designs/Links/DesignLinkManager.cs @@ -1,4 +1,5 @@ using Glamourer.Automation; +using Glamourer.Designs.History; using Glamourer.Events; using Glamourer.Services; using OtterGui.Services; @@ -67,7 +68,7 @@ public sealed class DesignLinkManager : IService, IDisposable _event.Invoke(DesignChanged.Type.ChangedLink, parent, null); } - private void OnDesignChanged(DesignChanged.Type type, Design deletedDesign, object? _) + private void OnDesignChanged(DesignChanged.Type type, Design deletedDesign, ITransaction? _) { if (type is not DesignChanged.Type.Deleted) return; diff --git a/Glamourer/Events/DesignChanged.cs b/Glamourer/Events/DesignChanged.cs index a8f35d5..6c2ba8a 100644 --- a/Glamourer/Events/DesignChanged.cs +++ b/Glamourer/Events/DesignChanged.cs @@ -1,4 +1,5 @@ using Glamourer.Designs; +using Glamourer.Designs.History; using Glamourer.Gui; using OtterGui.Classes; @@ -13,113 +14,116 @@ namespace Glamourer.Events; /// /// public sealed class DesignChanged() - : EventWrapper(nameof(DesignChanged)) + : EventWrapper(nameof(DesignChanged)) { public enum Type { - /// A new design was created. Data is a potential path to move it to [string?]. + /// A new design was created. Created, - /// An existing design was deleted. Data is null. + /// An existing design was deleted. Deleted, - /// Invoked on full reload. Design and Data are null. + /// Invoked on full reload. ReloadedAll, - /// An existing design was renamed. Data is the prior name [string]. + /// An existing design was renamed. Renamed, - /// An existing design had its description changed. Data is the prior description [string]. + /// An existing design had its description changed. ChangedDescription, - /// An existing design had its associated color changed. Data is the prior color [string]. + /// An existing design had its associated color changed. ChangedColor, - /// An existing design had a new tag added. Data is the new tag and the index it was added at [(string, int)]. + /// An existing design had a new tag added. AddedTag, - /// An existing design had an existing tag removed. Data is the removed tag and the index it had before removal [(string, int)]. + /// An existing design had an existing tag removed. RemovedTag, - /// An existing design had an existing tag renamed. Data is the old name of the tag, the new name of the tag, and the index it had before being resorted [(string, string, int)]. + /// An existing design had an existing tag renamed. ChangedTag, - /// An existing design had a new associated mod added. Data is the Mod and its Settings [(Mod, ModSettings)]. + /// An existing design had a new associated mod added. AddedMod, - /// An existing design had an existing associated mod removed. Data is the Mod and its Settings [(Mod, ModSettings)]. + /// An existing design had an existing associated mod removed. RemovedMod, - /// An existing design had a link to a different design added, removed or moved. Data is null. + /// An existing design had an existing associated mod updated. + UpdatedMod, + + /// An existing design had a link to a different design added, removed or moved. ChangedLink, - /// An existing design had a customization changed. Data is the old value, the new value and the type [(CustomizeValue, CustomizeValue, CustomizeIndex)]. + /// An existing design had a customization changed. Customize, - /// An existing design had its entire customize array changed. Data is the old array, the applied flags and the changed flags. [(CustomizeArray, CustomizeFlag, CustomizeFlag)]. + /// An existing design had its entire customize array changed. EntireCustomize, - /// An existing design had an equipment piece changed. Data is the old value, the new value and the slot [(EquipItem, EquipItem, EquipSlot)]. + /// An existing design had an equipment piece changed. Equip, - /// An existing design had a bonus item changed. Data is the old value, the new value and the slot [(BonusItem, BonusItem, BonusItemFlag)]. + /// An existing design had a bonus item changed. BonusItem, - /// An existing design had its weapons changed. Data is the old mainhand, the old offhand, the new mainhand, the new offhand (if any) and the new gauntlets (if any). [(EquipItem, EquipItem, EquipItem, EquipItem?, EquipItem?)]. + /// An existing design had its weapons changed. Weapon, - /// An existing design had a stain changed. Data is the old stain id, the new stain id and the slot [(StainIds, StainIds, EquipSlot)]. + /// An existing design had a stain changed. Stains, - /// An existing design had a crest visibility changed. Data is the old crest visibility, the new crest visibility and the slot [(bool, bool, EquipSlot)]. + /// An existing design had a crest visibility changed. Crest, - /// An existing design had a customize parameter changed. Data is the old value, the new value and the flag [(CustomizeParameterValue, CustomizeParameterValue, CustomizeParameterFlag)]. + /// An existing design had a customize parameter changed. Parameter, - /// An existing design had an advanced dye row added, changed, or deleted. Data is the old value, the new value and the index [(ColorRow?, ColorRow?, MaterialValueIndex)]. + /// An existing design had an advanced dye row added, changed, or deleted. Material, - /// An existing design had an advanced dye rows Revert state changed. Data is the index [MaterialValueIndex]. + /// An existing design had an advanced dye rows Revert state changed. MaterialRevert, /// An existing design had changed whether it always forces a redraw or not. ForceRedraw, - /// An existing design changed whether a specific customization is applied. Data is the type of customization [CustomizeIndex]. + /// An existing design changed whether a specific customization is applied. ApplyCustomize, - /// An existing design changed whether a specific equipment piece is applied. Data is the slot of the equipment [EquipSlot]. + /// An existing design changed whether a specific equipment piece is applied. ApplyEquip, - /// An existing design changed whether a specific bonus item is applied. Data is the slot of the item [BonusItemFlag]. + /// An existing design changed whether a specific bonus item is applied. ApplyBonusItem, - /// An existing design changed whether a specific stain is applied. Data is the slot of the equipment [EquipSlot]. + /// An existing design changed whether a specific stain is applied. ApplyStain, - /// An existing design changed whether a specific crest visibility is applied. Data is the slot of the equipment [EquipSlot]. + /// An existing design changed whether a specific crest visibility is applied. ApplyCrest, - /// An existing design changed whether a specific customize parameter is applied. Data is the flag for the parameter [CustomizeParameterFlag]. + /// An existing design changed whether a specific customize parameter is applied. ApplyParameter, - /// An existing design changed whether an advanced dye row is applied. Data is the index [MaterialValueIndex]. + /// An existing design changed whether an advanced dye row is applied. ApplyMaterial, - /// An existing design changed its write protection status. Data is the new value [bool]. + /// An existing design changed its write protection status. WriteProtection, - /// An existing design changed its display status for the quick design bar. Data is the new value [bool]. + /// An existing design changed its display status for the quick design bar. QuickDesignBar, - /// An existing design changed one of the meta flags. Data is the flag, whether it was about their applying and the new value [(MetaFlag, bool, bool)]. + /// An existing design changed one of the meta flags. Other, } public enum Priority { - /// + /// DesignLinkManager = 1, /// @@ -131,7 +135,10 @@ public sealed class DesignChanged() /// DesignFileSystemSelector = -1, - /// + /// DesignCombo = -2, + + /// + EditorHistory = -1000, } } diff --git a/Glamourer/Events/StateChanged.cs b/Glamourer/Events/StateChanged.cs index 641665c..c704195 100644 --- a/Glamourer/Events/StateChanged.cs +++ b/Glamourer/Events/StateChanged.cs @@ -1,4 +1,5 @@ using Glamourer.Api.Enums; +using Glamourer.Designs.History; using Glamourer.Interop.Structs; using Glamourer.State; using OtterGui.Classes; @@ -15,11 +16,17 @@ namespace Glamourer.Events; /// /// public sealed class StateChanged() - : EventWrapper(nameof(StateChanged)) + : EventWrapper(nameof(StateChanged)) { public enum Priority { - GlamourerIpc = int.MinValue, + /// + GlamourerIpc = int.MinValue, + + /// PenumbraAutoRedraw = 0, + + /// + EditorHistory = -1000, } -} \ No newline at end of file +} diff --git a/Glamourer/Gui/DesignCombo.cs b/Glamourer/Gui/DesignCombo.cs index 4bd18ab..dccfa44 100644 --- a/Glamourer/Gui/DesignCombo.cs +++ b/Glamourer/Gui/DesignCombo.cs @@ -2,6 +2,7 @@ using Dalamud.Interface.Utility.Raii; using Glamourer.Automation; using Glamourer.Designs; +using Glamourer.Designs.History; using Glamourer.Designs.Special; using Glamourer.Events; using ImGuiNET; @@ -29,7 +30,7 @@ public abstract class DesignComboBase : FilterComboCache "Undo the last change."; + + protected override FontAwesomeIcon Icon + => FontAwesomeIcon.Undo; + + public override bool Visible + => panel._state != null; + + protected override bool Disabled + => (panel._state?.IsLocked ?? true) || !panel._editorHistory.CanUndo(panel._state); + + protected override void OnClick() + => panel._editorHistory.Undo(panel._state!); + } + private sealed class LockedButton(ActorPanel panel) : HeaderDrawer.Button { protected override string Description diff --git a/Glamourer/Gui/Tabs/DesignTab/DesignFileSystemSelector.cs b/Glamourer/Gui/Tabs/DesignTab/DesignFileSystemSelector.cs index 11147c8..ea117c5 100644 --- a/Glamourer/Gui/Tabs/DesignTab/DesignFileSystemSelector.cs +++ b/Glamourer/Gui/Tabs/DesignTab/DesignFileSystemSelector.cs @@ -2,6 +2,7 @@ using Dalamud.Interface.ImGuiNotification; using Dalamud.Plugin.Services; using Glamourer.Designs; +using Glamourer.Designs.History; using Glamourer.Events; using Glamourer.Services; using ImGuiNET; @@ -178,7 +179,7 @@ public sealed class DesignFileSystemSelector : FileSystemSelector _config.OpenFoldersByDefault; - private void OnDesignChange(DesignChanged.Type type, Design design, object? oldData) + private void OnDesignChange(DesignChanged.Type type, Design design, ITransaction? _) { switch (type) { diff --git a/Glamourer/Gui/Tabs/DesignTab/DesignPanel.cs b/Glamourer/Gui/Tabs/DesignTab/DesignPanel.cs index 1a072cb..07eb432 100644 --- a/Glamourer/Gui/Tabs/DesignTab/DesignPanel.cs +++ b/Glamourer/Gui/Tabs/DesignTab/DesignPanel.cs @@ -277,7 +277,7 @@ public class DesignPanel { var apply = bigChange ? ((EquipFlag)flags).HasFlag(slot.ToStainFlag()) : _selector.Selected!.DoApplyStain(slot); if (ImGui.Checkbox($"Apply {slot.ToName()} Dye", ref apply) || bigChange) - _manager.ChangeApplyStain(_selector.Selected!, slot, apply); + _manager.ChangeApplyStains(_selector.Selected!, slot, apply); } else foreach (var slot in slots) diff --git a/Glamourer/Interop/Penumbra/PenumbraAutoRedraw.cs b/Glamourer/Interop/Penumbra/PenumbraAutoRedraw.cs index 72fb554..fbe0d9d 100644 --- a/Glamourer/Interop/Penumbra/PenumbraAutoRedraw.cs +++ b/Glamourer/Interop/Penumbra/PenumbraAutoRedraw.cs @@ -1,5 +1,6 @@ using Dalamud.Plugin.Services; using Glamourer.Api.Enums; +using Glamourer.Designs.History; using Glamourer.Events; using Glamourer.Interop.Structs; using Glamourer.State; @@ -30,21 +31,21 @@ public class PenumbraAutoRedraw : IDisposable, IRequiredService _stateChanged = stateChanged; _penumbra.ModSettingChanged += OnModSettingChange; _framework.Update += OnFramework; - _stateChanged.Subscribe(OnStateChange, StateChanged.Priority.PenumbraAutoRedraw); + _stateChanged.Subscribe(OnStateChanged, StateChanged.Priority.PenumbraAutoRedraw); } public void Dispose() { _penumbra.ModSettingChanged -= OnModSettingChange; _framework.Update -= OnFramework; - _stateChanged.Unsubscribe(OnStateChange); + _stateChanged.Unsubscribe(OnStateChanged); } private readonly ConcurrentQueue<(ActorState, Action, int)> _actions = []; private readonly ConcurrentSet _skips = []; private DateTime _frame; - private void OnStateChange(StateChangeType type, StateSource source, ActorState state, ActorData _1, object? _2) + private void OnStateChanged(StateChangeType type, StateSource source, ActorState state, ActorData _1, ITransaction? _2) { if (type is StateChangeType.Design && source.IsIpc()) _skips.TryAdd(state); diff --git a/Glamourer/State/StateEditor.cs b/Glamourer/State/StateEditor.cs index c627564..95c118d 100644 --- a/Glamourer/State/StateEditor.cs +++ b/Glamourer/State/StateEditor.cs @@ -1,5 +1,6 @@ using Glamourer.Api.Enums; using Glamourer.Designs; +using Glamourer.Designs.History; using Glamourer.Designs.Links; using Glamourer.Events; using Glamourer.GameData; @@ -39,7 +40,7 @@ public class StateEditor( var actors = Applier.ForceRedraw(state, source.RequiresChange()); Glamourer.Log.Verbose( $"Set model id in state {state.Identifier.Incognito(null)} from {old} to {modelId}. [Affecting {actors.ToLazyString("nothing")}.]"); - StateChanged.Invoke(StateChangeType.Model, source, state, actors, (old, modelId)); + StateChanged.Invoke(StateChangeType.Model, source, state, actors, null); } /// @@ -52,7 +53,7 @@ public class StateEditor( var actors = Applier.ChangeCustomize(state, settings.Source.RequiresChange()); Glamourer.Log.Verbose( $"Set {idx.ToDefaultName()} customizations in state {state.Identifier.Incognito(null)} from {old.Value} to {value.Value}. [Affecting {actors.ToLazyString("nothing")}.]"); - StateChanged.Invoke(StateChangeType.Customize, settings.Source, state, actors, (old, value, idx)); + StateChanged.Invoke(StateChangeType.Customize, settings.Source, state, actors, new CustomizeTransaction(idx, old, value)); } /// @@ -65,7 +66,8 @@ public class StateEditor( var actors = Applier.ChangeCustomize(state, settings.Source.RequiresChange()); Glamourer.Log.Verbose( $"Set {applied} customizations in state {state.Identifier.Incognito(null)} from {old} to {customizeInput}. [Affecting {actors.ToLazyString("nothing")}.]"); - StateChanged.Invoke(StateChangeType.EntireCustomize, settings.Source, state, actors, (old, applied)); + StateChanged.Invoke(StateChangeType.EntireCustomize, settings.Source, state, actors, + new EntireCustomizeTransaction(applied, old, customizeInput)); } /// @@ -86,7 +88,25 @@ public class StateEditor( Glamourer.Log.Verbose( $"Set {slot.ToName()} in state {state.Identifier.Incognito(null)} from {old.Name} ({old.ItemId}) to {item.Name} ({item.ItemId}). [Affecting {actors.ToLazyString("nothing")}.]"); - StateChanged.Invoke(type, settings.Source, state, actors, (old, item, slot)); + + if (type is StateChangeType.Equip) + { + StateChanged.Invoke(type, settings.Source, state, actors, new EquipTransaction(slot, old, item)); + } + else if (slot is EquipSlot.MainHand) + { + var oldOff = state.ModelData.Item(EquipSlot.OffHand); + var oldGauntlets = state.ModelData.Item(EquipSlot.Hands); + StateChanged.Invoke(type, settings.Source, state, actors, + new WeaponTransaction(old, oldOff, oldGauntlets, item, oldOff, oldGauntlets)); + } + else + { + var oldMain = state.ModelData.Item(EquipSlot.MainHand); + var oldGauntlets = state.ModelData.Item(EquipSlot.Hands); + StateChanged.Invoke(type, settings.Source, state, actors, + new WeaponTransaction(oldMain, old, oldGauntlets, oldMain, item, oldGauntlets)); + } } public void ChangeBonusItem(object data, BonusItemFlag slot, BonusItem item, ApplySettings settings = default) @@ -98,7 +118,7 @@ public class StateEditor( var actors = Applier.ChangeBonusItem(state, slot, settings.Source.RequiresChange()); Glamourer.Log.Verbose( $"Set {slot.ToName()} in state {state.Identifier.Incognito(null)} from {old.Name} ({old.Id}) to {item.Name} ({item.Id}). [Affecting {actors.ToLazyString("nothing")}.]"); - StateChanged.Invoke(StateChangeType.BonusItem, settings.Source, state, actors, (old, item, slot)); + StateChanged.Invoke(StateChangeType.BonusItem, settings.Source, state, actors, new BonusItemTransaction(slot, old, item)); } /// @@ -131,8 +151,26 @@ public class StateEditor( Glamourer.Log.Verbose( $"Set {slot.ToName()} in state {state.Identifier.Incognito(null)} from {old.Name} ({old.ItemId}) to {item!.Value.Name} ({item.Value.ItemId}) and its stain from {oldStains} to {stains!.Value}. [Affecting {actors.ToLazyString("nothing")}.]"); - StateChanged.Invoke(type, settings.Source, state, actors, (old, item!.Value, slot)); - StateChanged.Invoke(StateChangeType.Stains, settings.Source, state, actors, (oldStains, stains!.Value, slot)); + if (type is StateChangeType.Equip) + { + StateChanged.Invoke(type, settings.Source, state, actors, new EquipTransaction(slot, old, item!.Value)); + } + else if (slot is EquipSlot.MainHand) + { + var oldOff = state.ModelData.Item(EquipSlot.OffHand); + var oldGauntlets = state.ModelData.Item(EquipSlot.Hands); + StateChanged.Invoke(type, settings.Source, state, actors, + new WeaponTransaction(old, oldOff, oldGauntlets, item!.Value, oldOff, oldGauntlets)); + } + else + { + var oldMain = state.ModelData.Item(EquipSlot.MainHand); + var oldGauntlets = state.ModelData.Item(EquipSlot.Hands); + StateChanged.Invoke(type, settings.Source, state, actors, + new WeaponTransaction(oldMain, old, oldGauntlets, oldMain, item!.Value, oldGauntlets)); + } + + StateChanged.Invoke(StateChangeType.Stains, settings.Source, state, actors, new StainTransaction(slot, oldStains, stains!.Value)); } /// @@ -145,7 +183,7 @@ public class StateEditor( var actors = Applier.ChangeStain(state, slot, settings.Source.RequiresChange()); Glamourer.Log.Verbose( $"Set {slot.ToName()} stain in state {state.Identifier.Incognito(null)} from {old} to {stains}. [Affecting {actors.ToLazyString("nothing")}.]"); - StateChanged.Invoke(StateChangeType.Stains, settings.Source, state, actors, (old, stains, slot)); + StateChanged.Invoke(StateChangeType.Stains, settings.Source, state, actors, new StainTransaction(slot, old, stains)); } /// @@ -158,7 +196,7 @@ public class StateEditor( var actors = Applier.ChangeCrests(state, settings.Source.RequiresChange()); Glamourer.Log.Verbose( $"Set {slot.ToLabel()} crest in state {state.Identifier.Incognito(null)} from {old} to {crest}. [Affecting {actors.ToLazyString("nothing")}.]"); - StateChanged.Invoke(StateChangeType.Crest, settings.Source, state, actors, (old, crest, slot)); + StateChanged.Invoke(StateChangeType.Crest, settings.Source, state, actors, new CrestTransaction(slot, old, crest)); } /// @@ -176,7 +214,7 @@ public class StateEditor( var actors = Applier.ChangeParameters(state, flag, settings.Source.RequiresChange()); Glamourer.Log.Verbose( $"Set {flag} in state {state.Identifier.Incognito(null)} from {old} to {@new}. [Affecting {actors.ToLazyString("nothing")}.]"); - StateChanged.Invoke(StateChangeType.Parameter, settings.Source, state, actors, (old, @new, flag)); + StateChanged.Invoke(StateChangeType.Parameter, settings.Source, state, actors, new ParameterTransaction(flag, old, @new)); } public void ChangeMaterialValue(object data, MaterialValueIndex index, in MaterialValueState newValue, ApplySettings settings) @@ -188,7 +226,8 @@ public class StateEditor( var actors = Applier.ChangeMaterialValue(state, index, settings.Source.RequiresChange()); Glamourer.Log.Verbose( $"Set material value in state {state.Identifier.Incognito(null)} from {oldValue} to {newValue.Game}. [Affecting {actors.ToLazyString("nothing")}.]"); - StateChanged.Invoke(StateChangeType.MaterialValue, settings.Source, state, actors, (oldValue, newValue.Game, index)); + StateChanged.Invoke(StateChangeType.MaterialValue, settings.Source, state, actors, + new MaterialTransaction(index, oldValue, newValue.Game)); } public void ResetMaterialValue(object data, MaterialValueIndex index, ApplySettings settings) @@ -200,7 +239,7 @@ public class StateEditor( var actors = Applier.ChangeMaterialValue(state, index, true); Glamourer.Log.Verbose( $"Reset material value in state {state.Identifier.Incognito(null)} to game value. [Affecting {actors.ToLazyString("nothing")}.]"); - StateChanged.Invoke(StateChangeType.MaterialValue, settings.Source, state, actors, index); + StateChanged.Invoke(StateChangeType.MaterialValue, settings.Source, state, actors, new MaterialTransaction(index, null, null)); } /// @@ -213,7 +252,7 @@ public class StateEditor( var actors = Applier.ChangeMetaState(state, index, settings.Source.RequiresChange()); Glamourer.Log.Verbose( $"Set Head Gear Visibility in state {state.Identifier.Incognito(null)} from {old} to {value}. [Affecting {actors.ToLazyString("nothing")}.]"); - StateChanged.Invoke(StateChangeType.Other, settings.Source, state, actors, (old, value, MetaIndex.HatState)); + StateChanged.Invoke(StateChangeType.Other, settings.Source, state, actors, new MetaTransaction(index, old, value)); } /// @@ -377,7 +416,7 @@ public class StateEditor( Glamourer.Log.Verbose( $"Applied design to {state.Identifier.Incognito(null)}. [Affecting {actors.ToLazyString("nothing")}.]"); - StateChanged.Invoke(StateChangeType.Design, state.Sources[MetaIndex.Wetness], state, actors, mergedDesign.Design); + StateChanged.Invoke(StateChangeType.Design, state.Sources[MetaIndex.Wetness], state, actors, null); // FIXME: maybe later return;