Introduce history.

This commit is contained in:
Ottermandias 2024-07-31 19:33:23 +02:00
parent f6434cbc61
commit d8085dc022
19 changed files with 682 additions and 106 deletions

@ -1 +1 @@
Subproject commit 663702b57303368b3a897e9c6eae4b34b4339534
Subproject commit 4bad56d610132b419335b89896e1f387b9ba2039

View file

@ -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)

View file

@ -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<AutoDesignSet>, 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;

View file

@ -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));
}
/// <inheritdoc/>
@ -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));
}
/// <inheritdoc/>
@ -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));
}
/// <inheritdoc/>
@ -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));
}
/// <inheritdoc/>
@ -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));
}
/// <inheritdoc/>
@ -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));
}
/// <inheritdoc/>
@ -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));
}

View file

@ -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<Design>, 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<Design>, 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;

View file

@ -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));
}
/// <summary> Change the description of a design. </summary>
@ -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));
}
/// <summary> Change the associated color of a design. </summary>
@ -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));
}
/// <summary> Add a new tag to a design. The tags remain sorted. </summary>
@ -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));
}
/// <summary> Remove a tag from a design by its index. </summary>
@ -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));
}
/// <summary> Rename a tag from a design by its index. The tags stay sorted.</summary>
@ -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)));
}
/// <summary> Add an associated mod to a design. </summary>
@ -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));
}
/// <summary> Remove an associated mod from a design. </summary>
@ -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));
}
/// <summary> Add or update an associated mod to a design. </summary>
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));
}
/// <summary> Set the write protection status of a design. </summary>
@ -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);
}
/// <summary> Set the quick design bar display status of a design. </summary>
@ -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));
}
/// <summary> Change whether to apply a specific equipment piece. </summary>
@ -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));
}
/// <summary> Change whether to apply a specific equipment piece. </summary>
@ -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));
}
/// <summary> Change whether to apply a specific stain. </summary>
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));
}
/// <summary> Change whether to apply a specific crest visibility. </summary>
@ -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));
}
/// <summary> Change the application value of one of the meta flags. </summary>
@ -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));
}
/// <summary> Change the application value of a customize parameter. </summary>
@ -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

View file

@ -0,0 +1,181 @@
using Glamourer.GameData;
using Glamourer.Interop.Material;
using Glamourer.Interop.Penumbra;
using Penumbra.GameData.Enums;
namespace Glamourer.Designs.History;
/// <remarks> Only Designs. Can not be reverted. </remarks>
public readonly record struct CreationTransaction(string Name, string? Path)
: ITransaction
{
public ITransaction? Merge(ITransaction other)
=> null;
public void Revert(IDesignEditor editor, object data)
{ }
}
/// <remarks> Only Designs. </remarks>
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);
}
/// <remarks> Only Designs. </remarks>
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);
}
/// <remarks> Only Designs. </remarks>
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);
}
/// <remarks> Only Designs. </remarks>
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);
}
/// <remarks> Only Designs. </remarks>
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);
}
/// <remarks> Only Designs. </remarks>
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);
}
/// <remarks> Only Designs. </remarks>
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);
}
/// <remarks> Only Designs. </remarks>
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);
}
/// <remarks> Only Designs. </remarks>
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);
}
/// <remarks> Only Designs. </remarks>
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);
}
/// <remarks> Only Designs. </remarks>
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);
}
/// <remarks> Only Designs. </remarks>
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;
}
}
}

View file

@ -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<ITransaction>
{
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<ITransaction> 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<ActorState, Queue> _stateEntries = [];
private readonly Dictionary<Design, Queue> _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);
}
}

View file

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

View file

@ -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;

View file

