From 60443f6a5337a5fb09e3738c73bd4a1bd5597bc6 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 2 Jul 2023 15:34:27 +0200 Subject: [PATCH] . --- Glamourer/Automation/AutoDesignApplier.cs | 64 ++-- Glamourer/Configuration.cs | 4 +- Glamourer/Gui/MainWindow.cs | 9 +- Glamourer/Gui/PenumbraChangedItemTooltip.cs | 189 +++++++----- Glamourer/Gui/Tabs/DebugTab.cs | 32 +- Glamourer/Gui/Tabs/SettingsTab.cs | 72 +++-- .../Gui/Tabs/UnlocksTab/UnlockOverview.cs | 145 +++++++++ Glamourer/Gui/Tabs/UnlocksTab/UnlockTable.cs | 276 ++++++++++++++++++ Glamourer/Gui/Tabs/UnlocksTab/UnlocksTab.cs | 60 ++++ Glamourer/Services/CodeService.cs | 108 +++++++ Glamourer/Services/CustomizationService.cs | 11 +- Glamourer/Services/PhrasingService.cs | 54 ---- Glamourer/Services/ServiceManager.cs | 9 +- Glamourer/State/FunModule.cs | 137 +++++++++ Glamourer/State/StateListener.cs | 6 +- Glamourer/Unlocks/CustomizeUnlockManager.cs | 11 +- Glamourer/Unlocks/ItemUnlockManager.cs | 8 +- 17 files changed, 970 insertions(+), 225 deletions(-) create mode 100644 Glamourer/Gui/Tabs/UnlocksTab/UnlockOverview.cs create mode 100644 Glamourer/Gui/Tabs/UnlocksTab/UnlockTable.cs create mode 100644 Glamourer/Gui/Tabs/UnlocksTab/UnlocksTab.cs create mode 100644 Glamourer/Services/CodeService.cs delete mode 100644 Glamourer/Services/PhrasingService.cs create mode 100644 Glamourer/State/FunModule.cs diff --git a/Glamourer/Automation/AutoDesignApplier.cs b/Glamourer/Automation/AutoDesignApplier.cs index 50ff0bf..8a335b4 100644 --- a/Glamourer/Automation/AutoDesignApplier.cs +++ b/Glamourer/Automation/AutoDesignApplier.cs @@ -1,6 +1,5 @@ using System; using System.Diagnostics.CodeAnalysis; -using System.Security.AccessControl; using Glamourer.Customization; using Glamourer.Designs; using Glamourer.Events; @@ -9,8 +8,7 @@ using Glamourer.Interop.Structs; using Glamourer.Services; using Glamourer.State; using Glamourer.Structs; -using Lumina.Excel.GeneratedSheets; -using Newtonsoft.Json.Linq; +using Glamourer.Unlocks; using Penumbra.GameData.Actors; using Penumbra.GameData.Enums; @@ -18,25 +16,29 @@ namespace Glamourer.Automation; public class AutoDesignApplier : IDisposable { - private readonly Configuration _config; - private readonly AutoDesignManager _manager; - private readonly PhrasingService _phrasing; - private readonly StateManager _state; - private readonly JobService _jobs; - private readonly ActorService _actors; - private readonly CustomizationService _customizations; + private readonly Configuration _config; + private readonly AutoDesignManager _manager; + private readonly CodeService _code; + private readonly StateManager _state; + private readonly JobService _jobs; + private readonly ActorService _actors; + private readonly CustomizationService _customizations; + private readonly CustomizeUnlockManager _customizeUnlocks; + private readonly ItemUnlockManager _itemUnlocks; - public AutoDesignApplier(Configuration config, AutoDesignManager manager, PhrasingService phrasing, StateManager state, JobService jobs, - CustomizationService customizations, ActorService actors) + public AutoDesignApplier(Configuration config, AutoDesignManager manager, CodeService code, StateManager state, JobService jobs, + CustomizationService customizations, ActorService actors, ItemUnlockManager itemUnlocks, CustomizeUnlockManager customizeUnlocks) { - _config = config; - _manager = manager; - _phrasing = phrasing; - _state = state; - _jobs = jobs; - _customizations = customizations; - _actors = actors; - _jobs.JobChanged += OnJobChange; + _config = config; + _manager = manager; + _code = code; + _state = state; + _jobs = jobs; + _customizations = customizations; + _actors = actors; + _itemUnlocks = itemUnlocks; + _customizeUnlocks = customizeUnlocks; + _jobs.JobChanged += OnJobChange; } public void Dispose() @@ -79,7 +81,7 @@ public class AutoDesignApplier : IDisposable private unsafe void Reduce(Actor actor, ActorState state, AutoDesignSet set, bool respectManual) { EquipFlag totalEquipFlags = 0; - var totalCustomizeFlags = _phrasing.Phrasing2 ? 0 : CustomizeFlagExtensions.RedrawRequired; + var totalCustomizeFlags = _code.EnabledMesmer ? 0 : CustomizeFlagExtensions.RedrawRequired; byte totalMetaFlags = 0; foreach (var design in set.Designs) { @@ -118,15 +120,18 @@ public class AutoDesignApplier : IDisposable if (equipFlags == 0) return; - // TODO add item conditions foreach (var slot in EquipSlotExtensions.EqdpSlots) { var flag = slot.ToFlag(); if (equipFlags.HasFlag(flag)) { - if (!respectManual || state[slot, false] is not StateChanged.Source.Manual) - _state.ChangeItem(state, slot, design.Item(slot), StateChanged.Source.Fixed); - totalEquipFlags |= flag; + var item = design.Item(slot); + if (_code.EnabledInventory || _itemUnlocks.IsUnlocked(item.Id, out _)) + { + if (!respectManual || state[slot, false] is not StateChanged.Source.Manual) + _state.ChangeItem(state, slot, item, StateChanged.Source.Fixed); + totalEquipFlags |= flag; + } } var stainFlag = slot.ToStainFlag(); @@ -141,7 +146,8 @@ public class AutoDesignApplier : IDisposable if (equipFlags.HasFlag(EquipFlag.Mainhand)) { var item = design.Item(EquipSlot.MainHand); - if (state.ModelData.Item(EquipSlot.MainHand).Type == item.Type) + if (state.ModelData.Item(EquipSlot.MainHand).Type == item.Type + && (_code.EnabledInventory || _itemUnlocks.IsUnlocked(item.Id, out _))) { if (!respectManual || state[EquipSlot.MainHand, false] is not StateChanged.Source.Manual) _state.ChangeItem(state, EquipSlot.MainHand, item, StateChanged.Source.Fixed); @@ -152,7 +158,8 @@ public class AutoDesignApplier : IDisposable if (equipFlags.HasFlag(EquipFlag.Offhand)) { var item = design.Item(EquipSlot.OffHand); - if (state.ModelData.Item(EquipSlot.OffHand).Type == item.Type) + if (state.ModelData.Item(EquipSlot.OffHand).Type == item.Type + && (_code.EnabledInventory || _itemUnlocks.IsUnlocked(item.Id, out _))) { if (!respectManual || state[EquipSlot.OffHand, false] is not StateChanged.Source.Manual) _state.ChangeItem(state, EquipSlot.OffHand, item, StateChanged.Source.Fixed); @@ -220,7 +227,8 @@ public class AutoDesignApplier : IDisposable continue; var value = design.Customize[index]; - if (CustomizationService.IsCustomizationValid(set, face, index, value)) + if (CustomizationService.IsCustomizationValid(set, face, index, value, out var data) + && (_code.EnabledInventory || _customizeUnlocks.IsUnlocked(data.Value, out _))) { if (!respectManual || state[index] is not StateChanged.Source.Manual) _state.ChangeCustomize(state, index, value, StateChanged.Source.Fixed); diff --git a/Glamourer/Configuration.cs b/Glamourer/Configuration.cs index 6bd3490..406f0d2 100644 --- a/Glamourer/Configuration.cs +++ b/Glamourer/Configuration.cs @@ -23,6 +23,7 @@ public class Configuration : IPluginConfiguration, ISavable public bool AutoRedrawEquipOnChanges { get; set; } = false; public bool EnableAutoDesigns { get; set; } = true; public bool IncognitoMode { get; set; } = false; + public bool UnlockDetailMode { get; set; } = true; public MainWindow.TabType SelectedTab { get; set; } = MainWindow.TabType.Settings; public DoubleModifier DeleteDesignModifier { get; set; } = new(ModifierHotkey.Control, ModifierHotkey.Shift); @@ -30,8 +31,7 @@ public class Configuration : IPluginConfiguration, ISavable [JsonProperty(Order = int.MaxValue)] public ISortMode SortMode { get; set; } = ISortMode.FoldersFirst; - public string Phrasing1 { get; set; } = string.Empty; - public string Phrasing2 { get; set; } = string.Empty; + public List<(string Code, bool Enabled)> Codes { get; set; } = new List<(string Code, bool Enabled)>(); #if DEBUG public bool DebugMode { get; set; } = true; diff --git a/Glamourer/Gui/MainWindow.cs b/Glamourer/Gui/MainWindow.cs index 281de98..d9ebf9b 100644 --- a/Glamourer/Gui/MainWindow.cs +++ b/Glamourer/Gui/MainWindow.cs @@ -6,6 +6,7 @@ using Glamourer.Gui.Tabs; using Glamourer.Gui.Tabs.ActorTab; using Glamourer.Gui.Tabs.AutomationTab; using Glamourer.Gui.Tabs.DesignTab; +using Glamourer.Gui.Tabs.UnlocksTab; using ImGuiNET; using OtterGui.Custom; using OtterGui.Widgets; @@ -22,6 +23,7 @@ public class MainWindow : Window Actors = 2, Designs = 3, Automation = 4, + Unlocks = 5, } private readonly Configuration _config; @@ -32,11 +34,12 @@ public class MainWindow : Window public readonly DebugTab Debug; public readonly DesignTab Designs; public readonly AutomationTab Automation; + public readonly UnlocksTab Unlocks; public TabType SelectTab = TabType.None; public MainWindow(DalamudPluginInterface pi, Configuration config, SettingsTab settings, ActorTab actors, DesignTab designs, - DebugTab debugTab, AutomationTab automation) + DebugTab debugTab, AutomationTab automation, UnlocksTab unlocks) : base(GetLabel()) { pi.UiBuilder.DisableGposeUiHide = true; @@ -50,6 +53,7 @@ public class MainWindow : Window Designs = designs; Automation = automation; Debug = debugTab; + Unlocks = unlocks; _config = config; _tabs = new ITab[] { @@ -57,6 +61,7 @@ public class MainWindow : Window actors, designs, automation, + unlocks, debugTab, }; @@ -81,6 +86,7 @@ public class MainWindow : Window TabType.Actors => Actors.Label, TabType.Designs => Designs.Label, TabType.Automation => Automation.Label, + TabType.Unlocks => Unlocks.Label, _ => ReadOnlySpan.Empty, }; @@ -91,6 +97,7 @@ public class MainWindow : Window if (label == Designs.Label) return TabType.Designs; if (label == Settings.Label) return TabType.Settings; if (label == Automation.Label) return TabType.Automation; + if (label == Unlocks.Label) return TabType.Unlocks; if (label == Debug.Label) return TabType.Debug; // @formatter:on return TabType.None; diff --git a/Glamourer/Gui/PenumbraChangedItemTooltip.cs b/Glamourer/Gui/PenumbraChangedItemTooltip.cs index 009f294..44bdc1b 100644 --- a/Glamourer/Gui/PenumbraChangedItemTooltip.cs +++ b/Glamourer/Gui/PenumbraChangedItemTooltip.cs @@ -1,12 +1,15 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; +using Glamourer.Events; using Glamourer.Interop; using Glamourer.Interop.Penumbra; using Glamourer.Services; using Glamourer.State; using Glamourer.Structs; using ImGuiNET; +using OtterGui.Raii; using Penumbra.Api.Enums; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; @@ -45,10 +48,115 @@ public class PenumbraChangedItemTooltip : IDisposable _penumbra.Click -= OnPenumbraClick; } + public bool Player() + => _objects.Player.Valid; + + public bool Player([NotNullWhen(true)] out ActorState? player) + { + var (identifier, data) = _objects.PlayerData; + if (!data.Valid || !_stateManager.GetOrCreate(identifier, data.Objects[0], out player)) + { + player = null; + return false; + } + + return true; + } + + public void CreateTooltip(EquipItem item, string prefix, bool openTooltip) + { + var slot = item.Type.ToSlot(); + var last = _lastItems[slot.ToIndex()]; + switch (slot) + { + case EquipSlot.MainHand when !CanApplyWeapon(EquipSlot.MainHand, item): + case EquipSlot.OffHand when !CanApplyWeapon(EquipSlot.OffHand, item): + break; + case EquipSlot.RFinger: + using (var tt = !openTooltip ? null : ImRaii.Tooltip()) + { + ImGui.TextUnformatted($"{prefix}Right-Click to apply to current actor (Right Finger)."); + ImGui.TextUnformatted($"{prefix}Shift + Right-Click to apply to current actor (Left Finger)."); + if (last.Valid) + ImGui.TextUnformatted( + $"{prefix}Control + Right-Click to re-apply {last.Name} to current actor (Right Finger)."); + + var last2 = _lastItems[EquipSlot.LFinger.ToIndex()]; + if (last2.Valid) + ImGui.TextUnformatted( + $"{prefix}Shift + Control + Right-Click to re-apply {last.Name} to current actor (Left Finger)."); + } + + break; + default: + using (var tt = !openTooltip ? null : ImRaii.Tooltip()) + { + ImGui.TextUnformatted($"{prefix}Right-Click to apply to current actor."); + if (last.Valid) + ImGui.TextUnformatted($"{prefix}Control + Right-Click to re-apply {last.Name} to current actor."); + } + + break; + } + } + + public void ApplyItem(ActorState state, EquipItem item) + { + var slot = item.Type.ToSlot(); + var last = _lastItems[slot.ToIndex()]; + switch (slot) + { + case EquipSlot.MainHand when !CanApplyWeapon(EquipSlot.MainHand, item): + case EquipSlot.OffHand when !CanApplyWeapon(EquipSlot.OffHand, item): + break; + case EquipSlot.RFinger: + switch (ImGui.GetIO().KeyCtrl, ImGui.GetIO().KeyShift) + { + case (false, false): + Glamourer.Log.Information($"Applying {item.Name} to Right Finger."); + SetLastItem(EquipSlot.RFinger, item, state); + _stateManager.ChangeItem(state, EquipSlot.RFinger, item, StateChanged.Source.Manual); + break; + case (false, true): + Glamourer.Log.Information($"Applying {item.Name} to Left Finger."); + SetLastItem(EquipSlot.LFinger, item, state); + _stateManager.ChangeItem(state, EquipSlot.LFinger, item, StateChanged.Source.Manual); + break; + case (true, false) when last.Valid: + Glamourer.Log.Information($"Re-Applying {last.Name} to Right Finger."); + SetLastItem(EquipSlot.RFinger, default, state); + _stateManager.ChangeItem(state, EquipSlot.RFinger, last, StateChanged.Source.Manual); + break; + case (true, true) when _lastItems[EquipSlot.LFinger.ToIndex()].Valid: + Glamourer.Log.Information($"Re-Applying {last.Name} to Left Finger."); + SetLastItem(EquipSlot.LFinger, default, state); + _stateManager.ChangeItem(state, EquipSlot.LFinger, last, StateChanged.Source.Manual); + break; + } + + return; + default: + if (ImGui.GetIO().KeyCtrl && last.Valid) + { + Glamourer.Log.Information($"Re-Applying {last.Name} to {slot.ToName()}."); + SetLastItem(slot, default, state); + _stateManager.ChangeItem(state, slot, last, StateChanged.Source.Manual); + } + else + { + Glamourer.Log.Information($"Applying {item.Name} to {slot.ToName()}."); + SetLastItem(slot, item, state); + _stateManager.ChangeItem(state, slot, item, StateChanged.Source.Manual); + } + + return; + } + } + private void OnPenumbraTooltip(ChangedItemType type, uint id) { LastTooltip = DateTime.UtcNow; - if (!_objects.Player.Valid) + if (!Player()) return; switch (type) @@ -57,33 +165,7 @@ public class PenumbraChangedItemTooltip : IDisposable if (!_items.ItemService.AwaitedService.TryGetValue(id, out var item)) return; - var slot = item.Type.ToSlot(); - var last = _lastItems[slot.ToIndex()]; - switch (slot) - { - case EquipSlot.MainHand when !CanApplyWeapon(EquipSlot.MainHand, item): - case EquipSlot.OffHand when !CanApplyWeapon(EquipSlot.OffHand, item): - break; - case EquipSlot.RFinger: - ImGui.TextUnformatted("[Glamourer] Right-Click to apply to current actor (Right Finger)."); - ImGui.TextUnformatted("[Glamourer] Shift + Right-Click to apply to current actor (Left Finger)."); - if (last.Valid) - ImGui.TextUnformatted( - $"[Glamourer] Control + Right-Click to re-apply {last.Name} to current actor (Right Finger)."); - - var last2 = _lastItems[EquipSlot.LFinger.ToIndex()]; - if (last2.Valid) - ImGui.TextUnformatted( - $"[Glamourer] Shift + Control + Right-Click to re-apply {last.Name} to current actor (Left Finger)."); - - break; - default: - ImGui.TextUnformatted("[Glamourer] Right-Click to apply to current actor."); - if (last.Valid) - ImGui.TextUnformatted($"[Glamourer] Control + Right-Click to re-apply {last.Name} to current actor."); - break; - } - + CreateTooltip(item, "[Glamourer] ", false); return; } } @@ -107,60 +189,13 @@ public class PenumbraChangedItemTooltip : IDisposable if (button is not MouseButton.Right) return; - var (identifier, data) = _objects.PlayerData; - if (!data.Valid) - return; - - if (!_stateManager.GetOrCreate(identifier, data.Objects[0], out var state)) + if (!Player(out var state)) return; if (!_items.ItemService.AwaitedService.TryGetValue(id, out var item)) return; - var slot = item.Type.ToSlot(); - var last = _lastItems[slot.ToIndex()]; - switch (slot) - { - case EquipSlot.MainHand when !CanApplyWeapon(EquipSlot.MainHand, item): - case EquipSlot.OffHand when !CanApplyWeapon(EquipSlot.OffHand, item): - break; - case EquipSlot.RFinger: - switch (ImGui.GetIO().KeyCtrl, ImGui.GetIO().KeyShift) - { - case (false, false): - Glamourer.Log.Information($"Applying {item.Name} to Right Finger."); - SetLastItem(EquipSlot.RFinger, item, state); - break; - case (false, true): - Glamourer.Log.Information($"Applying {item.Name} to Left Finger."); - SetLastItem(EquipSlot.LFinger, item, state); - break; - case (true, false) when last.Valid: - Glamourer.Log.Information($"Re-Applying {last.Name} to Right Finger."); - SetLastItem(EquipSlot.RFinger, default, state); - break; - case (true, true) when _lastItems[EquipSlot.LFinger.ToIndex()].Valid: - Glamourer.Log.Information($"Re-Applying {last.Name} to Left Finger."); - SetLastItem(EquipSlot.LFinger, default, state); - break; - } - - return; - default: - if (ImGui.GetIO().KeyCtrl && last.Valid) - { - Glamourer.Log.Information($"Re-Applying {last.Name} to {slot.ToName()}."); - SetLastItem(slot, default, state); - } - else - { - Glamourer.Log.Information($"Applying {item.Name} to {slot.ToName()}."); - SetLastItem(slot, item, state); - } - - return; - } - + ApplyItem(state, item); return; } } diff --git a/Glamourer/Gui/Tabs/DebugTab.cs b/Glamourer/Gui/Tabs/DebugTab.cs index a88d49f..cdb275c 100644 --- a/Glamourer/Gui/Tabs/DebugTab.cs +++ b/Glamourer/Gui/Tabs/DebugTab.cs @@ -42,7 +42,7 @@ public unsafe class DebugTab : ITab private readonly ObjectTable _objects; private readonly ObjectManager _objectManager; private readonly GlamourerIpc _ipc; - private readonly PhrasingService _phrasing; + private readonly CodeService _code; private readonly ItemManager _items; private readonly ActorService _actors; @@ -69,7 +69,7 @@ public unsafe class DebugTab : ITab ActorService actors, ItemManager items, CustomizationService customization, ObjectManager objectManager, DesignFileSystem designFileSystem, DesignManager designManager, StateManager state, Configuration config, PenumbraChangedItemTooltip penumbraTooltip, MetaService metaService, GlamourerIpc ipc, DalamudPluginInterface pluginInterface, - AutoDesignManager autoDesignManager, JobService jobs, PhrasingService phrasing, CustomizeUnlockManager customizeUnlocks, + AutoDesignManager autoDesignManager, JobService jobs, CodeService code, CustomizeUnlockManager customizeUnlocks, ItemUnlockManager itemUnlocks) { _changeCustomizeService = changeCustomizeService; @@ -92,7 +92,7 @@ public unsafe class DebugTab : ITab _pluginInterface = pluginInterface; _autoDesignManager = autoDesignManager; _jobs = jobs; - _phrasing = phrasing; + _code = code; _customizeUnlocks = customizeUnlocks; _itemUnlocks = itemUnlocks; } @@ -1190,8 +1190,6 @@ public unsafe class DebugTab : ITab if (!ImGui.CollapsingHeader("Auto Designs")) return; - DrawPhrasingService(); - foreach (var (set, idx) in _autoDesignManager.WithIndex()) { using var id = ImRaii.PushId(idx); @@ -1223,24 +1221,6 @@ public unsafe class DebugTab : ITab } } - private void DrawPhrasingService() - { - using var tree = ImRaii.TreeNode("Phrasing"); - if (!tree) - return; - - using var table = ImRaii.Table("phrasing", 3, ImGuiTableFlags.SizingFixedFit); - if (!table) - return; - - ImGuiUtil.DrawTableColumn("Phrasing 1"); - ImGuiUtil.DrawTableColumn(_config.Phrasing1); - ImGuiUtil.DrawTableColumn(_phrasing.Phrasing1.ToString()); - ImGuiUtil.DrawTableColumn("Phrasing 2"); - ImGuiUtil.DrawTableColumn(_config.Phrasing2); - ImGuiUtil.DrawTableColumn(_phrasing.Phrasing2.ToString()); - } - #endregion #region Unlocks @@ -1279,7 +1259,7 @@ public unsafe class DebugTab : ITab ImGuiUtil.DrawTableColumn(t.Value.Data.ToString()); ImGuiUtil.DrawTableColumn(t.Value.Name); ImGuiUtil.DrawTableColumn(_customizeUnlocks.IsUnlocked(t.Key, out var time) - ? time == DateTimeOffset.MaxValue + ? time == DateTimeOffset.MinValue ? "Always" : time.LocalDateTime.ToString("g") : "Never"); @@ -1325,7 +1305,7 @@ public unsafe class DebugTab : ITab } ImGuiUtil.DrawTableColumn(_itemUnlocks.IsUnlocked(t.Key, out var time) - ? time == DateTimeOffset.MaxValue + ? time == DateTimeOffset.MinValue ? "Always" : time.LocalDateTime.ToString("g") : "Never"); @@ -1372,7 +1352,7 @@ public unsafe class DebugTab : ITab } ImGuiUtil.DrawTableColumn(_itemUnlocks.IsUnlocked(t.Key, out var time) - ? time == DateTimeOffset.MaxValue + ? time == DateTimeOffset.MinValue ? "Always" : time.LocalDateTime.ToString("g") : "Never"); diff --git a/Glamourer/Gui/Tabs/SettingsTab.cs b/Glamourer/Gui/Tabs/SettingsTab.cs index d8e00b4..f28ae6e 100644 --- a/Glamourer/Gui/Tabs/SettingsTab.cs +++ b/Glamourer/Gui/Tabs/SettingsTab.cs @@ -1,4 +1,5 @@ using System; +using System.Numerics; using System.Runtime.CompilerServices; using Dalamud.Interface; using Glamourer.Gui.Tabs.DesignTab; @@ -17,24 +18,24 @@ public class SettingsTab : ITab private readonly Configuration _config; private readonly DesignFileSystemSelector _selector; private readonly StateListener _stateListener; - private readonly PhrasingService _phrasingService; + private readonly CodeService _codeService; private readonly PenumbraAutoRedraw _autoRedraw; public SettingsTab(Configuration config, DesignFileSystemSelector selector, StateListener stateListener, - PhrasingService phrasingService, PenumbraAutoRedraw autoRedraw) + CodeService codeService, PenumbraAutoRedraw autoRedraw) { - _config = config; - _selector = selector; - _stateListener = stateListener; - _phrasingService = phrasingService; - _autoRedraw = autoRedraw; + _config = config; + _selector = selector; + _stateListener = stateListener; + _codeService = codeService; + _autoRedraw = autoRedraw; } public ReadOnlySpan Label => "Settings"u8; - private string? _tmpPhrasing1 = null; - private string? _tmpPhrasing2 = null; + private string _currentCode = string.Empty; + public void DrawContent() { @@ -62,27 +63,46 @@ public class SettingsTab : ITab Checkbox("Debug Mode", "Show the debug tab. Only useful for debugging or advanced use.", _config.DebugMode, v => _config.DebugMode = v); DrawColorSettings(); - _tmpPhrasing1 ??= _config.Phrasing1; - ImGui.InputText("Phrasing 1", ref _tmpPhrasing1, 512); - - if (ImGui.IsItemDeactivatedAfterEdit()) - { - _phrasingService.SetPhrasing1(_tmpPhrasing1); - _tmpPhrasing1 = null; - } - - _tmpPhrasing2 ??= _config.Phrasing2; - ImGui.InputText("Phrasing 2", ref _tmpPhrasing2, 512); - - if (ImGui.IsItemDeactivatedAfterEdit()) - { - _phrasingService.SetPhrasing2(_tmpPhrasing2); - _tmpPhrasing2 = null; - } + DrawCodes(); MainWindow.DrawSupportButtons(); } + private void DrawCodes() + { + if (!ImGui.CollapsingHeader("Codes")) + return; + + using (var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale, _currentCode.Length > 0)) + { + var color = _codeService.CheckCode(_currentCode) != null ? ColorId.ActorAvailable : ColorId.ActorUnavailable; + using var c = ImRaii.PushColor(ImGuiCol.Border, color.Value(), _currentCode.Length > 0); + if (ImGui.InputTextWithHint("##Code", "Enter Code...", ref _currentCode, 512, ImGuiInputTextFlags.EnterReturnsTrue)) + { + if (_codeService.AddCode(_currentCode)) + _currentCode = string.Empty; + } + } + + if (_config.Codes.Count <= 0) + return; + + for (var i = 0; i < _config.Codes.Count; ++i) + { + var (code, state) = _config.Codes[i]; + var action = _codeService.CheckCode(code); + if (action == null) + continue; + + if (ImGui.Checkbox(code, ref state)) + { + action(state); + _config.Codes[i] = (code, state); + _config.Save(); + } + } + } + /// Draw the entire Color subsection. private void DrawColorSettings() { diff --git a/Glamourer/Gui/Tabs/UnlocksTab/UnlockOverview.cs b/Glamourer/Gui/Tabs/UnlocksTab/UnlockOverview.cs new file mode 100644 index 0000000..32d5f6c --- /dev/null +++ b/Glamourer/Gui/Tabs/UnlocksTab/UnlockOverview.cs @@ -0,0 +1,145 @@ +using System; +using System.Linq; +using System.Numerics; +using Dalamud.Interface; +using Glamourer.Customization; +using Glamourer.Services; +using Glamourer.Unlocks; +using ImGuiNET; +using OtterGui.Raii; +using Penumbra.GameData.Enums; + +namespace Glamourer.Gui.Tabs.UnlocksTab; + +public class UnlockOverview +{ + private readonly ItemManager _items; + private readonly ItemUnlockManager _itemUnlocks; + private readonly CustomizationService _customizations; + private readonly CustomizeUnlockManager _customizeUnlocks; + private readonly PenumbraChangedItemTooltip _tooltip; + + private static readonly Vector4 UnavailableTint = new(0.3f, 0.3f, 0.3f, 1.0f); + + public UnlockOverview(ItemManager items, CustomizationService customizations, ItemUnlockManager itemUnlocks, + CustomizeUnlockManager customizeUnlocks, PenumbraChangedItemTooltip tooltip) + { + _items = items; + _customizations = customizations; + _itemUnlocks = itemUnlocks; + _customizeUnlocks = customizeUnlocks; + _tooltip = tooltip; + } + + public void Draw() + { + using var color = ImRaii.PushColor(ImGuiCol.Border, ImGui.GetColorU32(ImGuiCol.TableBorderStrong)); + using var child = ImRaii.Child("Panel", -Vector2.One, true); + if (!child) + return; + + var iconSize = ImGuiHelpers.ScaledVector2(32); + foreach (var type in Enum.GetValues()) + 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); + } + } + + private void DrawCustomizationHeader(Vector2 iconSize, SubRace subRace, Gender gender) + { + var set = _customizations.AwaitedService.GetList(subRace, gender); + if (set.HairStyles.Count == 0 && set.FacePaints.Count == 0) + return; + + if (!ImGui.CollapsingHeader($"Unlockable {subRace.ToName()} {gender.ToName()} Customizations")) + return; + + using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero); + foreach (var customization in set.HairStyles.Concat(set.FacePaints)) + { + if (!_customizeUnlocks.Unlockable.TryGetValue(customization, out var unlockData)) + continue; + + var unlocked = _customizeUnlocks.IsUnlocked(customization, out var time); + var icon = _customizations.AwaitedService.GetIcon(customization.IconId); + + ImGui.Image(icon.ImGuiHandle, iconSize, Vector2.Zero, Vector2.One, unlocked ? Vector4.One : UnavailableTint); + if (ImGui.IsItemHovered()) + { + using var tt = ImRaii.Tooltip(); + var size = new Vector2(icon.Width, icon.Height); + 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(unlocked ? $"Unlocked on {time:g}" : "Not unlocked."); + } + + ImGui.SameLine(); + if (ImGui.GetContentRegionAvail().X < iconSize.X) + ImGui.NewLine(); + } + + if (ImGui.GetCursorPosX() != 0) + ImGui.NewLine(); + } + + private void DrawEquipTypeHeader(Vector2 iconSize, FullEquipType type) + { + if (type.IsOffhandType() || !_items.ItemService.AwaitedService.TryGetValue(type, out var items) || items.Count == 0) + return; + + if (!ImGui.CollapsingHeader($"{type.ToName()}s")) + return; + + using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(2 * ImGuiHelpers.GlobalScale)); + foreach (var item in items) + { + if (!ImGui.IsItemVisible()) + { } + + var unlocked = _itemUnlocks.IsUnlocked(item.Id, out var time); + var icon = _customizations.AwaitedService.GetIcon(item.IconId); + + ImGui.Image(icon.ImGuiHandle, iconSize, Vector2.Zero, Vector2.One, unlocked ? Vector4.One : UnavailableTint); + if (ImGui.IsItemClicked()) + { + // TODO link + } + + if (ImGui.IsItemClicked(ImGuiMouseButton.Right) && _tooltip.Player(out var state)) + _tooltip.ApplyItem(state, item); + + if (ImGui.IsItemHovered()) + { + using var tt = ImRaii.Tooltip(); + var size = new Vector2(icon.Width, icon.Height); + if (size.X >= iconSize.X && size.Y >= iconSize.Y) + ImGui.Image(icon.ImGuiHandle, size); + ImGui.TextUnformatted(item.Name); + var slot = item.Type.ToSlot(); + ImGui.TextUnformatted($"{item.Type.ToName()} ({slot.ToName()})"); + if (item.Type.Offhand().IsOffhandType()) + ImGui.TextUnformatted( + $"{item.Weapon()}{(_items.ItemService.AwaitedService.TryGetValue(item.Id, false, out var offhand) ? $" | {offhand.Weapon()}" : string.Empty)}"); + else + ImGui.TextUnformatted(slot is EquipSlot.MainHand ? $"{item.Weapon()}" : $"{item.Armor()}"); + ImGui.TextUnformatted( + 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(); + } + + if (ImGui.GetCursorPosX() != 0) + ImGui.NewLine(); + } +} diff --git a/Glamourer/Gui/Tabs/UnlocksTab/UnlockTable.cs b/Glamourer/Gui/Tabs/UnlocksTab/UnlockTable.cs new file mode 100644 index 0000000..d8e6940 --- /dev/null +++ b/Glamourer/Gui/Tabs/UnlocksTab/UnlockTable.cs @@ -0,0 +1,276 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using Dalamud.Game.ClientState.Objects.SubKinds; +using Dalamud.Interface; +using Glamourer.Services; +using Glamourer.Structs; +using Glamourer.Unlocks; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using OtterGui.Table; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; + +namespace Glamourer.Gui.Tabs.UnlocksTab; + +public class UnlockTable : Table +{ + public UnlockTable(ItemManager items, CustomizationService customizations, ItemUnlockManager itemUnlocks, + PenumbraChangedItemTooltip tooltip) + : 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..." }) + { + Sortable = true; + Flags |= ImGuiTableFlags.Hideable; + } + + private sealed class NameColumn : ColumnString + { + private readonly CustomizationService _customizations; + private readonly PenumbraChangedItemTooltip _tooltip; + + public override float Width + => 400 * ImGuiHelpers.GlobalScale; + + public NameColumn(CustomizationService customizations, PenumbraChangedItemTooltip tooltip) + { + _customizations = customizations; + _tooltip = tooltip; + Flags |= ImGuiTableColumnFlags.NoHide | ImGuiTableColumnFlags.NoReorder; + } + + public override string ToName(EquipItem item) + => item.Name; + + public override void DrawColumn(EquipItem item, int _) + { + var icon = _customizations.AwaitedService.GetIcon(item.IconId); + ImGui.Image(icon.ImGuiHandle, new Vector2(ImGui.GetFrameHeight())); + ImGui.SameLine(); + ImGui.AlignTextToFramePadding(); + if (ImGui.Selectable(item.Name)) + { + // TODO link + } + + if (ImGui.IsItemClicked(ImGuiMouseButton.Right) && _tooltip.Player(out var state)) + _tooltip.ApplyItem(state, item); + + if (ImGui.IsItemHovered() && _tooltip.Player()) + _tooltip.CreateTooltip(item, string.Empty, true); + } + } + + private sealed class TypeColumn : ColumnString + { + public override float Width + => ImGui.CalcTextSize(FullEquipType.CrossPeinHammer.ToName()).X; + + public override string ToName(EquipItem item) + => item.Type.ToName(); + + public override void DrawColumn(EquipItem item, int _) + { + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted(item.Type.ToName()); + } + + public override int Compare(EquipItem lhs, EquipItem rhs) + => lhs.Type.CompareTo(rhs.Type); + } + + private sealed class SlotColumn : ColumnFlags + { + public override float Width + => ImGui.CalcTextSize("Equip Slotmm").X; + + private EquipFlag _filterValue; + + public SlotColumn() + { + AllFlags = Values.Aggregate((a, b) => a | b); + _filterValue = AllFlags; + } + + public override void DrawColumn(EquipItem item, int idx) + { + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted(ToString(item.Type.ToSlot())); + } + + public override EquipFlag FilterValue + => _filterValue; + + protected override IReadOnlyList Values + => new[] + { + EquipFlag.Mainhand, + EquipFlag.Offhand, + EquipFlag.Head, + EquipFlag.Body, + EquipFlag.Hands, + EquipFlag.Legs, + EquipFlag.Feet, + EquipFlag.Ears, + EquipFlag.Neck, + EquipFlag.Wrist, + EquipFlag.RFinger, + }; + + protected override string[] Names + => new[] + { + ToString(EquipSlot.MainHand), + ToString(EquipSlot.OffHand), + ToString(EquipSlot.Head), + ToString(EquipSlot.Body), + ToString(EquipSlot.Hands), + ToString(EquipSlot.Legs), + ToString(EquipSlot.Feet), + ToString(EquipSlot.Ears), + ToString(EquipSlot.Neck), + ToString(EquipSlot.Wrists), + ToString(EquipSlot.RFinger), + }; + + protected override void SetValue(EquipFlag value, bool enable) + => _filterValue = enable ? _filterValue | value : _filterValue & ~value; + + public override int Compare(EquipItem lhs, EquipItem rhs) + => lhs.Type.ToSlot().CompareTo(rhs.Type.ToSlot()); + + public override bool FilterFunc(EquipItem item) + => _filterValue.HasFlag(item.Type.ToSlot().ToFlag()); + + private static string ToString(EquipSlot slot) + => slot switch + { + EquipSlot.MainHand => "Mainhand", + EquipSlot.OffHand => "Offhand", + EquipSlot.Head => "Head", + EquipSlot.Body => "Body", + EquipSlot.Hands => "Hands", + EquipSlot.Legs => "Legs", + EquipSlot.Feet => "Feet", + EquipSlot.Ears => "Ears", + EquipSlot.Neck => "Neck", + EquipSlot.Wrists => "Wrists", + EquipSlot.RFinger => "Finger", + _ => string.Empty, + }; + } + + private sealed class UnlockDateColumn : Column + { + private readonly ItemUnlockManager _unlocks; + + public override float Width + => 110 * ImGuiHelpers.GlobalScale; + + public UnlockDateColumn(ItemUnlockManager unlocks) + => _unlocks = unlocks; + + public override void DrawColumn(EquipItem item, int idx) + { + if (!_unlocks.IsUnlocked(item.Id, out var time)) + return; + + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted(time == DateTimeOffset.MinValue ? "Always" : time.ToString("g")); + } + + 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 c1 = unlockedLhs.CompareTo(unlockedRhs); + return c1 != 0 ? c1 : timeLhs.CompareTo(timeRhs); + } + } + + private sealed class ItemIdColumn : ColumnString + { + public override float Width + => 70 * ImGuiHelpers.GlobalScale; + + public override int Compare(EquipItem lhs, EquipItem rhs) + => lhs.Id.CompareTo(rhs.Id); + + public override string ToName(EquipItem item) + => item.Id.ToString(); + + public override void DrawColumn(EquipItem item, int _) + { + ImGui.AlignTextToFramePadding(); + ImGuiUtil.RightAlign(item.Id.ToString()); + } + } + + private sealed class ModelDataColumn : ColumnString + { + private readonly ItemManager _items; + + public override float Width + => 100 * ImGuiHelpers.GlobalScale; + + public ModelDataColumn(ItemManager items) + => _items = items; + + public override void DrawColumn(EquipItem item, int _) + { + ImGui.AlignTextToFramePadding(); + ImGuiUtil.RightAlign(item.ModelString); + if (ImGui.IsItemHovered() + && item.Type.Offhand().IsOffhandType() + && _items.ItemService.AwaitedService.TryGetValue(item.Id, false, out var offhand)) + { + using var tt = ImRaii.Tooltip(); + ImGui.TextUnformatted("Offhand: " + offhand.ModelString); + } + } + + public override int Compare(EquipItem lhs, EquipItem rhs) + => lhs.Weapon().Value.CompareTo(rhs.Weapon().Value); + + public override bool FilterFunc(EquipItem item) + { + if (FilterValue.Length == 0) + return true; + + if (FilterRegex?.IsMatch(item.ModelString) ?? item.ModelString.Contains(FilterValue, StringComparison.OrdinalIgnoreCase)) + return true; + + if (item.Type.Offhand().IsOffhandType() && _items.ItemService.AwaitedService.TryGetValue(item.Id, false, out var offhand)) + return FilterRegex?.IsMatch(offhand.ModelString) + ?? offhand.ModelString.Contains(FilterValue, StringComparison.OrdinalIgnoreCase); + + return false; + } + } + + private sealed class ItemList : IReadOnlyCollection + { + private readonly ItemManager _items; + + public ItemList(ItemManager items) + => _items = items; + + public IEnumerator GetEnumerator() + => _items.ItemService.AwaitedService.AllItems(true).Select(i => i.Item2).GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + public int Count + => _items.ItemService.AwaitedService.TotalItemCount(true); + } +} diff --git a/Glamourer/Gui/Tabs/UnlocksTab/UnlocksTab.cs b/Glamourer/Gui/Tabs/UnlocksTab/UnlocksTab.cs new file mode 100644 index 0000000..c2bba60 --- /dev/null +++ b/Glamourer/Gui/Tabs/UnlocksTab/UnlocksTab.cs @@ -0,0 +1,60 @@ +using System; +using System.Numerics; +using Dalamud.Interface; +using ImGuiNET; +using OtterGui.Raii; +using OtterGui; +using OtterGui.Widgets; + +namespace Glamourer.Gui.Tabs.UnlocksTab; + +public class UnlocksTab : ITab +{ + private readonly Configuration _config; + private readonly UnlockOverview _overview; + private readonly UnlockTable _table; + + public UnlocksTab(Configuration config, UnlockOverview overview, UnlockTable table) + { + _config = config; + _overview = overview; + _table = table; + } + + private bool DetailMode + { + get => _config.UnlockDetailMode; + set + { + _config.UnlockDetailMode = value; + _config.Save(); + } + } + + public ReadOnlySpan Label + => "Unlocks"u8; + + public void DrawContent() + { + DrawTypeSelection(); + if (DetailMode) + _table.Draw(ImGui.GetFrameHeightWithSpacing()); + else + _overview.Draw(); + } + + private void DrawTypeSelection() + { + using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero) + .Push(ImGuiStyleVar.FrameRounding, 0); + var buttonSize = new Vector2(ImGui.GetContentRegionAvail().X / 2, ImGui.GetFrameHeight()); + + if (ImGuiUtil.DrawDisabledButton("Overview Mode", buttonSize, "Show tinted icons of sets of unlocks.", !DetailMode)) + DetailMode = false; + + ImGui.SameLine(); + if (ImGuiUtil.DrawDisabledButton("Detailed Mode", buttonSize, "Show all unlockable data as a combined filterable and sortable table.", + DetailMode)) + DetailMode = true; + } +} diff --git a/Glamourer/Services/CodeService.cs b/Glamourer/Services/CodeService.cs new file mode 100644 index 0000000..366663e --- /dev/null +++ b/Glamourer/Services/CodeService.cs @@ -0,0 +1,108 @@ +using System; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using Penumbra.GameData.Enums; + +namespace Glamourer.Services; + +public class CodeService +{ + private readonly Configuration _config; + private readonly SHA256 _hasher = SHA256.Create(); + + public enum Sizing + { + None, + Dwarf, + Giant, + } + + public bool EnabledClown { get; private set; } + public bool EnabledEmperor { get; private set; } + public bool EnabledIndividual { get; private set; } + public Sizing EnabledSizing { get; private set; } + public Race EnabledOops { get; private set; } + public bool EnabledMesmer { get; private set; } + public bool EnabledInventory { get; private set; } + + public CodeService(Configuration config) + => _config = config; + + private void Load() + { + var changes = false; + for (var i = 0; i < _config.Codes.Count; ++i) + { + var enabled = CheckCode(_config.Codes[i].Code); + if (enabled == null) + { + --i; + _config.Codes.RemoveAt(i); + changes = true; + } + else + { + enabled(_config.Codes[i].Enabled); + } + } + + if (changes) + _config.Save(); + } + + public bool AddCode(string name) + { + if (CheckCode(name) == null || _config.Codes.Any(p => p.Code == name)) + return false; + + _config.Codes.Add((name, false)); + _config.Save(); + return true; + + } + + public Action? CheckCode(string name) + { + using var stream = new MemoryStream(Encoding.UTF8.GetBytes(name)); + var sha = (ReadOnlySpan)_hasher.ComputeHash(stream); + return sha switch + { + _ when CodeClown.SequenceEqual(sha) => v => EnabledClown = v, + _ when CodeEmperor.SequenceEqual(sha) => v => EnabledEmperor = v, + _ when CodeIndividual.SequenceEqual(sha) => v => EnabledIndividual = v, + _ when CodeDwarf.SequenceEqual(sha) => v => EnabledSizing = v ? Sizing.Dwarf : Sizing.None, + _ when CodeGiant.SequenceEqual(sha) => v => EnabledSizing = v ? Sizing.Giant : Sizing.None, + _ when CodeMesmer.SequenceEqual(sha) => v => EnabledMesmer = v, + _ when CodeOops1.SequenceEqual(sha) => v => EnabledOops = v ? Race.Hyur : Race.Unknown, + _ when CodeOops2.SequenceEqual(sha) => v => EnabledOops = v ? Race.Elezen : Race.Unknown, + _ when CodeOops3.SequenceEqual(sha) => v => EnabledOops = v ? Race.Lalafell : Race.Unknown, + _ when CodeOops4.SequenceEqual(sha) => v => EnabledOops = v ? Race.Miqote : Race.Unknown, + _ when CodeOops5.SequenceEqual(sha) => v => EnabledOops = v ? Race.Roegadyn : Race.Unknown, + _ when CodeOops6.SequenceEqual(sha) => v => EnabledOops = v ? Race.AuRa : Race.Unknown, + _ when CodeOops7.SequenceEqual(sha) => v => EnabledOops = v ? Race.Hrothgar : Race.Unknown, + _ when CodeOops8.SequenceEqual(sha) => v => EnabledOops = v ? Race.Viera : Race.Unknown, + _ when CodeInventory.SequenceEqual(sha) => v => EnabledInventory = v, + _ => null, + }; + } + + // @formatter:off + private static ReadOnlySpan CodeClown => new byte[] { 0xC4, 0xEE, 0x1D, 0x6F, 0xC5, 0x5D, 0x47, 0xBE, 0x78, 0x63, 0x66, 0x86, 0x81, 0x15, 0xEB, 0xFA, 0xF6, 0x4A, 0x90, 0xEA, 0xC0, 0xE4, 0xEE, 0x86, 0x69, 0x01, 0x8E, 0xDB, 0xCC, 0x69, 0xD1, 0xBD }; + private static ReadOnlySpan CodeEmperor => new byte[] { 0xE2, 0x2D, 0x3E, 0x57, 0x16, 0x82, 0x65, 0x98, 0x7E, 0xE6, 0x8F, 0x45, 0x14, 0x7D, 0x65, 0x31, 0xE9, 0xD8, 0xDB, 0xEA, 0xDC, 0xBF, 0xEE, 0x2A, 0xBA, 0xD5, 0x69, 0x96, 0x78, 0x34, 0x3B, 0x57 }; + private static ReadOnlySpan CodeIndividual => new byte[] { 0x95, 0xA4, 0x71, 0xAC, 0xA3, 0xC2, 0x34, 0x94, 0xC1, 0x65, 0x07, 0xF3, 0x7F, 0x93, 0x57, 0xEE, 0xE3, 0x04, 0xC0, 0xE8, 0x1B, 0xA0, 0xE2, 0x08, 0x68, 0x02, 0x8D, 0xAD, 0x76, 0x03, 0x9B, 0xC5 }; + private static ReadOnlySpan CodeDwarf => new byte[] { 0x55, 0x97, 0xFE, 0xE9, 0x78, 0x64, 0xE8, 0x2F, 0xCD, 0x25, 0xD1, 0xAE, 0xDF, 0x35, 0xE6, 0xED, 0x03, 0x78, 0x54, 0x1D, 0x56, 0x22, 0x34, 0x75, 0x4B, 0x96, 0x6F, 0xBA, 0xAC, 0xEC, 0x00, 0x46 }; + private static ReadOnlySpan CodeGiant => new byte[] { 0x6E, 0xBB, 0x91, 0x1D, 0x67, 0xE3, 0x00, 0x07, 0xA1, 0x0F, 0x2A, 0xF0, 0x26, 0x91, 0x38, 0x63, 0xD3, 0x52, 0x82, 0xF7, 0x5D, 0x93, 0xE8, 0x83, 0xB1, 0xF6, 0xB9, 0x69, 0x78, 0x20, 0xC4, 0xCE }; + private static ReadOnlySpan CodeMesmer => new byte[] { 0x6A, 0x84, 0x12, 0xEA, 0x3B, 0x03, 0x2E, 0xD9, 0xA3, 0x51, 0xB0, 0x4F, 0xE7, 0x4D, 0x59, 0x87, 0xA9, 0xA1, 0x6E, 0x08, 0xC7, 0x3E, 0xD3, 0x15, 0xEE, 0x40, 0x2C, 0xB3, 0x44, 0x78, 0x1F, 0xA0 }; + private static ReadOnlySpan CodeOops1 => new byte[] { 0x4C, 0x51, 0xE2, 0x38, 0xEF, 0xAD, 0x84, 0x0E, 0x4E, 0x11, 0x0F, 0x5E, 0xDE, 0x45, 0x41, 0x9F, 0x6A, 0xF6, 0x5F, 0x5B, 0xA8, 0x91, 0x64, 0x22, 0xEE, 0x62, 0x97, 0x3C, 0x78, 0x18, 0xCD, 0xAF }; + private static ReadOnlySpan CodeOops2 => new byte[] { 0x3D, 0x5B, 0xA9, 0x62, 0xCE, 0xBE, 0x52, 0xF5, 0x94, 0x2A, 0xF9, 0xB7, 0xCF, 0xD9, 0x24, 0x2B, 0x38, 0xC7, 0x4F, 0x28, 0x97, 0x29, 0x1D, 0x01, 0x13, 0x53, 0x44, 0x11, 0x15, 0x6F, 0x9B, 0x56 }; + private static ReadOnlySpan CodeOops3 => new byte[] { 0x85, 0x8D, 0x5B, 0xC2, 0x66, 0x53, 0x2E, 0xB9, 0xE9, 0x85, 0xE5, 0xF8, 0xD3, 0x75, 0x18, 0x7C, 0x58, 0x55, 0xD4, 0x8C, 0x8E, 0x5F, 0x58, 0x2E, 0xF3, 0xF1, 0xAE, 0xA8, 0xA0, 0x81, 0xC6, 0x0E }; + private static ReadOnlySpan CodeOops4 => new byte[] { 0x44, 0x73, 0x8C, 0x39, 0x5A, 0xF1, 0xDB, 0x5F, 0x62, 0xA1, 0x6E, 0x5F, 0xE6, 0x97, 0x9E, 0x90, 0xD7, 0x5C, 0x97, 0x67, 0xB6, 0xC7, 0x99, 0x61, 0x36, 0xCA, 0x34, 0x7E, 0xB9, 0xAC, 0xC3, 0x76 }; + private static ReadOnlySpan CodeOops5 => new byte[] { 0xB7, 0x25, 0x73, 0xDB, 0xBE, 0xD0, 0x49, 0xFB, 0xFF, 0x9C, 0x32, 0x21, 0xB0, 0x8A, 0x2C, 0x0C, 0x77, 0x46, 0xD5, 0xCF, 0x0E, 0x63, 0x2F, 0x91, 0x85, 0x8B, 0x55, 0x5C, 0x4D, 0xD2, 0xB9, 0xB8 }; + private static ReadOnlySpan CodeOops6 => new byte[] { 0x69, 0x93, 0xAF, 0xE4, 0xB8, 0xEC, 0x5F, 0x40, 0xEB, 0x8A, 0x6F, 0xD1, 0x9B, 0xD9, 0x56, 0x0B, 0xEA, 0x64, 0x79, 0x9B, 0x54, 0xA1, 0x41, 0xED, 0xBC, 0x3E, 0x6E, 0x5C, 0xF1, 0x23, 0x60, 0xF8 }; + private static ReadOnlySpan CodeOops7 => new byte[] { 0x41, 0xEC, 0x65, 0x05, 0x8D, 0x20, 0x68, 0x5A, 0xB7, 0xEB, 0x92, 0x15, 0x43, 0xCF, 0x15, 0x05, 0x27, 0x51, 0xFE, 0x20, 0xC9, 0xB6, 0x2B, 0x84, 0xD9, 0x6A, 0x49, 0x5A, 0x5B, 0x7F, 0x2E, 0xE7 }; + private static ReadOnlySpan CodeOops8 => new byte[] { 0x16, 0xFF, 0x63, 0x85, 0x1C, 0xF5, 0x34, 0x33, 0x67, 0x8C, 0x46, 0x8E, 0x3E, 0xE3, 0xA6, 0x94, 0xF9, 0x74, 0x47, 0xAA, 0xC7, 0x29, 0x59, 0x1F, 0x6C, 0x6E, 0xF2, 0xF5, 0x87, 0x24, 0x9E, 0x2B }; + private static ReadOnlySpan CodeInventory => new byte[] { 0xD1, 0x35, 0xD7, 0x18, 0xBE, 0x45, 0x42, 0xBD, 0x88, 0x77, 0x7E, 0xC4, 0x41, 0x06, 0x34, 0x4D, 0x71, 0x3A, 0xC5, 0xCC, 0xA4, 0x1B, 0x7D, 0x3F, 0x3B, 0x86, 0x07, 0xCB, 0x63, 0xD7, 0xF9, 0xDB }; + // @formatter:on +} diff --git a/Glamourer/Services/CustomizationService.cs b/Glamourer/Services/CustomizationService.cs index 0e23120..0613418 100644 --- a/Glamourer/Services/CustomizationService.cs +++ b/Glamourer/Services/CustomizationService.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Runtime.CompilerServices; using Dalamud.Data; @@ -105,10 +106,16 @@ public sealed class CustomizationService : AsyncServiceWrapper race is Race.Hrothgar ? gender == Gender.Male : AwaitedService.Genders.Contains(gender); - /// Returns whether a customization value is valid for a given clan/gender set and face. + /// [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] public static bool IsCustomizationValid(CustomizationSet set, CustomizeValue face, CustomizeIndex type, CustomizeValue value) - => set.DataByValue(type, value, out _, face) >= 0; + => IsCustomizationValid(set, face, type, value, out _); + + /// Returns whether a customization value is valid for a given clan/gender set and face. + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public static bool IsCustomizationValid(CustomizationSet set, CustomizeValue face, CustomizeIndex type, CustomizeValue value, + [NotNullWhen(true)] out CustomizeData? data) + => set.DataByValue(type, value, out data, face) >= 0; /// Returns whether a customization value is valid for a given clan, gender and face. [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] diff --git a/Glamourer/Services/PhrasingService.cs b/Glamourer/Services/PhrasingService.cs deleted file mode 100644 index c411549..0000000 --- a/Glamourer/Services/PhrasingService.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System; -using System.IO; -using System.Security.Cryptography; -using System.Text; - -namespace Glamourer.Services; - -public class PhrasingService -{ - private readonly Configuration _config; - private readonly SHA256 _hasher = SHA256.Create(); - - public bool Phrasing1 { get; private set; } - public bool Phrasing2 { get; private set; } - - public PhrasingService(Configuration config) - { - _config = config; - Phrasing1 = CheckPhrasing(_config.Phrasing1, P1); - Phrasing2 = CheckPhrasing(_config.Phrasing2, P2); - } - - public void SetPhrasing1(string newPhrasing) - { - if (_config.Phrasing1 == newPhrasing) - return; - - _config.Phrasing1 = newPhrasing; - _config.Save(); - Phrasing1 = CheckPhrasing(newPhrasing, P1); - } - - public void SetPhrasing2(string newPhrasing) - { - if (_config.Phrasing2 == newPhrasing) - return; - - _config.Phrasing2 = newPhrasing; - _config.Save(); - Phrasing2 = CheckPhrasing(newPhrasing, P2); - } - - private bool CheckPhrasing(string phrasing, ReadOnlySpan data) - { - using var stream = new MemoryStream(Encoding.UTF8.GetBytes(phrasing)); - var sha = _hasher.ComputeHash(stream); - return data.SequenceEqual(sha); - } - - // @formatter:off - private static ReadOnlySpan P1 => new byte[] { 0xD1, 0x35, 0xD7, 0x18, 0xBE, 0x45, 0x42, 0xBD, 0x88, 0x77, 0x7E, 0xC4, 0x41, 0x06, 0x34, 0x4D, 0x71, 0x3A, 0xC5, 0xCC, 0xA4, 0x1B, 0x7D, 0x3F, 0x3B, 0x86, 0x07, 0xCB, 0x63, 0xD7, 0xF9, 0xDB }; - private static ReadOnlySpan P2 => new byte[] { 0x6A, 0x84, 0x12, 0xEA, 0x3B, 0x03, 0x2E, 0xD9, 0xA3, 0x51, 0xB0, 0x4F, 0xE7, 0x4D, 0x59, 0x87, 0xA9, 0xA1, 0x6E, 0x08, 0xC7, 0x3E, 0xD3, 0x15, 0xEE, 0x40, 0x2C, 0xB3, 0x44, 0x78, 0x1F, 0xA0 }; - // @formatter:on -} diff --git a/Glamourer/Services/ServiceManager.cs b/Glamourer/Services/ServiceManager.cs index 0860214..c695acf 100644 --- a/Glamourer/Services/ServiceManager.cs +++ b/Glamourer/Services/ServiceManager.cs @@ -10,6 +10,7 @@ using Glamourer.Gui.Tabs; using Glamourer.Gui.Tabs.ActorTab; using Glamourer.Gui.Tabs.AutomationTab; using Glamourer.Gui.Tabs.DesignTab; +using Glamourer.Gui.Tabs.UnlocksTab; using Glamourer.Interop; using Glamourer.Interop.Penumbra; using Glamourer.State; @@ -51,7 +52,7 @@ public static class ServiceManager .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton(); @@ -95,7 +96,8 @@ public static class ServiceManager private static IServiceCollection AddState(this IServiceCollection services) => services.AddSingleton() .AddSingleton() - .AddSingleton(); + .AddSingleton() + .AddSingleton(); private static IServiceCollection AddUi(this IServiceCollection services) => services.AddSingleton() @@ -110,6 +112,9 @@ public static class ServiceManager .AddSingleton() .AddSingleton() .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() diff --git a/Glamourer/State/FunModule.cs b/Glamourer/State/FunModule.cs new file mode 100644 index 0000000..f323def --- /dev/null +++ b/Glamourer/State/FunModule.cs @@ -0,0 +1,137 @@ +using System; +using System.Linq; +using Dalamud.Game.ClientState.Objects.Enums; +using Glamourer.Customization; +using Glamourer.Interop.Structs; +using Glamourer.Services; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; +using CustomizeIndex = Glamourer.Customization.CustomizeIndex; + +namespace Glamourer.State; + +public unsafe class FunModule +{ + private readonly ItemManager _items; + private readonly CustomizationService _customizations; + private readonly CodeService _codes; + private readonly Random _rng; + private readonly StainId[] _stains; + + public FunModule(CodeService codes, CustomizationService customizations, ItemManager items) + { + _codes = codes; + _customizations = customizations; + _items = items; + _rng = new Random(); + _stains = _items.Stains.Keys.Prepend((StainId)0).ToArray(); + } + + public void ApplyFun(Actor actor, ref CharacterArmor armor, EquipSlot slot) + { + if (actor.AsObject->ObjectKind is not (byte)ObjectKind.Player || !actor.IsCharacter) + return; + + if (actor.AsCharacter->CharacterData.ModelCharaId != 0) + return; + + ApplyEmperor(new Span(ref armor)); + ApplyClown(new Span(ref armor)); + } + + public void ApplyFun(Actor actor, Span armor, ref Customize customize) + { + if (actor.AsObject->ObjectKind is not (byte)ObjectKind.Player || !actor.IsCharacter) + return; + + if (actor.AsCharacter->CharacterData.ModelCharaId != 0) + return; + + ApplyEmperor(armor); + ApplyClown(armor); + + ApplyOops(ref customize); + ApplyIndividual(ref customize); + ApplySizing(actor, ref customize); + } + + public void ApplyClown(Span armors) + { + if (!_codes.EnabledClown) + return; + + foreach (ref var armor in armors) + { + var stainIdx = _rng.Next(0, _stains.Length - 1); + armor.Stain = _stains[stainIdx]; + } + } + + public void ApplyEmperor(Span armors, EquipSlot slot = EquipSlot.Unknown) + { + if (!_codes.EnabledEmperor) + return; + + if (armors.Length == 1) + { + ref var piece = ref armors[0]; + piece.Variant = 1; + piece.Set = (SetId)(slot.IsAccessory() ? 53 : 279); + return; + } + + for (var i = 0; i < armors.Length; ++i) + { + ref var piece = ref armors[i]; + piece.Variant = 1; + piece.Set = (SetId)(i < 5 ? 279 : 53); + } + } + + public void ApplyOops(ref Customize customize) + { + if (_codes.EnabledOops == Race.Unknown) + return; + + var targetClan = (SubRace)((int)_codes.EnabledOops * 2 - (int)customize.Clan % 2); + // TODO Female Hrothgar + if (_codes.EnabledOops is Race.Hrothgar && customize.Gender is Gender.Female) + targetClan = targetClan is SubRace.Lost ? SubRace.Seawolf : SubRace.Hellsguard; + _customizations.ChangeClan(ref customize, targetClan); + } + + public void ApplyIndividual(ref Customize customize) + { + if (!_codes.EnabledIndividual) + return; + + var set = _customizations.AwaitedService.GetList(customize.Clan, customize.Gender); + foreach (var index in Enum.GetValues()) + { + if (index is CustomizeIndex.Face || !set.IsAvailable(index)) + continue; + + var valueIdx = _rng.Next(0, set.Count(index) - 1); + customize[index] = set.Data(index, valueIdx).Value; + } + } + + public void ApplySizing(Actor actor, ref Customize customize) + { + if (_codes.EnabledSizing == CodeService.Sizing.None) + return; + + var size = _codes.EnabledSizing switch + { + CodeService.Sizing.Dwarf when actor.Index == 0 => 0, + CodeService.Sizing.Dwarf when actor.Index != 0 => 100, + CodeService.Sizing.Giant when actor.Index == 0 => 100, + CodeService.Sizing.Giant when actor.Index != 0 => 0, + _ => 0, + }; + + if (customize.Gender is Gender.Female) + customize[CustomizeIndex.BustSize] = (CustomizeValue)size; + customize[CustomizeIndex.Height] = (CustomizeValue)size; + } +} diff --git a/Glamourer/State/StateListener.cs b/Glamourer/State/StateListener.cs index 08ad5d2..0a292cf 100644 --- a/Glamourer/State/StateListener.cs +++ b/Glamourer/State/StateListener.cs @@ -29,6 +29,7 @@ public class StateListener : IDisposable private readonly VisorStateChanged _visorState; private readonly WeaponVisibilityChanged _weaponVisibility; private readonly AutoDesignApplier _autoDesignApplier; + private readonly FunModule _funModule; public bool Enabled { @@ -38,7 +39,7 @@ public class StateListener : IDisposable public StateListener(StateManager manager, ItemManager items, PenumbraService penumbra, ActorService actors, Configuration config, SlotUpdating slotUpdating, WeaponLoading weaponLoading, VisorStateChanged visorState, WeaponVisibilityChanged weaponVisibility, - HeadGearVisibilityChanged headGearVisibility, AutoDesignApplier autoDesignApplier) + HeadGearVisibilityChanged headGearVisibility, AutoDesignApplier autoDesignApplier, FunModule funModule) { _manager = manager; _items = items; @@ -51,6 +52,7 @@ public class StateListener : IDisposable _weaponVisibility = weaponVisibility; _headGearVisibility = headGearVisibility; _autoDesignApplier = autoDesignApplier; + _funModule = funModule; if (Enabled) Subscribe(); @@ -126,6 +128,7 @@ public class StateListener : IDisposable } } + _funModule.ApplyFun(actor, new Span((void*) equipDataPtr, 10), ref customize); if (modelId == 0) ProtectRestrictedGear(equipDataPtr, customize.Race, customize.Gender); } @@ -141,6 +144,7 @@ public class StateListener : IDisposable && _manager.TryGetValue(identifier, out var state)) HandleEquipSlot(actor, state, slot, ref armor.Value); + _funModule.ApplyFun(actor, ref armor.Value, slot); if (!_config.UseRestrictedGearProtection) return; diff --git a/Glamourer/Unlocks/CustomizeUnlockManager.cs b/Glamourer/Unlocks/CustomizeUnlockManager.cs index ddffa8f..f077481 100644 --- a/Glamourer/Unlocks/CustomizeUnlockManager.cs +++ b/Glamourer/Unlocks/CustomizeUnlockManager.cs @@ -48,9 +48,16 @@ public class CustomizeUnlockManager : IDisposable, ISavable /// Check if a customization is unlocked for Glamourer. public bool IsUnlocked(CustomizeData data, out DateTimeOffset time) { + // All other customizations are not unlockable. + if (data.Index is not CustomizeIndex.Hairstyle and not CustomizeIndex.FacePaint) + { + time = DateTime.MinValue; + return true; + } + if (!Unlockable.TryGetValue(data, out var pair)) { - time = DateTime.MaxValue; + time = DateTime.MinValue; return true; } @@ -62,7 +69,7 @@ public class CustomizeUnlockManager : IDisposable, ISavable if (!IsUnlockedGame(pair.Data)) { - time = DateTimeOffset.MinValue; + time = DateTimeOffset.MaxValue; return false; } diff --git a/Glamourer/Unlocks/ItemUnlockManager.cs b/Glamourer/Unlocks/ItemUnlockManager.cs index 11e10a6..1493ff3 100644 --- a/Glamourer/Unlocks/ItemUnlockManager.cs +++ b/Glamourer/Unlocks/ItemUnlockManager.cs @@ -196,6 +196,7 @@ public class ItemUnlockManager : ISavable, IDisposable } } + changes = false; var inventoryManager = InventoryManager.Instance(); if (inventoryManager != null) { @@ -216,7 +217,6 @@ public class ItemUnlockManager : ISavable, IDisposable _currentInventoryIndex = 0; } } - if (changes) Save(); } @@ -226,7 +226,7 @@ public class ItemUnlockManager : ISavable, IDisposable // Pseudo items are always unlocked. if (itemId >= _items.ItemSheet.RowCount) { - time = DateTimeOffset.MaxValue; + time = DateTimeOffset.MinValue; return true; } @@ -244,7 +244,7 @@ public class ItemUnlockManager : ISavable, IDisposable return true; } - time = DateTimeOffset.MinValue; + time = DateTimeOffset.MaxValue; return false; } @@ -283,7 +283,7 @@ public class ItemUnlockManager : ISavable, IDisposable => fileNames.UnlockFileItems; public void Save() - => _saveService.QueueSave(this); + => _saveService.DelaySave(this, TimeSpan.FromSeconds(10)); public void Save(StreamWriter writer) { }