This commit is contained in:
Ottermandias 2023-07-03 16:39:52 +02:00
parent 60443f6a53
commit 5c003d8cd4
14 changed files with 566 additions and 198 deletions

View file

@ -3,37 +3,39 @@ using Dalamud.Data;
using Dalamud.Plugin;
using Penumbra.GameData.Enums;
namespace Glamourer.Customization
namespace Glamourer.Customization;
public class CustomizationManager : ICustomizationManager
{
public class CustomizationManager : ICustomizationManager
private static CustomizationOptions? _options;
private CustomizationManager()
{ }
public static ICustomizationManager Create(DalamudPluginInterface pi, DataManager gameData)
{
private static CustomizationOptions? _options;
private CustomizationManager()
{ }
public static ICustomizationManager Create(DalamudPluginInterface pi, DataManager gameData)
{
_options ??= new CustomizationOptions(pi, gameData);
return new CustomizationManager();
}
public IReadOnlyList<Race> Races
=> CustomizationOptions.Races;
public IReadOnlyList<SubRace> Clans
=> CustomizationOptions.Clans;
public IReadOnlyList<Gender> Genders
=> CustomizationOptions.Genders;
public CustomizationSet GetList(SubRace clan, Gender gender)
=> _options!.GetList(clan, gender);
public ImGuiScene.TextureWrap GetIcon(uint iconId)
=> _options!.GetIcon(iconId);
public string GetName(CustomName name)
=> _options!.GetName(name);
_options ??= new CustomizationOptions(pi, gameData);
return new CustomizationManager();
}
public IReadOnlyList<Race> Races
=> CustomizationOptions.Races;
public IReadOnlyList<SubRace> Clans
=> CustomizationOptions.Clans;
public IReadOnlyList<Gender> Genders
=> CustomizationOptions.Genders;
public CustomizationSet GetList(SubRace clan, Gender gender)
=> _options!.GetList(clan, gender);
public ImGuiScene.TextureWrap GetIcon(uint iconId)
=> _options!.GetIcon(iconId);
public void RemoveIcon(uint iconId)
=> _options!.RemoveIcon(iconId);
public string GetName(CustomName name)
=> _options!.GetName(name);
}

View file

@ -4,7 +4,6 @@ using System.Linq;
using System.Reflection;
using Dalamud;
using Dalamud.Data;
using Dalamud.Logging;
using Dalamud.Plugin;
using Dalamud.Utility;
using Lumina.Excel;
@ -39,6 +38,9 @@ public partial class CustomizationOptions
internal ImGuiScene.TextureWrap GetIcon(uint id)
=> _icons.LoadIcon(id);
internal void RemoveIcon(uint id)
=> _icons.RemoveIcon(id);
private readonly IconStorage _icons;
private static readonly int ListSize = Clans.Length * Genders.Length;
@ -416,7 +418,7 @@ public partial class CustomizationOptions
// Obtain available hairstyles via reflection from the Hair sheet for the given subrace and gender.
private CustomizeData[] GetHairStyles(SubRace race, Gender gender)
{
var row = _hairSheet.GetRow(((uint)race - 1) * 2 - 1 + (uint)gender)!;
var row = _hairSheet.GetRow(((uint)race - 1) * 2 - 1 + (uint)gender)!;
// Unknown30 is the number of available hairstyles.
var hairList = new List<CustomizeData>(row.Unknown30);
// Hairstyles can be found starting at Unknown66.
@ -431,14 +433,10 @@ public partial class CustomizationOptions
// Hair Row from CustomizeSheet might not be set in case of unlockable hair.
var hairRow = _customizeSheet.GetRow(customizeIdx);
if (hairRow == null)
{
hairList.Add(new CustomizeData(CustomizeIndex.Hairstyle, (CustomizeValue)i, customizeIdx));
}
else if (_options._icons.IconExists(hairRow.Icon))
{
hairList.Add(new CustomizeData(CustomizeIndex.Hairstyle, (CustomizeValue)hairRow.FeatureID, hairRow.Icon,
(ushort)hairRow.RowId));
}
}
return hairList.ToArray();
@ -464,8 +462,8 @@ public partial class CustomizationOptions
// Get face paints from the hair sheet via reflection.
private CustomizeData[] GetFacePaints(SubRace race, Gender gender)
{
var row = _hairSheet.GetRow(((uint)race - 1) * 2 - 1 + (uint)gender)!;
var paintList = new List<CustomizeData>(row.Unknown37);
var row = _hairSheet.GetRow(((uint)race - 1) * 2 - 1 + (uint)gender)!;
var paintList = new List<CustomizeData>(row.Unknown37);
// Number of available face paints is at Unknown37.
for (var i = 0; i < row.Unknown37; ++i)
{
@ -480,13 +478,12 @@ public partial class CustomizationOptions
var paintRow = _customizeSheet.GetRow(customizeIdx);
// Facepaint Row from CustomizeSheet might not be set in case of unlockable facepaints.
if (paintRow != null)
{
paintList.Add(new CustomizeData(CustomizeIndex.FacePaint, (CustomizeValue)paintRow.FeatureID, paintRow.Icon,
(ushort)paintRow.RowId));
}
else
paintList.Add(new CustomizeData(CustomizeIndex.FacePaint, (CustomizeValue)i, customizeIdx));
}
return paintList.ToArray();
}

View file