@ -1,4 +1,5 @@
using Glamourer.Designs;
using Glamourer.Designs.History;
using Glamourer.Gui;
using OtterGui.Classes;
@ -13,113 +14,116 @@ namespace Glamourer.Events;
/// </list>
/// </summary>
public sealed class DesignChanged()
: EventWrapper<DesignChanged.Type, Design, object?, DesignChanged.Priority>(nameof(DesignChanged))
: EventWrapper<DesignChanged.Type, Design, ITransaction?, DesignChanged.Priority>(nameof(DesignChanged))
{
public enum Type
{
/// <summary> A new design was created. Data is a potential path to move it to [string?]. </summary>
/// <summary> A new design was created. </summary>
Created,
/// <summary> An existing design was deleted. Data is null. </summary>
/// <summary> An existing design was deleted. </summary>
Deleted,
/// <summary> Invoked on full reload. Design and Data are null. </summary>
/// <summary> Invoked on full reload. </summary>
ReloadedAll,
/// <summary> An existing design was renamed. Data is the prior name [string]. </summary>
/// <summary> An existing design was renamed. </summary>
Renamed,
/// <summary> An existing design had its description changed. Data is the prior description [string]. </summary>
/// <summary> An existing design had its description changed. </summary>
ChangedDescription,
/// <summary> An existing design had its associated color changed. Data is the prior color [string]. </summary>
/// <summary> An existing design had its associated color changed. </summary>
ChangedColor,
/// <summary> An existing design had a new tag added. Data is the new tag and the index it was added at [(string, int)]. </summary>
/// <summary> An existing design had a new tag added. </summary>
AddedTag,
/// <summary> An existing design had an existing tag removed. Data is the removed tag and the index it had before removal [(string, int)]. </summary>
/// <summary> An existing design had an existing tag removed. </summary>
RemovedTag,
/// <summary> 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)]. </summary>
/// <summary> An existing design had an existing tag renamed. </summary>
ChangedTag,
/// <summary> An existing design had a new associated mod added. Data is the Mod and its Settings [(Mod, ModSettings)]. </summary>
/// <summary> An existing design had a new associated mod added. </summary>
AddedMod,
/// <summary> An existing design had an existing associated mod removed. Data is the Mod and its Settings [(Mod, ModSettings)]. </summary>
/// <summary> An existing design had an existing associated mod removed. </summary>
RemovedMod,
/// <summary> An existing design had a link to a different design added, removed or moved. Data is null. </summary>
/// <summary> An existing design had an existing associated mod updated. </summary>
UpdatedMod,
/// <summary> An existing design had a link to a different design added, removed or moved. </summary>
ChangedLink,
/// <summary> An existing design had a customization changed. Data is the old value, the new value and the type [(CustomizeValue, CustomizeValue, CustomizeIndex)]. </summary>
/// <summary> An existing design had a customization changed. </summary>
Customize,
/// <summary> An existing design had its entire customize array changed. Data is the old array, the applied flags and the changed flags. [(CustomizeArray, CustomizeFlag, CustomizeFlag)]. </summary>
/// <summary> An existing design had its entire customize array changed. </summary>
EntireCustomize,
/// <summary> An existing design had an equipment piece changed. Data is the old value, the new value and the slot [(EquipItem, EquipItem, EquipSlot)]. </summary>
/// <summary> An existing design had an equipment piece changed. </summary>
Equip,
/// <summary> An existing design had a bonus item changed. Data is the old value, the new value and the slot [(BonusItem, BonusItem, BonusItemFlag)]. </summary>
/// <summary> An existing design had a bonus item changed. </summary>
BonusItem,
/// <summary> 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?)]. </summary>
/// <summary> An existing design had its weapons changed. </summary>
Weapon,
/// <summary> An existing design had a stain changed. Data is the old stain id, the new stain id and the slot [(StainIds, StainIds, EquipSlot)]. </summary>
/// <summary> An existing design had a stain changed. </summary>
Stains,
/// <summary> An existing design had a crest visibility changed. Data is the old crest visibility, the new crest visibility and the slot [(bool, bool, EquipSlot)]. </summary>
/// <summary> An existing design had a crest visibility changed. </summary>
Crest,
/// <summary> An existing design had a customize parameter changed. Data is the old value, the new value and the flag [(CustomizeParameterValue, CustomizeParameterValue, CustomizeParameterFlag)]. </summary>
/// <summary> An existing design had a customize parameter changed. </summary>
Parameter,
/// <summary> 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)]. </summary>
/// <summary> An existing design had an advanced dye row added, changed, or deleted. </summary>
Material,
/// <summary> An existing design had an advanced dye rows Revert state changed. Data is the index [MaterialValueIndex]. </summary>
/// <summary> An existing design had an advanced dye rows Revert state changed. </summary>
MaterialRevert,
/// <summary> An existing design had changed whether it always forces a redraw or not. </summary>
ForceRedraw,
/// <summary> An existing design changed whether a specific customization is applied. Data is the type of customization [CustomizeIndex]. </summary>
/// <summary> An existing design changed whether a specific customization is applied. </summary>
ApplyCustomize,
/// <summary> An existing design changed whether a specific equipment piece is applied. Data is the slot of the equipment [EquipSlot]. </summary>
/// <summary> An existing design changed whether a specific equipment piece is applied. </summary>
ApplyEquip,
/// <summary> An existing design changed whether a specific bonus item is applied. Data is the slot of the item [BonusItemFlag]. </summary>
/// <summary> An existing design changed whether a specific bonus item is applied. </summary>
ApplyBonusItem,
/// <summary> An existing design changed whether a specific stain is applied. Data is the slot of the equipment [EquipSlot]. </summary>
/// <summary> An existing design changed whether a specific stain is applied. </summary>
ApplyStain,
/// <summary> An existing design changed whether a specific crest visibility is applied. Data is the slot of the equipment [EquipSlot]. </summary>
/// <summary> An existing design changed whether a specific crest visibility is applied. </summary>
ApplyCrest,
/// <summary> An existing design changed whether a specific customize parameter is applied. Data is the flag for the parameter [CustomizeParameterFlag]. </summary>
/// <summary> An existing design changed whether a specific customize parameter is applied. </summary>
ApplyParameter,
/// <summary> An existing design changed whether an advanced dye row is applied. Data is the index [MaterialValueIndex]. </summary>
/// <summary> An existing design changed whether an advanced dye row is applied. </summary>
ApplyMaterial,
/// <summary> An existing design changed its write protection status. Data is the new value [bool]. </summary>
/// <summary> An existing design changed its write protection status. </summary>
WriteProtection,
/// <summary> An existing design changed its display status for the quick design bar. Data is the new value [bool]. </summary>
/// <summary> An existing design changed its display status for the quick design bar. </summary>
QuickDesignBar,
/// <summary> 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)]. </summary>
/// <summary> An existing design changed one of the meta flags. </summary>
Other,
}
public enum Priority
{
/// <seealso cref="Designs.Links.DesignLinkManager.OnDesignChange"/>
/// <seealso cref="Designs.Links.DesignLinkManager.OnDesignChanged"/>
DesignLinkManager = 1,
/// <seealso cref="Automation.AutoDesignManager.OnDesignChange"/>
@ -131,7 +135,10 @@ public sealed class DesignChanged()
/// <seealso cref="Gui.Tabs.DesignTab.DesignFileSystemSelector.OnDesignChange"/>
DesignFileSystemSelector = -1,
/// <seealso cref="SpecialDesignCombo.OnDesignChange"/>
/// <seealso cref="DesignComboBase.OnDesignChanged"/>
DesignCombo = -2,
/// <seealso cref="EditorHistory.OnDesignChanged" />
EditorHistory = -1000,
}
}

