Add NPC appearance tab.

This commit is contained in:
Ottermandias 2023-12-28 19:01:28 +01:00
parent 4b242bb3cf
commit 0c1dd50890
18 changed files with 684 additions and 35 deletions

View file

@ -26,9 +26,9 @@ public class DesignColorUi
public DesignColorUi(DesignColors colors, DesignManager designs, Configuration config)
{
_colors = colors;
_designs = designs;
_config = config;
_colors = colors;
_designs = designs;
_config = config;
}
public void Draw()
@ -78,7 +78,7 @@ public class DesignColorUi
{
using var id = ImRaii.PushId(idx);
ImGui.TableNextColumn();
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), buttonSize, tt, disabled, true))
{
changeString = name;
@ -119,7 +119,7 @@ public class DesignColorUi
changeString = _newName;
changeValue = 0xFFFFFFFF;
}
if (changeString.Length > 0)
{
@ -139,6 +139,7 @@ public class DesignColorUi
newColor = color;
return false;
}
ImGuiUtil.HoverTooltip(tooltip);
newColor = ImGui.ColorConvertFloat4ToU32(vec);
@ -148,12 +149,12 @@ public class DesignColorUi
public class DesignColors : ISavable, IReadOnlyDictionary<string, uint>
{
public const string AutomaticName = "Automatic";
public const string AutomaticName = "Automatic";
public const string MissingColorName = "Missing Color";
public const uint MissingColorDefault = 0xFF0000D0;
private readonly SaveService _saveService;
private readonly Dictionary<string, uint> _colors = new();
private readonly Dictionary<string, uint> _colors = [];
public uint MissingColor { get; private set; } = MissingColorDefault;
public event Action? ColorChanged;

View file

@ -39,12 +39,18 @@ public class DesignConverter(ItemManager _items, DesignManager _designs, Customi
=> ShareBase64(state, EquipFlagExtensions.All, CustomizeFlagExtensions.All, CrestExtensions.All);
public string ShareBase64(ActorState state, EquipFlag equipFlags, CustomizeFlag customizeFlags, CrestFlag crestFlags)
=> ShareBase64(state.ModelData, equipFlags, customizeFlags, crestFlags);
public string ShareBase64(in DesignData data, EquipFlag equipFlags, CustomizeFlag customizeFlags, CrestFlag crestFlags)
{
var design = Convert(state, equipFlags, customizeFlags, crestFlags);
var design = Convert(data, equipFlags, customizeFlags, crestFlags);
return ShareBase64(ShareJObject(design));
}
public DesignBase Convert(ActorState state, EquipFlag equipFlags, CustomizeFlag customizeFlags, CrestFlag crestFlags)
=> Convert(state.ModelData, equipFlags, customizeFlags, crestFlags);
public DesignBase Convert(in DesignData data, EquipFlag equipFlags, CustomizeFlag customizeFlags, CrestFlag crestFlags)
{
var design = _designs.CreateTemporary();
design.ApplyEquip = equipFlags & EquipFlagExtensions.All;
@ -54,7 +60,7 @@ public class DesignConverter(ItemManager _items, DesignManager _designs, Customi
design.SetApplyVisorToggle(design.DoApplyEquip(EquipSlot.Head));
design.SetApplyWeaponVisible(design.DoApplyEquip(EquipSlot.MainHand) || design.DoApplyEquip(EquipSlot.OffHand));
design.SetApplyWetness(true);
design.SetDesignData(_customize, state.ModelData);
design.SetDesignData(_customize, data);
return design;
}
@ -144,7 +150,7 @@ public class DesignConverter(ItemManager _items, DesignManager _designs, Customi
}
public IEnumerable<(EquipSlot Slot, EquipItem Item, StainId Stain)> FromDrawData(IReadOnlyList<CharacterArmor> armors,
CharacterWeapon mainhand, CharacterWeapon offhand)
CharacterWeapon mainhand, CharacterWeapon offhand, bool skipWarnings)
{
if (armors.Count != 10)
throw new ArgumentException("Invalid length of armor array.");
@ -156,7 +162,8 @@ public class DesignConverter(ItemManager _items, DesignManager _designs, Customi
var item = _items.Identify(slot, armor.Set, armor.Variant);
if (!item.Valid)
{
Glamourer.Log.Warning($"Appearance data {armor} for slot {slot} invalid, item could not be identified.");
if (!skipWarnings)
Glamourer.Log.Warning($"Appearance data {armor} for slot {slot} invalid, item could not be identified.");
item = ItemManager.NothingItem(slot);
}
@ -164,7 +171,7 @@ public class DesignConverter(ItemManager _items, DesignManager _designs, Customi
}
var mh = _items.Identify(EquipSlot.MainHand, mainhand.Skeleton, mainhand.Weapon, mainhand.Variant);
if (!mh.Valid)
if (!skipWarnings && !mh.Valid)
{
Glamourer.Log.Warning($"Appearance data {mainhand} for mainhand weapon invalid, item could not be identified.");
mh = _items.DefaultSword;
@ -173,7 +180,7 @@ public class DesignConverter(ItemManager _items, DesignManager _designs, Customi
yield return (EquipSlot.MainHand, mh, mainhand.Stain);
var oh = _items.Identify(EquipSlot.OffHand, offhand.Skeleton, offhand.Weapon, offhand.Variant, mh.Type);
if (!oh.Valid)
if (!skipWarnings && !oh.Valid)
{
Glamourer.Log.Warning($"Appearance data {offhand} for offhand weapon invalid, item could not be identified.");
oh = _items.GetDefaultOffhand(mh);

View file

@ -161,7 +161,7 @@ public class NpcCustomizeSet : IAsyncDataContainer, IReadOnlyList<NpcData>
// Convert the NPCs to a dictionary of lists grouped by name.
var groups = eNpcEquip.Concat(bNpcEquip).GroupBy(d => d.Name).ToDictionary(g => g.Key, g => g.ToList());
// Iterate through the sorted list.
foreach (var (name, duplicates) in groups.OrderBy(kvp => kvp.Key))
foreach (var (_, duplicates) in groups.OrderBy(kvp => kvp.Key))
{
// Remove any duplicate entries for a name with identical data.
for (var i = 0; i < duplicates.Count; ++i)
@ -177,7 +177,7 @@ public class NpcCustomizeSet : IAsyncDataContainer, IReadOnlyList<NpcData>
}
}
// If there is only a single entry, add that. This does not take additional string memory through interning.
// If there is only a single entry, add that.
if (duplicates.Count == 1)
{
_data.Add(duplicates[0]);
@ -185,13 +185,8 @@ public class NpcCustomizeSet : IAsyncDataContainer, IReadOnlyList<NpcData>
}
else
{
// Add all distinct duplicates with their ID specified in the name.
_data.AddRange(duplicates
.Select(duplicate => duplicate with
{
Name = $"{name} ({(duplicate.Kind is ObjectKind.BattleNpc ? 'B' : 'E')}{duplicate.Id})",
}));
Memory += 96 * duplicates.Count + duplicates.Sum(d => d.Name.Length * 2);
_data.AddRange(duplicates);
Memory += 96 * duplicates.Count;
}
}

View file

@ -28,6 +28,8 @@ public enum ColorId
TriStateCheck,
TriStateCross,
TriStateNeutral,
BattleNpc,
EventNpc,
}
public static class Colors
@ -60,7 +62,9 @@ public static class Colors
ColorId.QuickDesignBg => (0x00F0F0F0, "Quick Design Bar Window Background", "The color of the window background in the quick design bar." ),
ColorId.TriStateCheck => (0xFF00D000, "Checkmark in Tri-State Checkboxes", "The color of the checkmark indicating positive change in tri-state checkboxes." ),
ColorId.TriStateCross => (0xFF0000D0, "Cross in Tri-State Checkboxes", "The color of the cross indicating negative change in tri-state checkboxes." ),
ColorId.TriStateNeutral => (0xFFD0D0D0, "Dot in Tri-State Checkboxes", "The color of the dot indicating no change in tri-state checkboxes" ),
ColorId.TriStateNeutral => (0xFFD0D0D0, "Dot in Tri-State Checkboxes", "The color of the dot indicating no change in tri-state checkboxes." ),
ColorId.BattleNpc => (0xFFFFFFFF, "Battle NPC in NPC Tab", "The color of the names of battle NPCs in the NPC tab that do not have a more specific color assigned." ),
ColorId.EventNpc => (0xFFFFFFFF, "Event NPC in NPC Tab", "The color of the names of event NPCs in the NPC tab that do not have a more specific color assigned." ),
_ => (0x00000000, string.Empty, string.Empty ),
// @formatter:on
};

View file

@ -82,8 +82,11 @@ public partial class CustomizationDrawer
if (_customize.BodyType.Value == 1)
return;
if (!ImGuiUtil.DrawDisabledButton($"Reset Body Type {_customize.BodyType.Value} to Default",
new Vector2(_raceSelectorWidth + _framedIconSize.X + ImGui.GetStyle().ItemSpacing.X, 0), string.Empty, _lockedRedraw))
var label = _lockedRedraw
? $"Body Type {_customize.BodyType.Value}"
: $"Reset Body Type {_customize.BodyType.Value} to Default";
if (!ImGuiUtil.DrawDisabledButton(label, new Vector2(_raceSelectorWidth + _framedIconSize.X + ImGui.GetStyle().ItemSpacing.X, 0),
string.Empty, _lockedRedraw))
return;
Changed |= CustomizeFlag.BodyType;

View file

@ -1,6 +1,7 @@
using System;
using Glamourer.Designs;
using Glamourer.Events;
using Glamourer.Services;
using Glamourer.State;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;

View file

@ -10,6 +10,7 @@ using Glamourer.Gui.Tabs.ActorTab;
using Glamourer.Gui.Tabs.AutomationTab;
using Glamourer.Gui.Tabs.DebugTab;
using Glamourer.Gui.Tabs.DesignTab;
using Glamourer.Gui.Tabs.NpcTab;
using Glamourer.Gui.Tabs.UnlocksTab;
using ImGuiNET;
using OtterGui.Custom;
@ -29,6 +30,7 @@ public class MainWindow : Window, IDisposable
Automation = 4,
Unlocks = 5,
Messages = 6,
Npcs = 7,
}
private readonly Configuration _config;
@ -42,12 +44,14 @@ public class MainWindow : Window, IDisposable
public readonly DesignTab Designs;
public readonly AutomationTab Automation;
public readonly UnlocksTab Unlocks;
public readonly NpcTab Npcs;
public readonly MessagesTab Messages;
public TabType SelectTab = TabType.None;
public MainWindow(DalamudPluginInterface pi, Configuration config, SettingsTab settings, ActorTab actors, DesignTab designs,
DebugTab debugTab, AutomationTab automation, UnlocksTab unlocks, TabSelected @event, MessagesTab messages, DesignQuickBar quickBar)
DebugTab debugTab, AutomationTab automation, UnlocksTab unlocks, TabSelected @event, MessagesTab messages, DesignQuickBar quickBar,
NpcTab npcs)
: base(GetLabel())
{
pi.UiBuilder.DisableGposeUiHide = true;
@ -65,6 +69,7 @@ public class MainWindow : Window, IDisposable
_event = @event;
Messages = messages;
_quickBar = quickBar;
Npcs = npcs;
_config = config;
_tabs =
[
@ -73,6 +78,7 @@ public class MainWindow : Window, IDisposable
designs,
automation,
unlocks,
npcs,
messages,
debugTab,
];
@ -117,6 +123,7 @@ public class MainWindow : Window, IDisposable
TabType.Automation => Automation.Label,
TabType.Unlocks => Unlocks.Label,
TabType.Messages => Messages.Label,
TabType.Npcs => Npcs.Label,
_ => ReadOnlySpan<byte>.Empty,
};
@ -128,6 +135,7 @@ public class MainWindow : Window, IDisposable
if (label == Settings.Label) return TabType.Settings;
if (label == Automation.Label) return TabType.Automation;
if (label == Unlocks.Label) return TabType.Unlocks;
if (label == Npcs.Label) return TabType.Npcs;
if (label == Messages.Label) return TabType.Messages;
if (label == Debug.Label) return TabType.Debug;
// @formatter:on

View file

@ -35,7 +35,7 @@ public class NpcAppearancePanel(NpcCombo _npcCombo, StateManager _state, ObjectM
ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X);
var resetScroll = ImGui.InputTextWithHint("##npcFilter", "Filter...", ref _npcFilter, 64);
using var table = ImRaii.Table("npcs", 5, ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY | ImGuiTableFlags.SizingFixedFit,
using var table = ImRaii.Table("npcs", 6, ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY | ImGuiTableFlags.SizingFixedFit,
new Vector2(-1, 400 * ImGuiHelpers.GlobalScale));
if (!table)
return;
@ -46,6 +46,7 @@ public class NpcAppearancePanel(NpcCombo _npcCombo, StateManager _state, ObjectM
ImGui.TableSetupColumn("Button", ImGuiTableColumnFlags.WidthFixed);
ImGui.TableSetupColumn("Name", ImGuiTableColumnFlags.WidthFixed, ImGuiHelpers.GlobalScale * 300);
ImGui.TableSetupColumn("Kind", ImGuiTableColumnFlags.WidthFixed);
ImGui.TableSetupColumn("Id", ImGuiTableColumnFlags.WidthFixed);
ImGui.TableSetupColumn("Visor", ImGuiTableColumnFlags.WidthFixed);
ImGui.TableSetupColumn("Compare", ImGuiTableColumnFlags.WidthStretch);
@ -66,7 +67,7 @@ public class NpcAppearancePanel(NpcCombo _npcCombo, StateManager _state, ObjectM
ImGui.TableNextColumn();
if (ImGuiUtil.DrawDisabledButton("Apply", Vector2.Zero, string.Empty, disabled))
{
foreach (var (slot, item, stain) in _designConverter.FromDrawData(data.Equip.ToArray(), data.Mainhand, data.Offhand))
foreach (var (slot, item, stain) in _designConverter.FromDrawData(data.Equip.ToArray(), data.Mainhand, data.Offhand, true))
_state.ChangeEquip(state!, slot, item, stain, StateChanged.Source.Manual);
_state.ChangeVisorState(state!, data.VisorToggled, StateChanged.Source.Manual);
_state.ChangeCustomize(state!, data.Customize, CustomizeFlagExtensions.All, StateChanged.Source.Manual);
@ -80,6 +81,10 @@ public class NpcAppearancePanel(NpcCombo _npcCombo, StateManager _state, ObjectM
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted(data.Kind is ObjectKind.BattleNpc ? "B" : "E");
ImGui.TableNextColumn();
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted(data.Id.Id.ToString());
using (_ = ImRaii.PushFont(UiBuilder.IconFont))
{
ImGui.TableNextColumn();

View file

@ -0,0 +1,141 @@
using System;
using System.Collections.Generic;
using System.IO;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
using Glamourer.Designs;
using Glamourer.GameData;
using Glamourer.Services;
using ImGuiNET;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace Glamourer.Gui.Tabs.NpcTab;
public class LocalNpcAppearanceData : ISavable
{
private readonly DesignColors _colors;
public record struct Data(string Color = "", bool Favorite = false);
private readonly Dictionary<ulong, Data> _data = [];
public LocalNpcAppearanceData(DesignColors colors, SaveService saveService)
{
_colors = colors;
Load(saveService);
DataChanged += () => saveService.QueueSave(this);
}
public bool IsFavorite(in NpcData data)
=> _data.TryGetValue(ToKey(data), out var tuple) && tuple.Favorite;
public (uint Color, bool Favorite) GetData(in NpcData data)
=> _data.TryGetValue(ToKey(data), out var t)
? (GetColor(t.Color, t.Favorite, data.Kind), t.Favorite)
: (GetColor(string.Empty, false, data.Kind), false);
public string GetColor(in NpcData data)
=> _data.TryGetValue(ToKey(data), out var t) ? t.Color : string.Empty;
private uint GetColor(string color, bool favorite, ObjectKind kind)
{
if (color.Length == 0)
{
if (favorite)
return ColorId.FavoriteStarOn.Value();
return kind is ObjectKind.BattleNpc
? ColorId.BattleNpc.Value()
: ColorId.EventNpc.Value();
}
if (_colors.TryGetValue(color, out var value))
return value == 0 ? ImGui.GetColorU32(ImGuiCol.Text) : value;
return _colors.MissingColor;
}
public void ToggleFavorite(in NpcData data)
{
var key = ToKey(data);
if (_data.TryGetValue(key, out var t))
{
if (t is { Color: "", Favorite: true })
_data.Remove(key);
else
_data[key] = t with { Favorite = !t.Favorite };
}
else
{
_data[key] = new Data(string.Empty, true);
}
DataChanged.Invoke();
}
public void SetColor(in NpcData data, string color)
{
var key = ToKey(data);
if (_data.TryGetValue(key, out var t))
{
if (!t.Favorite && color.Length == 0)
_data.Remove(key);
else
_data[key] = t with { Color = color };
}
else if (color.Length != 0)
{
_data[key] = new Data(color);
}
DataChanged.Invoke();
}
private static ulong ToKey(in NpcData data)
=> (byte)data.Kind | ((ulong)data.Id.Id << 8);
public event Action DataChanged = null!;
public string ToFilename(FilenameService fileNames)
=> fileNames.NpcAppearanceFile;
public void Save(StreamWriter writer)
{
var jObj = new JObject()
{
["Version"] = 1,
["Data"] = JToken.FromObject(_data),
};
using var j = new JsonTextWriter(writer);
j.Formatting = Formatting.Indented;
jObj.WriteTo(j);
}
private void Load(SaveService save)
{
var file = save.FileNames.NpcAppearanceFile;
if (!File.Exists(file))
return;
try
{
var text = File.ReadAllText(file);
var jObj = JObject.Parse(text);
var version = jObj["Version"]?.ToObject<int>() ?? 0;
switch (version)
{
case 1:
var data = jObj["Data"]?.ToObject<Dictionary<ulong, Data>>() ?? [];
_data.EnsureCapacity(data.Count);
foreach (var kvp in data)
_data.Add(kvp.Key, kvp.Value);
return;
default: throw new Exception("Invalid version {version}.");
}
}
catch (Exception ex)
{
Glamourer.Log.Error($"Could not read local NPC appearance data:\n{ex}");
}
}
}

View file

@ -0,0 +1,45 @@
using System;
using Glamourer.Designs;
using Glamourer.GameData;
using OtterGui.Classes;
namespace Glamourer.Gui.Tabs.NpcTab;
public sealed class NpcFilter(LocalNpcAppearanceData _favorites) : FilterUtility<NpcData>
{
protected override string Tooltip
=> "Filter NPC appearances for those where their names contain the given substring.\n"
+ "Enter i:[number] to filter for NPCs of certain IDs.\n"
+ "Enter c:[string] to filter for NPC appearances set to specific colors.";
protected override (LowerString, long, int) FilterChange(string input)
=> input.Length switch
{
0 => (LowerString.Empty, 0, -1),
> 1 when input[1] == ':' =>
input[0] switch
{
'i' or 'I' => input.Length == 2 ? (LowerString.Empty, 0, -1) :
long.TryParse(input.AsSpan(2), out var r) ? (LowerString.Empty, r, 1) : (LowerString.Empty, 0, -1),
'c' or 'C' => input.Length == 2 ? (LowerString.Empty, 0, -1) : (new LowerString(input[2..]), 0, 2),
_ => (new LowerString(input), 0, 0),
},
_ => (new LowerString(input), 0, 0),
};
public override bool ApplyFilter(in NpcData value)
=> FilterMode switch
{
-1 => false,
0 => Filter.IsContained(value.Name),
1 => value.Id.Id == NumericalFilter,
2 => Filter.IsContained(GetColor(value)),
_ => false, // Should never happen
};
private string GetColor(in NpcData value)
{
var color = _favorites.GetColor(value);
return color.Length == 0 ? DesignColors.AutomaticName : color;
}
}

View file

@ -0,0 +1,294 @@
using System;
using System.Linq;
using System.Numerics;
using Dalamud.Interface;
using Dalamud.Interface.Internal.Notifications;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
using Glamourer.Designs;
using Glamourer.Events;
using Glamourer.Gui.Customization;
using Glamourer.Gui.Equipment;
using Glamourer.Gui.Tabs.DesignTab;
using Glamourer.Interop;
using Glamourer.State;
using ImGuiNET;
using Lumina.Data.Parsing.Scd;
using OtterGui;
using OtterGui.Classes;
using OtterGui.Raii;
using Penumbra.GameData.Enums;
namespace Glamourer.Gui.Tabs.NpcTab;
public class NpcPanel(
NpcSelector _selector,
LocalNpcAppearanceData _favorites,
CustomizationDrawer _customizeDrawer,
EquipmentDrawer _equipDrawer,
DesignConverter _converter,
DesignManager _designManager,
StateManager _state,
ObjectManager _objects,
DesignColors _colors)
{
private readonly DesignColorCombo _colorCombo = new(_colors, true);
private string _newName = string.Empty;
private DesignBase? _newDesign;
public void Draw()
{
using var group = ImRaii.Group();
DrawHeader();
DrawPanel();
}
private void DrawHeader()
{
HeaderDrawer.Draw(_selector.HasSelection ? _selector.Selection.Name : "No Selection", ColorId.NormalDesign.Value(),
ImGui.GetColorU32(ImGuiCol.FrameBg), 2, ExportToClipboardButton(), SaveAsDesignButton(), FavoriteButton());
SaveDesignDrawPopup();
}
private HeaderDrawer.Button FavoriteButton()
{
var (desc, color) = _favorites.IsFavorite(_selector.Selection)
? ("Remove this NPC appearance from your favorites.", ColorId.FavoriteStarOn.Value())
: ("Add this NPC Appearance to your favorites.", 0x80000000);
return new HeaderDrawer.Button
{
Icon = FontAwesomeIcon.Star,
OnClick = () => _favorites.ToggleFavorite(_selector.Selection),
Visible = _selector.HasSelection,
Description = desc,
TextColor = color,
};
}
private HeaderDrawer.Button ExportToClipboardButton()
=> new()
{
Description =
"Copy the current NPCs appearance to your clipboard.\nHold Control to disable applying of customizations for the copied design.\nHold Shift to disable applying of gear for the copied design.",
Icon = FontAwesomeIcon.Copy,
OnClick = ExportToClipboard,
Visible = _selector.HasSelection,
};
private HeaderDrawer.Button SaveAsDesignButton()
=> new()
{
Description =
"Save this NPCs appearance as a design.\nHold Control to disable applying of customizations for the saved design.\nHold Shift to disable applying of gear for the saved design.",
Icon = FontAwesomeIcon.Save,
OnClick = SaveDesignOpen,
Visible = _selector.HasSelection,
};
private void ExportToClipboard()
{
try
{
var (applyGear, applyCustomize, applyCrest) = UiHelpers.ConvertKeysToFlags();
var data = ToDesignData();
var text = _converter.ShareBase64(data, applyGear, applyCustomize, applyCrest);
ImGui.SetClipboardText(text);
}
catch (Exception ex)
{
Glamourer.Messager.NotificationMessage(ex, $"Could not copy {_selector.Selection.Name}'s data to clipboard.",
$"Could not copy data from NPC appearance {_selector.Selection.Kind} {_selector.Selection.Id.Id} to clipboard",
NotificationType.Error);
}
}
private void SaveDesignOpen()
{
ImGui.OpenPopup("Save as Design");
_newName = _selector.Selection.Name;
var (applyGear, applyCustomize, applyCrest) = UiHelpers.ConvertKeysToFlags();
var data = ToDesignData();
_newDesign = _converter.Convert(data, applyGear, applyCustomize, applyCrest);
}
private void SaveDesignDrawPopup()
{
if (!ImGuiUtil.OpenNameField("Save as Design", ref _newName))
return;
if (_newDesign != null && _newName.Length > 0)
_designManager.CreateClone(_newDesign, _newName, true);
_newDesign = null;
_newName = string.Empty;
}
private void DrawPanel()
{
using var child = ImRaii.Child("##Panel", -Vector2.One, true);
if (!child || !_selector.HasSelection)
return;
DrawButtonRow();
DrawCustomization();
DrawEquipment();
DrawAppearanceInfo();
}
private void DrawButtonRow()
{
DrawApplyToSelf();
ImGui.SameLine();
DrawApplyToTarget();
}
private void DrawCustomization()
{
if (!ImGui.CollapsingHeader("Customization"))
return;
_customizeDrawer.Draw(_selector.Selection.Customize, true, true);
ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
}
private void DrawEquipment()
{
if (!ImGui.CollapsingHeader("Equipment"))
return;
_equipDrawer.Prepare();
var designData = ToDesignData();
foreach (var slot in EquipSlotExtensions.EqdpSlots)
{
var data = new EquipDrawData(slot, designData) { Locked = true };
_equipDrawer.DrawEquip(data);
}
var mainhandData = new EquipDrawData(EquipSlot.MainHand, designData) { Locked = true };
var offhandData = new EquipDrawData(EquipSlot.OffHand, designData) { Locked = true };
_equipDrawer.DrawWeapons(mainhandData, offhandData, false);
ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
EquipmentDrawer.DrawMetaToggle(ToggleDrawData.FromValue(ActorState.MetaIndex.VisorState, _selector.Selection.VisorToggled));
ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
}
private DesignData ToDesignData()
{
var selection = _selector.Selection;
var items = _converter.FromDrawData(selection.Equip.ToArray(), selection.Mainhand, selection.Offhand, true).ToArray();
var designData = new DesignData { Customize = selection.Customize };
foreach (var (slot, item, stain) in items)
{
designData.SetItem(slot, item);
designData.SetStain(slot, stain);
}
return designData;
}
private void DrawApplyToSelf()
{
var (id, data) = _objects.PlayerData;
if (!ImGuiUtil.DrawDisabledButton("Apply to Yourself", Vector2.Zero,
"Apply the current NPC appearance to your character.\nHold Control to only apply gear.\nHold Shift to only apply customizations.",
!data.Valid))
return;
if (_state.GetOrCreate(id, data.Objects[0], out var state))
{
var (applyGear, applyCustomize, applyCrest) = UiHelpers.ConvertKeysToFlags();
var design = _converter.Convert(ToDesignData(), applyGear, applyCustomize, applyCrest);
_state.ApplyDesign(design, state, StateChanged.Source.Manual);
}
}
private void DrawApplyToTarget()
{
var (id, data) = _objects.TargetData;
var tt = id.IsValid
? data.Valid
? "Apply the current NPC appearance to your current target.\nHold Control to only apply gear.\nHold Shift to only apply customizations."
: "The current target can not be manipulated."
: "No valid target selected.";
if (!ImGuiUtil.DrawDisabledButton("Apply to Target", Vector2.Zero, tt, !data.Valid))
return;
if (_state.GetOrCreate(id, data.Objects[0], out var state))
{
var (applyGear, applyCustomize, applyCrest) = UiHelpers.ConvertKeysToFlags();
var design = _converter.Convert(ToDesignData(), applyGear, applyCustomize, applyCrest);
_state.ApplyDesign(design, state, StateChanged.Source.Manual);
}
}
private void DrawAppearanceInfo()
{
if (!ImGui.CollapsingHeader("Appearance Details"))
return;
using var table = ImRaii.Table("Details", 2);
if (!table)
return;
using var style = ImRaii.PushStyle(ImGuiStyleVar.ButtonTextAlign, new Vector2(0, 0.5f));
ImGui.TableSetupColumn("Type", ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize("Last Update Datem").X);
ImGui.TableSetupColumn("Data", ImGuiTableColumnFlags.WidthStretch);
var selection = _selector.Selection;
CopyButton("NPC Name", selection.Name);
CopyButton("NPC ID", selection.Id.Id.ToString());
ImGuiUtil.DrawFrameColumn("NPC Type");
ImGui.TableNextColumn();
var width = ImGui.GetContentRegionAvail().X;
ImGuiUtil.DrawTextButton(selection.Kind is ObjectKind.BattleNpc ? "Battle NPC" : "Event NPC", new Vector2(width, 0),
ImGui.GetColorU32(ImGuiCol.FrameBg));
ImGuiUtil.DrawFrameColumn("Color");
var color = _favorites.GetColor(selection);
var colorName = color.Length == 0 ? DesignColors.AutomaticName : color;
ImGui.TableNextColumn();
if (_colorCombo.Draw("##colorCombo", colorName,
"Associate a color with this NPC appearance. Right-Click to revert to automatic coloring.",
width - ImGui.GetStyle().ItemSpacing.X - ImGui.GetFrameHeight(), ImGui.GetTextLineHeight())
&& _colorCombo.CurrentSelection != null)
{
color = _colorCombo.CurrentSelection is DesignColors.AutomaticName ? string.Empty : _colorCombo.CurrentSelection;
_favorites.SetColor(selection, color);
}
if (ImGui.IsItemClicked(ImGuiMouseButton.Right))
{
_favorites.SetColor(selection, string.Empty);
color = string.Empty;
}
if (_colors.TryGetValue(color, out var currentColor))
{
ImGui.SameLine();
if (DesignColorUi.DrawColorButton($"Color associated with {color}", currentColor, out var newColor))
_colors.SetColor(color, newColor);
}
else if (color.Length != 0)
{
ImGui.SameLine();
var size = new Vector2(ImGui.GetFrameHeight());
using var font = ImRaii.PushFont(UiBuilder.IconFont);
ImGuiUtil.DrawTextButton(FontAwesomeIcon.ExclamationCircle.ToIconString(), size, 0, _colors.MissingColor);
ImGuiUtil.HoverTooltip("The color associated with this design does not exist.");
}
return;
static void CopyButton(string label, string text)
{
ImGuiUtil.DrawFrameColumn(label);
ImGui.TableNextColumn();
if (ImGui.Button(text, new Vector2(ImGui.GetContentRegionAvail().X, 0)))
ImGui.SetClipboardText(text);
ImGuiUtil.HoverTooltip("Click to copy to clipboard.");
}
}
}

View file

@ -0,0 +1,95 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using Glamourer.GameData;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using ImGuiClip = OtterGui.ImGuiClip;
namespace Glamourer.Gui.Tabs.NpcTab;
public class NpcSelector : IDisposable
{
private readonly NpcCustomizeSet _npcs;
private readonly LocalNpcAppearanceData _favorites;
private NpcFilter _filter;
private readonly List<int> _visibleOrdered = [];
private int _selectedGlobalIndex;
private bool _listDirty = true;
private Vector2 _defaultItemSpacing;
private float _width;
public NpcSelector(NpcCustomizeSet npcs, LocalNpcAppearanceData favorites)
{
_npcs = npcs;
_favorites = favorites;
_filter = new NpcFilter(_favorites);
_favorites.DataChanged += OnFavoriteChange;
}
public void Dispose()
{
_favorites.DataChanged -= OnFavoriteChange;
}
private void OnFavoriteChange()
=> _listDirty = true;
public void UpdateList()
{
if (!_listDirty)
return;
_listDirty = false;
_visibleOrdered.Clear();
var enumerable = _npcs.WithIndex();
if (!_filter.IsEmpty)
enumerable = enumerable.Where(d => _filter.ApplyFilter(d.Value));
var range = enumerable.OrderByDescending(d => _favorites.IsFavorite(d.Value))
.ThenBy(d => d.Index)
.Select(d => d.Index);
_visibleOrdered.AddRange(range);
}
public bool HasSelection
=> _selectedGlobalIndex >= 0 && _selectedGlobalIndex < _npcs.Count;
public NpcData Selection
=> HasSelection ? _npcs[_selectedGlobalIndex] : default;
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);
if (_filter.Draw(width))
_listDirty = true;
UpdateList();
DrawSelector();
}
private void DrawSelector()
{
using var child = ImRaii.Child("##Selector", new Vector2(_width, ImGui.GetContentRegionAvail().Y), true);
if (!child)
return;
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, _defaultItemSpacing);
ImGuiClip.ClippedDraw(_visibleOrdered, DrawSelectable, ImGui.GetTextLineHeight());
}
private void DrawSelectable(int globalIndex)
{
using var id = ImRaii.PushId(globalIndex);
using var color = ImRaii.PushColor(ImGuiCol.Text, _favorites.GetData(_npcs[globalIndex]).Color);
if (ImGui.Selectable(_npcs[globalIndex].Name, _selectedGlobalIndex == globalIndex, ImGuiSelectableFlags.AllowItemOverlap))
_selectedGlobalIndex = globalIndex;
}
}

View file

@ -0,0 +1,19 @@
using System;
using Dalamud.Interface.Utility;
using ImGuiNET;
using OtterGui.Widgets;
namespace Glamourer.Gui.Tabs.NpcTab;
public class NpcTab(NpcSelector _selector, NpcPanel _panel) : ITab
{
public ReadOnlySpan<byte> Label
=> "NPCs"u8;
public void DrawContent()
{
_selector.Draw(200 * ImGuiHelpers.GlobalScale);
ImGui.SameLine();
_panel.Draw();
}
}

View file

@ -100,4 +100,23 @@ public ref struct ToggleDrawData
SetValue = setValue,
};
}
public static ToggleDrawData FromValue(ActorState.MetaIndex index, bool value)
{
var (label, tooltip) = index switch
{
ActorState.MetaIndex.HatState => ("Hat Visible", "Hide or show the characters head gear."),
ActorState.MetaIndex.VisorState => ("Visor Toggled", "Toggle the visor state of the characters head gear."),
ActorState.MetaIndex.WeaponState => ("Weapon Visible", "Hide or show the characters weapons when not drawn."),
ActorState.MetaIndex.Wetness => ("Force Wetness", "Force the character to be wet or not."),
_ => throw new Exception("Unsupported meta index."),
};
return new ToggleDrawData
{
Label = label,
Tooltip = tooltip,
Locked = true,
CurrentValue = value,
};
}
}

View file

@ -47,9 +47,15 @@ public static class UiHelpers
public static bool DrawCheckbox(string label, string tooltip, bool value, out bool on, bool locked)
{
using var disabled = ImRaii.Disabled(locked);
var ret = ImGuiUtil.Checkbox(label, string.Empty, value, v => value = v);
ImGuiUtil.HoverTooltip(tooltip);
bool ret;
using (var disabled = ImRaii.Disabled(locked))
{
ret = ImGuiUtil.Checkbox("##" + label, string.Empty, value, v => value = v);
}
ImGui.SameLine(0, ImGui.GetStyle().ItemInnerSpacing.X);
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted(label);
ImGuiUtil.HoverTooltip(tooltip, ImGuiHoveredFlags.AllowWhenDisabled);
on = value;
return ret;
}
@ -58,7 +64,6 @@ public static class UiHelpers
out bool newApply, bool locked)
{
var flags = (sbyte)(currentApply ? currentValue ? 1 : -1 : 0);
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemInnerSpacing);
using (_ = ImRaii.Disabled(locked))
{
if (new TristateCheckbox(ColorId.TriStateCross.Value(), ColorId.TriStateCheck.Value(), ColorId.TriStateNeutral.Value()).Draw(
@ -80,7 +85,8 @@ public static class UiHelpers
ImGuiUtil.HoverTooltip($"This attribute will be {(currentApply ? currentValue ? "enabled." : "disabled." : "kept as is.")}");
ImGui.SameLine();
ImGui.SameLine(0, ImGui.GetStyle().ItemInnerSpacing.X);
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted(label);
return (currentValue != newValue, currentApply != newApply);

View file

@ -18,6 +18,7 @@ public class FilenameService
public readonly string FavoriteFile;
public readonly string DesignColorFile;
public readonly string EphemeralConfigFile;
public readonly string NpcAppearanceFile;
public FilenameService(DalamudPluginInterface pi)
{
@ -32,9 +33,9 @@ public class FilenameService
FavoriteFile = Path.Combine(ConfigDirectory, "favorites.json");
DesignColorFile = Path.Combine(ConfigDirectory, "design_colors.json");
EphemeralConfigFile = Path.Combine(ConfigDirectory, "ephemeral_config.json");
NpcAppearanceFile = Path.Combine(ConfigDirectory, "npc_appearance_data.json");
}
public IEnumerable<FileInfo> Designs()
{
if (!Directory.Exists(DesignDirectory))

View file

@ -11,6 +11,7 @@ using Glamourer.Gui.Tabs.ActorTab;
using Glamourer.Gui.Tabs.AutomationTab;
using Glamourer.Gui.Tabs.DebugTab;
using Glamourer.Gui.Tabs.DesignTab;
using Glamourer.Gui.Tabs.NpcTab;
using Glamourer.Gui.Tabs.UnlocksTab;
using Glamourer.Interop;
using Glamourer.Interop.Penumbra;
@ -131,6 +132,10 @@ public static class ServiceManagerA
.AddSingleton<ActorTab>()
.AddSingleton<ActorSelector>()
.AddSingleton<ActorPanel>()
.AddSingleton<NpcPanel>()
.AddSingleton<NpcSelector>()
.AddSingleton<LocalNpcAppearanceData>()
.AddSingleton<NpcTab>()
.AddSingleton<MainWindow>()
.AddSingleton<GenericPopupWindow>()
.AddSingleton<GlamourerWindowSystem>()

@ -1 +1 @@
Subproject commit 15203edf1dba72713f508b798048c56ad969fb95
Subproject commit e58c3c1240cda9d2d2b54f5ab7b8c729c1251fd4