diff --git a/Glamourer.GameData/Customization/CustomizeIndex.cs b/Glamourer.GameData/Customization/CustomizeIndex.cs index 0147c79..0976370 100644 --- a/Glamourer.GameData/Customization/CustomizeIndex.cs +++ b/Glamourer.GameData/Customization/CustomizeIndex.cs @@ -44,6 +44,8 @@ public enum CustomizeIndex : byte public static class CustomizationExtensions { + public const int NumIndices = ((int)CustomizeIndex.FacePaintColor + 1); + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] public static (int ByteIdx, byte Mask) ToByteAndMask(this CustomizeIndex index) => index switch diff --git a/Glamourer.GameData/Structs/EquipFlag.cs b/Glamourer.GameData/Structs/EquipFlag.cs index 52a6f48..81590f0 100644 --- a/Glamourer.GameData/Structs/EquipFlag.cs +++ b/Glamourer.GameData/Structs/EquipFlag.cs @@ -34,7 +34,8 @@ public enum EquipFlag : uint public static class EquipFlagExtensions { - public const EquipFlag All = (EquipFlag)(((uint)EquipFlag.OffhandStain << 1) - 1); + public const EquipFlag All = (EquipFlag)(((uint)EquipFlag.OffhandStain << 1) - 1); + public const int NumEquipFlags = 24; public static EquipFlag ToFlag(this EquipSlot slot) => slot switch diff --git a/Glamourer/Configuration.cs b/Glamourer/Configuration.cs index a3f9e55..ed3ea12 100644 --- a/Glamourer/Configuration.cs +++ b/Glamourer/Configuration.cs @@ -7,6 +7,7 @@ using Dalamud.Interface.Internal.Notifications; using Glamourer.Gui; using Glamourer.Services; using Newtonsoft.Json; +using OtterGui.Classes; using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs; namespace Glamourer; @@ -15,6 +16,7 @@ public class Configuration : IPluginConfiguration, ISavable { public bool UseRestrictedGearProtection { get; set; } = true; public MainWindow.TabType SelectedTab { get; set; } = MainWindow.TabType.Settings; + public DoubleModifier DeleteDesignModifier { get; set; } = new(ModifierHotkey.Control, ModifierHotkey.Shift); #if DEBUG diff --git a/Glamourer/Designs/DesignData.cs b/Glamourer/Designs/DesignData.cs index 0d92c8e..e09be27 100644 --- a/Glamourer/Designs/DesignData.cs +++ b/Glamourer/Designs/DesignData.cs @@ -1,7 +1,5 @@ using System; using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using FFXIVClientStructs.FFXIV.Client.Game.Gauge; using Glamourer.Customization; using Glamourer.Services; using Penumbra.GameData.Enums; @@ -161,7 +159,7 @@ public unsafe struct DesignData } public readonly bool IsWeaponVisible() - => (_states & 0x08) == 0x09; + => (_states & 0x08) == 0x08; public bool SetWeaponVisible(bool value) { diff --git a/Glamourer/Designs/DesignFileSystem.cs b/Glamourer/Designs/DesignFileSystem.cs index 4c6e130..d0eb79d 100644 --- a/Glamourer/Designs/DesignFileSystem.cs +++ b/Glamourer/Designs/DesignFileSystem.cs @@ -24,7 +24,7 @@ public sealed class DesignFileSystem : FileSystem, IDisposable, ISavable _designManager = designManager; _saveService = saveService; _designChanged = designChanged; - _designChanged.Subscribe(OnDataChange, DesignChanged.Priority.DesignFileSystem); + _designChanged.Subscribe(OnDesignChange, DesignChanged.Priority.DesignFileSystem); Changed += OnChange; Reload(); } @@ -39,7 +39,7 @@ public sealed class DesignFileSystem : FileSystem, IDisposable, ISavable public void Dispose() { - _designChanged.Unsubscribe(OnDataChange); + _designChanged.Unsubscribe(OnDesignChange); } public struct CreationDate : ISortMode @@ -96,7 +96,7 @@ public sealed class DesignFileSystem : FileSystem, IDisposable, ISavable _saveService.QueueSave(this); } - private void OnDataChange(DesignChanged.Type type, Design design, object? data) + private void OnDesignChange(DesignChanged.Type type, Design design, object? data) { switch (type) { diff --git a/Glamourer/Designs/DesignManager.cs b/Glamourer/Designs/DesignManager.cs index f8bc71c..df4c1ad 100644 --- a/Glamourer/Designs/DesignManager.cs +++ b/Glamourer/Designs/DesignManager.cs @@ -202,7 +202,9 @@ public class DesignManager break; default: - if (!design.DesignData.Customize.Set(idx, value)) + if (!_customizations.IsCustomizationValid(design.DesignData.Customize.Clan, design.DesignData.Customize.Gender, + design.DesignData.Customize.Face, idx, value) + || !design.DesignData.Customize.Set(idx, value)) return; break; @@ -228,7 +230,7 @@ public class DesignManager /// Change a non-weapon equipment piece. public void ChangeEquip(Design design, EquipSlot slot, EquipItem item) { - if (_items.ValidateItem(slot, item.Id, out item).Length > 0) + if (!_items.IsItemValid(slot, item.Id, out item)) return; var old = design.DesignData.Item(slot); @@ -250,32 +252,31 @@ public class DesignManager { case EquipSlot.MainHand: var newOff = currentOff; - if (item.Type == currentMain.Type) - { - if (_items.ValidateWeapons(item.Id, currentOff.Id, out _, out _).Length != 0) - return; - } - else + if (!_items.IsItemValid(EquipSlot.MainHand, item.Id, out item)) + return; + + if (item.Type != currentMain.Type) { + var newOffId = FullEquipTypeExtensions.OffhandTypes.Contains(item.Type) ? item.Id : ItemManager.NothingId(item.Type.Offhand()); - if (_items.ValidateWeapons(item.Id, newOffId, out _, out newOff).Length != 0) + if (!_items.IsOffhandValid(item, newOffId, out newOff)) return; } - design.DesignData.SetItem(EquipSlot.MainHand, item); - design.DesignData.SetItem(EquipSlot.OffHand, newOff); + if (!design.DesignData.SetItem(EquipSlot.MainHand, item) && !design.DesignData.SetItem(EquipSlot.OffHand, newOff)) + return; + design.LastEdit = DateTimeOffset.UtcNow; _saveService.QueueSave(design); Glamourer.Log.Debug( $"Set {EquipSlot.MainHand.ToName()} weapon in design {design.Identifier} from {currentMain.Name} ({currentMain.Id}) to {item.Name} ({item.Id})."); _event.Invoke(DesignChanged.Type.Weapon, design, (currentMain, currentOff, item, newOff)); + return; case EquipSlot.OffHand: - if (item.Type != currentOff.Type) - return; - if (_items.ValidateWeapons(currentMain.Id, item.Id, out _, out _).Length > 0) + if (!_items.IsOffhandValid(currentOff.Type, item.Id, out item)) return; if (!design.DesignData.SetItem(EquipSlot.OffHand, item)) diff --git a/Glamourer/Events/DesignChanged.cs b/Glamourer/Events/DesignChanged.cs index bce39f1..27f9e6e 100644 --- a/Glamourer/Events/DesignChanged.cs +++ b/Glamourer/Events/DesignChanged.cs @@ -60,11 +60,18 @@ public sealed class DesignChanged : EventWrapper An existing design changed whether a specific stain is applied. Data is the slot of the equipment [EquipSlot]. ApplyStain, + + /// An existing design changed one of the meta flags. Data is null. + Other, } public enum Priority { + /// DesignFileSystem = 0, + + /// + DesignFileSystemSelector = -1, } public DesignChanged() diff --git a/Glamourer/Events/StateChanged.cs b/Glamourer/Events/StateChanged.cs new file mode 100644 index 0000000..ee86283 --- /dev/null +++ b/Glamourer/Events/StateChanged.cs @@ -0,0 +1,55 @@ +using System; +using Glamourer.Interop.Structs; +using Glamourer.State; +using OtterGui.Classes; +using Penumbra.GameData.Actors; + +namespace Glamourer.Events; + +/// +/// Triggered when a Design is edited in any way. +/// +/// Parameter is the type of the change +/// Parameter is the changed saved state. +/// Parameter is the existing actors using this saved state. +/// Parameter is any additional data depending on the type of change. +/// +/// +public sealed class StateChanged : EventWrapper, StateChanged.Priority> +{ + public enum Type + { + /// A characters saved state had a customization value changed. Data is the old value, the new value and the type. [(CustomizeValue, CustomizeValue, CustomizeIndex)]. + Customize, + + /// A characters saved state had an equipment piece changed. Data is the old value, the new value and the slot [(EquipItem, EquipItem, EquipSlot)]. + Equip, + + /// A characters saved state had its weapons changed. Data is the old mainhand, the old offhand, the new mainhand and the new offhand [(EquipItem, EquipItem, EquipItem, EquipItem)]. + Weapon, + + /// A characters saved state had a stain changed. Data is the old stain id, the new stain id and the slot [(StainId, StainId, EquipSlot)]. + Stain, + + /// A characters saved state had a meta toggle changed. Data is the old stain id, the new stain id and the slot [(StainId, StainId, EquipSlot)]. + Other, + } + + public enum Source : byte + { + Game, + Manual, + Fixed, + } + + public enum Priority + { + } + + public StateChanged() + : base(nameof(StateChanged)) + { } + + public void Invoke(Type type, Source source, ActorState state, ActorData actors, object? data = null) + => Invoke(this, type, source, state, actors, data); +} diff --git a/Glamourer/Events/UpdatedSlot.cs b/Glamourer/Events/UpdatedSlot.cs index b967416..14581f6 100644 --- a/Glamourer/Events/UpdatedSlot.cs +++ b/Glamourer/Events/UpdatedSlot.cs @@ -18,7 +18,10 @@ namespace Glamourer.Events; public sealed class UpdatedSlot : EventWrapper, Ref>, UpdatedSlot.Priority> { public enum Priority - { } + { + /// + StateManager = 0, + } public UpdatedSlot() : base(nameof(UpdatedSlot)) diff --git a/Glamourer/Gui/Colors.cs b/Glamourer/Gui/Colors.cs index ae5b2da..daa23a1 100644 --- a/Glamourer/Gui/Colors.cs +++ b/Glamourer/Gui/Colors.cs @@ -7,22 +7,26 @@ public enum ColorId CustomizationDesign, StateDesign, EquipmentDesign, + ActorAvailable, + ActorUnavailable, } public static class Colors { - public const uint DiscordColor = 0xFFDA8972; - public const uint ReniColorButton = 0xFFCC648D; - public const uint ReniColorHovered = 0xFFB070B0; - public const uint ReniColorActive = 0xFF9070E0; + public const uint DiscordColor = 0xFFDA8972; + public const uint ReniColorButton = 0xFFCC648D; + public const uint ReniColorHovered = 0xFFB070B0; + public const uint ReniColorActive = 0xFF9070E0; public static (uint DefaultColor, string Name, string Description) Data(this ColorId color) => color switch { // @formatter:off - ColorId.CustomizationDesign => (0xFFC000C0, "Customization Design", "A design that only changes customizations on a character." ), - ColorId.StateDesign => (0xFF00C0C0, "State Design", "A design that only changes meta state on a character." ), - ColorId.EquipmentDesign => (0xFF00C000, "Equipment Design", "A design that only changes equipment on a character." ), + ColorId.CustomizationDesign => (0xFFC000C0, "Customization Design", "A design that only changes customizations on a character." ), + ColorId.StateDesign => (0xFF00C0C0, "State Design", "A design that only changes meta state on a character." ), + ColorId.EquipmentDesign => (0xFF00C000, "Equipment Design", "A design that only changes equipment on a character." ), + ColorId.ActorAvailable => (0xFF18C018, "Actor Available", "The header in the Actor tab panel if the currently selected actor exists in the game world at least once." ), + ColorId.ActorUnavailable => (0xFF1818C0, "Actor Unavailable", "The Header in the Actor tab panel if the currently selected actor does not exist in the game world." ), _ => (0x00000000, string.Empty, string.Empty ), // @formatter:on }; diff --git a/Glamourer/Gui/Customization/CustomizationDrawer.Color.cs b/Glamourer/Gui/Customization/CustomizationDrawer.Color.cs new file mode 100644 index 0000000..b68624e --- /dev/null +++ b/Glamourer/Gui/Customization/CustomizationDrawer.Color.cs @@ -0,0 +1,64 @@ +using System; +using System.Numerics; +using Glamourer.Customization; +using ImGuiNET; +using OtterGui.Raii; + +namespace Glamourer.Gui.Customization; + +public partial class CustomizationDrawer +{ + private const string ColorPickerPopupName = "ColorPicker"; + + private void DrawColorPicker(CustomizeIndex index) + { + using var _ = SetId(index); + var (current, custom) = GetCurrentCustomization(index); + var color = ImGui.ColorConvertU32ToFloat4(custom.Color); + + // Print 1-based index instead of 0. + if (ImGui.ColorButton($"{current + 1}##color", color, ImGuiColorEditFlags.None, _framedIconSize)) + ImGui.OpenPopup(ColorPickerPopupName); + + ImGui.SameLine(); + + using (var group = ImRaii.Group()) + { + DataInputInt(current); + ImGui.TextUnformatted(_currentOption); + } + DrawColorPickerPopup(); + } + + private void DrawColorPickerPopup() + { + using var popup = ImRaii.Popup(ColorPickerPopupName, ImGuiWindowFlags.AlwaysAutoResize); + if (!popup) + return; + + using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero) + .Push(ImGuiStyleVar.FrameRounding, 0); + for (var i = 0; i < _currentCount; ++i) + { + var custom = _set.Data(_currentIndex, i, _customize[CustomizeIndex.Face]); + if (ImGui.ColorButton((i + 1).ToString(), ImGui.ColorConvertU32ToFloat4(custom.Color))) + { + UpdateValue(custom.Value); + ImGui.CloseCurrentPopup(); + } + + if (i % 8 != 7) + ImGui.SameLine(); + } + } + + // Obtain the current customization and print a warning if it is not known. + private (int, CustomizeData) GetCurrentCustomization(CustomizeIndex index) + { + var current = _set.DataByValue(index, _customize[index], out var custom, _customize.Face); + if (_set.IsAvailable(index) && current < 0) + throw new Exception($"Read invalid customization value {_customize[index]} for {index}."); + + return (current, custom!.Value); + } +} \ No newline at end of file diff --git a/Glamourer/Gui/Customization/CustomizationDrawer.GenderRace.cs b/Glamourer/Gui/Customization/CustomizationDrawer.GenderRace.cs new file mode 100644 index 0000000..0ee09ff --- /dev/null +++ b/Glamourer/Gui/Customization/CustomizationDrawer.GenderRace.cs @@ -0,0 +1,56 @@ +using System; +using System.Linq; +using Dalamud.Interface; +using Glamourer.Customization; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using Penumbra.GameData.Enums; + +namespace Glamourer.Gui.Customization; + +public partial class CustomizationDrawer +{ + private void DrawRaceGenderSelector() + { + DrawGenderSelector(); + ImGui.SameLine(); + using var group = ImRaii.Group(); + DrawRaceCombo(); + var gender = _service.AwaitedService.GetName(CustomName.Gender); + var clan = _service.AwaitedService.GetName(CustomName.Clan); + ImGui.TextUnformatted($"{gender} & {clan}"); + } + + private void DrawGenderSelector() + { + using var font = ImRaii.PushFont(UiBuilder.IconFont); + var icon = _customize.Gender switch + { + Gender.Male when _customize.Race is Race.Hrothgar => FontAwesomeIcon.MarsDouble, + Gender.Male => FontAwesomeIcon.Mars, + Gender.Female => FontAwesomeIcon.Venus, + + _ => throw new Exception($"Gender value {_customize.Gender} is not a valid gender for a design."), + }; + + if (!ImGuiUtil.DrawDisabledButton(icon.ToIconString(), _framedIconSize, string.Empty, icon == FontAwesomeIcon.MarsDouble, true)) + return; + + _service.ChangeGender(ref _customize, _customize.Gender is Gender.Male ? Gender.Female : Gender.Male); + } + + private void DrawRaceCombo() + { + ImGui.SetNextItemWidth(_raceSelectorWidth); + using var combo = ImRaii.Combo("##subRaceCombo", _service.ClanName(_customize.Clan, _customize.Gender)); + if (!combo) + return; + + foreach (var subRace in Enum.GetValues().Skip(1)) // Skip Unknown + { + if (ImGui.Selectable(_service.ClanName(subRace, _customize.Gender), subRace == _customize.Clan)) + _service.ChangeClan(ref _customize, subRace); + } + } +} diff --git a/Glamourer/Gui/Customization/CustomizationDrawer.Icon.cs b/Glamourer/Gui/Customization/CustomizationDrawer.Icon.cs new file mode 100644 index 0000000..3d09e58 --- /dev/null +++ b/Glamourer/Gui/Customization/CustomizationDrawer.Icon.cs @@ -0,0 +1,150 @@ +using System; +using System.Numerics; +using Glamourer.Customization; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using Penumbra.GameData.Enums; + +namespace Glamourer.Gui.Customization; + +public partial class CustomizationDrawer +{ + private const string IconSelectorPopup = "Style Picker"; + + private void DrawIconSelector(CustomizeIndex index) + { + using var _ = SetId(index); + using var bigGroup = ImRaii.Group(); + var label = _currentOption; + + var current = _set.DataByValue(index, _currentByte, out var custom, _customize.Face); + if (current < 0) + { + label = $"{_currentOption} (Custom #{_customize[index]})"; + current = 0; + custom = _set.Data(index, 0); + } + + var icon = _service.AwaitedService.GetIcon(custom!.Value.IconId); + if (ImGui.ImageButton(icon.ImGuiHandle, _iconSize)) + ImGui.OpenPopup(IconSelectorPopup); + ImGuiUtil.HoverIconTooltip(icon, _iconSize); + + ImGui.SameLine(); + using (var group = ImRaii.Group()) + { + if (_currentIndex == CustomizeIndex.Face) + FaceInputInt(current); + else + DataInputInt(current); + + ImGui.TextUnformatted($"{label} ({custom.Value.Value})"); + } + + DrawIconPickerPopup(); + } + + private bool UpdateFace(CustomizeData data) + { + // Hrothgar Hack + var value = _set.Race == Race.Hrothgar ? data.Value + 4 : data.Value; + if (_customize.Face == value) + return false; + + _customize.Face = value; + Changed |= CustomizeFlag.Face; + return true; + } + + private void FaceInputInt(int currentIndex) + { + ++currentIndex; + ImGui.SetNextItemWidth(_inputIntSize); + if (ImGui.InputInt("##text", ref currentIndex, 1, 1)) + { + currentIndex = Math.Clamp(currentIndex - 1, 0, _currentCount - 1); + var data = _set.Data(_currentIndex, currentIndex, _customize.Face); + UpdateFace(data); + } + + ImGuiUtil.HoverTooltip($"Input Range: [1, {_currentCount}]"); + } + + private void DrawIconPickerPopup() + { + using var popup = ImRaii.Popup(IconSelectorPopup, ImGuiWindowFlags.AlwaysAutoResize); + if (!popup) + return; + + using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero) + .Push(ImGuiStyleVar.FrameRounding, 0); + for (var i = 0; i < _currentCount; ++i) + { + var custom = _set.Data(_currentIndex, i, _customize.Face); + var icon = _service.AwaitedService.GetIcon(custom.IconId); + using (var _ = ImRaii.Group()) + { + if (ImGui.ImageButton(icon.ImGuiHandle, _iconSize)) + { + if (_currentIndex == CustomizeIndex.Face) + UpdateFace(custom); + else + UpdateValue(custom.Value); + ImGui.CloseCurrentPopup(); + } + + ImGuiUtil.HoverIconTooltip(icon, _iconSize); + + var text = custom.Value.ToString(); + var textWidth = ImGui.CalcTextSize(text).X; + ImGui.SetCursorPosX(ImGui.GetCursorPosX() + (_iconSize.X - textWidth + 2 * ImGui.GetStyle().FramePadding.X) / 2); + ImGui.TextUnformatted(text); + } + + if (i % 8 != 7) + ImGui.SameLine(); + } + } + + + // Only used for facial features, so fixed ID. + private void DrawMultiIconSelector() + { + using var bigGroup = ImRaii.Group(); + DrawMultiIcons(); + ImGui.SameLine(); + using var group = ImRaii.Group(); + ImGui.Dummy(new Vector2(0, ImGui.GetTextLineHeightWithSpacing() + ImGui.GetStyle().ItemSpacing.Y / 2)); + + _currentCount = 256; + PercentageInputInt(); + + ImGui.TextUnformatted(_set.Option(CustomizeIndex.LegacyTattoo)); + } + + private void DrawMultiIcons() + { + var options = _set.Order[CharaMakeParams.MenuType.IconCheckmark]; + using var _ = ImRaii.Group(); + foreach (var (featureIdx, idx) in options.WithIndex()) + { + using var id = SetId(featureIdx); + var enabled = _customize.Get(featureIdx) != CustomizeValue.Zero; + var feature = _set.Data(featureIdx, 0, _customize.Face); + var icon = featureIdx == CustomizeIndex.LegacyTattoo + ? _legacyTattoo ?? _service.AwaitedService.GetIcon(feature.IconId) + : _service.AwaitedService.GetIcon(feature.IconId); + if (ImGui.ImageButton(icon.ImGuiHandle, _iconSize, Vector2.Zero, Vector2.One, (int)ImGui.GetStyle().FramePadding.X, + Vector4.Zero, enabled ? Vector4.One : _redTint)) + { + _customize.Set(featureIdx, enabled ? CustomizeValue.Zero : CustomizeValue.Max); + Changed |= _currentFlag; + } + + ImGuiUtil.HoverIconTooltip(icon, _iconSize); + if (idx % 4 != 3) + ImGui.SameLine(); + } + } +} diff --git a/Glamourer/Gui/Customization/CustomizationDrawer.Simple.cs b/Glamourer/Gui/Customization/CustomizationDrawer.Simple.cs new file mode 100644 index 0000000..6c5fb82 --- /dev/null +++ b/Glamourer/Gui/Customization/CustomizationDrawer.Simple.cs @@ -0,0 +1,102 @@ +using System; +using Glamourer.Customization; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; + +namespace Glamourer.Gui.Customization; + +public partial class CustomizationDrawer +{ + private void PercentageSelector(CustomizeIndex index) + { + using var _ = SetId(index); + using var bigGroup = ImRaii.Group(); + + DrawPercentageSlider(); + ImGui.SameLine(); + PercentageInputInt(); + ImGui.SameLine(); + ImGui.TextUnformatted(_currentOption); + } + + private void DrawPercentageSlider() + { + var tmp = (int)_currentByte.Value; + ImGui.SetNextItemWidth(_comboSelectorSize); + if (ImGui.SliderInt("##slider", ref tmp, 0, _currentCount - 1, "%i", ImGuiSliderFlags.AlwaysClamp)) + UpdateValue((CustomizeValue)tmp); + } + + private void PercentageInputInt() + { + var tmp = (int)_currentByte.Value; + ImGui.SetNextItemWidth(_inputIntSize); + if (ImGui.InputInt("##text", ref tmp, 1, 1)) + UpdateValue((CustomizeValue)Math.Clamp(tmp, 0, _currentCount - 1)); + ImGuiUtil.HoverTooltip($"Input Range: [0, {_currentCount - 1}]"); + } + + // Integral input for an icon- or color based item. + private void DataInputInt(int currentIndex) + { + ++currentIndex; + ImGui.SetNextItemWidth(_inputIntSize); + if (ImGui.InputInt("##text", ref currentIndex, 1, 1)) + { + currentIndex = Math.Clamp(currentIndex - 1, 0, _currentCount - 1); + var data = _set.Data(_currentIndex, currentIndex, _customize.Face); + UpdateValue(data.Value); + } + + ImGuiUtil.HoverTooltip($"Input Range: [1, {_currentCount}]"); + } + + private void DrawListSelector(CustomizeIndex index) + { + using var _ = SetId(index); + using var bigGroup = ImRaii.Group(); + + ListCombo(); + ImGui.SameLine(); + ListInputInt(); + ImGui.SameLine(); + ImGui.TextUnformatted(_currentOption); + } + + private void ListCombo() + { + ImGui.SetNextItemWidth(_comboSelectorSize * ImGui.GetIO().FontGlobalScale); + using var combo = ImRaii.Combo("##combo", $"{_currentOption} #{_currentByte.Value + 1}"); + + if (!combo) + return; + + for (var i = 0; i < _currentCount; ++i) + { + if (ImGui.Selectable($"{_currentOption} #{i + 1}##combo", i == _currentByte.Value)) + UpdateValue((CustomizeValue)i); + } + } + + private void ListInputInt() + { + var tmp = _currentByte.Value + 1; + ImGui.SetNextItemWidth(_inputIntSize); + if (ImGui.InputInt("##text", ref tmp, 1, 1) && tmp > 0 && tmp <= _currentCount) + UpdateValue((CustomizeValue)Math.Clamp(tmp - 1, 0, _currentCount - 1)); + ImGuiUtil.HoverTooltip($"Input Range: [1, {_currentCount}]"); + } + + // Draw a customize checkbox. + private void DrawCheckbox(CustomizeIndex idx) + { + using var id = SetId(idx); + var tmp = _currentByte != CustomizeValue.Zero; + if (ImGui.Checkbox(_currentOption, ref tmp)) + { + _customize.Set(idx, tmp ? CustomizeValue.Max : CustomizeValue.Zero); + Changed |= _currentFlag; + } + } +} diff --git a/Glamourer/Gui/Customization/CustomizationDrawer.cs b/Glamourer/Gui/Customization/CustomizationDrawer.cs new file mode 100644 index 0000000..d72800b --- /dev/null +++ b/Glamourer/Gui/Customization/CustomizationDrawer.cs @@ -0,0 +1,152 @@ +using System; +using System.Numerics; +using System.Reflection; +using Dalamud.Plugin; +using Glamourer.Customization; +using Glamourer.Services; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; + +namespace Glamourer.Gui.Customization; + +public partial class CustomizationDrawer : IDisposable +{ + private readonly Vector4 _redTint = new(0.6f, 0.3f, 0.3f, 1f); + private readonly ImGuiScene.TextureWrap? _legacyTattoo; + + private Exception? _terminate = null; + + private Customize _customize; + private CustomizationSet _set = null!; + + public Customize Customize; + + public CustomizeFlag CurrentFlag { get; private set; } + public CustomizeFlag Changed { get; private set; } + + public bool RequiresRedraw + => Changed.RequiresRedraw(); + + private bool _locked = false; + private Vector2 _iconSize; + private Vector2 _framedIconSize; + private float _inputIntSize; + private float _comboSelectorSize; + private float _raceSelectorWidth; + + private readonly CustomizationService _service; + + public CustomizationDrawer(DalamudPluginInterface pi, CustomizationService service) + { + _service = service; + _legacyTattoo = GetLegacyTattooIcon(pi); + Customize = Customize.Default; + } + + public void Dispose() + { + _legacyTattoo?.Dispose(); + } + + public bool Draw(Customize current, bool locked) + { + CurrentFlag = CustomizeFlagExtensions.All; + Init(current, locked); + return DrawInternal(); + } + + private void Init(Customize current, bool locked) + { + UpdateSizes(); + _terminate = null; + Changed = 0; + _customize.Load(current); + _locked = locked; + } + + // Set state for drawing of current customization. + private CustomizeIndex _currentIndex; + private CustomizeFlag _currentFlag; + private CustomizeValue _currentByte = CustomizeValue.Zero; + private int _currentCount; + private string _currentOption = string.Empty; + + // Prepare a new customization option. + private ImRaii.Id SetId(CustomizeIndex index) + { + _currentIndex = index; + _currentFlag = index.ToFlag(); + _currentByte = _customize[index]; + _currentCount = _set.Count(index, _customize.Face); + _currentOption = _set.Option(index); + return ImRaii.PushId((int)index); + } + + // Update the current id with a new value. + private void UpdateValue(CustomizeValue value) + { + if (_currentByte == value) + return; + + _customize[_currentIndex] = value; + Changed |= _currentFlag; + } + + private bool DrawInternal() + { + using var disabled = ImRaii.Disabled(_locked); + + try + { + DrawRaceGenderSelector(); + _set = _service.AwaitedService.GetList(_customize.Clan, _customize.Gender); + + foreach (var id in _set.Order[CharaMakeParams.MenuType.Percentage]) + PercentageSelector(id); + + Functions.IteratePairwise(_set.Order[CharaMakeParams.MenuType.IconSelector], DrawIconSelector, ImGui.SameLine); + + DrawMultiIconSelector(); + + foreach (var id in _set.Order[CharaMakeParams.MenuType.ListSelector]) + DrawListSelector(id); + + Functions.IteratePairwise(_set.Order[CharaMakeParams.MenuType.ColorPicker], DrawColorPicker, ImGui.SameLine); + + Functions.IteratePairwise(_set.Order[CharaMakeParams.MenuType.Checkmark], DrawCheckbox, + () => ImGui.SameLine(_inputIntSize + _framedIconSize.X + 3 * ImGui.GetStyle().ItemSpacing.X)); + return Changed != 0; + } + catch (Exception ex) + { + _terminate = ex; + using var color = ImRaii.PushColor(ImGuiCol.Text, 0xFF4040FF); + ImGui.NewLine(); + ImGuiUtil.TextWrapped(_terminate.ToString()); + return false; + } + } + + private void UpdateSizes() + { + _iconSize = new Vector2(ImGui.GetTextLineHeightWithSpacing() * 2); + _framedIconSize = _iconSize + 2 * ImGui.GetStyle().FramePadding; + _inputIntSize = 2 * _framedIconSize.X + ImGui.GetStyle().ItemSpacing.X; + _comboSelectorSize = 4 * _framedIconSize.X + 3 * ImGui.GetStyle().ItemSpacing.X; + _raceSelectorWidth = _inputIntSize + _comboSelectorSize - _framedIconSize.X; + } + + private static ImGuiScene.TextureWrap? GetLegacyTattooIcon(DalamudPluginInterface pi) + { + using var resource = Assembly.GetExecutingAssembly().GetManifestResourceStream("Glamourer.LegacyTattoo.raw"); + if (resource == null) + return null; + + var rawImage = new byte[resource.Length]; + var length = resource.Read(rawImage, 0, (int)resource.Length); + return length == resource.Length + ? pi.UiBuilder.LoadImageRaw(rawImage, 192, 192, 4) + : null; + } +} diff --git a/Glamourer/Gui/MainWindow.cs b/Glamourer/Gui/MainWindow.cs index f773394..ebc7712 100644 --- a/Glamourer/Gui/MainWindow.cs +++ b/Glamourer/Gui/MainWindow.cs @@ -3,6 +3,8 @@ using System.Numerics; using Dalamud.Interface.Windowing; using Dalamud.Plugin; using Glamourer.Gui.Tabs; +using Glamourer.Gui.Tabs.ActorTab; +using Glamourer.Gui.Tabs.DesignTab; using ImGuiNET; using OtterGui.Custom; using OtterGui.Widgets; @@ -16,17 +18,22 @@ public class MainWindow : Window None = -1, Settings = 0, Debug = 1, + Actors = 2, + Designs = 3, } private readonly Configuration _config; private readonly ITab[] _tabs; public readonly SettingsTab Settings; + public readonly ActorTab Actors; public readonly DebugTab Debug; + public readonly DesignTab Designs; public TabType SelectTab = TabType.None; - public MainWindow(DalamudPluginInterface pi, Configuration config, SettingsTab settings, DebugTab debugTab) + public MainWindow(DalamudPluginInterface pi, Configuration config, SettingsTab settings, ActorTab actors, DesignTab designs, + DebugTab debugTab) : base(GetLabel()) { pi.UiBuilder.DisableGposeUiHide = true; @@ -37,10 +44,14 @@ public class MainWindow : Window }; Settings = settings; Debug = debugTab; + Designs = designs; + Actors = actors; _config = config; _tabs = new ITab[] { settings, + actors, + designs, debugTab, }; @@ -62,12 +73,16 @@ public class MainWindow : Window { TabType.Settings => Settings.Label, TabType.Debug => Debug.Label, + TabType.Actors => Actors.Label, + TabType.Designs => Designs.Label, _ => ReadOnlySpan.Empty, }; private TabType FromLabel(ReadOnlySpan label) { // @formatter:off + if (label == Actors.Label) return TabType.Actors; + if (label == Designs.Label) return TabType.Designs; if (label == Settings.Label) return TabType.Settings; if (label == Debug.Label) return TabType.Debug; // @formatter:on diff --git a/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs b/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs new file mode 100644 index 0000000..58fd541 --- /dev/null +++ b/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs @@ -0,0 +1,218 @@ +using System.Numerics; +using Glamourer.Gui.Customization; +using Glamourer.Interop.Structs; +using Glamourer.State; +using ImGuiNET; +using OtterGui.Raii; +using Penumbra.GameData.Actors; +using Penumbra.GameData.Enums; + +namespace Glamourer.Gui.Tabs.ActorTab; + +public class ActorPanel +{ + private readonly ActorSelector _selector; + private readonly StateManager _stateManager; + private readonly CustomizationDrawer _customizationDrawer; + + private ActorIdentifier _identifier; + private string _actorName = string.Empty; + private Actor _actor = Actor.Null; + private ActorData _data; + private ActorState? _state; + + public ActorPanel(ActorSelector selector, StateManager stateManager, CustomizationDrawer customizationDrawer) + { + _selector = selector; + _stateManager = stateManager; + _customizationDrawer = customizationDrawer; + } + + public void Draw() + { + if (!_selector.HasSelection) + return; + + (_identifier, _data) = _selector.Selection; + if (_data.Valid) + { + _actorName = _data.Label; + _actor = _data.Objects[0]; + } + else + { + _actorName = _identifier.ToString(); + _actor = Actor.Null; + } + + if (!_stateManager.GetOrCreate(_identifier, _actor, out _state)) + return; + + //if (_state != null) + // _stateManager.Update(ref _state.Data, _actor); + + using var group = ImRaii.Group(); + DrawHeader(); + DrawPanel(); + } + + private void DrawHeader() + { + var color = _data.Valid ? ColorId.ActorAvailable.Value() : ColorId.ActorUnavailable.Value(); + var buttonColor = ImGui.GetColorU32(ImGuiCol.FrameBg); + using var c = ImRaii.PushColor(ImGuiCol.Text, color) + .Push(ImGuiCol.Button, buttonColor) + .Push(ImGuiCol.ButtonHovered, buttonColor) + .Push(ImGuiCol.ButtonActive, buttonColor); + using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero) + .Push(ImGuiStyleVar.FrameRounding, 0); + ImGui.Button($"{(_data.Valid ? _data.Label : _identifier.ToString())}##playerHeader", -Vector2.UnitX); + } + + private unsafe void DrawPanel() + { + using var child = ImRaii.Child("##ActorPanel", -Vector2.One, true); + if (!child || _state == null) + return; + + if (_customizationDrawer.Draw(_state.Data.Customize, false)) + { + } + // if (_currentData.Valid) + // _currentSave.Initialize(_items, _currentData.Objects[0]); + // + // RevertButton(); + // ActorDebug.Draw(_currentSave.ModelData); + // return; + // + // if (_main._customizationDrawer.Draw(_currentSave.ModelData.Customize, _identifier.Type == IdentifierType.Special)) + // _activeDesigns.ChangeCustomize(_currentSave, _main._customizationDrawer.Changed, _main._customizationDrawer.Customize.Data, + // false); + // + // foreach (var slot in EquipSlotExtensions.EqdpSlots) + // { + // var current = _currentSave.Armor(slot); + // if (_main._equipmentDrawer.DrawStain(current.Stain, slot, out var stain)) + // _activeDesigns.ChangeStain(_currentSave, slot, stain.RowIndex, false); + // ImGui.SameLine(); + // if (_main._equipmentDrawer.DrawArmor(current, slot, out var armor, _currentSave.ModelData.Customize.Gender, + // _currentSave.ModelData.Customize.Race)) + // _activeDesigns.ChangeEquipment(_currentSave, slot, armor, false); + // } + // + // var currentMain = _currentSave.WeaponMain; + // if (_main._equipmentDrawer.DrawStain(currentMain.Stain, EquipSlot.MainHand, out var stainMain)) + // _activeDesigns.ChangeStain(_currentSave, EquipSlot.MainHand, stainMain.RowIndex, false); + // ImGui.SameLine(); + // _main._equipmentDrawer.DrawMainhand(currentMain, true, out var main); + // if (currentMain.Type.Offhand() != FullEquipType.Unknown) + // { + // var currentOff = _currentSave.WeaponOff; + // if (_main._equipmentDrawer.DrawStain(currentOff.Stain, EquipSlot.OffHand, out var stainOff)) + // _activeDesigns.ChangeStain(_currentSave, EquipSlot.OffHand, stainOff.RowIndex, false); + // ImGui.SameLine(); + // _main._equipmentDrawer.DrawOffhand(currentOff, main.Type, out var off); + // } + // + // if (_main._equipmentDrawer.DrawVisor(_currentSave, out var value)) + // _activeDesigns.ChangeVisor(_currentSave, value, false); + } + + + private unsafe void RevertButton() + { + //if (ImGui.Button("Revert")) + // _activeDesigns.RevertDesign(_currentSave!); + //foreach (var actor in _currentData.Objects) + // _currentSave!.ApplyToActor(actor); + // + //if (_currentData.Objects.Count > 0) + // _currentSave = _manipulations.GetOrCreateSave(_currentData.Objects[0]); + // + //_currentSave!.Reset(); + //if (_currentData.Objects.Count > 0) + // ImGui.TextUnformatted(_currentData.Objects[0].Pointer->GameObject.DataID.ToString()); + //VisorBox(); + } + + //private unsafe void VisorBox() + //{ + // var (flags, mask) = (_currentSave!.Data.Flags & (ApplicationFlags.SetVisor | ApplicationFlags.Visor)) switch + // { + // ApplicationFlags.SetVisor => (0u, 3u), + // ApplicationFlags.Visor => (1u, 3u), + // ApplicationFlags.SetVisor | ApplicationFlags.Visor => (3u, 3u), + // _ => (2u, 3u), + // }; + // var tmp = flags; + // if (ImGui.CheckboxFlags("Visor Toggled", ref tmp, mask)) + // { + // _currentSave.Data.Flags = flags switch + // { + // 0 => (_currentSave.Data.Flags | ApplicationFlags.Visor) & ~ApplicationFlags.SetVisor, + // 1 => _currentSave.Data.Flags | ApplicationFlags.SetVisor, + // 2 => _currentSave.Data.Flags | ApplicationFlags.SetVisor, + // _ => _currentSave.Data.Flags & ~(ApplicationFlags.SetVisor | ApplicationFlags.Visor), + // }; + // if (_currentSave.Data.Flags.HasFlag(ApplicationFlags.SetVisor)) + // { + // var on = _currentSave.Data.Flags.HasFlag(ApplicationFlags.Visor); + // foreach (var actor in _currentData.Objects.Where(a => a.IsHuman && a.DrawObject)) + // RedrawManager.SetVisor(actor.DrawObject.Pointer, on); + // } + // } + //} + + + //private void DrawActorPanel() + //{ + // using var group = ImRaii.Group(); + // if (!_data.Identifier.IsValid) + // return; + // + // if (DrawCustomization(_currentSave.Customize, _currentSave.Equipment, !_data.Modifiable)) + // //Glamourer.RedrawManager.Set(_data.Actor.Address, _character); + // Glamourer.Penumbra.RedrawObject(_data.Actor.Character, RedrawType.Redraw, true); + // + // if (ImGui.Button("Set Machinist Goggles")) + // Glamourer.RedrawManager.ChangeEquip(_data.Actor, EquipSlot.Head, new CharacterArmor(265, 1, 0)); + // + // if (ImGui.Button("Set Weapon")) + // Glamourer.RedrawManager.LoadWeapon(_data.Actor.Address, new CharacterWeapon(0x00C9, 0x004E, 0x0001, 0x00), + // new CharacterWeapon(0x0065, 0x003D, 0x0001, 0x00)); + // + // if (ImGui.Button("Set Customize")) + // { + // unsafe + // { + // var data = _data.Actor.Customize.Data->Clone(); + // Glamourer.RedrawManager.UpdateCustomize(_data.Actor.DrawObject, new Customize(&data) + // { + // SkinColor = 154, + // }); + // } + // } + //} + // + //private void DrawMonsterPanel() + //{ + // using var group = ImRaii.Group(); + // var currentModel = (uint)_data.Actor.ModelId; + // var models = GameData.Models(Dalamud.GameData); + // var currentData = models.Models.TryGetValue(currentModel, out var c) ? c.FirstName : $"#{currentModel}"; + // using var combo = ImRaii.Combo("Model Id", currentData); + // if (!combo) + // return; + // + // foreach (var (id, data) in models.Models) + // { + // if (ImGui.Selectable(data.FirstName, id == currentModel) && id != currentModel) + // { + // _data.Actor.SetModelId((int)id); + // Glamourer.Penumbra.RedrawObject(_data.Actor.Character, RedrawType.Redraw, true); + // } + // + // ImGuiUtil.HoverTooltip(data.AllNames); + // } + //} +} diff --git a/Glamourer/Gui/Tabs/ActorTab/ActorSelector.cs b/Glamourer/Gui/Tabs/ActorTab/ActorSelector.cs new file mode 100644 index 0000000..4576808 --- /dev/null +++ b/Glamourer/Gui/Tabs/ActorTab/ActorSelector.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Generic; +using System.Numerics; +using Dalamud.Game.ClientState.Objects; +using Dalamud.Interface; +using Glamourer.Interop; +using Glamourer.Interop.Structs; +using Glamourer.Services; +using ImGuiNET; +using OtterGui; +using OtterGui.Classes; +using OtterGui.Raii; +using Penumbra.GameData.Actors; + +namespace Glamourer.Gui.Tabs.ActorTab; + +public class ActorSelector +{ + private readonly ObjectManager _objects; + private readonly ActorService _actors; + private readonly TargetManager _targets; + + private ActorIdentifier _identifier = ActorIdentifier.Invalid; + + public ActorSelector(ObjectManager objects, TargetManager targets, ActorService actors) + { + _objects = objects; + _targets = targets; + _actors = actors; + } + + private LowerString _actorFilter = LowerString.Empty; + private Vector2 _defaultItemSpacing; + private float _width; + + public (ActorIdentifier Identifier, ActorData Data) Selection + => _objects.TryGetValue(_identifier, out var data) ? (_identifier, data) : (_identifier, ActorData.Invalid); + + public bool HasSelection + => _identifier.IsValid; + + public void Draw(float width) + { + _width = width; + using var group = ImRaii.Group(); + _defaultItemSpacing = ImGui.GetStyle().ItemSpacing; + using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero) + .Push(ImGuiStyleVar.FrameRounding, 0); + ImGui.SetNextItemWidth(_width); + LowerString.InputWithHint("##actorFilter", "Filter...", ref _actorFilter, 64); + + DrawSelector(); + DrawSelectionButtons(); + } + + private void DrawSelector() + { + using var child = ImRaii.Child("##actorSelector", new Vector2(_width, -ImGui.GetFrameHeight()), true); + if (!child) + return; + + _objects.Update(); + using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, _defaultItemSpacing); + var skips = ImGuiClip.GetNecessarySkips(ImGui.GetTextLineHeight()); + var remainder = ImGuiClip.FilteredClippedDraw(_objects, skips, CheckFilter, DrawSelectable); + ImGuiClip.DrawEndDummy(remainder, ImGui.GetTextLineHeight()); + } + + private bool CheckFilter(KeyValuePair pair) + => _actorFilter.IsEmpty || pair.Value.Label.Contains(_actorFilter.Lower, StringComparison.OrdinalIgnoreCase); + + private void DrawSelectable(KeyValuePair pair) + { + var equals = pair.Key.Equals(_identifier); + if (ImGui.Selectable(pair.Value.Label, equals) && !equals) + _identifier = pair.Key.CreatePermanent(); + } + + private void DrawSelectionButtons() + { + using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero) + .Push(ImGuiStyleVar.FrameRounding, 0); + var buttonWidth = new Vector2(_width / 2, 0); + + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.UserCircle.ToIconString(), buttonWidth + , "Select the local player character.", !_objects.Player, true)) + _identifier = _objects.Player.GetIdentifier(_actors.AwaitedService); + + ImGui.SameLine(); + Actor targetActor = _targets.Target?.Address; + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.HandPointer.ToIconString(), buttonWidth, + "Select the current target, if it is in the list.", _objects.IsInGPose || !targetActor, true)) + _identifier = targetActor.GetIdentifier(_actors.AwaitedService); + } +} diff --git a/Glamourer/Gui/Tabs/ActorTab/ActorTab.cs b/Glamourer/Gui/Tabs/ActorTab/ActorTab.cs new file mode 100644 index 0000000..4590f45 --- /dev/null +++ b/Glamourer/Gui/Tabs/ActorTab/ActorTab.cs @@ -0,0 +1,28 @@ +using System; +using Dalamud.Interface; +using ImGuiNET; +using OtterGui.Widgets; + +namespace Glamourer.Gui.Tabs.ActorTab; + +public class ActorTab : ITab +{ + private readonly ActorSelector _selector; + private readonly ActorPanel _panel; + + public ReadOnlySpan Label + => "Actors"u8; + + public void DrawContent() + { + _selector.Draw(200 * ImGuiHelpers.GlobalScale); + ImGui.SameLine(); + _panel.Draw(); + } + + public ActorTab(ActorSelector selector, ActorPanel panel) + { + _selector = selector; + _panel = panel; + } +} diff --git a/Glamourer/Gui/Tabs/DebugTab.cs b/Glamourer/Gui/Tabs/DebugTab.cs index bb089e9..3c6f08d 100644 --- a/Glamourer/Gui/Tabs/DebugTab.cs +++ b/Glamourer/Gui/Tabs/DebugTab.cs @@ -66,7 +66,7 @@ public unsafe class DebugTab : ITab _designFileSystem = designFileSystem; _designManager = designManager; _state = state; - _config = config; + _config = config; } public ReadOnlySpan Label @@ -74,6 +74,10 @@ public unsafe class DebugTab : ITab public void DrawContent() { + using var child = ImRaii.Child("MainWindowChild"); + if (!child) + return; + DrawInteropHeader(); DrawGameDataHeader(); DrawPenumbraHeader(); @@ -829,7 +833,7 @@ public unsafe class DebugTab : ITab } } - private static void DrawDesignData(in DesignData data) + public static void DrawDesignData(in DesignData data) { if (data.ModelId == 0) { diff --git a/Glamourer/Gui/Tabs/DesignTab/DesignFileSystemSelector.cs b/Glamourer/Gui/Tabs/DesignTab/DesignFileSystemSelector.cs new file mode 100644 index 0000000..9b786c7 --- /dev/null +++ b/Glamourer/Gui/Tabs/DesignTab/DesignFileSystemSelector.cs @@ -0,0 +1,66 @@ +using System.Numerics; +using Dalamud.Game.ClientState.Keys; +using Dalamud.Interface; +using Glamourer.Designs; +using Glamourer.Events; +using OtterGui; +using OtterGui.Filesystem; +using OtterGui.FileSystem.Selector; + +namespace Glamourer.Gui.Tabs.DesignTab; + +public sealed class DesignFileSystemSelector : FileSystemSelector +{ + private readonly DesignManager _designManager; + private readonly DesignChanged _event; + private readonly Configuration _config; + + public struct DesignState + { } + + public DesignFileSystemSelector(DesignManager designManager, DesignFileSystem fileSystem, KeyState keyState, DesignChanged @event, + Configuration config) + : base(fileSystem, keyState) + { + _designManager = designManager; + _event = @event; + _config = config; + _event.Subscribe(OnDesignChange, DesignChanged.Priority.DesignFileSystemSelector); + AddButton(DeleteButton, 1000); + } + + public override void Dispose() + { + base.Dispose(); + _event.Unsubscribe(OnDesignChange); + } + + private void OnDesignChange(DesignChanged.Type type, Design design, object? oldData) + { + switch (type) + { + case DesignChanged.Type.ReloadedAll: + case DesignChanged.Type.Renamed: + case DesignChanged.Type.AddedTag: + case DesignChanged.Type.ChangedTag: + case DesignChanged.Type.RemovedTag: + SetFilterDirty(); + break; + } + } + + private void DeleteButton(Vector2 size) + { + var keys = _config.DeleteDesignModifier.IsActive(); + var tt = SelectedLeaf == null + ? "No design selected." + : "Delete the currently selected design entirely from your drive.\n" + + "This can not be undone."; + if (!keys) + tt += $"\nHold {_config.DeleteDesignModifier} while clicking to delete the mod."; + + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), size, tt, SelectedLeaf == null || !keys, true) + && Selected != null) + _designManager.Delete(Selected); + } +} diff --git a/Glamourer/Gui/Tabs/DesignTab/DesignPanel.cs b/Glamourer/Gui/Tabs/DesignTab/DesignPanel.cs new file mode 100644 index 0000000..df624e9 --- /dev/null +++ b/Glamourer/Gui/Tabs/DesignTab/DesignPanel.cs @@ -0,0 +1,33 @@ +using System.Numerics; +using Glamourer.Designs; +using Glamourer.Gui.Customization; +using OtterGui.Raii; + +namespace Glamourer.Gui.Tabs.DesignTab; + +public class DesignPanel +{ + private readonly DesignFileSystemSelector _selector; + private readonly DesignManager _manager; + private readonly CustomizationDrawer _customizationDrawer; + + public DesignPanel(DesignFileSystemSelector selector, CustomizationDrawer customizationDrawer, DesignManager manager) + { + _selector = selector; + _customizationDrawer = customizationDrawer; + _manager = manager; + } + + public void Draw() + { + var design = _selector.Selected; + if (design == null) + return; + + using var child = ImRaii.Child("##panel", -Vector2.One, true); + if (!child) + return; + + _customizationDrawer.Draw(design.DesignData.Customize, design.WriteProtected()); + } +} diff --git a/Glamourer/Gui/Tabs/DesignTab/DesignTab.cs b/Glamourer/Gui/Tabs/DesignTab/DesignTab.cs new file mode 100644 index 0000000..08d6502 --- /dev/null +++ b/Glamourer/Gui/Tabs/DesignTab/DesignTab.cs @@ -0,0 +1,39 @@ +using System; +using Dalamud.Interface; +using Glamourer.Designs; +using Glamourer.Interop; +using ImGuiNET; +using OtterGui.Widgets; + +namespace Glamourer.Gui.Tabs.DesignTab; + +public class DesignTab : ITab +{ + public readonly DesignFileSystemSelector Selector; + private readonly DesignFileSystem _fileSystem; + private readonly DesignManager _designManager; + private readonly DesignPanel _panel; + private readonly ObjectManager _objects; + + public DesignTab(DesignFileSystemSelector selector, DesignFileSystem fileSystem, DesignManager designManager, ObjectManager objects, DesignPanel panel) + { + Selector = selector; + _fileSystem = fileSystem; + _designManager = designManager; + _objects = objects; + _panel = panel; + } + + public ReadOnlySpan Label + => "Designs"u8; + + public void DrawContent() + { + Selector.Draw(GetDesignSelectorSize()); + ImGui.SameLine(); + _panel.Draw(); + } + + public float GetDesignSelectorSize() + => 200f * ImGuiHelpers.GlobalScale; +} diff --git a/Glamourer/Gui/Tabs/SettingsTab.cs b/Glamourer/Gui/Tabs/SettingsTab.cs index 0cdf6a1..4b514be 100644 --- a/Glamourer/Gui/Tabs/SettingsTab.cs +++ b/Glamourer/Gui/Tabs/SettingsTab.cs @@ -1,8 +1,10 @@ using System; using System.Numerics; using System.Runtime.CompilerServices; +using Dalamud.Interface; using ImGuiNET; using OtterGui; +using OtterGui.Classes; using OtterGui.Raii; using OtterGui.Widgets; @@ -20,20 +22,23 @@ public class SettingsTab : ITab public void DrawContent() { - using var child = ImRaii.Child("##SettingsTab", -Vector2.One, false); + using var child = ImRaii.Child("MainWindowChild"); if (!child) return; Checkbox("Restricted Gear Protection", "Use gender- and race-appropriate models when detecting certain items not available for a characters current gender and race.", _config.UseRestrictedGearProtection, v => _config.UseRestrictedGearProtection = v); + if (Widget.DoubleModifierSelector("Design Deletion Modifier", + "A modifier you need to hold while clicking the Delete Design button for it to take effect.", 100 * ImGuiHelpers.GlobalScale, + _config.DeleteDesignModifier, v => _config.DeleteDesignModifier = v)) + _config.Save(); Checkbox("Debug Mode", "Show the debug tab. Only useful for debugging or advanced use.", _config.DebugMode, v => _config.DebugMode = v); DrawColorSettings(); MainWindow.DrawSupportButtons(); } - /// Draw the entire Color subsection. private void DrawColorSettings() { diff --git a/Glamourer/Services/CustomizationService.cs b/Glamourer/Services/CustomizationService.cs index 8611090..41e316e 100644 --- a/Glamourer/Services/CustomizationService.cs +++ b/Glamourer/Services/CustomizationService.cs @@ -1,8 +1,7 @@ using System; using System.Linq; -using System.Security.AccessControl; +using System.Runtime.CompilerServices; using Dalamud.Data; -using Dalamud.Interface.Internal.Notifications; using Dalamud.Plugin; using Glamourer.Customization; using Penumbra.GameData.Enums; @@ -60,6 +59,26 @@ public sealed class CustomizationService : AsyncServiceWrapper Returns whether a clan is valid. + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public bool IsClanValid(SubRace clan) + => AwaitedService.Clans.Contains(clan); + + /// Returns whether a gender is valid for the given race. + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public bool IsGenderValid(Race race, Gender gender) + => race is Race.Hrothgar ? gender == Gender.Male : AwaitedService.Genders.Contains(gender); + + /// Returns whether a customization value is valid for a given clan/gender set and face. + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public static bool IsCustomizationValid(CustomizationSet set, CustomizeValue face, CustomizeIndex type, CustomizeValue value) + => set.DataByValue(type, value, out _, face) >= 0; + + /// Returns whether a customization value is valid for a given clan, gender and face. + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public bool IsCustomizationValid(SubRace race, Gender gender, CustomizeValue face, CustomizeIndex type, CustomizeValue value) + => AwaitedService.GetList(race, gender).DataByValue(type, value, out _, face) >= 0; + /// /// Check that the given race and clan are valid. /// The returned race and clan fit together and are valid. @@ -67,7 +86,7 @@ public sealed class CustomizationService : AsyncServiceWrapper public string ValidateClan(SubRace clan, Race race, out Race actualRace, out SubRace actualClan) { - if (AwaitedService.Clans.Contains(clan)) + if (IsClanValid(clan)) { actualClan = clan; actualRace = actualClan.ToRace(); @@ -113,7 +132,7 @@ public sealed class CustomizationService : AsyncServiceWrapper /// Validate a single customization value against a given set of race and gender (and face). /// The returned actualValue is either the correct value or the one with index 0. @@ -143,9 +161,7 @@ public sealed class CustomizationService : AsyncServiceWrapper= 0 && idx < count) + if (IsCustomizationValid(set, face, index, value)) { actualValue = value; return string.Empty; diff --git a/Glamourer/Services/ItemManager.cs b/Glamourer/Services/ItemManager.cs index befac06..3e3cf8e 100644 --- a/Glamourer/Services/ItemManager.cs +++ b/Glamourer/Services/ItemManager.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using System.Runtime.CompilerServices; using Dalamud.Data; using Dalamud.Plugin; using Lumina.Excel; @@ -135,6 +136,14 @@ public class ItemManager : IDisposable : new EquipItem($"Unknown ({id.Value}-{type.Value}-{variant})", 0, 0, id, type, variant, 0); } + /// Returns whether an item id represents a valid item for a slot and gives the item. + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public bool IsItemValid(EquipSlot slot, uint itemId, out EquipItem item) + { + item = Resolve(slot, itemId); + return item.Valid; + } + /// /// Check whether an item id resolves to an existing item of the correct slot (which should not be weapons.) /// The returned item is either the resolved correct item, or the Nothing item for that slot. @@ -145,22 +154,26 @@ public class ItemManager : IDisposable if (slot is EquipSlot.MainHand or EquipSlot.OffHand) throw new Exception("Internal Error: Used armor functionality for weapons."); - item = Resolve(slot, itemId); - if (item.Valid) + if (IsItemValid(slot, itemId, out item)) return string.Empty; item = NothingItem(slot); return $"The {slot.ToName()} item {itemId} does not exist, reset to Nothing."; } + /// Returns whether a stain id is a valid stain. + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public bool IsStainValid(StainId stain) + => stain.Value == 0 || Stains.ContainsKey(stain); + /// /// Check whether a stain id is an existing stain. /// The returned stain id is either the input or 0. /// The return value is an empty string if there was no problem and a warning otherwise. /// - public string ValidateStain(StainId stain, out StainId ret) + public string ValidateStain(StainId stain, out StainId ret) { - if (stain.Value == 0 || Stains.ContainsKey(stain)) + if (IsStainValid(stain)) { ret = stain; return string.Empty; @@ -170,6 +183,19 @@ public class ItemManager : IDisposable return $"The Stain {stain} does not exist, reset to unstained."; } + /// Returns whether an offhand is valid given the required offhand type. + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public bool IsOffhandValid(FullEquipType offType, uint offId, out EquipItem off) + { + off = Resolve(offType, offId); + return off.Valid; + } + + /// Returns whether an offhand is valid given mainhand. + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public bool IsOffhandValid(in EquipItem main, uint offId, out EquipItem off) + => IsOffhandValid(main.Type.Offhand(), offId, out off); + /// /// Check whether a combination of an item id for a mainhand and for an offhand is valid. /// The returned items are either the resolved correct items, @@ -180,42 +206,30 @@ public class ItemManager : IDisposable public string ValidateWeapons(uint mainId, uint offId, out EquipItem main, out EquipItem off) { var ret = string.Empty; - main = Resolve(EquipSlot.MainHand, mainId); - if (!main.Valid) + if (!IsItemValid(EquipSlot.MainHand, mainId, out main)) { main = DefaultSword; - ret = $"The mainhand weapon {mainId} does not exist, reset to default sword."; + ret = $"The mainhand weapon {mainId} does not exist, reset to default sword."; } - var offhandType = main.Type.Offhand(); - off = Resolve(offhandType, offId); - if (off.Valid) + var offType = main.Type.Offhand(); + if (IsOffhandValid(offType, offId, out off)) return ret; // Try implicit offhand. - off = Resolve(offhandType, mainId); - if (off.Valid) + // Can not be set to default sword before because then it could not be valid. + if (IsOffhandValid(offType, mainId, out off)) + return $"The offhand weapon {offId} does not exist, reset to implied offhand."; + + if (FullEquipTypeExtensions.OffhandTypes.Contains(offType)) { - // Can not be set to default sword before because then it could not be valid. - ret = $"The offhand weapon {offId} does not exist, reset to implied offhand."; - } - else - { - if (FullEquipTypeExtensions.OffhandTypes.Contains(offhandType)) - { - main = DefaultSword; - off = NothingItem(FullEquipType.Shield); - ret = - $"The offhand weapon {offId} does not exist, but no default could be restored, reset mainhand to default sword and offhand to nothing."; - } - else - { - off = NothingItem(offhandType); - if (ret.Length == 0) - ret = $"The offhand weapon {offId} does not exist, reset to no offhand."; - } + main = DefaultSword; + off = NothingItem(FullEquipType.Shield); + return + $"The offhand weapon {offId} does not exist, but no default could be restored, reset mainhand to default sword and offhand to nothing."; } - return ret; + off = NothingItem(offType); + return ret.Length == 0 ? $"The offhand weapon {offId} does not exist, reset to no offhand." : ret; } } diff --git a/Glamourer/Services/ServiceManager.cs b/Glamourer/Services/ServiceManager.cs index 2c82354..dadb1e8 100644 --- a/Glamourer/Services/ServiceManager.cs +++ b/Glamourer/Services/ServiceManager.cs @@ -2,7 +2,10 @@ using Glamourer.Designs; using Glamourer.Events; using Glamourer.Gui; +using Glamourer.Gui.Customization; using Glamourer.Gui.Tabs; +using Glamourer.Gui.Tabs.ActorTab; +using Glamourer.Gui.Tabs.DesignTab; using Glamourer.Interop; using Glamourer.Interop.Penumbra; using Glamourer.State; @@ -49,7 +52,8 @@ public static class ServiceManager private static IServiceCollection AddEvents(this IServiceCollection services) => services.AddSingleton() .AddSingleton() - .AddSingleton(); + .AddSingleton() + .AddSingleton(); private static IServiceCollection AddData(this IServiceCollection services) => services.AddSingleton() @@ -76,8 +80,15 @@ public static class ServiceManager private static IServiceCollection AddUi(this IServiceCollection services) => services.AddSingleton() .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() .AddSingleton() - .AddSingleton(); + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton(); private static IServiceCollection AddApi(this IServiceCollection services) => services.AddSingleton(); diff --git a/Glamourer/State/ActorState.cs b/Glamourer/State/ActorState.cs index 9adebe8..411336a 100644 --- a/Glamourer/State/ActorState.cs +++ b/Glamourer/State/ActorState.cs @@ -1,13 +1,39 @@ -using Glamourer.Designs; +using Glamourer.Customization; +using Glamourer.Designs; +using Glamourer.Events; +using Glamourer.Structs; using Penumbra.GameData.Actors; +using Penumbra.GameData.Enums; +using System.Linq; +using CustomizeIndex = Glamourer.Customization.CustomizeIndex; namespace Glamourer.State; public class ActorState { + public enum MetaFlag + { + Wetness = EquipFlagExtensions.NumEquipFlags + CustomizationExtensions.NumIndices, + HatState, + VisorState, + WeaponState, + } + public ActorIdentifier Identifier { get; internal init; } - public DesignData Data { get; internal set; } + public DesignData Data; + + private readonly StateChanged.Source[] _sources = Enumerable + .Repeat(StateChanged.Source.Game, EquipFlagExtensions.NumEquipFlags + CustomizationExtensions.NumIndices + 4).ToArray(); internal ActorState(ActorIdentifier identifier) => Identifier = identifier; + + public ref StateChanged.Source this[EquipSlot slot, bool stain] + => ref _sources[slot.ToIndex() + (stain ? EquipFlagExtensions.NumEquipFlags / 2 : 0)]; + + public ref StateChanged.Source this[CustomizeIndex type] + => ref _sources[EquipFlagExtensions.NumEquipFlags + (int)type]; + + public ref StateChanged.Source this[MetaFlag flag] + => ref _sources[(int)flag]; } diff --git a/Glamourer/State/StateEditor.cs b/Glamourer/State/StateEditor.cs new file mode 100644 index 0000000..9ce9516 --- /dev/null +++ b/Glamourer/State/StateEditor.cs @@ -0,0 +1,123 @@ +using System.Linq; +using Glamourer.Customization; +using Glamourer.Interop; +using Glamourer.Interop.Structs; +using Glamourer.Services; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; + +namespace Glamourer.State; + +public class StateEditor +{ + private readonly UpdateSlotService _updateSlot; + private readonly VisorService _visor; + private readonly WeaponService _weapon; + private readonly ChangeCustomizeService _changeCustomize; + private readonly ItemManager _items; + + public StateEditor(UpdateSlotService updateSlot, VisorService visor, WeaponService weapon, ChangeCustomizeService changeCustomize, + ItemManager items) + { + _updateSlot = updateSlot; + _visor = visor; + _weapon = weapon; + _changeCustomize = changeCustomize; + _items = items; + } + + public void ChangeCustomize(ActorData data, Customize customize) + { + foreach (var actor in data.Objects) + _changeCustomize.UpdateCustomize(actor, customize.Data); + } + + public void ChangeCustomize(ActorData data, CustomizeIndex idx, CustomizeValue value) + { + foreach (var actor in data.Objects.Where(a => a.IsCharacter)) + { + var mdl = actor.Model; + var customize = mdl.GetCustomize(); + customize[idx] = value; + _changeCustomize.UpdateCustomize(mdl, customize.Data); + } + } + + public void ChangeArmor(ActorData data, EquipSlot slot, EquipItem item) + { + var idx = slot.ToIndex(); + if (idx >= 10) + return; + + var armor = item.Armor(); + foreach (var actor in data.Objects.Where(a => a.IsCharacter)) + { + var mdl = actor.Model; + var customize = mdl.IsHuman ? mdl.GetCustomize() : actor.GetCustomize(); + var (_, resolvedItem) = _items.RestrictedGear.ResolveRestricted(armor, slot, customize.Race, customize.Gender); + _updateSlot.UpdateArmor(actor.Model, slot, resolvedItem); + } + } + + public void ChangeStain(ActorData data, EquipSlot slot, StainId stain) + { + var idx = slot.ToIndex(); + switch (idx) + { + case < 10: + foreach (var actor in data.Objects.Where(a => a.IsCharacter)) + _updateSlot.UpdateStain(actor.Model, slot, stain); + break; + case 10: + foreach (var actor in data.Objects.Where(a => a.IsCharacter)) + _weapon.LoadStain(actor, EquipSlot.MainHand, stain); + break; + case 11: + foreach (var actor in data.Objects.Where(a => a.IsCharacter)) + _weapon.LoadStain(actor, EquipSlot.OffHand, stain); + break; + } + } + + public void ChangeMainhand(ActorData data, EquipItem weapon) + { + foreach (var actor in data.Objects.Where(a => a.IsCharacter)) + _weapon.LoadWeapon(actor, EquipSlot.MainHand, weapon.Weapon()); + } + + public void ChangeOffhand(ActorData data, EquipItem weapon) + { + foreach (var actor in data.Objects.Where(a => a.IsCharacter)) + _weapon.LoadWeapon(actor, EquipSlot.OffHand, weapon.Weapon()); + } + + public void ChangeVisor(ActorData data, bool value) + { + foreach (var actor in data.Objects.Where(a => a.IsCharacter)) + { + var mdl = actor.Model; + if (!mdl.IsHuman) + continue; + + _visor.SetVisorState(mdl, value); + } + } + + public unsafe void ChangeWetness(ActorData data, bool value) + { + foreach (var actor in data.Objects.Where(a => a.IsCharacter)) + actor.AsCharacter->IsGPoseWet = value; + } + + public unsafe void ChangeHatState(ActorData data, bool value) + { + foreach (var actor in data.Objects.Where(a => a.IsCharacter)) + actor.AsCharacter->DrawData.HideHeadgear(0, !value); + } + + public unsafe void ChangeWeaponState(ActorData data, bool value) + { + foreach (var actor in data.Objects.Where(a => a.IsCharacter)) + actor.AsCharacter->DrawData.HideWeapons(!value); + } +} diff --git a/Glamourer/State/StateManager.cs b/Glamourer/State/StateManager.cs index 56b2122..ad981c2 100644 --- a/Glamourer/State/StateManager.cs +++ b/Glamourer/State/StateManager.cs @@ -5,30 +5,78 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using Glamourer.Customization; using Glamourer.Designs; +using Glamourer.Events; using Glamourer.Interop; +using Glamourer.Interop.Penumbra; using Glamourer.Interop.Structs; using Glamourer.Services; +using OtterGui.Classes; using Penumbra.GameData.Actors; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; namespace Glamourer.State; -public class StateManager : IReadOnlyDictionary +public class StateManager : IReadOnlyDictionary, IDisposable { private readonly ActorService _actors; private readonly ItemManager _items; private readonly CustomizationService _customizations; private readonly VisorService _visor; + private readonly StateChanged _event; + + private readonly PenumbraService _penumbra; + private readonly UpdatedSlot _updatedSlot; private readonly Dictionary _states = new(); - public StateManager(ActorService actors, ItemManager items, CustomizationService customizations, VisorService visor) + public StateManager(ActorService actors, ItemManager items, CustomizationService customizations, VisorService visor, StateChanged @event, + UpdatedSlot updatedSlot, PenumbraService penumbra) { _actors = actors; _items = items; _customizations = customizations; _visor = visor; + _event = @event; + _updatedSlot = updatedSlot; + _penumbra = penumbra; + _updatedSlot.Subscribe(OnSlotUpdated, UpdatedSlot.Priority.StateManager); + } + + public void Dispose() + { + _updatedSlot.Unsubscribe(OnSlotUpdated); + } + + private unsafe void OnSlotUpdated(Model model, EquipSlot slot, Ref armor, Ref returnValue) + { + var actor = _penumbra.GameObjectFromDrawObject(model); + var customize = model.GetCustomize(); + if (!actor.AsCharacter->DrawData.IsHatHidden && actor.Identifier(_actors.AwaitedService, out var identifier) && _states.TryGetValue(identifier, out var state)) + { + ref var armorState = ref state[slot, false]; + ref var stainState = ref state[slot, true]; + if (armorState != StateChanged.Source.Fixed) + { + armorState = StateChanged.Source.Game; + var current = state.Data.Item(slot); + if (current.ModelId.Value != armor.Value.Set.Value || current.Variant != armor.Value.Variant) + { + var item = _items.Identify(slot, armor.Value.Set, armor.Value.Variant); + state.Data.SetItem(slot, item); + } + } + + if (stainState != StateChanged.Source.Fixed) + { + stainState = StateChanged.Source.Game; + state.Data.SetStain(slot, armor.Value.Stain); + } + } + + var (replaced, replacedArmor) = _items.RestrictedGear.ResolveRestricted(armor, slot, customize.Race, customize.Gender); + if (replaced) + armor.Assign(replacedArmor); } public bool GetOrCreate(Actor actor, [NotNullWhen(true)] out ActorState? state) @@ -52,6 +100,87 @@ public class StateManager : IReadOnlyDictionary } } + public unsafe void Update(ref DesignData data, Actor actor) + { + if (!actor.IsCharacter) + return; + + if (actor.AsCharacter->ModelCharaId != data.ModelId) + return; + + var model = actor.Model; + + static bool EqualArmor(CharacterArmor armor, EquipItem item) + => armor.Set.Value == item.ModelId.Value && armor.Variant == item.Variant; + + static bool EqualWeapon(CharacterWeapon weapon, EquipItem item) + => weapon.Set.Value == item.ModelId.Value && weapon.Type.Value == item.WeaponType.Value && weapon.Variant == item.Variant; + + data.SetHatVisible(!actor.AsCharacter->DrawData.IsHatHidden); + data.SetIsWet(actor.AsCharacter->IsGPoseWet); + data.SetWeaponVisible(!actor.AsCharacter->DrawData.IsWeaponHidden); + + CharacterWeapon main; + CharacterWeapon off; + if (model.IsHuman) + { + var head = data.IsHatVisible() ? model.GetArmor(EquipSlot.Head) : actor.GetArmor(EquipSlot.Head); + data.SetStain(EquipSlot.Head, head.Stain); + if (!EqualArmor(head, data.Item(EquipSlot.Head))) + { + var headItem = _items.Identify(EquipSlot.Head, head.Set, head.Variant); + data.SetItem(EquipSlot.Head, headItem); + } + + foreach (var slot in EquipSlotExtensions.EqdpSlots.Skip(1)) + { + var armor = model.GetArmor(slot); + data.SetStain(slot, armor.Stain); + if (EqualArmor(armor, data.Item(slot))) + continue; + + var item = _items.Identify(slot, armor.Set, armor.Variant); + data.SetItem(slot, item); + } + + data.Customize = model.GetCustomize(); + (_, _, main, off) = model.GetWeapons(actor); + data.SetVisor(_visor.GetVisorState(model)); + } + else + { + foreach (var slot in EquipSlotExtensions.EqdpSlots) + { + var armor = actor.GetArmor(slot); + data.SetStain(slot, armor.Stain); + if (EqualArmor(armor, data.Item(slot))) + continue; + + var item = _items.Identify(slot, armor.Set, armor.Variant); + data.SetItem(slot, item); + } + + data.Customize = actor.GetCustomize(); + main = actor.GetMainhand(); + off = actor.GetOffhand(); + data.SetVisor(actor.AsCharacter->DrawData.IsVisorToggled); + } + + data.SetStain(EquipSlot.MainHand, main.Stain); + data.SetStain(EquipSlot.OffHand, off.Stain); + if (!EqualWeapon(main, data.Item(EquipSlot.MainHand))) + { + var mainItem = _items.Identify(EquipSlot.MainHand, main.Set, main.Type, (byte)main.Variant); + data.SetItem(EquipSlot.MainHand, mainItem); + } + + if (!EqualWeapon(off, data.Item(EquipSlot.OffHand))) + { + var offItem = _items.Identify(EquipSlot.OffHand, off.Set, off.Type, (byte)off.Variant, data.Item(EquipSlot.MainHand).Type); + data.SetItem(EquipSlot.OffHand, offItem); + } + } + public IEnumerator> GetEnumerator() => _states.GetEnumerator(); @@ -143,4 +272,141 @@ public class StateManager : IReadOnlyDictionary ret.SetWeaponVisible(!actor.AsCharacter->DrawData.IsWeaponHidden); return ret; } + + /// Change a customization value. + public void ChangeCustomize(ActorState state, ActorData data, CustomizeIndex idx, CustomizeValue value, StateChanged.Source source, + bool force) + { + ref var s = ref state[idx]; + if (s is StateChanged.Source.Fixed && source is StateChanged.Source.Game) + return; + + var oldValue = state.Data.Customize[idx]; + if (oldValue == value && !force) + return; + + state.Data.Customize[idx] = value; + + Glamourer.Log.Excessive( + $"Changed customize {idx.ToDefaultName()} for {state.Identifier} ({string.Join(", ", data.Objects.Select(o => $"0x{o.Address}"))}) from {oldValue.Value} to {value.Value}."); + _event.Invoke(StateChanged.Type.Customize, source, state, data, (oldValue, value, idx)); + } + // + ///// Change whether to apply a specific customize value. + //public void ChangeApplyCustomize(Design design, CustomizeIndex idx, bool value) + //{ + // if (!design.SetApplyCustomize(idx, value)) + // return; + // + // design.LastEdit = DateTimeOffset.UtcNow; + // _saveService.QueueSave(design); + // Glamourer.Log.Debug($"Set applying of customization {idx.ToDefaultName()} to {value}."); + // _event.Invoke(DesignChanged.Type.ApplyCustomize, design, idx); + //} + // + ///// Change a non-weapon equipment piece. + //public void ChangeEquip(Design design, EquipSlot slot, EquipItem item) + //{ + // if (_items.ValidateItem(slot, item.Id, out item).Length > 0) + // return; + // + // var old = design.DesignData.Item(slot); + // if (!design.DesignData.SetItem(slot, item)) + // return; + // + // Glamourer.Log.Debug( + // $"Set {slot.ToName()} equipment piece in design {design.Identifier} from {old.Name} ({old.Id}) to {item.Name} ({item.Id})."); + // _saveService.QueueSave(design); + // _event.Invoke(DesignChanged.Type.Equip, design, (old, item, slot)); + //} + // + ///// Change a weapon. + //public void ChangeWeapon(Design design, EquipSlot slot, EquipItem item) + //{ + // var currentMain = design.DesignData.Item(EquipSlot.MainHand); + // var currentOff = design.DesignData.Item(EquipSlot.OffHand); + // switch (slot) + // { + // case EquipSlot.MainHand: + // var newOff = currentOff; + // if (item.Type == currentMain.Type) + // { + // if (_items.ValidateWeapons(item.Id, currentOff.Id, out _, out _).Length != 0) + // return; + // } + // else + // { + // var newOffId = FullEquipTypeExtensions.OffhandTypes.Contains(item.Type) + // ? item.Id + // : ItemManager.NothingId(item.Type.Offhand()); + // if (_items.ValidateWeapons(item.Id, newOffId, out _, out newOff).Length != 0) + // return; + // } + // + // design.DesignData.SetItem(EquipSlot.MainHand, item); + // design.DesignData.SetItem(EquipSlot.OffHand, newOff); + // design.LastEdit = DateTimeOffset.UtcNow; + // _saveService.QueueSave(design); + // Glamourer.Log.Debug( + // $"Set {EquipSlot.MainHand.ToName()} weapon in design {design.Identifier} from {currentMain.Name} ({currentMain.Id}) to {item.Name} ({item.Id})."); + // _event.Invoke(DesignChanged.Type.Weapon, design, (currentMain, currentOff, item, newOff)); + // return; + // case EquipSlot.OffHand: + // if (item.Type != currentOff.Type) + // return; + // if (_items.ValidateWeapons(currentMain.Id, item.Id, out _, out _).Length > 0) + // return; + // + // if (!design.DesignData.SetItem(EquipSlot.OffHand, item)) + // return; + // + // design.LastEdit = DateTimeOffset.UtcNow; + // _saveService.QueueSave(design); + // Glamourer.Log.Debug( + // $"Set {EquipSlot.OffHand.ToName()} weapon in design {design.Identifier} from {currentOff.Name} ({currentOff.Id}) to {item.Name} ({item.Id})."); + // _event.Invoke(DesignChanged.Type.Weapon, design, (currentMain, currentOff, currentMain, item)); + // return; + // default: return; + // } + //} + // + ///// Change whether to apply a specific equipment piece. + //public void ChangeApplyEquip(Design design, EquipSlot slot, bool value) + //{ + // if (!design.SetApplyEquip(slot, value)) + // return; + // + // design.LastEdit = DateTimeOffset.UtcNow; + // _saveService.QueueSave(design); + // Glamourer.Log.Debug($"Set applying of {slot} equipment piece to {value}."); + // _event.Invoke(DesignChanged.Type.ApplyEquip, design, slot); + //} + // + ///// Change the stain for any equipment piece. + //public void ChangeStain(Design design, EquipSlot slot, StainId stain) + //{ + // if (_items.ValidateStain(stain, out _).Length > 0) + // return; + // + // var oldStain = design.DesignData.Stain(slot); + // if (!design.DesignData.SetStain(slot, stain)) + // return; + // + // design.LastEdit = DateTimeOffset.UtcNow; + // _saveService.QueueSave(design); + // Glamourer.Log.Debug($"Set stain of {slot} equipment piece to {stain.Value}."); + // _event.Invoke(DesignChanged.Type.Stain, design, (oldStain, stain, slot)); + //} + // + ///// Change whether to apply a specific stain. + //public void ChangeApplyStain(Design design, EquipSlot slot, bool value) + //{ + // if (!design.SetApplyStain(slot, value)) + // return; + // + // design.LastEdit = DateTimeOffset.UtcNow; + // _saveService.QueueSave(design); + // Glamourer.Log.Debug($"Set applying of stain of {slot} equipment piece to {value}."); + // _event.Invoke(DesignChanged.Type.ApplyStain, design, slot); + //} } diff --git a/GlamourerOld/Designs/EquipFlag.cs b/GlamourerOld/Designs/EquipFlag.cs index bbd700f..33aca6f 100644 --- a/GlamourerOld/Designs/EquipFlag.cs +++ b/GlamourerOld/Designs/EquipFlag.cs @@ -34,7 +34,8 @@ public enum EquipFlag : uint public static class EquipFlagExtensions { - public const EquipFlag All = (EquipFlag)(((uint)EquipFlag.OffhandStain << 1) - 1); + public const EquipFlag All = (EquipFlag)(((uint)EquipFlag.OffhandStain << 1) - 1); + public const int NumEquipFlags = 24; public static EquipFlag ToFlag(this EquipSlot slot) => slot switch