@ -1,4 +1,6 @@
using System.Runtime.CompilerServices;
using System;
using System.Linq;
using System.Runtime.CompilerServices;
namespace Glamourer.Customization;
@ -44,7 +46,10 @@ public enum CustomizeIndex : byte
public static class CustomizationExtensions
{
public const int NumIndices = ((int)CustomizeIndex.FacePaintColor + 1);
public const int NumIndices = (int)CustomizeIndex.FacePaintColor + 1;
public static readonly CustomizeIndex[] All = Enum.GetValues<CustomizeIndex>()
.Where(v => v is not CustomizeIndex.Race and not CustomizeIndex.BodyType).ToArray();
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static (int ByteIdx, byte Mask) ToByteAndMask(this CustomizeIndex index)

View file

@ -12,5 +12,6 @@ public interface ICustomizationManager
public CustomizationSet GetList(SubRace race, Gender gender);
public ImGuiScene.TextureWrap GetIcon(uint iconId);
public void RemoveIcon(uint iconId);
public string GetName(CustomName name);
}

View file

@ -0,0 +1,34 @@
using System;
using OtterGui.Classes;
namespace Glamourer.Events;
/// <summary>
/// Triggered when a new item or customization is unlocked.
/// <list type="number">
/// <item>Parameter is the type of the unlocked object </item>
/// <item>Parameter is the id of the unlocked object. </item>
/// <item>Parameter is the timestamp of the unlock. </item>
/// </list>
/// </summary>
public sealed class ObjectUnlocked : EventWrapper<Action<ObjectUnlocked.Type, uint, DateTimeOffset>, ObjectUnlocked.Priority>
{
public enum Type
{
Item,
Customization,
}
public enum Priority
{
/// <seealso cref="Gui.Tabs.UnlocksTab.UnlockTable.OnObjectUnlock"/>
UnlockTable = 0,
}
public ObjectUnlocked()
: base(nameof(ObjectUnlocked))
{ }
public void Invoke(Type type, uint id, DateTimeOffset timestamp)
=> Invoke(this, type, id, timestamp);
}

View file