View file

@ -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;
/// </list>
/// </summary>
public sealed class StateChanged()
: EventWrapper<StateChangeType, StateSource, ActorState, ActorData, object?, StateChanged.Priority>(nameof(StateChanged))
: EventWrapper<StateChangeType, StateSource, ActorState, ActorData, ITransaction?, StateChanged.Priority>(nameof(StateChanged))
{
public enum Priority
{
GlamourerIpc = int.MinValue,
/// <seealso cref="Api.StateApi.OnStateChanged" />
GlamourerIpc = int.MinValue,
/// <seealso cref="Interop.Penumbra.PenumbraAutoRedraw.OnStateChanged" />
PenumbraAutoRedraw = 0,
/// <seealso cref="EditorHistory.OnStateChanged" />
EditorHistory = -1000,
}
}
}

View file

@ -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<Tuple<IDesignStandIn, s
TabSelected = tabSelected;
Config = config;
DesignColors = designColors;
DesignChanged.Subscribe(OnDesignChange, DesignChanged.Priority.DesignCombo);
DesignChanged.Subscribe(OnDesignChanged, DesignChanged.Priority.DesignCombo);
}
public bool Incognito
@ -37,7 +38,7 @@ public abstract class DesignComboBase : FilterComboCache<Tuple<IDesignStandIn, s
void IDisposable.Dispose()
{
DesignChanged.Unsubscribe(OnDesignChange);
DesignChanged.Unsubscribe(OnDesignChanged);
GC.SuppressFinalize(this);
}
@ -126,7 +127,7 @@ public abstract class DesignComboBase : FilterComboCache<Tuple<IDesignStandIn, s
return filter.IsContained(path) || filter.IsContained(design.ResolveName(false));
}
private void OnDesignChange(DesignChanged.Type type, Design design, object? data = null)
private void OnDesignChanged(DesignChanged.Type type, Design design, ITransaction? _ = null)
{
switch (type)
{

View file

@ -37,7 +37,7 @@ public struct EquipDrawData(EquipSlot slot, in DesignData designData)
{
var manager = (DesignManager)_editor;
var design = (Design)_object;
manager.ChangeApplyStain(design, Slot, value);
manager.ChangeApplyStains(design, Slot, value);
}
public EquipItem CurrentItem = designData.Item(slot);

View file

@ -5,6 +5,7 @@ using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game;
using Glamourer.Automation;
using Glamourer.Designs;
using Glamourer.Designs.History;
using Glamourer.Gui.Customization;
using Glamourer.Gui.Equipment;
using Glamourer.Gui.Materials;
@ -39,6 +40,7 @@ public class ActorPanel
private readonly DictModelChara _modelChara;
private readonly CustomizeParameterDrawer _parameterDrawer;
private readonly AdvancedDyePopup _advancedDyes;
private readonly EditorHistory _editorHistory;
private readonly HeaderDrawer.Button[] _leftButtons;
private readonly HeaderDrawer.Button[] _rightButtons;
@ -55,7 +57,8 @@ public class ActorPanel
ICondition conditions,
DictModelChara modelChara,
CustomizeParameterDrawer parameterDrawer,
AdvancedDyePopup advancedDyes)
AdvancedDyePopup advancedDyes,
EditorHistory editorHistory)
{
_selector = selector;
_stateManager = stateManager;
@ -71,11 +74,13 @@ public class ActorPanel
_modelChara = modelChara;
_parameterDrawer = parameterDrawer;
_advancedDyes = advancedDyes;
_editorHistory = editorHistory;
_leftButtons =
[
new SetFromClipboardButton(this),
new ExportToClipboardButton(this),
new SaveAsDesignButton(this),
new UndoButton(this),
];
_rightButtons =
[
@ -477,6 +482,24 @@ public class ActorPanel
}
}
private sealed class UndoButton(ActorPanel panel) : HeaderDrawer.Button
{
protected override string Description
=> "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

View file

@ -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<Design, Design
protected override bool FoldersDefaultOpen
=> _config.OpenFoldersByDefault;
private void OnDesignChange(DesignChanged.Type type, Design design, object? oldData)
private void OnDesignChange(DesignChanged.Type type, Design design, ITransaction? _)
{
switch (type)
{

View file

@ -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)

View file

@ -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<ActorState> _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);

View file

@ -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);
}
/// <inheritdoc/>
@ -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));
}
/// <inheritdoc/>
@ -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));
}
/// <inheritdoc/>
@ -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));
}
/// <inheritdoc/>
@ -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));
}
/// <inheritdoc/>
@ -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));
}
/// <inheritdoc/>
@ -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));
}
/// <inheritdoc/>
@ -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));
}
/// <inheritdoc/>
@ -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));
}
/// <inheritdoc/>
@ -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;