diff --git a/Glamourer/Automation/AutoDesignApplier.cs b/Glamourer/Automation/AutoDesignApplier.cs index 4c252f6..5f47780 100644 --- a/Glamourer/Automation/AutoDesignApplier.cs +++ b/Glamourer/Automation/AutoDesignApplier.cs @@ -68,24 +68,27 @@ public class AutoDesignApplier : IDisposable case AutomationChanged.Type.ChangedDesign: case AutomationChanged.Type.ChangedConditions: _objects.Update(); - if (_objects.TryGetValue(set.Identifier, out var data)) + foreach (var id1 in set.Identifiers) { - if (_state.GetOrCreate(set.Identifier, data.Objects[0], out var state)) + if (_objects.TryGetValue(id1, out var data)) { - Reduce(data.Objects[0], state, set, false); - foreach (var actor in data.Objects) - _state.ReapplyState(actor); - } - } - else if (_objects.TryGetValueAllWorld(set.Identifier, out data)) - { - foreach (var actor in data.Objects) - { - var id = actor.GetIdentifier(_actors.AwaitedService); - if (_state.GetOrCreate(id, actor, out var state)) + if (_state.GetOrCreate(id1, data.Objects[0], out var state)) { - Reduce(actor, state, set, false); - _state.ReapplyState(actor); + Reduce(data.Objects[0], state, set, false); + foreach (var actor in data.Objects) + _state.ReapplyState(actor); + } + } + else if (_objects.TryGetValueAllWorld(id1, out data)) + { + foreach (var actor in data.Objects) + { + var id = actor.GetIdentifier(_actors.AwaitedService); + if (_state.GetOrCreate(id, actor, out var state)) + { + Reduce(actor, state, set, false); + _state.ReapplyState(actor); + } } } } @@ -265,6 +268,10 @@ public class AutoDesignApplier : IDisposable var customize = state.ModelData.Customize; CustomizeFlag fixFlags = 0; + // Skip anything not human. + if (!state.ModelData.IsHuman || !design.IsHuman) + return; + // Skip invalid designs entirely. if (_config.SkipInvalidCustomizations && !_code.EnabledMesmer diff --git a/Glamourer/Automation/AutoDesignManager.cs b/Glamourer/Automation/AutoDesignManager.cs index 0b31265..e57d022 100644 --- a/Glamourer/Automation/AutoDesignManager.cs +++ b/Glamourer/Automation/AutoDesignManager.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.RegularExpressions; +using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Interface.Internal.Notifications; using Dalamud.Utility; using Glamourer.Designs; @@ -11,7 +12,6 @@ using Glamourer.Events; using Glamourer.Interop; using Glamourer.Services; using Glamourer.Structs; -using Glamourer.Unlocks; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui.Filesystem; @@ -29,7 +29,6 @@ public class AutoDesignManager : ISavable, IReadOnlyList private readonly DesignManager _designs; private readonly ActorService _actors; private readonly AutomationChanged _event; - private readonly ItemUnlockManager _unlockManager; private readonly List _data = new(); private readonly Dictionary _enabled = new(); @@ -38,14 +37,13 @@ public class AutoDesignManager : ISavable, IReadOnlyList => _enabled; public AutoDesignManager(JobService jobs, ActorService actors, SaveService saveService, DesignManager designs, AutomationChanged @event, - FixedDesignMigrator migrator, DesignFileSystem fileSystem, ItemUnlockManager unlockManager) + FixedDesignMigrator migrator, DesignFileSystem fileSystem) { - _jobs = jobs; - _actors = actors; - _saveService = saveService; - _designs = designs; - _event = @event; - _unlockManager = unlockManager; + _jobs = jobs; + _actors = actors; + _saveService = saveService; + _designs = designs; + _event = @event; Load(); migrator.ConsumeMigratedData(_actors, fileSystem, this); } @@ -64,13 +62,13 @@ public class AutoDesignManager : ISavable, IReadOnlyList public void AddDesignSet(string name, ActorIdentifier identifier) { - if (!IdentifierValid(identifier) || name.Length == 0) + if (!IdentifierValid(identifier, out var group) || name.Length == 0) return; - var newSet = new AutoDesignSet(name, identifier.CreatePermanent()) { Enabled = false }; + var newSet = new AutoDesignSet(name, group) { Enabled = false }; _data.Add(newSet); Save(); - Glamourer.Log.Debug($"Created new design set for {newSet.Identifier.Incognito(null)}."); + Glamourer.Log.Debug($"Created new design set for {newSet.Identifiers[0].Incognito(null)}."); _event.Invoke(AutomationChanged.Type.AddedSet, newSet, (_data.Count - 1, name)); } @@ -90,12 +88,12 @@ public class AutoDesignManager : ISavable, IReadOnlyList name += " (Duplicate)"; } - var newSet = new AutoDesignSet(name, set.Identifier) { Enabled = false }; + var newSet = new AutoDesignSet(name, set.Identifiers) { Enabled = false }; newSet.Designs.AddRange(set.Designs.Select(d => d.Clone())); _data.Add(newSet); Save(); Glamourer.Log.Debug( - $"Duplicated new design set for {newSet.Identifier.Incognito(null)} with {newSet.Designs.Count} auto designs from existing set."); + $"Duplicated new design set for {newSet.Identifiers[0].Incognito(null)} with {newSet.Designs.Count} auto designs from existing set."); _event.Invoke(AutomationChanged.Type.AddedSet, newSet, (_data.Count - 1, name)); } @@ -108,7 +106,8 @@ public class AutoDesignManager : ISavable, IReadOnlyList if (set.Enabled) { set.Enabled = false; - _enabled.Remove(set.Identifier); + foreach (var id in set.Identifiers) + _enabled.Remove(id); } _data.RemoveAt(whichSet); @@ -146,26 +145,33 @@ public class AutoDesignManager : ISavable, IReadOnlyList public void ChangeIdentifier(int whichSet, ActorIdentifier to) { - if (whichSet >= _data.Count || whichSet < 0 || !IdentifierValid(to)) + if (whichSet >= _data.Count || whichSet < 0 || !IdentifierValid(to, out var group)) return; var set = _data[whichSet]; - if (set.Identifier == to) + if (set.Identifiers.Any(id => id == to)) return; - var old = set.Identifier; - set.Identifier = to.CreatePermanent(); + var old = set.Identifiers; + set.Identifiers = group; AutoDesignSet? oldEnabled = null; if (set.Enabled) { - _enabled.Remove(old); + foreach (var id in old) + _enabled.Remove(id); if (_enabled.Remove(to, out oldEnabled)) + { + foreach (var id in oldEnabled.Identifiers) + _enabled.Remove(id); oldEnabled.Enabled = false; - _enabled.Add(set.Identifier, set); + } + + foreach (var id in group) + _enabled.Add(id, set); } Save(); - Glamourer.Log.Debug($"Changed Identifier of design set {whichSet + 1} from {old.Incognito(null)} to {to.Incognito(null)}."); + Glamourer.Log.Debug($"Changed Identifier of design set {whichSet + 1} from {old[0].Incognito(null)} to {to.Incognito(null)}."); _event.Invoke(AutomationChanged.Type.ChangeIdentifier, set, (old, to, oldEnabled)); } @@ -182,13 +188,20 @@ public class AutoDesignManager : ISavable, IReadOnlyList AutoDesignSet? oldEnabled = null; if (value) { - if (_enabled.Remove(set.Identifier, out oldEnabled)) + if (_enabled.Remove(set.Identifiers[0], out oldEnabled)) + { + foreach (var id in oldEnabled.Identifiers) + _enabled.Remove(id); oldEnabled.Enabled = false; - _enabled.Add(set.Identifier, set); + } + + foreach (var id in set.Identifiers) + _enabled.Add(id, set); } - else + else if (_enabled.Remove(set.Identifiers[0], out oldEnabled)) { - _enabled.Remove(set.Identifier, out oldEnabled); + foreach (var id in oldEnabled.Identifiers) + _enabled.Remove(id); } Save(); @@ -353,7 +366,7 @@ public class AutoDesignManager : ISavable, IReadOnlyList } var id = _actors.AwaitedService.FromJson(obj["Identifier"] as JObject); - if (!IdentifierValid(id)) + if (!IdentifierValid(id, out var group)) { Glamourer.Chat.NotificationMessage("Skipped loading Automation Set: Invalid Identifier.", "Warning", NotificationType.Warning); continue; @@ -365,8 +378,13 @@ public class AutoDesignManager : ISavable, IReadOnlyList }; if (set.Enabled) - if (!_enabled.TryAdd(set.Identifier, set)) + { + if (_enabled.TryAdd(group[0], set)) + foreach (var id2 in group.Skip(1)) + _enabled[id2] = set; + else set.Enabled = false; + } _data.Add(set); @@ -377,7 +395,7 @@ public class AutoDesignManager : ISavable, IReadOnlyList { if (designObj is not JObject j) { - Glamourer.Chat.NotificationMessage($"Skipped loading design in Automation Set for {set.Identifier}: Unknown design."); + Glamourer.Chat.NotificationMessage($"Skipped loading design in Automation Set for {id.Incognito(null)}: Unknown design."); continue; } @@ -442,16 +460,57 @@ public class AutoDesignManager : ISavable, IReadOnlyList private void Save() => _saveService.DelaySave(this); - private static bool IdentifierValid(ActorIdentifier identifier) + private bool IdentifierValid(ActorIdentifier identifier, out ActorIdentifier[] group) { - if (!identifier.IsValid) - return false; - - return identifier.Type switch + var validType = identifier.Type switch { IdentifierType.Player => true, IdentifierType.Retainer => true, + IdentifierType.Npc => true, _ => false, }; + + if (!validType) + { + group = Array.Empty(); + return false; + } + + group = GetGroup(identifier); + return group.Length > 0; + } + + private ActorIdentifier[] GetGroup(ActorIdentifier identifier) + { + if (!identifier.IsValid) + return Array.Empty(); + + static ActorIdentifier[] CreateNpcs(ActorManager manager, ActorIdentifier identifier) + { + var name = manager.Data.ToName(identifier.Kind, identifier.DataId); + var table = identifier.Kind switch + { + ObjectKind.BattleNpc => manager.Data.BNpcs, + ObjectKind.EventNpc => manager.Data.ENpcs, + _ => throw new NotImplementedException(), + }; + return table.Where(kvp => kvp.Value == name) + .Select(kvp => manager.CreateIndividualUnchecked(identifier.Type, identifier.PlayerName, identifier.HomeWorld, identifier.Kind, + kvp.Key)).ToArray(); + } + + return identifier.Type switch + { + IdentifierType.Player => new[] + { + identifier.CreatePermanent(), + }, + IdentifierType.Retainer => new[] + { + identifier.CreatePermanent(), + }, + IdentifierType.Npc => CreateNpcs(_actors.AwaitedService, identifier), + _ => Array.Empty(), + }; } } diff --git a/Glamourer/Automation/AutoDesignSet.cs b/Glamourer/Automation/AutoDesignSet.cs index fb76c99..d08aa46 100644 --- a/Glamourer/Automation/AutoDesignSet.cs +++ b/Glamourer/Automation/AutoDesignSet.cs @@ -8,9 +8,9 @@ public class AutoDesignSet { public readonly List Designs; - public string Name; - public ActorIdentifier Identifier; - public bool Enabled; + public string Name; + public ActorIdentifier[] Identifiers; + public bool Enabled; public JObject Serialize() { @@ -21,20 +21,20 @@ public class AutoDesignSet return new JObject() { ["Name"] = Name, - ["Identifier"] = Identifier.ToJson(), + ["Identifier"] = Identifiers[0].ToJson(), ["Enabled"] = Enabled, ["Designs"] = list, }; } - public AutoDesignSet(string name, ActorIdentifier identifier) - : this(name, identifier, new List()) + public AutoDesignSet(string name, params ActorIdentifier[] identifiers) + : this(name, identifiers, new List()) { } - public AutoDesignSet(string name, ActorIdentifier identifier, List designs) + public AutoDesignSet(string name, ActorIdentifier[] identifiers, List designs) { - Name = name; - Identifier = identifier; - Designs = designs; + Name = name; + Identifiers = identifiers; + Designs = designs; } } diff --git a/Glamourer/Gui/Equipment/EquipmentDrawer.cs b/Glamourer/Gui/Equipment/EquipmentDrawer.cs index e5b8a1d..e5cffde 100644 --- a/Glamourer/Gui/Equipment/EquipmentDrawer.cs +++ b/Glamourer/Gui/Equipment/EquipmentDrawer.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Numerics; using Dalamud.Data; using Dalamud.Interface; using Glamourer.Designs; @@ -24,16 +25,17 @@ public class EquipmentDrawer private readonly ItemCombo[] _itemCombo; private readonly Dictionary _weaponCombo; private readonly CodeService _codes; + private readonly TextureService _textures; - - public EquipmentDrawer(DataManager gameData, ItemManager items, CodeService codes) + public EquipmentDrawer(DataManager gameData, ItemManager items, CodeService codes, TextureService textures) { _items = items; _codes = codes; + _textures = textures; _stainData = items.Stains; _stainCombo = new FilterComboColors(140, _stainData.Data.Prepend(new KeyValuePair(0, ("None", 0, false)))); - _itemCombo = EquipSlotExtensions.EqdpSlots.Select(e => new ItemCombo(gameData, items, e)).ToArray(); + _itemCombo = EquipSlotExtensions.EqdpSlots.Select(e => new ItemCombo(gameData, items, e, textures)).ToArray(); _weaponCombo = new Dictionary(FullEquipTypeExtensions.WeaponTypes.Count * 2); foreach (var type in Enum.GetValues()) { @@ -46,6 +48,15 @@ public class EquipmentDrawer _weaponCombo.Add(FullEquipType.Unknown, new WeaponCombo(items, FullEquipType.Unknown)); } + private Vector2 _iconSize; + private float _comboLength; + + public void Prepare() + { + _iconSize = new Vector2(2 * ImGui.GetFrameHeight() + ImGui.GetStyle().ItemSpacing.Y); + _comboLength = 320 * ImGuiHelpers.GlobalScale; + } + private string VerifyRestrictedGear(EquipItem gear, EquipSlot slot, Gender gender, Race race) { if (slot.IsAccessory()) @@ -61,12 +72,20 @@ public class EquipmentDrawer public bool DrawArmor(EquipItem current, EquipSlot slot, out EquipItem armor, Gender gender = Gender.Unknown, Race race = Race.Unknown) { Debug.Assert(slot.IsEquipment() || slot.IsAccessory(), $"Called {nameof(DrawArmor)} on {slot}."); + if (_codes.EnabledArtisan) return DrawArmorArtisan(current, slot, out armor, gender, race); + current.DrawIcon(_textures, _iconSize); + ImGui.SameLine(); + using var group = ImRaii.Group(); + var combo = _itemCombo[slot.ToIndex()]; armor = current; - var change = combo.Draw(VerifyRestrictedGear(armor, slot, gender, race), armor.ItemId, 320 * ImGuiHelpers.GlobalScale); + var change = combo.Draw(VerifyRestrictedGear(armor, slot, gender, race), armor.ItemId, _comboLength); + if (change) + armor = combo.CurrentSelection; + if (armor.ModelId.Value != 0) { ImGuiUtil.HoverTooltip("Right-click to clear."); @@ -75,95 +94,30 @@ public class EquipmentDrawer change = true; armor = ItemManager.NothingItem(slot); } - else if (change) - { - armor = combo.CurrentSelection; - } - } - else if (change) - { - armor = combo.CurrentSelection; } return change; } - public bool DrawArmorArtisan(EquipItem current, EquipSlot slot, out EquipItem armor, Gender gender = Gender.Unknown, - Race race = Race.Unknown) - { - using var id = ImRaii.PushId((int)slot); - int setId = current.ModelId.Value; - int variant = current.Variant; - var ret = false; - armor = current; - ImGui.SetNextItemWidth(80 * ImGuiHelpers.GlobalScale); - if (ImGui.InputInt("##setId", ref setId, 0, 0)) - { - var newSetId = (SetId)Math.Clamp(setId, 0, ushort.MaxValue); - if (newSetId.Value != current.ModelId.Value) - { - armor = _items.Identify(slot, newSetId, current.Variant); - ret = true; - } - } - - ImGui.SameLine(); - ImGui.SetNextItemWidth(40 * ImGuiHelpers.GlobalScale); - if (ImGui.InputInt("##variant", ref variant, 0, 0)) - { - var newVariant = (byte)Math.Clamp(variant, 0, byte.MaxValue); - if (newVariant != current.Variant) - { - armor = _items.Identify(slot, current.ModelId, newVariant); - ret = true; - } - } - - return ret; - } - public bool DrawStain(StainId current, EquipSlot slot, out StainId ret) { if (_codes.EnabledArtisan) return DrawStainArtisan(current, slot, out ret); var found = _stainData.TryGetValue(current, out var stain); - var change = _stainCombo.Draw($"##stain{slot}", stain.RgbaColor, stain.Name, found); + var change = _stainCombo.Draw($"##stain{slot}", stain.RgbaColor, stain.Name, found, stain.Gloss, _comboLength); + ret = current; + if (change && _stainData.TryGetValue(_stainCombo.CurrentSelection.Key, out stain)) + ret = stain.RowIndex; + ImGuiUtil.HoverTooltip("Right-click to clear."); if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) { - stain = Stain.None; - ret = stain.RowIndex; - return true; + ret = Stain.None.RowIndex; + change = true; } - if (change && _stainData.TryGetValue(_stainCombo.CurrentSelection.Key, out stain)) - { - ret = stain.RowIndex; - return true; - } - - ret = current; - return false; - } - - public bool DrawStainArtisan(StainId current, EquipSlot slot, out StainId stain) - { - using var id = ImRaii.PushId((int)slot); - int stainId = current.Value; - ImGui.SetNextItemWidth(40 * ImGuiHelpers.GlobalScale); - if (ImGui.InputInt("##stain", ref stainId, 0, 0)) - { - var newStainId = (StainId)Math.Clamp(stainId, 0, byte.MaxValue); - if (newStainId != current) - { - stain = newStainId; - return true; - } - } - - stain = current; - return false; + return change; } public bool DrawMainhand(EquipItem current, bool drawAll, out EquipItem weapon) @@ -191,7 +145,9 @@ public class EquipmentDrawer var change = combo.Draw(weapon.Name, weapon.ItemId, 320 * ImGuiHelpers.GlobalScale); if (change) + { weapon = combo.CurrentSelection; + } else if (!offType.IsOffhandType() && weapon.ModelId.Value != 0) { ImGuiUtil.HoverTooltip("Right-click to clear."); @@ -229,4 +185,59 @@ public class EquipmentDrawer public bool DrawWetness(bool current, out bool on) => DrawCheckbox("##wetness", current, out on); + + /// Draw an input for armor that can set arbitrary values instead of choosing items. + private bool DrawArmorArtisan(EquipItem current, EquipSlot slot, out EquipItem armor, Gender gender = Gender.Unknown, + Race race = Race.Unknown) + { + using var id = ImRaii.PushId((int)slot); + int setId = current.ModelId.Value; + int variant = current.Variant; + var ret = false; + armor = current; + ImGui.SetNextItemWidth(80 * ImGuiHelpers.GlobalScale); + if (ImGui.InputInt("##setId", ref setId, 0, 0)) + { + var newSetId = (SetId)Math.Clamp(setId, 0, ushort.MaxValue); + if (newSetId.Value != current.ModelId.Value) + { + armor = _items.Identify(slot, newSetId, current.Variant); + ret = true; + } + } + + ImGui.SameLine(); + ImGui.SetNextItemWidth(40 * ImGuiHelpers.GlobalScale); + if (ImGui.InputInt("##variant", ref variant, 0, 0)) + { + var newVariant = (byte)Math.Clamp(variant, 0, byte.MaxValue); + if (newVariant != current.Variant) + { + armor = _items.Identify(slot, current.ModelId, newVariant); + ret = true; + } + } + + return ret; + } + + /// Draw an input for stain that can set arbitrary values instead of choosing valid stains. + private bool DrawStainArtisan(StainId current, EquipSlot slot, out StainId stain) + { + using var id = ImRaii.PushId((int)slot); + int stainId = current.Value; + ImGui.SetNextItemWidth(40 * ImGuiHelpers.GlobalScale); + if (ImGui.InputInt("##stain", ref stainId, 0, 0)) + { + var newStainId = (StainId)Math.Clamp(stainId, 0, byte.MaxValue); + if (newStainId != current) + { + stain = newStainId; + return true; + } + } + + stain = current; + return false; + } } diff --git a/Glamourer/Gui/Equipment/ItemCombo.cs b/Glamourer/Gui/Equipment/ItemCombo.cs index d8163be..805049a 100644 --- a/Glamourer/Gui/Equipment/ItemCombo.cs +++ b/Glamourer/Gui/Equipment/ItemCombo.cs @@ -15,12 +15,15 @@ namespace Glamourer.Gui.Equipment; public sealed class ItemCombo : FilterComboCache { + private readonly TextureService _textures; + public readonly string Label; private uint _currentItem; - public ItemCombo(DataManager gameData, ItemManager items, EquipSlot slot) + public ItemCombo(DataManager gameData, ItemManager items, EquipSlot slot, TextureService textures) : base(() => GetItems(items, slot)) { + _textures = textures; Label = GetLabel(gameData, slot); _currentItem = ItemManager.NothingId(slot); } @@ -40,7 +43,6 @@ public sealed class ItemCombo : FilterComboCache CurrentSelectionIdx = Items.IndexOf(i => i.ItemId == _currentItem); CurrentSelection = CurrentSelectionIdx >= 0 ? Items[CurrentSelectionIdx] : default; return base.UpdateCurrentSelected(CurrentSelectionIdx); - } public bool Draw(string previewName, uint previewIdx, float width) diff --git a/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs b/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs index 9ea5cb1..8ac3fa4 100644 --- a/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs +++ b/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs @@ -130,6 +130,7 @@ public class ActorPanel if (_customizationDrawer.Draw(_state!.ModelData.Customize, false)) _stateManager.ChangeCustomize(_state, _customizationDrawer.Customize, _customizationDrawer.Changed, StateChanged.Source.Manual); + _equipmentDrawer.Prepare(); foreach (var slot in EquipSlotExtensions.EqdpSlots) { var stain = _state.ModelData.Stain(slot); diff --git a/Glamourer/Gui/Tabs/AutomationTab/HumanNpcCombo.cs b/Glamourer/Gui/Tabs/AutomationTab/HumanNpcCombo.cs new file mode 100644 index 0000000..ae1d1d4 --- /dev/null +++ b/Glamourer/Gui/Tabs/AutomationTab/HumanNpcCombo.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Dalamud.Game.ClientState.Objects.Enums; +using Dalamud.Utility; +using Glamourer.Services; +using ImGuiNET; +using OtterGui.Custom; +using OtterGui.Widgets; +using Penumbra.GameData.Data; + +namespace Glamourer.Gui.Tabs.AutomationTab; + +public sealed class HumanNpcCombo : FilterComboCache<(string Name, ObjectKind Kind, uint[] Ids)> +{ + private readonly string _label; + + public HumanNpcCombo(string label, IdentifierService service, HumanModelList humans) + : base(() => CreateList(service, humans)) + => _label = label; + + protected override string ToString((string Name, ObjectKind Kind, uint[] Ids) obj) + => obj.Name; + + protected override bool DrawSelectable(int globalIdx, bool selected) + { + var (name, kind, ids) = Items[globalIdx]; + if (globalIdx > 0 && Items[globalIdx - 1].Name == name || globalIdx + 1 < Items.Count && Items[globalIdx + 1].Name == name) + name = $"{name} ({kind.ToName()})"; + var ret = ImGui.Selectable(name, selected); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip(string.Join('\n', ids.Select(i => i.ToString()))); + + return ret; + } + + public bool Draw(float width) + => Draw(_label, CurrentSelection.Name, string.Empty, width, ImGui.GetTextLineHeightWithSpacing()); + + + /// Compare strings in a way that letters and numbers are sorted before any special symbols. + private class NameComparer : IComparer<(string, ObjectKind)> + { + public int Compare((string, ObjectKind) x, (string, ObjectKind) y) + { + if (x.Item1.IsNullOrWhitespace() || y.Item1.IsNullOrWhitespace()) + return StringComparer.OrdinalIgnoreCase.Compare(x.Item1, y.Item1); + + var comp = (char.IsAsciiLetterOrDigit(x.Item1[0]), char.IsAsciiLetterOrDigit(y.Item1[0])) switch + { + (true, false) => -1, + (false, true) => 1, + _ => StringComparer.OrdinalIgnoreCase.Compare(x.Item1, y.Item1), + }; + + if (comp != 0) + return comp; + + return Comparer.Default.Compare(x.Item2, y.Item2); + } + } + + private static IReadOnlyList<(string Name, ObjectKind Kind, uint[] Ids)> CreateList(IdentifierService service, HumanModelList humans) + { + var ret = new List<(string Name, ObjectKind Kind, uint Id)>(1024); + for (var modelChara = 0u; modelChara < service.AwaitedService.NumModelChara; ++modelChara) + { + if (!humans.IsHuman(modelChara)) + continue; + + var list = service.AwaitedService.ModelCharaNames(modelChara); + if (list.Count == 0) + continue; + + foreach (var (name, kind, id) in list.Where(t => !t.Name.IsNullOrWhitespace())) + { + switch (kind) + { + case ObjectKind.BattleNpc: + var nameIds = service.AwaitedService.GetBnpcNames(id); + ret.AddRange(nameIds.Select(nameId => (name, kind, nameId))); + break; + case ObjectKind.EventNpc: + ret.Add((name, kind, id)); + break; + } + } + } + + return ret.GroupBy(t => (t.Name, t.Kind)).OrderBy(g => g.Key, Comparer) + .Select(g => (g.Key.Name, g.Key.Kind, g.Select(g => g.Id).ToArray())).ToList(); + } + + private static readonly NameComparer Comparer = new(); +} diff --git a/Glamourer/Gui/Tabs/AutomationTab/IdentifierDrawer.cs b/Glamourer/Gui/Tabs/AutomationTab/IdentifierDrawer.cs new file mode 100644 index 0000000..9d087eb --- /dev/null +++ b/Glamourer/Gui/Tabs/AutomationTab/IdentifierDrawer.cs @@ -0,0 +1,70 @@ +using Dalamud.Game.ClientState.Objects.Enums; +using Glamourer.Services; +using ImGuiNET; +using OtterGui.Custom; +using Penumbra.GameData.Actors; +using Penumbra.GameData.Data; +using Penumbra.String; + +namespace Glamourer.Gui.Tabs.AutomationTab; + +public class IdentifierDrawer +{ + private readonly WorldCombo _worldCombo; + private readonly HumanNpcCombo _humanNpcCombo; + private readonly ActorService _actors; + + private string _characterName = string.Empty; + + public ActorIdentifier NpcIdentifier { get; private set; } = ActorIdentifier.Invalid; + public ActorIdentifier PlayerIdentifier { get; private set; } = ActorIdentifier.Invalid; + public ActorIdentifier RetainerIdentifier { get; private set; } = ActorIdentifier.Invalid; + + public IdentifierDrawer(ActorService actors, IdentifierService identifier, HumanModelList humans) + { + _actors = actors; + _worldCombo = new WorldCombo(actors.AwaitedService.Data.Worlds); + _humanNpcCombo = new HumanNpcCombo("Human Event NPCs", identifier, humans); + } + + public void DrawName(float width) + { + ImGui.SetNextItemWidth(width); + if (ImGui.InputTextWithHint("##Name", "Character Name...", ref _characterName, 32)) + UpdateIdentifiers(); + } + + public void DrawWorld(float width) + { + if (_worldCombo.Draw(width)) + UpdateIdentifiers(); + } + + public void DrawNpcs(float width) + { + if (_humanNpcCombo.Draw(width)) + UpdateIdentifiers(); + } + + public bool CanSetPlayer + => PlayerIdentifier.IsValid; + + public bool CanSetRetainer + => RetainerIdentifier.IsValid; + + public bool CanSetNpc + => NpcIdentifier.IsValid; + + private void UpdateIdentifiers() + { + if (ByteString.FromString(_characterName, out var byteName)) + { + PlayerIdentifier = _actors.AwaitedService.CreatePlayer(byteName, _worldCombo.CurrentSelection.Key); + RetainerIdentifier = _actors.AwaitedService.CreateRetainer(byteName, ActorIdentifier.RetainerType.Both); + } + + NpcIdentifier = _humanNpcCombo.CurrentSelection.Kind is ObjectKind.EventNpc or ObjectKind.BattleNpc + ? _actors.AwaitedService.CreateNpc(_humanNpcCombo.CurrentSelection.Kind, _humanNpcCombo.CurrentSelection.Ids[0]) + : ActorIdentifier.Invalid; + } +} diff --git a/Glamourer/Gui/Tabs/AutomationTab/SetPanel.cs b/Glamourer/Gui/Tabs/AutomationTab/SetPanel.cs index 8cc6d49..9d02a3d 100644 --- a/Glamourer/Gui/Tabs/AutomationTab/SetPanel.cs +++ b/Glamourer/Gui/Tabs/AutomationTab/SetPanel.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using System.Numerics; using System.Text; @@ -12,12 +11,12 @@ using Glamourer.Services; using Glamourer.Structs; using Glamourer.Unlocks; using ImGuiNET; -using Lumina.Excel.GeneratedSheets; using OtterGui; using OtterGui.Raii; using OtterGui.Widgets; using Penumbra.GameData.Enums; using Action = System.Action; +using CustomizeIndex = Glamourer.Customization.CustomizeIndex; namespace Glamourer.Gui.Tabs.AutomationTab; @@ -29,8 +28,9 @@ public class SetPanel private readonly CustomizeUnlockManager _customizeUnlocks; private readonly CustomizationService _customizations; - private readonly DesignCombo _designCombo; - private readonly JobGroupCombo _jobGroupCombo; + private readonly DesignCombo _designCombo; + private readonly JobGroupCombo _jobGroupCombo; + private readonly IdentifierDrawer _identifierDrawer; private string? _tempName; private int _dragIndex = -1; @@ -38,13 +38,14 @@ public class SetPanel private Action? _endAction; public SetPanel(SetSelector selector, AutoDesignManager manager, DesignManager designs, JobService jobs, ItemUnlockManager itemUnlocks, - CustomizeUnlockManager customizeUnlocks, CustomizationService customizations) + CustomizeUnlockManager customizeUnlocks, CustomizationService customizations, IdentifierDrawer identifierDrawer) { _selector = selector; _manager = manager; _itemUnlocks = itemUnlocks; _customizeUnlocks = customizeUnlocks; _customizations = customizations; + _identifierDrawer = identifierDrawer; _designCombo = new DesignCombo(_manager, designs); _jobGroupCombo = new JobGroupCombo(manager, jobs); } @@ -101,6 +102,9 @@ public class SetPanel if (ImGui.Checkbox("Enabled", ref enabled)) _manager.SetState(_selector.SelectionIndex, enabled); + ImGui.Separator(); + DrawIdentifierSelection(_selector.SelectionIndex); + DrawDesignTable(); } @@ -201,7 +205,10 @@ public class SetPanel sb.Clear(); var sb2 = new StringBuilder(); var customize = design.Design.DesignData.Customize; - var set = _customizations.AwaitedService.GetList(customize.Clan, customize.Gender); + if (!design.Design.DesignData.IsHuman) + sb.AppendLine("The base model id can not be changed automatically to something non-human."); + + var set = _customizations.AwaitedService.GetList(customize.Clan, customize.Gender); foreach (var type in CustomizationExtensions.All) { var flag = type.ToFlag(); @@ -268,6 +275,21 @@ public class SetPanel _manager.ChangeApplicationType(set, autoDesignIndex, newType); } + private void DrawIdentifierSelection(int setIndex) + { + using var id = ImRaii.PushId("Identifiers"); + _identifierDrawer.DrawWorld(200); + _identifierDrawer.DrawName(300); + _identifierDrawer.DrawNpcs(300); + if (ImGuiUtil.DrawDisabledButton("Set to Retainer", new Vector2(100, 0), string.Empty, !_identifierDrawer.CanSetRetainer)) + _manager.ChangeIdentifier(setIndex, _identifierDrawer.RetainerIdentifier); + ImGui.SameLine(); + if (ImGuiUtil.DrawDisabledButton("Set to Character", new Vector2(100, 0), string.Empty, !_identifierDrawer.CanSetPlayer)) + _manager.ChangeIdentifier(setIndex, _identifierDrawer.PlayerIdentifier); + ImGui.SameLine(); + if (ImGuiUtil.DrawDisabledButton("Set to Npc", new Vector2(100, 0), string.Empty, !_identifierDrawer.CanSetNpc)) + _manager.ChangeIdentifier(setIndex, _identifierDrawer.NpcIdentifier); + } private static readonly IReadOnlyList<(AutoDesign.Type, string)> Types = new[] diff --git a/Glamourer/Gui/Tabs/AutomationTab/SetSelector.cs b/Glamourer/Gui/Tabs/AutomationTab/SetSelector.cs index cce8b83..28aa7e1 100644 --- a/Glamourer/Gui/Tabs/AutomationTab/SetSelector.cs +++ b/Glamourer/Gui/Tabs/AutomationTab/SetSelector.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Numerics; using Dalamud.Interface; using Glamourer.Automation; @@ -118,7 +119,7 @@ public class SetSelector : IDisposable _list.Clear(); foreach (var set in _manager) { - var id = set.Identifier.ToString(); + var id = set.Identifiers[0].ToString(); if (CheckFilters(set, id)) _list.Add(set); } @@ -195,11 +196,11 @@ public class SetSelector : IDisposable DrawDragDrop(set, index); - var text = set.Identifier.ToString(); + var text = set.Identifiers[0].ToString(); if (IncognitoMode) - text = set.Identifier.Incognito(text); + text = set.Identifiers[0].Incognito(text); var textSize = ImGui.CalcTextSize(text); - var textColor = _objects.ContainsKey(set.Identifier) ? ColorId.AutomationActorAvailable : ColorId.AutomationActorUnavailable; + var textColor = set.Identifiers.Any(_objects.ContainsKey) ? ColorId.AutomationActorAvailable : ColorId.AutomationActorUnavailable; ImGui.SetCursorPos(new Vector2(ImGui.GetContentRegionAvail().X - textSize.X, ImGui.GetCursorPosY() - ImGui.GetTextLineHeightWithSpacing())); ImGuiUtil.TextColored(textColor.Value(), text); diff --git a/Glamourer/Gui/Tabs/DebugTab.cs b/Glamourer/Gui/Tabs/DebugTab.cs index 1da882b..183099a 100644 --- a/Glamourer/Gui/Tabs/DebugTab.cs +++ b/Glamourer/Gui/Tabs/DebugTab.cs @@ -1304,7 +1304,7 @@ public unsafe class DebugTab : ITab ImGuiUtil.DrawTableColumn(set.Enabled.ToString()); ImGuiUtil.DrawTableColumn("Actor"); - ImGuiUtil.DrawTableColumn(set.Identifier.ToString()); + ImGuiUtil.DrawTableColumn(set.Identifiers[0].ToString()); foreach (var (design, designIdx) in set.Designs.WithIndex()) { diff --git a/Glamourer/Gui/Tabs/DesignTab/DesignPanel.cs b/Glamourer/Gui/Tabs/DesignTab/DesignPanel.cs index 58ceac1..637b53a 100644 --- a/Glamourer/Gui/Tabs/DesignTab/DesignPanel.cs +++ b/Glamourer/Gui/Tabs/DesignTab/DesignPanel.cs @@ -127,6 +127,7 @@ public class DesignPanel if (!ImGui.CollapsingHeader("Equipment")) return; + _equipmentDrawer.Prepare(); foreach (var slot in EquipSlotExtensions.EqdpSlots) { var stain = _selector.Selected!.DesignData.Stain(slot); diff --git a/Glamourer/Gui/Tabs/UnlocksTab/UnlockOverview.cs b/Glamourer/Gui/Tabs/UnlocksTab/UnlockOverview.cs index d4aae47..97f7d1d 100644 --- a/Glamourer/Gui/Tabs/UnlocksTab/UnlockOverview.cs +++ b/Glamourer/Gui/Tabs/UnlocksTab/UnlockOverview.cs @@ -21,7 +21,7 @@ public class UnlockOverview private readonly CustomizationService _customizations; private readonly CustomizeUnlockManager _customizeUnlocks; private readonly PenumbraChangedItemTooltip _tooltip; - private readonly TextureCache _textureCache; + private readonly TextureService _textures; private static readonly Vector4 UnavailableTint = new(0.3f, 0.3f, 0.3f, 1.0f); @@ -67,14 +67,14 @@ public class UnlockOverview } public UnlockOverview(ItemManager items, CustomizationService customizations, ItemUnlockManager itemUnlocks, - CustomizeUnlockManager customizeUnlocks, PenumbraChangedItemTooltip tooltip, TextureCache textureCache) + CustomizeUnlockManager customizeUnlocks, PenumbraChangedItemTooltip tooltip, TextureService textures) { _items = items; _customizations = customizations; _itemUnlocks = itemUnlocks; _customizeUnlocks = customizeUnlocks; _tooltip = tooltip; - _textureCache = textureCache; + _textures = textures; } public void Draw() @@ -154,7 +154,7 @@ public class UnlockOverview void DrawItem(EquipItem item) { var unlocked = _itemUnlocks.IsUnlocked(item.ItemId, out var time); - var iconHandle = _textureCache.LoadIcon(item.IconId); + var iconHandle = _textures.LoadIcon(item.IconId); if (!iconHandle.HasValue) return; diff --git a/Glamourer/Gui/Tabs/UnlocksTab/UnlockTable.cs b/Glamourer/Gui/Tabs/UnlocksTab/UnlockTable.cs index 2f3faff..a6b6b1e 100644 --- a/Glamourer/Gui/Tabs/UnlocksTab/UnlockTable.cs +++ b/Glamourer/Gui/Tabs/UnlocksTab/UnlockTable.cs @@ -10,7 +10,6 @@ using Glamourer.Structs; using Glamourer.Unlocks; using ImGuiNET; using OtterGui; -using OtterGui.Classes; using OtterGui.Raii; using OtterGui.Table; using Penumbra.GameData.Enums; @@ -22,10 +21,10 @@ public class UnlockTable : Table, IDisposable { private readonly ObjectUnlocked _event; - public UnlockTable(ItemManager items, TextureCache cache, ItemUnlockManager itemUnlocks, + public UnlockTable(ItemManager items, TextureService textures, ItemUnlockManager itemUnlocks, PenumbraChangedItemTooltip tooltip, ObjectUnlocked @event) : base("ItemUnlockTable", new ItemList(items), - new NameColumn(cache, tooltip) { Label = "Item Name..." }, + new NameColumn(textures, tooltip) { Label = "Item Name..." }, new SlotColumn() { Label = "Equip Slot" }, new TypeColumn() { Label = "Item Type..." }, new UnlockDateColumn(itemUnlocks) { Label = "Unlocked" }, @@ -36,7 +35,7 @@ public class UnlockTable : Table, IDisposable Sortable = true; Flags |= ImGuiTableFlags.Hideable; _event.Subscribe(OnObjectUnlock, ObjectUnlocked.Priority.UnlockTable); - cache.Logger = Glamourer.Log; + textures.Logger = Glamourer.Log; } public void Dispose() @@ -44,13 +43,13 @@ public class UnlockTable : Table, IDisposable private sealed class NameColumn : ColumnString { - private readonly TextureCache _textures; + private readonly TextureService _textures; private readonly PenumbraChangedItemTooltip _tooltip; public override float Width => 400 * ImGuiHelpers.GlobalScale; - public NameColumn(TextureCache textures, PenumbraChangedItemTooltip tooltip) + public NameColumn(TextureService textures, PenumbraChangedItemTooltip tooltip) { _textures = textures; _tooltip = tooltip; diff --git a/Glamourer/Gui/UiHelpers.cs b/Glamourer/Gui/UiHelpers.cs new file mode 100644 index 0000000..69d4287 --- /dev/null +++ b/Glamourer/Gui/UiHelpers.cs @@ -0,0 +1,34 @@ + +using System.Numerics; +using Dalamud.Interface; +using Glamourer.Services; +using ImGuiNET; +using OtterGui; +using Penumbra.GameData.Structs; + +namespace Glamourer.Gui; + +public static class UiHelpers +{ + + public static void DrawIcon(this EquipItem item, TextureService textures, Vector2 size) + { + var isEmpty = item.ModelId.Value == 0; + var (ptr, textureSize, empty) = textures.GetIcon(item); + if (empty) + { + var pos = ImGui.GetCursorScreenPos(); + ImGui.GetWindowDrawList().AddRectFilled(pos, pos + size, + ImGui.GetColorU32(isEmpty ? ImGuiCol.FrameBg : ImGuiCol.FrameBgActive), 5 * ImGuiHelpers.GlobalScale); + if (ptr != nint.Zero) + ImGui.Image(ptr, size, Vector2.Zero, Vector2.One, + isEmpty ? new Vector4(0.1f, 0.1f, 0.1f, 0.5f) : new Vector4(0.3f, 0.3f, 0.3f, 0.8f)); + else + ImGui.Dummy(size); + } + else + { + ImGuiUtil.HoverIcon(ptr, textureSize, size); + } + } +} \ No newline at end of file diff --git a/Glamourer/Services/ServiceManager.cs b/Glamourer/Services/ServiceManager.cs index d2e50df..294a594 100644 --- a/Glamourer/Services/ServiceManager.cs +++ b/Glamourer/Services/ServiceManager.cs @@ -56,7 +56,7 @@ public static class ServiceManager .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton(); + .AddSingleton(); private static IServiceCollection AddEvents(this IServiceCollection services) => services.AddSingleton() @@ -126,7 +126,8 @@ public static class ServiceManager .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton(); + .AddSingleton() + .AddSingleton(); private static IServiceCollection AddApi(this IServiceCollection services) => services.AddSingleton() diff --git a/Glamourer/Services/TextureService.cs b/Glamourer/Services/TextureService.cs new file mode 100644 index 0000000..a8b8ac3 --- /dev/null +++ b/Glamourer/Services/TextureService.cs @@ -0,0 +1,71 @@ +using System; +using System.Linq; +using Dalamud.Data; +using System.Numerics; +using Dalamud.Game; +using Dalamud.Interface; +using ImGuiScene; +using OtterGui.Classes; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; + +namespace Glamourer.Services; + +public sealed class TextureService : TextureCache, IDisposable +{ + public TextureService(Framework framework, UiBuilder uiBuilder, DataManager dataManager) + : base(framework, uiBuilder, dataManager) + => _slotIcons = CreateSlotIcons(uiBuilder); + + private readonly TextureWrap?[] _slotIcons; + + public (nint, Vector2, bool) GetIcon(EquipItem item) + { + if (item.IconId != 0 && TryLoadIcon(item.IconId, out var ret)) + return (ret.Value.Texture, ret.Value.Dimensions, false); + + var idx = item.Type.ToSlot().ToIndex(); + return idx < 12 && _slotIcons[idx] != null + ? (_slotIcons[idx]!.ImGuiHandle, new Vector2(_slotIcons[idx]!.Width, _slotIcons[idx]!.Height), true) + : (nint.Zero, Vector2.Zero, true); + } + + public new void Dispose() + { + base.Dispose(); + for (var i = 0; i < _slotIcons.Length; ++i) + { + _slotIcons[i]?.Dispose(); + _slotIcons[i] = null; + } + } + + private static TextureWrap[] CreateSlotIcons(UiBuilder uiBuilder) + { + var ret = new TextureWrap[12]; + + using var uldWrapper = uiBuilder.LoadUld("ui/uld/ArmouryBoard.uld"); + + if (!uldWrapper.Valid) + { + Glamourer.Log.Error($"Could not get empty slot uld."); + return ret; + } + + ret[0] = uldWrapper.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 1)!; + ret[1] = uldWrapper.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 2)!; + ret[2] = uldWrapper.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 3)!; + ret[3] = uldWrapper.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 5)!; + ret[4] = uldWrapper.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 6)!; + ret[5] = uldWrapper.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 8)!; + ret[6] = uldWrapper.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 9)!; + ret[7] = uldWrapper.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 10)!; + ret[8] = uldWrapper.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 11)!; + ret[9] = ret[10]; + ret[10] = uldWrapper.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 0)!; + ret[11] = uldWrapper.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 7)!; + + uldWrapper.Dispose(); + return ret; + } +}