@ -3,7 +3,6 @@ using Dalamud.Plugin;
using Glamourer.Gui;
using Glamourer.Interop;
using Glamourer.Services;
using Lumina.Excel.GeneratedSheets;
using Microsoft.Extensions.DependencyInjection;
using OtterGui.Classes;
using OtterGui.Log;
@ -36,7 +35,7 @@ public class Glamourer : IDalamudPlugin
_services.GetRequiredService<BackupService>(); // call backup service.
_services.GetRequiredService<GlamourerWindowSystem>(); // initialize ui.
_services.GetRequiredService<CommandService>(); // initialize commands.
_services.GetRequiredService<VisorService>();
_services.GetRequiredService<VisorService>();
}
catch
{

View file

@ -2,22 +2,32 @@
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Text;
using Dalamud.Interface;
using Glamourer.Automation;
using Glamourer.Customization;
using Glamourer.Designs;
using Glamourer.Interop;
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;
namespace Glamourer.Gui.Tabs.AutomationTab;
public class SetPanel
{
private readonly AutoDesignManager _manager;
private readonly SetSelector _selector;
private readonly AutoDesignManager _manager;
private readonly SetSelector _selector;
private readonly ItemUnlockManager _itemUnlocks;
private readonly CustomizeUnlockManager _customizeUnlocks;
private readonly CustomizationService _customizations;
private readonly DesignCombo _designCombo;
private readonly JobGroupCombo _jobGroupCombo;
@ -27,12 +37,16 @@ public class SetPanel
private Action? _endAction;
public SetPanel(SetSelector selector, AutoDesignManager manager, DesignManager designs, JobService jobs)
public SetPanel(SetSelector selector, AutoDesignManager manager, DesignManager designs, JobService jobs, ItemUnlockManager itemUnlocks,
CustomizeUnlockManager customizeUnlocks, CustomizationService customizations)
{
_selector = selector;
_manager = manager;
_designCombo = new DesignCombo(_manager, designs);
_jobGroupCombo = new JobGroupCombo(manager, jobs);
_selector = selector;
_manager = manager;
_itemUnlocks = itemUnlocks;
_customizeUnlocks = customizeUnlocks;
_customizations = customizations;
_designCombo = new DesignCombo(_manager, designs);
_jobGroupCombo = new JobGroupCombo(manager, jobs);
}
private AutoDesignSet Selection
@ -92,7 +106,7 @@ public class SetPanel
private void DrawDesignTable()
{
using var table = ImRaii.Table("SetTable", 5, ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollX | ImGuiTableFlags.ScrollY);
using var table = ImRaii.Table("SetTable", 6, ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollX | ImGuiTableFlags.ScrollY);
if (!table)
return;
@ -101,6 +115,7 @@ public class SetPanel
ImGui.TableSetupColumn("Design", ImGuiTableColumnFlags.WidthFixed, 220 * ImGuiHelpers.GlobalScale);
ImGui.TableSetupColumn("Application", ImGuiTableColumnFlags.WidthFixed, 5 * ImGui.GetFrameHeight() + 4 * 2 * ImGuiHelpers.GlobalScale);
ImGui.TableSetupColumn("Job Restrictions", ImGuiTableColumnFlags.WidthStretch);
ImGui.TableSetupColumn("Warnings", ImGuiTableColumnFlags.WidthFixed, 4 * ImGui.GetFrameHeight() + 3 * 2 * ImGuiHelpers.GlobalScale);
ImGui.TableHeadersRow();
foreach (var (design, idx) in Selection.Designs.WithIndex())
@ -120,6 +135,8 @@ public class SetPanel
DrawApplicationTypeBoxes(Selection, design, idx);
ImGui.TableNextColumn();
_jobGroupCombo.Draw(Selection, design, idx);
ImGui.TableNextColumn();
DrawWarnings(design, idx);
}
ImGui.TableNextColumn();
@ -135,6 +152,79 @@ public class SetPanel
_endAction = null;
}
private void DrawWarnings(AutoDesign design, int idx)
{
var size = new Vector2(ImGui.GetFrameHeight());
size.X += ImGuiHelpers.GlobalScale;
var (equipFlags, customizeFlags, _, _, _, _) = design.ApplyWhat();
equipFlags &= design.Design.ApplyEquip;
customizeFlags &= design.Design.ApplyCustomize;
var sb = new StringBuilder();
foreach (var slot in EquipSlotExtensions.EqdpSlots.Append(EquipSlot.MainHand).Append(EquipSlot.OffHand))
{
var flag = slot.ToFlag();
if (!equipFlags.HasFlag(flag))
continue;
var item = design.Design.DesignData.Item(slot);
if (!_itemUnlocks.IsUnlocked(item.Id, out _))
sb.AppendLine($"{item.Name} in {slot.ToName()} slot is not unlocked but should be applied.");
}
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(2 * ImGuiHelpers.GlobalScale, 0));
static void DrawWarning(StringBuilder sb, uint color, Vector2 size, string suffix, string good)
{
using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale);
if (sb.Length > 0)
{
sb.Append(suffix);
using (var font = ImRaii.PushFont(UiBuilder.IconFont))
{
ImGuiUtil.DrawTextButton(FontAwesomeIcon.ExclamationCircle.ToIconString(), size, color);
}
ImGuiUtil.HoverTooltip(sb.ToString());
}
else
{
ImGuiUtil.DrawTextButton(string.Empty, size, 0);
ImGuiUtil.HoverTooltip(good);
}
}
DrawWarning(sb, 0xA03030F0, size, "\nThese items will be skipped when applied automatically. To change this, see",
"All equipment to be applied is unlocked."); // TODO
sb.Clear();
var sb2 = new StringBuilder();
var customize = design.Design.DesignData.Customize;
var set = _customizations.AwaitedService.GetList(customize.Clan, customize.Gender);
foreach (var type in CustomizationExtensions.All)
{
var flag = type.ToFlag();
if (!customizeFlags.HasFlag(flag))
continue;
if (flag.RequiresRedraw())
sb.AppendLine($"{type.ToDefaultName()} Customization can not be changed automatically."); // TODO
else if (type is CustomizeIndex.Hairstyle or CustomizeIndex.FacePaint
&& set.DataByValue(type, customize[type], out var data, customize.Face) >= 0
&& !_customizeUnlocks.IsUnlocked(data!.Value, out _))
sb2.AppendLine(
$"{type.ToDefaultName()} Customization {_customizeUnlocks.Unlockable[data.Value].Name} is not unlocked but should be applied.");
}
ImGui.SameLine();
DrawWarning(sb2, 0xA03030F0, size, "\nThese customizations will be skipped when applied automatically. To change this, see",
"All customizations to be applied are unlocked."); // TODO
ImGui.SameLine();
DrawWarning(sb, 0xA030F0F0, size, "\nThese customizations will be skipped when applied automatically.",
"No customizations unable to be applied automatically are set to be applied."); // TODO
}
private void DrawDragDrop(AutoDesignSet set, int index)
{
const string dragDropLabel = "DesignDragDrop";

View file

@ -6,8 +6,11 @@ using Glamourer.Customization;
using Glamourer.Services;
using Glamourer.Unlocks;
using ImGuiNET;
using OtterGui;
using OtterGui.Classes;
using OtterGui.Raii;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer.Gui.Tabs.UnlocksTab;
@ -18,55 +21,110 @@ public class UnlockOverview
private readonly CustomizationService _customizations;
private readonly CustomizeUnlockManager _customizeUnlocks;
private readonly PenumbraChangedItemTooltip _tooltip;
private readonly TextureCache _textureCache;
private static readonly Vector4 UnavailableTint = new(0.3f, 0.3f, 0.3f, 1.0f);
private FullEquipType _selected1 = FullEquipType.Unknown;
private SubRace _selected2 = SubRace.Unknown;
private Gender _selected3 = Gender.Unknown;
private void DrawSelector()
{
using var child = ImRaii.Child("Selector", new Vector2(200 * ImGuiHelpers.GlobalScale, -1), true);
if (!child)
return;
foreach (var type in Enum.GetValues<FullEquipType>())
{
if (type.IsOffhandType() || !_items.ItemService.AwaitedService.TryGetValue(type, out var items) || items.Count == 0)
continue;
if (ImGui.Selectable(type.ToName(), _selected1 == type))
{
ClearIcons(_selected1);
_selected1 = type;
_selected2 = SubRace.Unknown;
_selected3 = Gender.Unknown;
}
}
foreach (var clan in _customizations.AwaitedService.Clans)
{
foreach (var gender in _customizations.AwaitedService.Genders)
{
if (_customizations.AwaitedService.GetList(clan, gender).HairStyles.Count == 0)
continue;
if (ImGui.Selectable($"{(gender is Gender.Male ? '♂' : '♀')} {clan.ToShortName()} Hair & Paint",
_selected2 == clan && _selected3 == gender))
{
ClearIcons(_selected1);
_selected1 = FullEquipType.Unknown;
_selected2 = clan;
_selected3 = gender;
}
}
}
}
private void ClearIcons(FullEquipType type)
{
if (!_items.ItemService.AwaitedService.TryGetValue(type, out var items))
return;
foreach (var item in items)
_customizations.AwaitedService.RemoveIcon(item.IconId);
}
public UnlockOverview(ItemManager items, CustomizationService customizations, ItemUnlockManager itemUnlocks,
CustomizeUnlockManager customizeUnlocks, PenumbraChangedItemTooltip tooltip)
CustomizeUnlockManager customizeUnlocks, PenumbraChangedItemTooltip tooltip, TextureCache textureCache)
{
_items = items;
_customizations = customizations;
_itemUnlocks = itemUnlocks;
_customizeUnlocks = customizeUnlocks;
_tooltip = tooltip;
_textureCache = textureCache;
}
public void Draw()
{
using var color = ImRaii.PushColor(ImGuiCol.Border, ImGui.GetColorU32(ImGuiCol.TableBorderStrong));
DrawSelector();
ImGui.SameLine();
DrawPanel();
}
private void DrawPanel()
{
using var child = ImRaii.Child("Panel", -Vector2.One, true);
if (!child)
return;
var iconSize = ImGuiHelpers.ScaledVector2(32);
foreach (var type in Enum.GetValues<FullEquipType>())
DrawEquipTypeHeader(iconSize, type);
iconSize = ImGuiHelpers.ScaledVector2(64);
foreach (var gender in _customizations.AwaitedService.Genders)
{
foreach (var clan in _customizations.AwaitedService.Clans)
DrawCustomizationHeader(iconSize, clan, gender);
}
if (_selected1 is not FullEquipType.Unknown)
DrawItems();
else if (_selected2 is not SubRace.Unknown && _selected3 is not Gender.Unknown)
DrawCustomizations();
}
private void DrawCustomizationHeader(Vector2 iconSize, SubRace subRace, Gender gender)
private void DrawCustomizations()
{
var set = _customizations.AwaitedService.GetList(subRace, gender);
if (set.HairStyles.Count == 0 && set.FacePaints.Count == 0)
return;
var set = _customizations.AwaitedService.GetList(_selected2, _selected3);
if (!ImGui.CollapsingHeader($"Unlockable {subRace.ToName()} {gender.ToName()} Customizations"))
return;
var spacing = IconSpacing;
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, spacing);
var iconSize = ImGuiHelpers.ScaledVector2(128);
var iconsPerRow = IconsPerRow(iconSize.X, spacing.X);
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero);
foreach (var customization in set.HairStyles.Concat(set.FacePaints))
var counter = 0;
foreach (var customize in set.HairStyles.Concat(set.FacePaints))
{
if (!_customizeUnlocks.Unlockable.TryGetValue(customization, out var unlockData))
if (!_customizeUnlocks.Unlockable.TryGetValue(customize, out var unlockData))
continue;
var unlocked = _customizeUnlocks.IsUnlocked(customization, out var time);
var icon = _customizations.AwaitedService.GetIcon(customization.IconId);
var unlocked = _customizeUnlocks.IsUnlocked(customize, out var time);
var icon = _customizations.AwaitedService.GetIcon(customize.IconId);
ImGui.Image(icon.ImGuiHandle, iconSize, Vector2.Zero, Vector2.One, unlocked ? Vector4.One : UnavailableTint);
if (ImGui.IsItemHovered())
@ -76,37 +134,44 @@ public class UnlockOverview
if (size.X >= iconSize.X && size.Y >= iconSize.Y)
ImGui.Image(icon.ImGuiHandle, size);
ImGui.TextUnformatted(unlockData.Name);
ImGui.TextUnformatted($"{customization.Index.ToDefaultName()} {customization.Value.Value}");
ImGui.TextUnformatted($"{customize.Index.ToDefaultName()} {customize.Value.Value}");
ImGui.TextUnformatted(unlocked ? $"Unlocked on {time:g}" : "Not unlocked.");
}
ImGui.SameLine();
if (ImGui.GetContentRegionAvail().X < iconSize.X)
ImGui.NewLine();
if (counter != iconsPerRow - 1)
{
ImGui.SameLine();
++counter;
}
else
{
counter = 0;
}
}
if (ImGui.GetCursorPosX() != 0)
ImGui.NewLine();
}
private void DrawEquipTypeHeader(Vector2 iconSize, FullEquipType type)
private void DrawItems()
{
if (type.IsOffhandType() || !_items.ItemService.AwaitedService.TryGetValue(type, out var items) || items.Count == 0)
if (!_items.ItemService.AwaitedService.TryGetValue(_selected1, out var items))
return;
if (!ImGui.CollapsingHeader($"{type.ToName()}s"))
return;
var spacing = IconSpacing;
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, spacing);
var iconSize = ImGuiHelpers.ScaledVector2(64);
var iconsPerRow = IconsPerRow(iconSize.X, spacing.X);
var numRows = (items.Count + iconsPerRow - 1) / iconsPerRow;
var numVisibleRows = (int)(Math.Ceiling(ImGui.GetContentRegionAvail().Y / (iconSize.Y + spacing.Y)) + 0.5f);
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(2 * ImGuiHelpers.GlobalScale));
foreach (var item in items)
void DrawItem(EquipItem item)
{
if (!ImGui.IsItemVisible())
{ }
var unlocked = _itemUnlocks.IsUnlocked(item.Id, out var time);
var iconHandle = _textureCache.LoadIcon(item.IconId);
if (!iconHandle.HasValue)
return;
var unlocked = _itemUnlocks.IsUnlocked(item.Id, out var time);
var icon = _customizations.AwaitedService.GetIcon(item.IconId);
var (icon, size) = iconHandle.Value.Value;
ImGui.Image(icon.ImGuiHandle, iconSize, Vector2.Zero, Vector2.One, unlocked ? Vector4.One : UnavailableTint);
ImGui.Image(icon, iconSize, Vector2.Zero, Vector2.One, unlocked ? Vector4.One : UnavailableTint);
if (ImGui.IsItemClicked())
{
// TODO link
@ -117,10 +182,9 @@ public class UnlockOverview
if (ImGui.IsItemHovered())
{
using var tt = ImRaii.Tooltip();
var size = new Vector2(icon.Width, icon.Height);
using var tt = ImRaii.Tooltip();
if (size.X >= iconSize.X && size.Y >= iconSize.Y)
ImGui.Image(icon.ImGuiHandle, size);
ImGui.Image(icon, size);
ImGui.TextUnformatted(item.Name);
var slot = item.Type.ToSlot();
ImGui.TextUnformatted($"{item.Type.ToName()} ({slot.ToName()})");
@ -133,13 +197,35 @@ public class UnlockOverview
unlocked ? time == DateTimeOffset.MinValue ? "Always Unlocked" : $"Unlocked on {time:g}" : "Not Unlocked.");
_tooltip.CreateTooltip(item, string.Empty, false);
}
}
ImGui.SameLine();
if (ImGui.GetContentRegionAvail().X < iconSize.X)
ImGui.NewLine();
var skips = ImGuiClip.GetNecessarySkips(iconSize.Y + spacing.Y);
var end = Math.Min(numVisibleRows * iconsPerRow + skips * iconsPerRow, items.Count);
var counter = 0;
for (var idx = skips * iconsPerRow; idx < end; ++idx)
{
DrawItem(items[idx]);
if (counter != iconsPerRow - 1)
{
ImGui.SameLine();
++counter;
}
else
{
counter = 0;
}
}
if (ImGui.GetCursorPosX() != 0)
ImGui.NewLine();
var remainder = numRows - numVisibleRows - skips;
if (remainder > 0)
ImGuiClip.DrawEndDummy(remainder, iconSize.Y + spacing.Y);
}
private static Vector2 IconSpacing
=> ImGuiHelpers.ScaledVector2(2);
private static int IconsPerRow(float iconWidth, float iconSpacing)
=> (int)(ImGui.GetContentRegionAvail().X / (iconWidth + iconSpacing));
}

