mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-30 12:23:41 +01:00
317 lines
14 KiB
C#
317 lines
14 KiB
C#
using Dalamud.Interface;
|
|
using Dalamud.Interface.Textures;
|
|
using Dalamud.Interface.Textures.TextureWraps;
|
|
using Dalamud.Plugin.Services;
|
|
using Dalamud.Utility;
|
|
using ImGuiNET;
|
|
using Lumina.Data.Files;
|
|
using OtterGui;
|
|
using OtterGui.Classes;
|
|
using OtterGui.Raii;
|
|
using OtterGui.Services;
|
|
using OtterGui.Text;
|
|
using Penumbra.Api.Enums;
|
|
using Penumbra.GameData.Data;
|
|
using Penumbra.Services;
|
|
using Penumbra.UI.Classes;
|
|
|
|
namespace Penumbra.UI;
|
|
|
|
public class ChangedItemDrawer : IDisposable, IUiService
|
|
{
|
|
private static readonly string[] LowerNames = ChangedItemFlagExtensions.Order.Select(f => f.ToDescription().ToLowerInvariant()).ToArray();
|
|
|
|
public static bool TryParseIndex(ReadOnlySpan<char> input, out ChangedItemIconFlag slot)
|
|
{
|
|
// Handle numeric cases before TryParse because numbers
|
|
// are not logical otherwise.
|
|
if (int.TryParse(input, out var idx))
|
|
{
|
|
// We assume users will use 1-based index, but if they enter 0, just use the first.
|
|
if (idx == 0)
|
|
{
|
|
slot = ChangedItemFlagExtensions.Order[0];
|
|
return true;
|
|
}
|
|
|
|
// Use 1-based index.
|
|
--idx;
|
|
if (idx >= 0 && idx < ChangedItemFlagExtensions.Order.Count)
|
|
{
|
|
slot = ChangedItemFlagExtensions.Order[idx];
|
|
return true;
|
|
}
|
|
}
|
|
|
|
slot = 0;
|
|
return false;
|
|
}
|
|
|
|
public static bool TryParsePartial(string lowerInput, out ChangedItemIconFlag slot)
|
|
{
|
|
if (TryParseIndex(lowerInput, out slot))
|
|
return true;
|
|
|
|
slot = 0;
|
|
foreach (var (item, flag) in LowerNames.Zip(ChangedItemFlagExtensions.Order))
|
|
{
|
|
if (item.Contains(lowerInput, StringComparison.Ordinal))
|
|
slot |= flag;
|
|
}
|
|
|
|
return slot != 0;
|
|
}
|
|
|
|
|
|
private readonly Configuration _config;
|
|
private readonly CommunicatorService _communicator;
|
|
private readonly Dictionary<ChangedItemIconFlag, IDalamudTextureWrap> _icons = new(16);
|
|
private float _smallestIconWidth;
|
|
|
|
public static Vector2 TypeFilterIconSize
|
|
=> new(2 * ImGui.GetTextLineHeight());
|
|
|
|
public ChangedItemDrawer(IUiBuilder uiBuilder, IDataManager gameData, ITextureProvider textureProvider, CommunicatorService communicator,
|
|
Configuration config)
|
|
{
|
|
uiBuilder.RunWhenUiPrepared(() => CreateEquipSlotIcons(uiBuilder, gameData, textureProvider), true);
|
|
_communicator = communicator;
|
|
_config = config;
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
foreach (var wrap in _icons.Values.Distinct())
|
|
wrap.Dispose();
|
|
_icons.Clear();
|
|
}
|
|
|
|
/// <summary> Check if a changed item should be drawn based on its category. </summary>
|
|
public bool FilterChangedItem(string name, IIdentifiedObjectData data, LowerString filter)
|
|
=> (_config.Ephemeral.ChangedItemFilter == ChangedItemFlagExtensions.AllFlags
|
|
|| _config.Ephemeral.ChangedItemFilter.HasFlag(data.GetIcon().ToFlag()))
|
|
&& (filter.IsEmpty || !data.IsFilteredOut(name, filter));
|
|
|
|
/// <summary> Draw the icon corresponding to the category of a changed item. </summary>
|
|
public void DrawCategoryIcon(IIdentifiedObjectData data, float height)
|
|
=> DrawCategoryIcon(data.GetIcon().ToFlag(), height);
|
|
|
|
public void DrawCategoryIcon(ChangedItemIconFlag iconFlagType)
|
|
=> DrawCategoryIcon(iconFlagType, ImGui.GetFrameHeight());
|
|
|
|
public void DrawCategoryIcon(ChangedItemIconFlag iconFlagType, float height)
|
|
{
|
|
if (!_icons.TryGetValue(iconFlagType, out var icon))
|
|
{
|
|
ImGui.Dummy(new Vector2(height));
|
|
return;
|
|
}
|
|
|
|
ImGui.Image(icon.ImGuiHandle, new Vector2(height));
|
|
if (ImGui.IsItemHovered())
|
|
{
|
|
using var tt = ImRaii.Tooltip();
|
|
ImGui.Image(icon.ImGuiHandle, new Vector2(_smallestIconWidth));
|
|
ImGui.SameLine();
|
|
ImGuiUtil.DrawTextButton(iconFlagType.ToDescription(), new Vector2(0, _smallestIconWidth), 0);
|
|
}
|
|
}
|
|
|
|
public void ChangedItemHandling(IIdentifiedObjectData data, bool leftClicked)
|
|
{
|
|
var ret = leftClicked ? MouseButton.Left : MouseButton.None;
|
|
ret = ImGui.IsItemClicked(ImGuiMouseButton.Right) ? MouseButton.Right : ret;
|
|
ret = ImGui.IsItemClicked(ImGuiMouseButton.Middle) ? MouseButton.Middle : ret;
|
|
if (ret != MouseButton.None)
|
|
_communicator.ChangedItemClick.Invoke(ret, data);
|
|
if (!ImGui.IsItemHovered())
|
|
return;
|
|
|
|
using var tt = ImUtf8.Tooltip();
|
|
if (data.Count == 1)
|
|
ImUtf8.Text("This item is changed through a single effective change.\n");
|
|
else
|
|
ImUtf8.Text($"This item is changed through {data.Count} distinct effective changes.\n");
|
|
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + 3 * ImUtf8.GlobalScale);
|
|
ImGui.Separator();
|
|
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + 3 * ImUtf8.GlobalScale);
|
|
_communicator.ChangedItemHover.Invoke(data);
|
|
}
|
|
|
|
/// <summary> Draw the model information, right-justified. </summary>
|
|
public static void DrawModelData(IIdentifiedObjectData data, float height)
|
|
{
|
|
var additionalData = data.AdditionalData;
|
|
if (additionalData.Length == 0)
|
|
return;
|
|
|
|
ImGui.SameLine();
|
|
using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.ItemId.Value());
|
|
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (height - ImGui.GetTextLineHeight()) / 2);
|
|
ImUtf8.TextRightAligned(additionalData, ImGui.GetStyle().ItemInnerSpacing.X);
|
|
}
|
|
|
|
/// <summary> Draw the model information, right-justified. </summary>
|
|
public static void DrawModelData(ReadOnlySpan<byte> text, float height)
|
|
{
|
|
if (text.Length == 0)
|
|
return;
|
|
|
|
ImGui.SameLine();
|
|
using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.ItemId.Value());
|
|
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (height - ImGui.GetTextLineHeight()) / 2);
|
|
ImUtf8.TextRightAligned(text, ImGui.GetStyle().ItemInnerSpacing.X);
|
|
}
|
|
|
|
/// <summary> Draw a header line with the different icon types to filter them. </summary>
|
|
public void DrawTypeFilter()
|
|
{
|
|
if (_config.HideChangedItemFilters)
|
|
return;
|
|
|
|
var typeFilter = _config.Ephemeral.ChangedItemFilter;
|
|
if (DrawTypeFilter(ref typeFilter))
|
|
{
|
|
_config.Ephemeral.ChangedItemFilter = typeFilter;
|
|
_config.Ephemeral.Save();
|
|
}
|
|
}
|
|
|
|
/// <summary> Draw a header line with the different icon types to filter them. </summary>
|
|
public bool DrawTypeFilter(ref ChangedItemIconFlag typeFilter)
|
|
{
|
|
var ret = false;
|
|
using var _ = ImRaii.PushId("ChangedItemIconFilter");
|
|
var size = TypeFilterIconSize;
|
|
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero);
|
|
|
|
|
|
foreach (var iconType in ChangedItemFlagExtensions.Order)
|
|
{
|
|
ret |= DrawIcon(iconType, ref typeFilter);
|
|
ImGui.SameLine();
|
|
}
|
|
|
|
ImGui.SetCursorPosX(ImGui.GetContentRegionMax().X - size.X);
|
|
ImGui.Image(_icons[ChangedItemFlagExtensions.AllFlags].ImGuiHandle, size, Vector2.Zero, Vector2.One,
|
|
typeFilter switch
|
|
{
|
|
0 => new Vector4(0.6f, 0.3f, 0.3f, 1f),
|
|
ChangedItemFlagExtensions.AllFlags => new Vector4(0.75f, 0.75f, 0.75f, 1f),
|
|
_ => new Vector4(0.5f, 0.5f, 1f, 1f),
|
|
});
|
|
if (ImGui.IsItemClicked())
|
|
{
|
|
typeFilter = typeFilter == ChangedItemFlagExtensions.AllFlags ? 0 : ChangedItemFlagExtensions.AllFlags;
|
|
ret = true;
|
|
}
|
|
|
|
return ret;
|
|
|
|
bool DrawIcon(ChangedItemIconFlag type, ref ChangedItemIconFlag typeFilter)
|
|
{
|
|
var localRet = false;
|
|
var icon = _icons[type];
|
|
var flag = typeFilter.HasFlag(type);
|
|
ImGui.Image(icon.ImGuiHandle, size, Vector2.Zero, Vector2.One, flag ? Vector4.One : new Vector4(0.6f, 0.3f, 0.3f, 1f));
|
|
if (ImGui.IsItemClicked(ImGuiMouseButton.Left))
|
|
{
|
|
typeFilter = flag ? typeFilter & ~type : typeFilter | type;
|
|
localRet = true;
|
|
}
|
|
|
|
using var popup = ImRaii.ContextPopupItem(type.ToString());
|
|
if (popup)
|
|
if (ImGui.MenuItem("Enable Only This"))
|
|
{
|
|
typeFilter = type;
|
|
localRet = true;
|
|
ImGui.CloseCurrentPopup();
|
|
}
|
|
|
|
if (ImGui.IsItemHovered())
|
|
{
|
|
using var tt = ImRaii.Tooltip();
|
|
ImGui.Image(icon.ImGuiHandle, new Vector2(_smallestIconWidth));
|
|
ImGui.SameLine();
|
|
ImGuiUtil.DrawTextButton(type.ToDescription(), new Vector2(0, _smallestIconWidth), 0);
|
|
}
|
|
|
|
return localRet;
|
|
}
|
|
}
|
|
|
|
/// <summary> Initialize the icons. </summary>
|
|
private bool CreateEquipSlotIcons(IUiBuilder uiBuilder, IDataManager gameData, ITextureProvider textureProvider)
|
|
{
|
|
using var equipTypeIcons = uiBuilder.LoadUld("ui/uld/ArmouryBoard.uld");
|
|
|
|
if (!equipTypeIcons.Valid)
|
|
return false;
|
|
|
|
void Add(ChangedItemIconFlag icon, IDalamudTextureWrap? tex)
|
|
{
|
|
if (tex != null)
|
|
_icons.Add(icon, tex);
|
|
}
|
|
|
|
Add(ChangedItemIconFlag.Mainhand, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 0));
|
|
Add(ChangedItemIconFlag.Head, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 1));
|
|
Add(ChangedItemIconFlag.Body, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 2));
|
|
Add(ChangedItemIconFlag.Hands, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 3));
|
|
Add(ChangedItemIconFlag.Legs, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 5));
|
|
Add(ChangedItemIconFlag.Feet, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 6));
|
|
Add(ChangedItemIconFlag.Offhand, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 7));
|
|
Add(ChangedItemIconFlag.Ears, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 8));
|
|
Add(ChangedItemIconFlag.Neck, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 9));
|
|
Add(ChangedItemIconFlag.Wrists, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 10));
|
|
Add(ChangedItemIconFlag.Finger, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 11));
|
|
Add(ChangedItemIconFlag.Monster, textureProvider.CreateFromTexFile(gameData.GetFile<TexFile>("ui/icon/062000/062044_hr1.tex")!));
|
|
Add(ChangedItemIconFlag.Demihuman, textureProvider.CreateFromTexFile(gameData.GetFile<TexFile>("ui/icon/062000/062043_hr1.tex")!));
|
|
Add(ChangedItemIconFlag.Customization, textureProvider.CreateFromTexFile(gameData.GetFile<TexFile>("ui/icon/062000/062045_hr1.tex")!));
|
|
Add(ChangedItemIconFlag.Action, textureProvider.CreateFromTexFile(gameData.GetFile<TexFile>("ui/icon/062000/062001_hr1.tex")!));
|
|
Add(ChangedItemIconFlag.Emote, LoadEmoteTexture(gameData, textureProvider));
|
|
Add(ChangedItemIconFlag.Unknown, LoadUnknownTexture(gameData, textureProvider));
|
|
Add(ChangedItemFlagExtensions.AllFlags, textureProvider.CreateFromTexFile(gameData.GetFile<TexFile>("ui/icon/114000/114052_hr1.tex")!));
|
|
|
|
_smallestIconWidth = _icons.Values.Min(i => i.Width);
|
|
|
|
return true;
|
|
}
|
|
|
|
private static IDalamudTextureWrap? LoadUnknownTexture(IDataManager gameData, ITextureProvider textureProvider)
|
|
{
|
|
var unk = gameData.GetFile<TexFile>("ui/uld/levelup2_hr1.tex");
|
|
if (unk == null)
|
|
return null;
|
|
|
|
var image = unk.GetRgbaImageData();
|
|
var bytes = new byte[unk.Header.Height * unk.Header.Height * 4];
|
|
var diff = 2 * (unk.Header.Height - unk.Header.Width);
|
|
for (var y = 0; y < unk.Header.Height; ++y)
|
|
image.AsSpan(4 * y * unk.Header.Width, 4 * unk.Header.Width).CopyTo(bytes.AsSpan(4 * y * unk.Header.Height + diff));
|
|
|
|
return textureProvider.CreateFromRaw(RawImageSpecification.Rgba32(unk.Header.Height, unk.Header.Height), bytes, "Penumbra.UnkItemIcon");
|
|
}
|
|
|
|
private static unsafe IDalamudTextureWrap? LoadEmoteTexture(IDataManager gameData, ITextureProvider textureProvider)
|
|
{
|
|
var emote = gameData.GetFile<TexFile>("ui/icon/000000/000019_hr1.tex");
|
|
if (emote == null)
|
|
return null;
|
|
|
|
var image2 = emote.GetRgbaImageData();
|
|
fixed (byte* ptr = image2)
|
|
{
|
|
var color = (uint*)ptr;
|
|
for (var i = 0; i < image2.Length / 4; ++i)
|
|
{
|
|
if (color[i] == 0xFF000000)
|
|
image2[i * 4 + 3] = 0;
|
|
}
|
|
}
|
|
|
|
return textureProvider.CreateFromRaw(RawImageSpecification.Rgba32(emote.Header.Width, emote.Header.Height), image2,
|
|
"Penumbra.EmoteItemIcon");
|
|
}
|
|
}
|