diff --git a/Glamourer.GameData/GameData.cs b/Glamourer.GameData/GameData.cs index 0af2def..9d255f1 100644 --- a/Glamourer.GameData/GameData.cs +++ b/Glamourer.GameData/GameData.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using Dalamud.Data; +using Lumina.Excel.GeneratedSheets; using Penumbra.GameData.Enums; namespace Glamourer @@ -10,6 +11,20 @@ namespace Glamourer { private static Dictionary? _stains; private static Dictionary>? _itemsBySlot; + private static SortedList? _models; + + public static IReadOnlyDictionary Models(DataManager dataManager) + { + if (_models != null) + return _models; + + var sheet = dataManager.GetExcelSheet()!; + + _models = new SortedList((int) sheet.RowCount); + foreach (var model in sheet.Where(m => m.Type != 0)) + _models.Add(model.RowId, model); + return _models; + } public static IReadOnlyDictionary Stains(DataManager dataManager) { @@ -40,7 +55,7 @@ namespace Glamourer [EquipSlot.Feet] = new(200) { EmptySlot(EquipSlot.Feet) }, [EquipSlot.RFinger] = new(200) { EmptySlot(EquipSlot.RFinger) }, [EquipSlot.Neck] = new(200) { EmptySlot(EquipSlot.Neck) }, - [EquipSlot.MainHand] = new(200) { EmptySlot(EquipSlot.MainHand) }, + [EquipSlot.MainHand] = new(1000) { EmptySlot(EquipSlot.MainHand) }, [EquipSlot.OffHand] = new(200) { EmptySlot(EquipSlot.OffHand) }, [EquipSlot.Wrists] = new(200) { EmptySlot(EquipSlot.Wrists) }, [EquipSlot.Ears] = new(200) { EmptySlot(EquipSlot.Ears) }, diff --git a/Glamourer/CharacterSave.cs b/Glamourer/CharacterSave.cs index a37341f..86163f5 100644 --- a/Glamourer/CharacterSave.cs +++ b/Glamourer/CharacterSave.cs @@ -1,8 +1,11 @@ using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; using Dalamud.Game.ClientState.Objects.Types; using Glamourer.Customization; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; namespace Glamourer @@ -129,6 +132,99 @@ namespace Glamourer } } + private static Dictionary Offsets() + { + var stainOffsetWeapon = (int) Marshal.OffsetOf("Stain"); + var stainOffsetEquip = (int) Marshal.OffsetOf("Stain"); + + (int, int, bool) ToOffsets(IntPtr offset, bool weapon) + { + var off = 4 + CharacterCustomization.CustomizationBytes + (int) offset; + return weapon ? (off, off + stainOffsetWeapon, weapon) : (off, off + stainOffsetEquip, weapon); + } + + return new Dictionary(12) + { + [EquipSlot.MainHand] = ToOffsets(Marshal.OffsetOf("MainHand"), true), + [EquipSlot.OffHand] = ToOffsets(Marshal.OffsetOf("OffHand"), true), + [EquipSlot.Head] = ToOffsets(Marshal.OffsetOf("Head"), false), + [EquipSlot.Body] = ToOffsets(Marshal.OffsetOf("Body"), false), + [EquipSlot.Hands] = ToOffsets(Marshal.OffsetOf("Hands"), false), + [EquipSlot.Legs] = ToOffsets(Marshal.OffsetOf("Legs"), false), + [EquipSlot.Feet] = ToOffsets(Marshal.OffsetOf("Feet"), false), + [EquipSlot.Ears] = ToOffsets(Marshal.OffsetOf("Ears"), false), + [EquipSlot.Neck] = ToOffsets(Marshal.OffsetOf("Neck"), false), + [EquipSlot.Wrists] = ToOffsets(Marshal.OffsetOf("Wrists"), false), + [EquipSlot.RFinger] = ToOffsets(Marshal.OffsetOf("RFinger"), false), + [EquipSlot.LFinger] = ToOffsets(Marshal.OffsetOf("LFinger"), false), + }; + } + + private static readonly IReadOnlyDictionary FieldOffsets = Offsets(); + + public bool WriteStain(EquipSlot slot, StainId stainId) + { + if (WriteProtected) + return false; + + var (_, stainOffset, _) = FieldOffsets[slot]; + if (_bytes[stainOffset] == (byte) stainId) + return false; + + _bytes[stainOffset] = stainId.Value; + return true; + } + + private bool WriteItem(int offset, SetId id, WeaponType type, ushort variant, bool weapon) + { + var idBytes = BitConverter.GetBytes(id.Value); + + static bool WriteIfDifferent(ref byte x, byte y) + { + if (x == y) + return false; + + x = y; + return true; + } + + var ret = WriteIfDifferent(ref _bytes[offset], idBytes[0]); + ret |= WriteIfDifferent(ref _bytes[offset + 1], idBytes[1]); + if (weapon) + { + var typeBytes = BitConverter.GetBytes(type.Value); + var variantBytes = BitConverter.GetBytes(variant); + ret |= WriteIfDifferent(ref _bytes[offset + 2], typeBytes[0]); + ret |= WriteIfDifferent(ref _bytes[offset + 3], typeBytes[1]); + ret |= WriteIfDifferent(ref _bytes[offset + 4], variantBytes[0]); + ret |= WriteIfDifferent(ref _bytes[offset + 5], variantBytes[1]); + } + else + { + ret |= WriteIfDifferent(ref _bytes[offset + 2], (byte) variant); + } + + return ret; + } + + public bool WriteItem(Item item) + { + if (WriteProtected) + return false; + + var (itemOffset, _, isWeapon) = FieldOffsets[item.EquippableTo]; + var (id, type, variant) = item.MainModel; + var ret = WriteItem(itemOffset, id, type, variant, isWeapon); + if (item.EquippableTo == EquipSlot.MainHand && item.HasSubModel) + { + var (subOffset, _, _) = FieldOffsets[EquipSlot.OffHand]; + var (subId, subType, subVariant) = item.SubModel; + ret |= WriteItem(subOffset, subId, subType, subVariant, true); + } + + return ret; + } + public unsafe float Alpha { get @@ -204,6 +300,8 @@ namespace Glamourer public void Apply(Character a) { + Glamourer.RevertableDesigns.Add(a); + if (WriteCustomizations) Customizations.Write(a.Address); if (WriteEquipment != CharacterEquipMask.None) diff --git a/Glamourer/Designs/RevertableDesigns.cs b/Glamourer/Designs/RevertableDesigns.cs new file mode 100644 index 0000000..99d9e9b --- /dev/null +++ b/Glamourer/Designs/RevertableDesigns.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using Dalamud.Game.ClientState.Objects.Types; + +namespace Glamourer.Designs +{ + public class RevertableDesigns + { + public readonly Dictionary Saves = new(); + + public bool Add(Character actor) + { + var name = actor.Name.ToString(); + if (Saves.TryGetValue(name, out var save)) + return false; + + save = new CharacterSave(); + save.LoadCharacter(actor); + Saves[name] = save; + return true; + } + + public bool Revert(Character actor) + { + if (!Saves.TryGetValue(actor.Name.ToString(), out var save)) + return false; + + save.Apply(actor); + return true; + } + } +} diff --git a/Glamourer/Glamourer.cs b/Glamourer/Glamourer.cs index 73dc9c3..c411068 100644 --- a/Glamourer/Glamourer.cs +++ b/Glamourer/Glamourer.cs @@ -26,6 +26,7 @@ namespace Glamourer private readonly Interface _interface; public readonly DesignManager Designs; public readonly FixedDesigns FixedDesigns; + public static RevertableDesigns RevertableDesigns = new(); public static string Version = string.Empty; @@ -41,7 +42,7 @@ namespace Glamourer Penumbra = new PenumbraAttach(Config.AttachToPenumbra); PlayerWatcher = PlayerWatchFactory.Create(Dalamud.Framework, Dalamud.ClientState, Dalamud.Objects); FixedDesigns = new FixedDesigns(Designs); - + if (Config.ApplyFixedDesigns) PlayerWatcher.Enable(); diff --git a/Glamourer/Gui/Interface.cs b/Glamourer/Gui/Interface.cs index f9c6ce0..139b938 100644 --- a/Glamourer/Gui/Interface.cs +++ b/Glamourer/Gui/Interface.cs @@ -2,8 +2,11 @@ using System.Collections.Generic; using System.Linq; using System.Numerics; +using System.Reflection; +using Dalamud.Game.ClientState.Objects.Types; using Glamourer.Designs; using ImGuiNET; +using Lumina.Excel.GeneratedSheets; using Penumbra.GameData; using Penumbra.GameData.Enums; @@ -13,11 +16,12 @@ namespace Glamourer.Gui { public const float SelectorWidth = 200; public const float MinWindowWidth = 675; - public const int GPoseObjectId = 201; + public const int GPoseObjectId = 201; private const string PluginName = "Glamourer"; private readonly string _glamourerHeader; private readonly IReadOnlyDictionary _stains; + private readonly IReadOnlyDictionary _models; private readonly IObjectIdentifier _identifier; private readonly Dictionary, ComboWithFilter)> _combos; private readonly ImGuiScene.TextureWrap? _legacyTattooIcon; @@ -39,11 +43,18 @@ namespace Glamourer.Gui Dalamud.PluginInterface.UiBuilder.Draw += Draw; Dalamud.PluginInterface.UiBuilder.OpenConfigUi += ToggleVisibility; + _characterConstructor = typeof(Character).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, new[] + { + typeof(IntPtr), + }, null)!; + _equipSlotNames = GetEquipSlotNames(); _stains = GameData.Stains(Dalamud.GameData); + _models = GameData.Models(Dalamud.GameData); _identifier = Penumbra.GameData.GameData.GetIdentifier(Dalamud.GameData, Dalamud.ClientState.ClientLanguage); + var stainCombo = CreateDefaultStainCombo(_stains.Values.ToArray()); var equip = GameData.ItemsBySlot(Dalamud.GameData); @@ -93,6 +104,7 @@ namespace Glamourer.Gui DrawSaves(); DrawFixedDesignsTab(); DrawConfigTab(); + DrawRevertablesTab(); } finally { diff --git a/Glamourer/Gui/InterfaceActorPanel.cs b/Glamourer/Gui/InterfaceActorPanel.cs index d16bf03..3c873f4 100644 --- a/Glamourer/Gui/InterfaceActorPanel.cs +++ b/Glamourer/Gui/InterfaceActorPanel.cs @@ -1,9 +1,13 @@ using System; using System.Linq; using System.Numerics; +using System.Reflection; +using Dalamud.Game.ClientState.Objects.Enums; +using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Interface; using Dalamud.Logging; +using Glamourer.Customization; using Glamourer.Designs; using Glamourer.FileSystem; using ImGuiNET; @@ -12,12 +16,13 @@ namespace Glamourer.Gui { internal partial class Interface { - private readonly CharacterSave _currentSave = new(); - private string _newDesignName = string.Empty; - private bool _keyboardFocus; - private const string DesignNamePopupLabel = "Save Design As..."; - private const uint RedHeaderColor = 0xFF1818C0; - private const uint GreenHeaderColor = 0xFF18C018; + private readonly CharacterSave _currentSave = new(); + private string _newDesignName = string.Empty; + private bool _keyboardFocus; + private const string DesignNamePopupLabel = "Save Design As..."; + private const uint RedHeaderColor = 0xFF1818C0; + private const uint GreenHeaderColor = 0xFF18C018; + private readonly ConstructorInfo _characterConstructor; private void DrawPlayerHeader() { @@ -30,7 +35,7 @@ namespace Glamourer.Gui .PushColor(ImGuiCol.ButtonActive, buttonColor) .PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero) .PushStyle(ImGuiStyleVar.FrameRounding, 0); - ImGui.Button($"{_currentPlayerName}##playerHeader", -Vector2.UnitX * 0.0001f); + ImGui.Button($"{_currentLabel}##playerHeader", -Vector2.UnitX * 0.0001f); } private static void DrawCopyClipboardButton(CharacterSave save) @@ -109,22 +114,81 @@ namespace Glamourer.Gui Glamourer.Penumbra.UpdateCharacters(player, fallback); } + private const int ModelTypeOffset = 0x01B4; + + private static unsafe int ModelType(GameObject actor) + => *(int*) (actor.Address + ModelTypeOffset); + + private static unsafe void SetModelType(GameObject actor, int value) + => *(int*) (actor.Address + ModelTypeOffset) = value; + + private Character Character(IntPtr address) + => (Character) _characterConstructor.Invoke(new object[] + { + address, + }); + + private Character? CreateCharacter(GameObject? actor) + { + if (actor == null) + return null; + + return actor switch + { + PlayerCharacter p => p, + BattleChara b => b, + _ => actor.ObjectKind switch + { + ObjectKind.BattleNpc => Character(actor.Address), + ObjectKind.Companion => Character(actor.Address), + ObjectKind.EventNpc => Character(actor.Address), + _ => null, + }, + }; + } + + + private static Character? TransformToCustomizable(Character? actor) + { + if (actor == null) + return null; + + if (ModelType(actor) == 0) + return actor; + + SetModelType(actor, 0); + CharacterCustomization.Default.Write(actor.Address); + return actor; + } + private void DrawApplyToTargetButton(CharacterSave save) { if (!ImGui.Button("Apply to Target")) return; - var player = Dalamud.Targets.Target as Character; + var player = TransformToCustomizable(CreateCharacter(Dalamud.Targets.Target)); if (player == null) return; - var fallBackCharacter = _playerNames[player.Name.ToString()]; + var fallBackCharacter = _gPoseActors.TryGetValue(player.Name.ToString(), out var f) ? f : null; save.Apply(player); if (fallBackCharacter != null) save.Apply(fallBackCharacter); Glamourer.Penumbra.UpdateCharacters(player, fallBackCharacter); } + private void DrawRevertButton() + { + if (!DrawDisableButton("Revert", _player == null)) + return; + + Glamourer.RevertableDesigns.Revert(_player!); + var fallBackCharacter = _gPoseActors.TryGetValue(_player!.Name.ToString(), out var f) ? f : null; + if (fallBackCharacter != null) + Glamourer.RevertableDesigns.Revert(fallBackCharacter); + Glamourer.Penumbra.UpdateCharacters(_player, fallBackCharacter); + } + private void SaveNewDesign(CharacterSave save) { try @@ -144,13 +208,42 @@ namespace Glamourer.Gui } } - private void DrawPlayerPanel() + private void DrawMonsterPanel() { - ImGui.BeginGroup(); - DrawPlayerHeader(); - if (!ImGui.BeginChild("##playerData", -Vector2.One, true)) + if (DrawApplyClipboardButton()) + Glamourer.Penumbra.UpdateCharacters(_player!); + + ImGui.SameLine(); + if (ImGui.Button("Convert to Character")) + { + TransformToCustomizable(_player); + _currentLabel = _currentLabel.Replace("(Monster)", "(NPC)"); + Glamourer.Penumbra.UpdateCharacters(_player!); + } + + if (!_inGPose) + { + ImGui.SameLine(); + DrawTargetPlayerButton(); + } + + var currentModel = ModelType(_player!); + using var raii = new ImGuiRaii(); + if (!raii.Begin(() => ImGui.BeginCombo("Model Id", currentModel.ToString()), ImGui.EndCombo)) return; + foreach (var (id, _) in _models.Skip(1)) + { + if (!ImGui.Selectable($"{id:D6}##models", id == currentModel) || id == currentModel) + continue; + + SetModelType(_player!, (int) id); + Glamourer.Penumbra.UpdateCharacters(_player!); + } + } + + private void DrawPlayerPanel() + { DrawCopyClipboardButton(_currentSave); ImGui.SameLine(); var changes = DrawApplyClipboardButton(); @@ -169,9 +262,12 @@ namespace Glamourer.Gui } } + ImGui.SameLine(); + DrawRevertButton(); if (DrawCustomization(ref _currentSave.Customizations) && _player != null) { + Glamourer.RevertableDesigns.Add(_player); _currentSave.Customizations.Write(_player.Address); changes = true; } @@ -181,8 +277,24 @@ namespace Glamourer.Gui if (_player != null && changes) Glamourer.Penumbra.UpdateCharacters(_player); + } + + private void DrawActorPanel() + { + using var raii = ImGuiRaii.NewGroup(); + DrawPlayerHeader(); + if (!ImGui.BeginChild("##playerData", -Vector2.One, true)) + { + ImGui.EndChild(); + return; + } + + if (_player == null || ModelType(_player) == 0) + DrawPlayerPanel(); + else + DrawMonsterPanel(); + ImGui.EndChild(); - ImGui.EndGroup(); } } } diff --git a/Glamourer/Gui/InterfaceActorSelector.cs b/Glamourer/Gui/InterfaceActorSelector.cs index 2d4b6ba..10f5fea 100644 --- a/Glamourer/Gui/InterfaceActorSelector.cs +++ b/Glamourer/Gui/InterfaceActorSelector.cs @@ -10,11 +10,12 @@ namespace Glamourer.Gui { internal partial class Interface { - private Character? _player; - private string _currentPlayerName = string.Empty; - private string _playerFilter = string.Empty; - private string _playerFilterLower = string.Empty; - private readonly Dictionary _playerNames = new(400); + private Character? _player; + private string _currentLabel = string.Empty; + private string _playerFilter = string.Empty; + private string _playerFilterLower = string.Empty; + private readonly Dictionary _playerNames = new(100); + private readonly Dictionary _gPoseActors = new(48); private void DrawPlayerFilter() { @@ -26,35 +27,66 @@ namespace Glamourer.Gui _playerFilterLower = _playerFilter.ToLowerInvariant(); } - private void DrawPlayerSelectable(Character player, bool gPose) + private void DrawGPoseSelectable(Character player) { var playerName = player.Name.ToString(); if (!playerName.Any()) return; - if (_playerNames.ContainsKey(playerName)) + _gPoseActors[playerName] = null; + + DrawSelectable(player, $"{playerName} (GPose)"); + } + + private static string GetLabel(Character player, string playerName, int num) + { + if (player.ObjectKind == ObjectKind.Player) + return num == 1 ? playerName : $"{playerName} #{num}"; + + if (ModelType(player) == 0) + return num == 1 ? $"{playerName} (NPC)" : $"{playerName} #{num} (NPC)"; + + return num == 1 ? $"{playerName} (Monster)" : $"{playerName} #{num} (Monster)"; + } + + private void DrawPlayerSelectable(Character player) + { + var playerName = player.Name.ToString(); + if (!playerName.Any()) + return; + + if (_playerNames.TryGetValue(playerName, out var num)) + _playerNames[playerName] = ++num; + else + _playerNames[playerName] = num = 1; + + if (_gPoseActors.ContainsKey(playerName)) { - _playerNames[playerName] = player; + _gPoseActors[playerName] = player; return; } - _playerNames.Add(playerName, null); + var label = GetLabel(player, playerName, num); + DrawSelectable(player, label); + } - var label = gPose ? $"{playerName} (GPose)" : playerName; - if (!_playerFilterLower.Any() || playerName.ToLowerInvariant().Contains(_playerFilterLower)) - if (ImGui.Selectable(label, _currentPlayerName == playerName)) + + private void DrawSelectable(Character player, string label) + { + if (!_playerFilterLower.Any() || label.ToLowerInvariant().Contains(_playerFilterLower)) + if (ImGui.Selectable(label, _currentLabel == label)) { - _currentPlayerName = playerName; + _currentLabel = label; _currentSave.LoadCharacter(player); _player = player; return; } - if (_currentPlayerName == playerName) - { - _currentSave.LoadCharacter(player); - _player = player; - } + if (_currentLabel != label) + return; + + _currentSave.LoadCharacter(player); + _player = player; } private void DrawSelectionButtons() @@ -64,7 +96,7 @@ namespace Glamourer.Gui .PushStyle(ImGuiStyleVar.FrameRounding, 0) .PushFont(UiBuilder.IconFont); Character? select = null; - var buttonWidth = Vector2.UnitX * SelectorWidth / 2; + var buttonWidth = Vector2.UnitX * SelectorWidth / 2; if (ImGui.Button(FontAwesomeIcon.UserCircle.ToIconString(), buttonWidth)) select = Dalamud.ClientState.LocalPlayer; raii.PopFonts(); @@ -81,18 +113,18 @@ namespace Glamourer.Gui else { if (ImGui.Button(FontAwesomeIcon.HandPointer.ToIconString(), buttonWidth)) - select = Dalamud.Targets.Target as Character; + select = CreateCharacter(Dalamud.Targets.Target); } raii.PopFonts(); if (ImGui.IsItemHovered()) - ImGui.SetTooltip("Select the current target, if it is a player object."); + ImGui.SetTooltip("Select the current target, if it is in the list."); - if (select == null || select.ObjectKind != ObjectKind.Player) + if (select == null) return; - _player = select; - _currentPlayerName = _player.Name.ToString(); + _player = select; + _currentLabel = _player.Name.ToString(); _currentSave.LoadCharacter(_player); } @@ -102,24 +134,35 @@ namespace Glamourer.Gui DrawPlayerFilter(); if (!ImGui.BeginChild("##playerSelector", new Vector2(SelectorWidth * ImGui.GetIO().FontGlobalScale, -ImGui.GetFrameHeight() - 1), true)) + { + ImGui.EndChild(); + ImGui.EndGroup(); return; + } _playerNames.Clear(); + _gPoseActors.Clear(); for (var i = GPoseObjectId; i < GPoseObjectId + 48; ++i) { - var player = Dalamud.Objects[i] as Character; + var player = CreateCharacter(Dalamud.Objects[i]); if (player == null) break; - if (player.ObjectKind == ObjectKind.Player) - DrawPlayerSelectable(player, true); + DrawGPoseSelectable(player); } - for (var i = 0; i < GPoseObjectId; i += 2) + for (var i = 0; i < GPoseObjectId; ++i) { - var player = Dalamud.Objects[i] as Character; - if (player != null && player.ObjectKind == ObjectKind.Player) - DrawPlayerSelectable(player, false); + var player = CreateCharacter(Dalamud.Objects[i])!; + if (player != null) + DrawPlayerSelectable(player); + } + + for (var i = GPoseObjectId + 48; i < Dalamud.Objects.Length; ++i) + { + var player = CreateCharacter(Dalamud.Objects[i])!; + if (player != null) + DrawPlayerSelectable(player); } @@ -135,17 +178,17 @@ namespace Glamourer.Gui private void DrawPlayerTab() { using var raii = new ImGuiRaii(); + _player = null; if (!raii.Begin(() => ImGui.BeginTabItem("Current Players"), ImGui.EndTabItem)) return; - _player = null; DrawPlayerSelector(); - if (!_currentPlayerName.Any()) + if (!_currentLabel.Any()) return; ImGui.SameLine(); - DrawPlayerPanel(); + DrawActorPanel(); } } } diff --git a/Glamourer/Gui/InterfaceDesigns.cs b/Glamourer/Gui/InterfaceDesigns.cs index 2630eb7..557feb3 100644 --- a/Glamourer/Gui/InterfaceDesigns.cs +++ b/Glamourer/Gui/InterfaceDesigns.cs @@ -13,6 +13,7 @@ namespace Glamourer.Gui { private int _totalObject; + private bool _inDesignMode; private Design? _selection; private string _newChildName = string.Empty; @@ -207,7 +208,8 @@ namespace Glamourer.Gui { using var raii = new ImGuiRaii(); raii.PushStyle(ImGuiStyleVar.IndentSpacing, 12.5f * ImGui.GetIO().FontGlobalScale); - if (!raii.Begin(() => ImGui.BeginTabItem("Saves"), ImGui.EndTabItem)) + _inDesignMode = raii.Begin(() => ImGui.BeginTabItem("Designs"), ImGui.EndTabItem); + if (!_inDesignMode) return; DrawDesignSelector(); @@ -278,7 +280,7 @@ namespace Glamourer.Gui private void ContextMenu(IFileSystemBase child) { - var label = $"##fsPopup{child.FullName()}"; + var label = $"##fsPopup{child.FullName()}"; if (ImGui.BeginPopup(label)) { if (ImGui.MenuItem("Delete")) diff --git a/Glamourer/Gui/InterfaceEquipment.cs b/Glamourer/Gui/InterfaceEquipment.cs index e63059d..d9cc282 100644 --- a/Glamourer/Gui/InterfaceEquipment.cs +++ b/Glamourer/Gui/InterfaceEquipment.cs @@ -15,10 +15,17 @@ namespace Glamourer.Gui stainCombo.PostPreview = () => ImGui.PopStyleColor(previewPush); } - if (stainCombo.Draw(string.Empty, out var newStain) && _player != null && !newStain.RowIndex.Equals(stainIdx)) + if (stainCombo.Draw(string.Empty, out var newStain) && !newStain.RowIndex.Equals(stainIdx)) { - newStain.Write(_player.Address, slot); - return true; + if (_player != null) + { + Glamourer.RevertableDesigns.Add(_player); + newStain.Write(_player.Address, slot); + return true; + } + + if (_inDesignMode && (_selection?.Data.WriteStain(slot, newStain.RowIndex) ?? false)) + return true; } return false; @@ -27,10 +34,17 @@ namespace Glamourer.Gui private bool DrawItemSelector(ComboWithFilter equipCombo, Lumina.Excel.GeneratedSheets.Item? item) { var currentName = item?.Name.ToString() ?? "Nothing"; - if (equipCombo.Draw(currentName, out var newItem, _itemComboWidth) && _player != null && newItem.Base.RowId != item?.RowId) + if (equipCombo.Draw(currentName, out var newItem, _itemComboWidth) && newItem.Base.RowId != item?.RowId) { - newItem.Write(_player.Address); - return true; + if (_player != null) + { + Glamourer.RevertableDesigns.Add(_player); + newItem.Write(_player.Address); + return true; + } + + if (_inDesignMode && (_selection?.Data.WriteItem(newItem) ?? false)) + return true; } return false; diff --git a/Glamourer/Gui/InterfaceMiscellaneous.cs b/Glamourer/Gui/InterfaceMiscellaneous.cs index 87a4495..c589139 100644 --- a/Glamourer/Gui/InterfaceMiscellaneous.cs +++ b/Glamourer/Gui/InterfaceMiscellaneous.cs @@ -18,6 +18,17 @@ namespace Glamourer.Gui return false; } + private static bool DrawDisableButton(string label, bool disabled) + { + if (!disabled) + return ImGui.Button(label); + + using var raii = new ImGuiRaii(); + raii.PushStyle(ImGuiStyleVar.Alpha, 0.5f); + ImGui.Button(label); + return false; + } + private static bool DrawMiscellaneous(CharacterSave save, Character? player) { var ret = false; diff --git a/Glamourer/Gui/InterfaceRevertables.cs b/Glamourer/Gui/InterfaceRevertables.cs new file mode 100644 index 0000000..d0b2686 --- /dev/null +++ b/Glamourer/Gui/InterfaceRevertables.cs @@ -0,0 +1,88 @@ +using System.Diagnostics; +using System.Linq; +using System.Numerics; +using ImGuiNET; + +namespace Glamourer.Gui +{ + internal partial class Interface + { + private string? _currentRevertableName; + private CharacterSave? _currentRevertable; + + private void DrawRevertablesSelector() + { + ImGui.BeginGroup(); + DrawPlayerFilter(); + if (!ImGui.BeginChild("##playerSelector", + new Vector2(SelectorWidth * ImGui.GetIO().FontGlobalScale, -ImGui.GetFrameHeight() - 1), true)) + { + ImGui.EndChild(); + ImGui.EndGroup(); + return; + } + + foreach (var (name, save) in Glamourer.RevertableDesigns.Saves) + { + if (name.ToLowerInvariant().Contains(_playerFilterLower) && ImGui.Selectable(name, name == _currentRevertableName)) + { + _currentRevertableName = name; + _currentRevertable = save; + } + } + + using (var _ = new ImGuiRaii().PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero)) + { + ImGui.EndChild(); + } + + DrawSelectionButtons(); + ImGui.EndGroup(); + } + + private void DrawRevertablePanel() + { + using var group = ImGuiRaii.NewGroup(); + { + var buttonColor = ImGui.GetColorU32(ImGuiCol.FrameBg); + using var raii = new ImGuiRaii() + .PushColor(ImGuiCol.Text, GreenHeaderColor) + .PushColor(ImGuiCol.Button, buttonColor) + .PushColor(ImGuiCol.ButtonHovered, buttonColor) + .PushColor(ImGuiCol.ButtonActive, buttonColor) + .PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero) + .PushStyle(ImGuiStyleVar.FrameRounding, 0); + ImGui.Button($"{_currentRevertableName}##playerHeader", -Vector2.UnitX * 0.0001f); + } + + if (!ImGui.BeginChild("##revertableData", -Vector2.One, true)) + { + ImGui.EndChild(); + return; + } + + var save = _currentRevertable!.Copy(); + DrawCustomization(ref save.Customizations); + DrawEquip(save.Equipment); + DrawMiscellaneous(save, null); + + ImGui.EndChild(); + } + + [Conditional("DEBUG")] + private void DrawRevertablesTab() + { + using var raii = new ImGuiRaii(); + if (!raii.Begin(() => ImGui.BeginTabItem("Revertables"), ImGui.EndTabItem)) + return; + + DrawRevertablesSelector(); + + if (_currentRevertableName == null) + return; + + ImGui.SameLine(); + DrawRevertablePanel(); + } + } +}