View file

@ -3,13 +3,14 @@ using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using Dalamud.Game.ClientState.Objects.SubKinds;
using Dalamud.Interface;
using Glamourer.Events;
using Glamourer.Services;
using Glamourer.Structs;
using Glamourer.Unlocks;
using ImGuiNET;
using OtterGui;
using OtterGui.Classes;
using OtterGui.Raii;
using OtterGui.Table;
using Penumbra.GameData.Enums;
@ -17,35 +18,43 @@ using Penumbra.GameData.Structs;
namespace Glamourer.Gui.Tabs.UnlocksTab;
public class UnlockTable : Table<EquipItem>
public class UnlockTable : Table<EquipItem>, IDisposable
{
public UnlockTable(ItemManager items, CustomizationService customizations, ItemUnlockManager itemUnlocks,
PenumbraChangedItemTooltip tooltip)
private readonly ObjectUnlocked _event;
public UnlockTable(ItemManager items, TextureCache cache, ItemUnlockManager itemUnlocks,
PenumbraChangedItemTooltip tooltip, ObjectUnlocked @event)
: base("ItemUnlockTable", new ItemList(items),
new NameColumn(customizations, tooltip) { Label = "Item Name..." },
new SlotColumn() { Label = "Equip Slot" },
new TypeColumn() { Label = "Item Type..." },
new UnlockDateColumn(itemUnlocks) { Label = "Unlocked" },
new ItemIdColumn() { Label = "Item Id..." },
new ModelDataColumn(items) { Label = "Model Data..." })
new NameColumn(cache, tooltip) { Label = "Item Name..." },
new SlotColumn() { Label = "Equip Slot" },
new TypeColumn() { Label = "Item Type..." },
new UnlockDateColumn(itemUnlocks) { Label = "Unlocked" },
new ItemIdColumn() { Label = "Item Id..." },
new ModelDataColumn(items) { Label = "Model Data..." })
{
_event = @event;
Sortable = true;
Flags |= ImGuiTableFlags.Hideable;
_event.Subscribe(OnObjectUnlock, ObjectUnlocked.Priority.UnlockTable);
cache.Logger = Glamourer.Log;
}
public void Dispose()
=> _event.Unsubscribe(OnObjectUnlock);
private sealed class NameColumn : ColumnString<EquipItem>
{
private readonly CustomizationService _customizations;
private readonly TextureCache _textures;
private readonly PenumbraChangedItemTooltip _tooltip;
public override float Width
=> 400 * ImGuiHelpers.GlobalScale;
public NameColumn(CustomizationService customizations, PenumbraChangedItemTooltip tooltip)
public NameColumn(TextureCache textures, PenumbraChangedItemTooltip tooltip)
{
_customizations = customizations;
_tooltip = tooltip;
Flags |= ImGuiTableColumnFlags.NoHide | ImGuiTableColumnFlags.NoReorder;
_textures = textures;
_tooltip = tooltip;
Flags |= ImGuiTableColumnFlags.NoHide | ImGuiTableColumnFlags.NoReorder;
}
public override string ToName(EquipItem item)
@ -53,8 +62,11 @@ public class UnlockTable : Table<EquipItem>
public override void DrawColumn(EquipItem item, int _)
{
var icon = _customizations.AwaitedService.GetIcon(item.IconId);
ImGui.Image(icon.ImGuiHandle, new Vector2(ImGui.GetFrameHeight()));
var iconHandle = _textures.LoadIcon(item.IconId);
if (iconHandle.HasValue)
ImGuiUtil.HoverIcon(iconHandle.Value, new Vector2(ImGui.GetFrameHeight()));
else
ImGui.Dummy(new Vector2(ImGui.GetFrameHeight()));
ImGui.SameLine();
ImGui.AlignTextToFramePadding();
if (ImGui.Selectable(item.Name))
@ -191,7 +203,7 @@ public class UnlockTable : Table<EquipItem>
public override int Compare(EquipItem lhs, EquipItem rhs)
{
var unlockedLhs = _unlocks.IsUnlocked(lhs.Id, out var timeLhs);
var unlockedRhs = _unlocks.IsUnlocked(lhs.Id, out var timeRhs);
var unlockedRhs = _unlocks.IsUnlocked(rhs.Id, out var timeRhs);
var c1 = unlockedLhs.CompareTo(unlockedRhs);
return c1 != 0 ? c1 : timeLhs.CompareTo(timeRhs);
}
@ -273,4 +285,10 @@ public class UnlockTable : Table<EquipItem>
public int Count
=> _items.ItemService.AwaitedService.TotalItemCount(true);
}
private void OnObjectUnlock(ObjectUnlocked.Type _1, uint _2, DateTimeOffset _3)
{
FilterDirty = true;
SortDirty = true;
}
}

View file

@ -54,7 +54,8 @@ public static class ServiceManager
.AddSingleton<SaveService>()
.AddSingleton<CodeService>()
.AddSingleton<ConfigMigrationService>()
.AddSingleton<Configuration>();
.AddSingleton<Configuration>()
.AddSingleton<TextureCache>();
private static IServiceCollection AddEvents(this IServiceCollection services)
=> services.AddSingleton<VisorStateChanged>()
@ -64,7 +65,8 @@ public static class ServiceManager
.AddSingleton<StateChanged>()
.AddSingleton<WeaponLoading>()
.AddSingleton<HeadGearVisibilityChanged>()
.AddSingleton<WeaponVisibilityChanged>();
.AddSingleton<WeaponVisibilityChanged>()
.AddSingleton<ObjectUnlocked>();
private static IServiceCollection AddData(this IServiceCollection services)
=> services.AddSingleton<IdentifierService>()

View file

@ -10,6 +10,7 @@ using Dalamud.Utility;
using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.Game.UI;
using Glamourer.Customization;
using Glamourer.Events;
using Glamourer.Services;
using Lumina.Excel.GeneratedSheets;
@ -17,8 +18,10 @@ namespace Glamourer.Unlocks;
public class CustomizeUnlockManager : IDisposable, ISavable
{
private readonly SaveService _saveService;
private readonly ClientState _clientState;
private readonly SaveService _saveService;
private readonly ClientState _clientState;
private readonly ObjectUnlocked _event;
private readonly Dictionary<uint, long> _unlocked = new();
public readonly IReadOnlyDictionary<CustomizeData, (uint Data, string Name)> Unlockable;
@ -27,11 +30,12 @@ public class CustomizeUnlockManager : IDisposable, ISavable
=> _unlocked;
public unsafe CustomizeUnlockManager(SaveService saveService, CustomizationService customizations, DataManager gameData,
ClientState clientState)
ClientState clientState, ObjectUnlocked @event)
{
SignatureHelper.Initialise(this);
_saveService = saveService;
_clientState = clientState;
_event = @event;
Unlockable = CreateUnlockableCustomizations(customizations, gameData);
Load();
_setUnlockLinkValueHook.Enable();
@ -51,13 +55,13 @@ public class CustomizeUnlockManager : IDisposable, ISavable
// All other customizations are not unlockable.
if (data.Index is not CustomizeIndex.Hairstyle and not CustomizeIndex.FacePaint)
{
time = DateTime.MinValue;
time = DateTimeOffset.MinValue;
return true;
}
if (!Unlockable.TryGetValue(data, out var pair))
{
time = DateTime.MinValue;
time = DateTimeOffset.MinValue;
return true;
}
@ -74,8 +78,9 @@ public class CustomizeUnlockManager : IDisposable, ISavable
}
_unlocked.TryAdd(pair.Data, DateTimeOffset.UtcNow.ToUnixTimeMilliseconds());
Save();
time = DateTimeOffset.UtcNow;
_event.Invoke(ObjectUnlocked.Type.Customization, pair.Data, time);
Save();
return true;
}
@ -107,7 +112,10 @@ public class CustomizeUnlockManager : IDisposable, ISavable
foreach (var (_, (id, _)) in Unlockable)
{
if (instance->IsUnlockLinkUnlocked(id) && _unlocked.TryAdd(id, time))
{
_event.Invoke(ObjectUnlocked.Type.Customization, id, DateTimeOffset.FromUnixTimeMilliseconds(time));
++count;
}
}
if (count <= 0)
@ -141,6 +149,7 @@ public class CustomizeUnlockManager : IDisposable, ISavable
if (id != data || !_unlocked.TryAdd(id, time))
continue;
_event.Invoke(ObjectUnlocked.Type.Customization, id, DateTimeOffset.FromUnixTimeMilliseconds(time));
Save();
break;
}
@ -161,16 +170,11 @@ public class CustomizeUnlockManager : IDisposable, ISavable
=> _saveService.QueueSave(this);
public void Save(StreamWriter writer)
{ }
=> UnlockDictionaryHelpers.Save(writer, Unlocked);
private void Load()
{
var file = ToFilename(_saveService.FileNames);
if (!File.Exists(file))
return;
_unlocked.Clear();
}
=> UnlockDictionaryHelpers.Load(ToFilename(_saveService.FileNames), _unlocked, id => Unlockable.Any(c => c.Value.Data == id),
"customization");
/// <summary> Create a list of all unlockable hairstyles and facepaints. </summary>
private static Dictionary<CustomizeData, (uint Data, string Name)> CreateUnlockableCustomizations(CustomizationService customizations,

View file

@ -7,6 +7,7 @@ using Dalamud.Game.ClientState;
using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.Game.UI;
using Glamourer.Events;
using Glamourer.Services;
using Lumina.Excel.GeneratedSheets;
using Cabinet = Lumina.Excel.GeneratedSheets.Cabinet;
@ -15,10 +16,12 @@ namespace Glamourer.Unlocks;
public class ItemUnlockManager : ISavable, IDisposable
{
private readonly SaveService _saveService;
private readonly ItemManager _items;
private readonly ClientState _clientState;
private readonly Framework _framework;
private readonly SaveService _saveService;
private readonly ItemManager _items;
private readonly ClientState _clientState;
private readonly Framework _framework;
private readonly ObjectUnlocked _event;
private readonly Dictionary<uint, long> _unlocked = new();
private bool _lastArmoireState;
@ -37,65 +40,20 @@ public class ItemUnlockManager : ISavable, IDisposable
Cabinet = 0x08,
}
public readonly record struct UnlockRequirements(uint Quest1, uint Quest2, uint Achievement, ushort State, UnlockType Type)
{
public override string ToString()
{
return Type switch
{
UnlockType.Quest1 => $"Quest {Quest1}",
UnlockType.Quest1 | UnlockType.Quest2 => $"Quests {Quest1} & {Quest2}",
UnlockType.Achievement => $"Achievement {Achievement}",
UnlockType.Quest1 | UnlockType.Achievement => $"Quest {Quest1} & Achievement {Achievement}",
UnlockType.Quest1 | UnlockType.Quest2 | UnlockType.Achievement => $"Quests {Quest1} & {Quest2}, Achievement {Achievement}",
UnlockType.Cabinet => $"Cabinet {Quest1}",
_ => string.Empty,
};
}
public unsafe bool IsUnlocked(ItemUnlockManager manager)
{
if (Type == 0)
return true;
var uiState = UIState.Instance();
if (uiState == null)
return false;
bool CheckQuest(uint quest)
=> uiState->IsUnlockLinkUnlockedOrQuestCompleted(quest);
// TODO ClientStructs
bool CheckAchievement(uint achievement)
=> false;
return Type switch
{
UnlockType.Quest1 => CheckQuest(Quest1),
UnlockType.Quest1 | UnlockType.Quest2 => CheckQuest(Quest1) && CheckQuest(Quest2),
UnlockType.Achievement => CheckAchievement(Achievement),
UnlockType.Quest1 | UnlockType.Achievement => CheckQuest(Quest1) && CheckAchievement(Achievement),
UnlockType.Quest1 | UnlockType.Quest2 | UnlockType.Achievement => CheckQuest(Quest1)
&& CheckQuest(Quest2)
&& CheckAchievement(Achievement),
UnlockType.Cabinet => uiState->Cabinet.IsCabinetLoaded() && uiState->Cabinet.IsItemInCabinet((int)Quest1),
_ => false,
};
}
}
public readonly IReadOnlyDictionary<uint, UnlockRequirements> Unlockable;
public IReadOnlyDictionary<uint, long> Unlocked
=> _unlocked;
public ItemUnlockManager(SaveService saveService, ItemManager items, ClientState clientState, DataManager gameData, Framework framework)
public ItemUnlockManager(SaveService saveService, ItemManager items, ClientState clientState, DataManager gameData, Framework framework,
ObjectUnlocked @event)
{
SignatureHelper.Initialise(this);
_saveService = saveService;
_items = items;
_clientState = clientState;
_framework = framework;
_event = @event;
Unlockable = CreateUnlockData(gameData, items);
Load();
_clientState.Login += OnLogin;
@ -166,7 +124,13 @@ public class ItemUnlockManager : ISavable, IDisposable
var time = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
bool AddItem(uint itemId)
=> _items.ItemService.AwaitedService.TryGetValue(itemId, out var equip) && _unlocked.TryAdd(equip.Id, time);
{
if (!_items.ItemService.AwaitedService.TryGetValue(itemId, out var equip) || !_unlocked.TryAdd(equip.Id, time))
return false;
_event.Invoke(ObjectUnlocked.Type.Item, equip.Id, DateTimeOffset.FromUnixTimeMilliseconds(time));
return true;
}
var mirageManager = MirageManager.Instance();
var changes = false;
@ -200,7 +164,7 @@ public class ItemUnlockManager : ISavable, IDisposable
var inventoryManager = InventoryManager.Instance();
if (inventoryManager != null)
{
var type = ScannableInventories[_currentInventory];
var type = ScannableInventories[_currentInventory];
var container = inventoryManager->GetInventoryContainer(type);
if (container != null && container->Loaded != 0 && _currentInventoryIndex < container->Size)
{
@ -217,6 +181,7 @@ public class ItemUnlockManager : ISavable, IDisposable
_currentInventoryIndex = 0;
}
}
if (changes)
Save();
}
@ -239,8 +204,12 @@ public class ItemUnlockManager : ISavable, IDisposable
if (IsGameUnlocked(itemId))
{
time = DateTimeOffset.UtcNow;
_unlocked.TryAdd(itemId, time.ToUnixTimeMilliseconds());
Save();
if (_unlocked.TryAdd(itemId, time.ToUnixTimeMilliseconds()))
{
_event.Invoke(ObjectUnlocked.Type.Item, itemId, time);
Save();
}
return true;
}
@ -269,8 +238,11 @@ public class ItemUnlockManager : ISavable, IDisposable
var changes = false;
foreach (var (itemId, unlock) in Unlockable)
{
if (unlock.IsUnlocked(this))
changes |= _unlocked.TryAdd(itemId, time);
if (unlock.IsUnlocked(this) && _unlocked.TryAdd(itemId, time))
{
_event.Invoke(ObjectUnlocked.Type.Item, itemId, DateTimeOffset.FromUnixTimeMilliseconds(time));
changes = true;
}
}
// TODO inventories
@ -286,16 +258,11 @@ public class ItemUnlockManager : ISavable, IDisposable
=> _saveService.DelaySave(this, TimeSpan.FromSeconds(10));
public void Save(StreamWriter writer)
{ }
=> UnlockDictionaryHelpers.Save(writer, Unlocked);
private void Load()
{
var file = ToFilename(_saveService.FileNames);
if (!File.Exists(file))
return;
_unlocked.Clear();
}
=> UnlockDictionaryHelpers.Load(ToFilename(_saveService.FileNames), _unlocked,
id => _items.ItemService.AwaitedService.TryGetValue(id, out _), "item");
private void OnLogin(object? _, EventArgs _2)
=> Scan();

View file

@ -0,0 +1,113 @@
using System;
using System.Collections.Generic;
using System.IO;
using Dalamud.Interface.Internal.Notifications;
namespace Glamourer.Unlocks;
public static class UnlockDictionaryHelpers
{
public const int Magic = 0x00C0FFEE;
public const int Version = 1;
public static void Save(StreamWriter writer, IReadOnlyDictionary<uint, long> data)
{
// Not using by choice, as this would close the stream prematurely.
var b = new BinaryWriter(writer.BaseStream);
b.Write(Magic);
b.Write(Version);
b.Write(data.Count);
foreach (var (id, timestamp) in data)
{
b.Write(id);
b.Write(timestamp);
}
b.Flush();
}
public static void Load(string filePath, Dictionary<uint, long> data, Func<uint, bool> validate, string type)
{
data.Clear();
if (!File.Exists(filePath))
return;
try
{
using var fileStream = File.OpenRead(filePath);
using var b = new BinaryReader(fileStream);
var magic = b.ReadUInt32();
bool revertEndian;
switch (magic)
{
case 0x00C0FFEE:
revertEndian = false;
break;
case 0xEEFFC000:
revertEndian = true;
break;
default:
Glamourer.Chat.NotificationMessage($"Loading unlocked {type}s failed: Invalid magic number.", "Warning",
NotificationType.Warning);
return;
}
var version = b.ReadInt32();
var skips = 0;
var now = DateTimeOffset.UtcNow;
switch (version)
{
case Version:
var count = b.ReadInt32();
data.EnsureCapacity(count);
for (var i = 0; i < count; ++i)
{
var id = b.ReadUInt32();
var timestamp = b.ReadInt64();
if (revertEndian)
{
id = RevertEndianness(id);
timestamp = (long)RevertEndianness(timestamp);
}
var date = DateTimeOffset.FromUnixTimeMilliseconds(timestamp);
if (!validate(id)
|| date < DateTimeOffset.UnixEpoch
|| date > now
|| !data.TryAdd(id, timestamp))
++skips;
}
if (skips > 0)
Glamourer.Chat.NotificationMessage($"Skipped {skips} unlocked {type}s while loading unlocked {type}s.", "Warning",
NotificationType.Warning);
break;
default:
Glamourer.Chat.NotificationMessage($"Loading unlocked {type}s failed: Version {version} is unknown.", "Warning",
NotificationType.Warning);
return;
}
Glamourer.Log.Debug($"[UnlockManager] Loaded {data.Count} unlocked {type}s.");
}
catch (Exception ex)
{
Glamourer.Chat.NotificationMessage(ex, $"Loading unlocked {type}s failed: Unknown Error.", $"Loading unlocked {type}s failed:\n",
"Error", NotificationType.Error);
}
}
private static uint RevertEndianness(uint value)
=> ((value & 0x000000FFU) << 24) | ((value & 0x0000FF00U) << 8) | ((value & 0x00FF0000U) >> 8) | ((value & 0xFF000000U) >> 24);
private static ulong RevertEndianness(long value)
=> (((ulong)value & 0x00000000000000FFU) << 56)
| (((ulong)value & 0x000000000000FF00U) << 40)
| (((ulong)value & 0x0000000000FF0000U) << 24)
| (((ulong)value & 0x00000000FF000000U) << 8)
| (((ulong)value & 0x000000FF00000000U) >> 8)
| (((ulong)value & 0x0000FF0000000000U) >> 24)
| (((ulong)value & 0x00FF000000000000U) >> 40)
| (((ulong)value & 0xFF00000000000000U) >> 56);
}

View file

@ -0,0 +1,50 @@
using FFXIVClientStructs.FFXIV.Client.Game.UI;
namespace Glamourer.Unlocks;
public readonly record struct UnlockRequirements(uint Quest1, uint Quest2, uint Achievement, ushort State, ItemUnlockManager.UnlockType Type)
{
public override string ToString()
{
return Type switch
{
ItemUnlockManager.UnlockType.Quest1 => $"Quest {Quest1}",
ItemUnlockManager.UnlockType.Quest1 | ItemUnlockManager.UnlockType.Quest2 => $"Quests {Quest1} & {Quest2}",
ItemUnlockManager.UnlockType.Achievement => $"Achievement {Achievement}",
ItemUnlockManager.UnlockType.Quest1 | ItemUnlockManager.UnlockType.Achievement => $"Quest {Quest1} & Achievement {Achievement}",
ItemUnlockManager.UnlockType.Quest1 | ItemUnlockManager.UnlockType.Quest2 | ItemUnlockManager.UnlockType.Achievement => $"Quests {Quest1} & {Quest2}, Achievement {Achievement}",
ItemUnlockManager.UnlockType.Cabinet => $"Cabinet {Quest1}",
_ => string.Empty,
};
}
public unsafe bool IsUnlocked(ItemUnlockManager manager)
{
if (Type == 0)
return true;
var uiState = UIState.Instance();
if (uiState == null)
return false;
bool CheckQuest(uint quest)
=> uiState->IsUnlockLinkUnlockedOrQuestCompleted(quest);
// TODO ClientStructs
bool CheckAchievement(uint achievement)
=> false;
return Type switch
{
ItemUnlockManager.UnlockType.Quest1 => CheckQuest(Quest1),
ItemUnlockManager.UnlockType.Quest1 | ItemUnlockManager.UnlockType.Quest2 => CheckQuest(Quest1) && CheckQuest(Quest2),
ItemUnlockManager.UnlockType.Achievement => CheckAchievement(Achievement),
ItemUnlockManager.UnlockType.Quest1 | ItemUnlockManager.UnlockType.Achievement => CheckQuest(Quest1) && CheckAchievement(Achievement),
ItemUnlockManager.UnlockType.Quest1 | ItemUnlockManager.UnlockType.Quest2 | ItemUnlockManager.UnlockType.Achievement => CheckQuest(Quest1)
&& CheckQuest(Quest2)
&& CheckAchievement(Achievement),
ItemUnlockManager.UnlockType.Cabinet => uiState->Cabinet.IsCabinetLoaded() && uiState->Cabinet.IsItemInCabinet((int)Quest1),
_ => false,
};
}
}