From 65ce3910517ea42ce697e90857b2ad2d2cf54516 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 20 Jun 2023 18:54:33 +0200 Subject: [PATCH] . --- Glamourer/Configuration.cs | 46 +++ .../{UpdatedSlot.cs => SlotUpdating.cs} | 10 +- Glamourer/Events/WeaponLoading.cs | 35 +++ Glamourer/Gui/Colors.cs | 21 +- Glamourer/Gui/GlamourerWindowSystem.cs | 14 +- Glamourer/Gui/PenumbraChangedItemTooltip.cs | 182 +++++++++++ Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs | 2 +- Glamourer/Gui/Tabs/DebugTab.cs | 27 +- .../DesignTab/DesignFileSystemSelector.cs | 15 + Glamourer/Gui/Tabs/SettingsTab.cs | 46 ++- Glamourer/Interop/ObjectManager.cs | 11 + Glamourer/Interop/UpdateSlotService.cs | 6 +- Glamourer/Interop/VisorService.cs | 9 +- Glamourer/Interop/WeaponService.cs | 32 +- Glamourer/Services/ItemManager.cs | 2 - Glamourer/Services/ServiceManager.cs | 11 +- Glamourer/State/ActorState.cs | 3 +- Glamourer/State/StateListener.cs | 283 ++++++++++++++++++ Glamourer/State/StateManager.cs | 160 ++++------ 19 files changed, 757 insertions(+), 158 deletions(-) rename Glamourer/Events/{UpdatedSlot.cs => SlotUpdating.cs} (76%) create mode 100644 Glamourer/Events/WeaponLoading.cs create mode 100644 Glamourer/Gui/PenumbraChangedItemTooltip.cs create mode 100644 Glamourer/State/StateListener.cs diff --git a/Glamourer/Configuration.cs b/Glamourer/Configuration.cs index ed3ea12..a023a83 100644 --- a/Glamourer/Configuration.cs +++ b/Glamourer/Configuration.cs @@ -4,20 +4,29 @@ using System.IO; using System.Linq; using Dalamud.Configuration; using Dalamud.Interface.Internal.Notifications; +using Glamourer.Designs; using Glamourer.Gui; using Glamourer.Services; using Newtonsoft.Json; +using OtterGui; using OtterGui.Classes; +using OtterGui.Filesystem; using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs; namespace Glamourer; public class Configuration : IPluginConfiguration, ISavable { + public bool Enabled { get; set; } = true; public bool UseRestrictedGearProtection { get; set; } = true; + public bool OpenFoldersByDefault { get; set; } = false; public MainWindow.TabType SelectedTab { get; set; } = MainWindow.TabType.Settings; public DoubleModifier DeleteDesignModifier { get; set; } = new(ModifierHotkey.Control, ModifierHotkey.Shift); + [JsonConverter(typeof(SortModeConverter))] + [JsonProperty(Order = int.MaxValue)] + public ISortMode SortMode { get; set; } = ISortMode.FoldersFirst; + #if DEBUG public bool DebugMode { get; set; } = true; @@ -86,5 +95,42 @@ public class Configuration : IPluginConfiguration, ISavable public static class Constants { public const int CurrentVersion = 2; + + public static readonly ISortMode[] ValidSortModes = + { + ISortMode.FoldersFirst, + ISortMode.Lexicographical, + new DesignFileSystem.CreationDate(), + new DesignFileSystem.InverseCreationDate(), + new DesignFileSystem.UpdateDate(), + new DesignFileSystem.InverseUpdateDate(), + ISortMode.InverseFoldersFirst, + ISortMode.InverseLexicographical, + ISortMode.FoldersLast, + ISortMode.InverseFoldersLast, + ISortMode.InternalOrder, + ISortMode.InverseInternalOrder, + }; + } + + /// Convert SortMode Types to their name. + private class SortModeConverter : JsonConverter> + { + public override void WriteJson(JsonWriter writer, ISortMode? value, JsonSerializer serializer) + { + value ??= ISortMode.FoldersFirst; + serializer.Serialize(writer, value.GetType().Name); + } + + public override ISortMode ReadJson(JsonReader reader, Type objectType, ISortMode? existingValue, + bool hasExistingValue, + JsonSerializer serializer) + { + var name = serializer.Deserialize(reader); + if (name == null || !Constants.ValidSortModes.FindFirst(s => s.GetType().Name == name, out var mode)) + return existingValue ?? ISortMode.FoldersFirst; + + return mode; + } } } diff --git a/Glamourer/Events/UpdatedSlot.cs b/Glamourer/Events/SlotUpdating.cs similarity index 76% rename from Glamourer/Events/UpdatedSlot.cs rename to Glamourer/Events/SlotUpdating.cs index 14581f6..d83822b 100644 --- a/Glamourer/Events/UpdatedSlot.cs +++ b/Glamourer/Events/SlotUpdating.cs @@ -15,16 +15,16 @@ namespace Glamourer.Events; /// Parameter is the return value the function should return, if it is ulong.MaxValue, the original will be called and returned. /// /// -public sealed class UpdatedSlot : EventWrapper, Ref>, UpdatedSlot.Priority> +public sealed class SlotUpdating : EventWrapper, Ref>, SlotUpdating.Priority> { public enum Priority { - /// - StateManager = 0, + /// + StateListener = 0, } - public UpdatedSlot() - : base(nameof(UpdatedSlot)) + public SlotUpdating() + : base(nameof(SlotUpdating)) { } public void Invoke(Model model, EquipSlot slot, ref CharacterArmor armor, ref ulong returnValue) diff --git a/Glamourer/Events/WeaponLoading.cs b/Glamourer/Events/WeaponLoading.cs new file mode 100644 index 0000000..e0230a1 --- /dev/null +++ b/Glamourer/Events/WeaponLoading.cs @@ -0,0 +1,35 @@ +using System; +using Glamourer.Interop.Structs; +using OtterGui.Classes; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; + +namespace Glamourer.Events; + +/// +/// Triggered when a model flags an equipment slot for an update. +/// +/// Parameter is the actor that has its weapons changed. +/// Parameter is the equipment slot changed (Mainhand or Offhand). +/// Parameter is the model values to change the weapon to. +/// +/// +public sealed class WeaponLoading : EventWrapper>, WeaponLoading.Priority> +{ + public enum Priority + { + /// + StateListener = 0, + } + + public WeaponLoading() + : base(nameof(WeaponLoading)) + { } + + public void Invoke(Actor actor, EquipSlot slot, ref CharacterWeapon weapon) + { + var value = new Ref(weapon); + Invoke(this, actor, slot, value); + weapon = value; + } +} diff --git a/Glamourer/Gui/Colors.cs b/Glamourer/Gui/Colors.cs index daa23a1..8dd8ee0 100644 --- a/Glamourer/Gui/Colors.cs +++ b/Glamourer/Gui/Colors.cs @@ -9,24 +9,25 @@ public enum ColorId EquipmentDesign, ActorAvailable, ActorUnavailable, + FolderExpanded, + FolderCollapsed, + FolderLine, } public static class Colors { - public const uint DiscordColor = 0xFFDA8972; - public const uint ReniColorButton = 0xFFCC648D; - public const uint ReniColorHovered = 0xFFB070B0; - public const uint ReniColorActive = 0xFF9070E0; - public static (uint DefaultColor, string Name, string Description) Data(this ColorId color) => color switch { // @formatter:off - ColorId.CustomizationDesign => (0xFFC000C0, "Customization Design", "A design that only changes customizations on a character." ), - ColorId.StateDesign => (0xFF00C0C0, "State Design", "A design that only changes meta state on a character." ), - ColorId.EquipmentDesign => (0xFF00C000, "Equipment Design", "A design that only changes equipment on a character." ), - ColorId.ActorAvailable => (0xFF18C018, "Actor Available", "The header in the Actor tab panel if the currently selected actor exists in the game world at least once." ), - ColorId.ActorUnavailable => (0xFF1818C0, "Actor Unavailable", "The Header in the Actor tab panel if the currently selected actor does not exist in the game world." ), + ColorId.CustomizationDesign => (0xFFC000C0, "Customization Design", "A design that only changes customizations on a character." ), + ColorId.StateDesign => (0xFF00C0C0, "State Design", "A design that only changes meta state on a character." ), + ColorId.EquipmentDesign => (0xFF00C000, "Equipment Design", "A design that only changes equipment on a character." ), + ColorId.ActorAvailable => (0xFF18C018, "Actor Available", "The header in the Actor tab panel if the currently selected actor exists in the game world at least once." ), + ColorId.ActorUnavailable => (0xFF1818C0, "Actor Unavailable", "The Header in the Actor tab panel if the currently selected actor does not exist in the game world." ), + ColorId.FolderExpanded => (0xFFFFF0C0, "Expanded Design Folder", "A design folder that is currently expanded." ), + ColorId.FolderCollapsed => (0xFFFFF0C0, "Collapsed Design Folder", "A design folder that is currently collapsed." ), + ColorId.FolderLine => (0xFFFFF0C0, "Expanded Design Folder Line", "The line signifying which descendants belong to an expanded design folder." ), _ => (0x00000000, string.Empty, string.Empty ), // @formatter:on }; diff --git a/Glamourer/Gui/GlamourerWindowSystem.cs b/Glamourer/Gui/GlamourerWindowSystem.cs index 0413b50..f9046d6 100644 --- a/Glamourer/Gui/GlamourerWindowSystem.cs +++ b/Glamourer/Gui/GlamourerWindowSystem.cs @@ -6,14 +6,16 @@ namespace Glamourer.Gui; public class GlamourerWindowSystem : IDisposable { - private readonly WindowSystem _windowSystem = new("Glamourer"); - private readonly UiBuilder _uiBuilder; - private readonly MainWindow _ui; + private readonly WindowSystem _windowSystem = new("Glamourer"); + private readonly UiBuilder _uiBuilder; + private readonly MainWindow _ui; + private readonly PenumbraChangedItemTooltip _penumbraTooltip; - public GlamourerWindowSystem(UiBuilder uiBuilder, MainWindow ui) + public GlamourerWindowSystem(UiBuilder uiBuilder, MainWindow ui, PenumbraChangedItemTooltip penumbraTooltip) { - _uiBuilder = uiBuilder; - _ui = ui; + _uiBuilder = uiBuilder; + _ui = ui; + _penumbraTooltip = penumbraTooltip; _windowSystem.AddWindow(ui); _uiBuilder.Draw += _windowSystem.Draw; _uiBuilder.OpenConfigUi += _ui.Toggle; diff --git a/Glamourer/Gui/PenumbraChangedItemTooltip.cs b/Glamourer/Gui/PenumbraChangedItemTooltip.cs new file mode 100644 index 0000000..009f294 --- /dev/null +++ b/Glamourer/Gui/PenumbraChangedItemTooltip.cs @@ -0,0 +1,182 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Glamourer.Interop; +using Glamourer.Interop.Penumbra; +using Glamourer.Services; +using Glamourer.State; +using Glamourer.Structs; +using ImGuiNET; +using Penumbra.Api.Enums; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; + +namespace Glamourer.Gui; + +public class PenumbraChangedItemTooltip : IDisposable +{ + private readonly PenumbraService _penumbra; + private readonly StateManager _stateManager; + private readonly ItemManager _items; + private readonly ObjectManager _objects; + + private readonly EquipItem[] _lastItems = new EquipItem[EquipFlagExtensions.NumEquipFlags / 2]; + + public IEnumerable> LastItems + => EquipSlotExtensions.EqdpSlots.Append(EquipSlot.MainHand).Append(EquipSlot.OffHand).Zip(_lastItems) + .Select(p => new KeyValuePair(p.First, p.Second)); + + public DateTime LastTooltip { get; private set; } = DateTime.MinValue; + public DateTime LastClick { get; private set; } = DateTime.MinValue; + + public PenumbraChangedItemTooltip(PenumbraService penumbra, StateManager stateManager, ItemManager items, ObjectManager objects) + { + _penumbra = penumbra; + _stateManager = stateManager; + _items = items; + _objects = objects; + _penumbra.Tooltip += OnPenumbraTooltip; + _penumbra.Click += OnPenumbraClick; + } + + public void Dispose() + { + _penumbra.Tooltip -= OnPenumbraTooltip; + _penumbra.Click -= OnPenumbraClick; + } + + private void OnPenumbraTooltip(ChangedItemType type, uint id) + { + LastTooltip = DateTime.UtcNow; + if (!_objects.Player.Valid) + return; + + switch (type) + { + case ChangedItemType.Item: + 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; + } + + return; + } + } + + private bool CanApplyWeapon(EquipSlot slot, EquipItem item) + { + var main = _objects.Player.GetMainhand(); + var mainItem = _items.Identify(slot, main.Set, main.Type, (byte)main.Variant); + if (slot == EquipSlot.MainHand) + return item.Type == mainItem.Type; + + return item.Type == mainItem.Type.Offhand(); + } + + private void OnPenumbraClick(MouseButton button, ChangedItemType type, uint id) + { + LastClick = DateTime.UtcNow; + switch (type) + { + case ChangedItemType.Item: + 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)) + 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; + } + + return; + } + } + + private void SetLastItem(EquipSlot slot, EquipItem item, ActorState state) + { + ref var last = ref _lastItems[slot.ToIndex()]; + if (!item.Valid) + { + last = default; + } + else + { + var oldItem = state.ModelData.Item(slot); + if (oldItem.Id != item.Id) + _lastItems[slot.ToIndex()] = oldItem; + } + } +} diff --git a/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs b/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs index 58fd541..b220ad7 100644 --- a/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs +++ b/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs @@ -75,7 +75,7 @@ public class ActorPanel if (!child || _state == null) return; - if (_customizationDrawer.Draw(_state.Data.Customize, false)) + if (_customizationDrawer.Draw(_state.ModelData.Customize, false)) { } // if (_currentData.Valid) diff --git a/Glamourer/Gui/Tabs/DebugTab.cs b/Glamourer/Gui/Tabs/DebugTab.cs index 3c6f08d..143e0c8 100644 --- a/Glamourer/Gui/Tabs/DebugTab.cs +++ b/Glamourer/Gui/Tabs/DebugTab.cs @@ -41,6 +41,8 @@ public unsafe class DebugTab : ITab private readonly DesignManager _designManager; private readonly DesignFileSystem _designFileSystem; + private readonly PenumbraChangedItemTooltip _penumbraTooltip; + private readonly StateManager _state; private int _gameObjectIndex; @@ -51,7 +53,8 @@ public unsafe class DebugTab : ITab public DebugTab(ChangeCustomizeService changeCustomizeService, VisorService visorService, ObjectTable objects, UpdateSlotService updateSlotService, WeaponService weaponService, PenumbraService penumbra, ActorService actors, ItemManager items, CustomizationService customization, ObjectManager objectManager, - DesignFileSystem designFileSystem, DesignManager designManager, StateManager state, Configuration config) + DesignFileSystem designFileSystem, DesignManager designManager, StateManager state, Configuration config, + PenumbraChangedItemTooltip penumbraTooltip) { _changeCustomizeService = changeCustomizeService; _visorService = visorService; @@ -67,6 +70,7 @@ public unsafe class DebugTab : ITab _designManager = designManager; _state = state; _config = config; + _penumbraTooltip = penumbraTooltip; } public ReadOnlySpan Label @@ -434,6 +438,23 @@ public unsafe class DebugTab : ITab if (ImGui.SmallButton("Redraw")) _penumbra.RedrawObject(_objects.GetObjectAddress(_gameObjectIndex), RedrawType.Redraw); } + + ImGuiUtil.DrawTableColumn("Last Tooltip Date"); + ImGuiUtil.DrawTableColumn(_penumbraTooltip.LastTooltip > DateTime.MinValue ? _penumbraTooltip.LastTooltip.ToLongTimeString() : "Never"); + ImGui.TableNextColumn(); + + ImGuiUtil.DrawTableColumn("Last Click Date"); + ImGuiUtil.DrawTableColumn(_penumbraTooltip.LastClick > DateTime.MinValue ? _penumbraTooltip.LastClick.ToLongTimeString() : "Never"); + ImGui.TableNextColumn(); + + ImGui.Separator(); + ImGui.Separator(); + foreach (var (slot, item) in _penumbraTooltip.LastItems) + { + ImGuiUtil.DrawTableColumn($"{slot.ToName()} Revert-Item"); + ImGuiUtil.DrawTableColumn(item.Valid ? item.Name : "None"); + ImGui.TableNextColumn(); + } } #endregion @@ -990,7 +1011,7 @@ public unsafe class DebugTab : ITab continue; if (_state.GetOrCreate(identifier, actors.Objects[0], out var state)) - DrawDesignData(state.Data); + DrawDesignData(state.ModelData); else ImGui.TextUnformatted("Invalid actor."); } @@ -1006,7 +1027,7 @@ public unsafe class DebugTab : ITab { using var t = ImRaii.TreeNode(identifier.ToString()); if (t) - DrawDesignData(state.Data); + DrawDesignData(state.ModelData); } } diff --git a/Glamourer/Gui/Tabs/DesignTab/DesignFileSystemSelector.cs b/Glamourer/Gui/Tabs/DesignTab/DesignFileSystemSelector.cs index 9b786c7..3e4a627 100644 --- a/Glamourer/Gui/Tabs/DesignTab/DesignFileSystemSelector.cs +++ b/Glamourer/Gui/Tabs/DesignTab/DesignFileSystemSelector.cs @@ -35,6 +35,21 @@ public sealed class DesignFileSystemSelector : FileSystemSelector SortMode + => _config.SortMode; + + protected override uint ExpandedFolderColor + => ColorId.FolderExpanded.Value(); + + protected override uint CollapsedFolderColor + => ColorId.FolderCollapsed.Value(); + + protected override uint FolderLineColor + => ColorId.FolderLine.Value(); + + protected override bool FoldersDefaultOpen + => _config.OpenFoldersByDefault; + private void OnDesignChange(DesignChanged.Type type, Design design, object? oldData) { switch (type) diff --git a/Glamourer/Gui/Tabs/SettingsTab.cs b/Glamourer/Gui/Tabs/SettingsTab.cs index 4b514be..904f446 100644 --- a/Glamourer/Gui/Tabs/SettingsTab.cs +++ b/Glamourer/Gui/Tabs/SettingsTab.cs @@ -1,10 +1,10 @@ using System; -using System.Numerics; using System.Runtime.CompilerServices; using Dalamud.Interface; +using Glamourer.Gui.Tabs.DesignTab; +using Glamourer.State; using ImGuiNET; using OtterGui; -using OtterGui.Classes; using OtterGui.Raii; using OtterGui.Widgets; @@ -12,10 +12,16 @@ namespace Glamourer.Gui.Tabs; public class SettingsTab : ITab { - private readonly Configuration _config; + private readonly Configuration _config; + private readonly DesignFileSystemSelector _selector; + private readonly StateListener _stateListener; - public SettingsTab(Configuration config) - => _config = config; + public SettingsTab(Configuration config, DesignFileSystemSelector selector, StateListener stateListener) + { + _config = config; + _selector = selector; + _stateListener = stateListener; + } public ReadOnlySpan Label => "Settings"u8; @@ -25,7 +31,7 @@ public class SettingsTab : ITab using var child = ImRaii.Child("MainWindowChild"); if (!child) return; - + Checkbox("Enabled", "Enable main functionality of keeping and applying state.", _stateListener.Enabled, _stateListener.Enable); Checkbox("Restricted Gear Protection", "Use gender- and race-appropriate models when detecting certain items not available for a characters current gender and race.", _config.UseRestrictedGearProtection, v => _config.UseRestrictedGearProtection = v); @@ -33,6 +39,10 @@ public class SettingsTab : ITab "A modifier you need to hold while clicking the Delete Design button for it to take effect.", 100 * ImGuiHelpers.GlobalScale, _config.DeleteDesignModifier, v => _config.DeleteDesignModifier = v)) _config.Save(); + DrawFolderSortType(); + Checkbox("Auto-Open Design Folders", + "Have design folders open or closed as their default state after launching.", _config.OpenFoldersByDefault, + v => _config.OpenFoldersByDefault = v); Checkbox("Debug Mode", "Show the debug tab. Only useful for debugging or advanced use.", _config.DebugMode, v => _config.DebugMode = v); DrawColorSettings(); @@ -70,4 +80,28 @@ public class SettingsTab : ITab ImGui.SameLine(); ImGuiUtil.LabeledHelpMarker(label, tooltip); } + + /// Different supported sort modes as a combo. + private void DrawFolderSortType() + { + var sortMode = _config.SortMode; + ImGui.SetNextItemWidth(300 * ImGuiHelpers.GlobalScale); + using (var combo = ImRaii.Combo("##sortMode", sortMode.Name)) + { + if (combo) + foreach (var val in Configuration.Constants.ValidSortModes) + { + if (ImGui.Selectable(val.Name, val.GetType() == sortMode.GetType()) && val.GetType() != sortMode.GetType()) + { + _config.SortMode = val; + _selector.SetFilterDirty(); + _config.Save(); + } + + ImGuiUtil.HoverTooltip(val.Description); + } + } + + ImGuiUtil.LabeledHelpMarker("Sort Mode", "Choose the sort mode for the mod selector in the designs tab."); + } } diff --git a/Glamourer/Interop/ObjectManager.cs b/Glamourer/Interop/ObjectManager.cs index 17295d6..763e14d 100644 --- a/Glamourer/Interop/ObjectManager.cs +++ b/Glamourer/Interop/ObjectManager.cs @@ -113,6 +113,17 @@ public class ObjectManager : IReadOnlyDictionary public Actor Player => _objects.GetObjectAddress(0); + public (ActorIdentifier Identifier, ActorData Data) PlayerData + { + get + { + Update(); + return Player.Identifier(_actors.AwaitedService, out var ident) && _identifiers.TryGetValue(ident, out var data) + ? (ident, data) + : (ActorIdentifier.Invalid, ActorData.Invalid); + } + } + public IEnumerator> GetEnumerator() => Identifiers.GetEnumerator(); diff --git a/Glamourer/Interop/UpdateSlotService.cs b/Glamourer/Interop/UpdateSlotService.cs index b4aaa6a..f515944 100644 --- a/Glamourer/Interop/UpdateSlotService.cs +++ b/Glamourer/Interop/UpdateSlotService.cs @@ -10,11 +10,11 @@ namespace Glamourer.Interop; public unsafe class UpdateSlotService : IDisposable { - public readonly UpdatedSlot Event; + public readonly SlotUpdating Event; - public UpdateSlotService(UpdatedSlot updatedSlot) + public UpdateSlotService(SlotUpdating slotUpdating) { - Event = updatedSlot; + Event = slotUpdating; SignatureHelper.Initialise(this); _flagSlotForUpdateHook.Enable(); } diff --git a/Glamourer/Interop/VisorService.cs b/Glamourer/Interop/VisorService.cs index 226ee00..f6bb0ab 100644 --- a/Glamourer/Interop/VisorService.cs +++ b/Glamourer/Interop/VisorService.cs @@ -23,13 +23,7 @@ public class VisorService : IDisposable /// Obtain the current state of the Visor for the given draw object (true: toggled). public unsafe bool GetVisorState(Model characterBase) - { - if (!characterBase.IsCharacterBase) - return false; - - // TODO: use client structs. - return (characterBase.AsCharacterBase->UnkFlags_01 & Offsets.DrawObjectVisorStateFlag) != 0; - } + => characterBase.IsCharacterBase && characterBase.AsCharacterBase->VisorToggled; /// Manually set the state of the Visor for the given draw object. /// The draw object. @@ -45,7 +39,6 @@ public class VisorService : IDisposable if (oldState == on) return false; - SetupVisorHook(human, (ushort)human.AsHuman->HeadSetID, on); return true; } diff --git a/Glamourer/Interop/WeaponService.cs b/Glamourer/Interop/WeaponService.cs index 273813b..84c9037 100644 --- a/Glamourer/Interop/WeaponService.cs +++ b/Glamourer/Interop/WeaponService.cs @@ -2,6 +2,7 @@ using Dalamud.Hooking; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.Game.Character; +using Glamourer.Events; using Glamourer.Interop.Structs; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; @@ -10,8 +11,11 @@ namespace Glamourer.Interop; public unsafe class WeaponService : IDisposable { - public WeaponService() + private readonly WeaponLoading _event; + + public WeaponService(WeaponLoading @event) { + _event = @event; SignatureHelper.Initialise(this); _loadWeaponHook = Hook.FromAddress((nint)DrawDataContainer.MemberFunctionPointers.LoadWeapon, LoadWeaponDetour); _loadWeaponHook.Enable(); @@ -22,20 +26,38 @@ public unsafe class WeaponService : IDisposable _loadWeaponHook.Dispose(); } + // Weapons for a specific character are reloaded with this function. + // slot is 0 for main hand, 1 for offhand, 2 for combat effects. + // weapon argument is the new weapon data. + // redrawOnEquality controls whether the game does anything if the new weapon is identical to the old one. + // skipGameObject seems to control whether the new weapons are written to the game object or just influence the draw object. (1 = skip, 0 = change) + // unk4 seemed to be the same as unk1. private delegate void LoadWeaponDelegate(DrawDataContainer* drawData, uint slot, ulong weapon, byte redrawOnEquality, byte unk2, byte skipGameObject, byte unk4); private readonly Hook _loadWeaponHook; - private void LoadWeaponDetour(DrawDataContainer* drawData, uint slot, ulong weapon, byte redrawOnEquality, byte unk2, byte skipGameObject, + + private void LoadWeaponDetour(DrawDataContainer* drawData, uint slot, ulong weaponValue, byte redrawOnEquality, byte unk2, + byte skipGameObject, byte unk4) { - var actor = (Actor) (nint)(drawData + 1); + var actor = (Actor)((nint*)drawData)[1]; + var weapon = new CharacterWeapon(weaponValue); + var equipSlot = slot switch + { + 0 => EquipSlot.MainHand, + 1 => EquipSlot.OffHand, + _ => EquipSlot.Unknown, + }; // First call the regular function. - _loadWeaponHook.Original(drawData, slot, weapon, redrawOnEquality, unk2, skipGameObject, unk4); + if (equipSlot is not EquipSlot.Unknown) + _event.Invoke(actor, equipSlot, ref weapon); + + _loadWeaponHook.Original(drawData, slot, weapon.Value, redrawOnEquality, unk2, skipGameObject, unk4); Glamourer.Log.Excessive( - $"Weapon reloaded for 0x{actor.Address:X} with attributes {slot} {weapon:X14}, {redrawOnEquality}, {unk2}, {skipGameObject}, {unk4}"); + $"Weapon reloaded for 0x{actor.Address:X} ({actor.Utf8Name}) with attributes {slot} {weapon.Value:X14}, {redrawOnEquality}, {unk2}, {skipGameObject}, {unk4}"); } // Load a specific weapon for a character by its data and slot. diff --git a/Glamourer/Services/ItemManager.cs b/Glamourer/Services/ItemManager.cs index 3e3cf8e..130f553 100644 --- a/Glamourer/Services/ItemManager.cs +++ b/Glamourer/Services/ItemManager.cs @@ -45,11 +45,9 @@ public class ItemManager : IDisposable RestrictedGear.Dispose(); } - public (bool, CharacterArmor) ResolveRestrictedGear(CharacterArmor armor, EquipSlot slot, Race race, Gender gender) => _config.UseRestrictedGearProtection ? RestrictedGear.ResolveRestricted(armor, slot, race, gender) : (false, armor); - public static uint NothingId(EquipSlot slot) => uint.MaxValue - 128 - (uint)slot.ToSlot(); diff --git a/Glamourer/Services/ServiceManager.cs b/Glamourer/Services/ServiceManager.cs index dadb1e8..687ccd9 100644 --- a/Glamourer/Services/ServiceManager.cs +++ b/Glamourer/Services/ServiceManager.cs @@ -51,9 +51,10 @@ public static class ServiceManager private static IServiceCollection AddEvents(this IServiceCollection services) => services.AddSingleton() - .AddSingleton() + .AddSingleton() .AddSingleton() - .AddSingleton(); + .AddSingleton() + .AddSingleton(); private static IServiceCollection AddData(this IServiceCollection services) => services.AddSingleton() @@ -75,7 +76,8 @@ public static class ServiceManager .AddSingleton(); private static IServiceCollection AddState(this IServiceCollection services) - => services.AddSingleton(); + => services.AddSingleton() + .AddSingleton(); private static IServiceCollection AddUi(this IServiceCollection services) => services.AddSingleton() @@ -88,7 +90,8 @@ public static class ServiceManager .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton(); + .AddSingleton() + .AddSingleton(); private static IServiceCollection AddApi(this IServiceCollection services) => services.AddSingleton(); diff --git a/Glamourer/State/ActorState.cs b/Glamourer/State/ActorState.cs index 411336a..2d963f4 100644 --- a/Glamourer/State/ActorState.cs +++ b/Glamourer/State/ActorState.cs @@ -20,7 +20,8 @@ public class ActorState } public ActorIdentifier Identifier { get; internal init; } - public DesignData Data; + public DesignData ActorData; + public DesignData ModelData; private readonly StateChanged.Source[] _sources = Enumerable .Repeat(StateChanged.Source.Game, EquipFlagExtensions.NumEquipFlags + CustomizationExtensions.NumIndices + 4).ToArray(); diff --git a/Glamourer/State/StateListener.cs b/Glamourer/State/StateListener.cs new file mode 100644 index 0000000..e146df2 --- /dev/null +++ b/Glamourer/State/StateListener.cs @@ -0,0 +1,283 @@ +using System; +using Glamourer.Customization; +using Glamourer.Events; +using Glamourer.Interop.Penumbra; +using Glamourer.Interop.Structs; +using Glamourer.Services; +using OtterGui.Classes; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; + +namespace Glamourer.State; + +public class StateListener : IDisposable +{ + private readonly Configuration _config; + private readonly ActorService _actors; + private readonly StateManager _manager; + private readonly ItemManager _items; + private readonly PenumbraService _penumbra; + private readonly SlotUpdating _slotUpdating; + private readonly WeaponLoading _weaponLoading; + + public bool Enabled + { + get => _config.Enabled; + set => Enable(value); + } + + public StateListener(StateManager manager, ItemManager items, PenumbraService penumbra, ActorService actors, Configuration config, + SlotUpdating slotUpdating, WeaponLoading weaponLoading) + { + _manager = manager; + _items = items; + _penumbra = penumbra; + _actors = actors; + _config = config; + _slotUpdating = slotUpdating; + _weaponLoading = weaponLoading; + + if (Enabled) + Subscribe(); + } + + public void Enable(bool value) + { + if (value == Enabled) + return; + + _config.Enabled = value; + _config.Save(); + + if (value) + Subscribe(); + else + Unsubscribe(); + } + + void IDisposable.Dispose() + { + if (Enabled) + Unsubscribe(); + } + + private unsafe void OnCreatingCharacterBase(nint actorPtr, string _, nint modelPtr, nint customizePtr, nint equipDataPtr) + { + // TODO: Fixed Designs. + var actor = (Actor)actorPtr; + var identifier = actor.GetIdentifier(_actors.AwaitedService); + + if (*(int*)modelPtr != actor.AsCharacter->ModelCharaId) + return; + + ref var customize = ref *(Customize*)customizePtr; + if (_manager.TryGetValue(identifier, out var state)) + { + ApplyCustomize(actor, state, ref customize); + ApplyEquipment(actor, state, (CharacterArmor*)equipDataPtr); + if (_config.UseRestrictedGearProtection) + ProtectRestrictedGear(equipDataPtr, customize.Race, customize.Gender); + } + else if (_config.UseRestrictedGearProtection && *(uint*)modelPtr == 0) + { + ProtectRestrictedGear(equipDataPtr, customize.Race, customize.Gender); + } + } + + private void OnSlotUpdating(Model model, EquipSlot slot, Ref armor, Ref returnValue) + { + // TODO handle hat state + // TODO handle fixed designs + var actor = _penumbra.GameObjectFromDrawObject(model); + var customize = model.GetCustomize(); + if (actor.Identifier(_actors.AwaitedService, out var identifier) + && _manager.TryGetValue(identifier, out var state)) + ApplyEquipmentPiece(actor, state, slot, ref armor.Value); + + var (replaced, replacedArmor) = _items.RestrictedGear.ResolveRestricted(armor, slot, customize.Race, customize.Gender); + if (replaced) + armor.Assign(replacedArmor); + } + + private void OnWeaponLoading(Actor actor, EquipSlot slot, Ref weapon) + { + if (!actor.Identifier(_actors.AwaitedService, out var identifier) + || !_manager.TryGetValue(identifier, out var state)) + return; + + ref var actorWeapon = ref weapon.Value; + var stateItem = state.ModelData.Item(slot); + if (actorWeapon.Set.Value != stateItem.ModelId.Value + || actorWeapon.Type.Value != stateItem.WeaponType + || actorWeapon.Variant != stateItem.Variant) + { + var oldActorItem = state.ActorData.Item(slot); + if (oldActorItem.ModelId.Value == actorWeapon.Set.Value + && oldActorItem.WeaponType.Value == actorWeapon.Type.Value + && oldActorItem.Variant == actorWeapon.Variant) + { + actorWeapon.Set = stateItem.ModelId; + actorWeapon.Type = stateItem.WeaponType; + actorWeapon.Variant = stateItem.Variant; + } + else + { + var identified = _items.Identify(slot, actorWeapon.Set, actorWeapon.Type, (byte)actorWeapon.Variant, + slot == EquipSlot.OffHand ? state.ActorData.Item(EquipSlot.MainHand).Type : FullEquipType.Unknown); + state.ActorData.SetItem(slot, identified); + if (state[slot, false] is not StateChanged.Source.Fixed) + { + state.ModelData.SetItem(slot, identified); + state[slot, false] = StateChanged.Source.Game; + } + else + { + actorWeapon.Set = stateItem.ModelId; + actorWeapon.Type = stateItem.Variant; + actorWeapon.Variant = stateItem.Variant; + } + } + } + + var stateStain = state.ModelData.Stain(slot); + if (actorWeapon.Stain.Value != stateStain.Value) + { + var oldActorStain = state.ActorData.Stain(slot); + if (state[slot, true] is not StateChanged.Source.Fixed) + { + state.ModelData.SetStain(slot, actorWeapon.Stain); + state[slot, true] = StateChanged.Source.Game; + } + else + { + actorWeapon.Stain = stateStain; + } + } + } + + + private void ApplyCustomize(Actor actor, ActorState state, ref Customize customize) + { + var actorCustomize = actor.GetCustomize(); + ref var oldActorCustomize = ref state.ActorData.Customize; + ref var stateCustomize = ref state.ModelData.Customize; + foreach (var idx in Enum.GetValues()) + { + var value = customize[idx]; + var actorValue = actorCustomize[idx]; + if (value.Value != actorValue.Value) + continue; + + var stateValue = stateCustomize[idx]; + if (value.Value == stateValue.Value) + continue; + + if (oldActorCustomize[idx].Value == actorValue.Value) + { + customize[idx] = stateValue; + } + else + { + oldActorCustomize[idx] = actorValue; + if (state[idx] is StateChanged.Source.Fixed) + { + state.ModelData.Customize[idx] = value; + state[idx] = StateChanged.Source.Game; + } + else + { + customize[idx] = stateValue; + } + } + } + } + + private unsafe void ApplyEquipment(Actor actor, ActorState state, CharacterArmor* equipData) + { + // TODO: Handle hat state + foreach (var slot in EquipSlotExtensions.EqdpSlots) + ApplyEquipmentPiece(actor, state, slot, ref *equipData++); + } + + private void ApplyEquipmentPiece(Actor actor, ActorState state, EquipSlot slot, ref CharacterArmor armor) + { + var actorArmor = actor.GetArmor(slot); + if (armor.Value != actorArmor.Value) + return; + + var stateArmor = state.ModelData.Item(slot); + if (armor.Set.Value != stateArmor.ModelId.Value || armor.Variant != stateArmor.Variant) + { + var oldActorArmor = state.ActorData.Item(slot); + if (oldActorArmor.ModelId.Value == actorArmor.Set.Value && oldActorArmor.Variant == actorArmor.Variant) + { + armor.Set = stateArmor.ModelId; + armor.Variant = stateArmor.Variant; + } + else + { + var identified = _items.Identify(slot, actorArmor.Set, actorArmor.Variant); + state.ActorData.SetItem(slot, identified); + if (state[slot, false] is not StateChanged.Source.Fixed) + { + state.ModelData.SetItem(slot, identified); + state[slot, false] = StateChanged.Source.Game; + } + else + { + armor.Set = stateArmor.ModelId; + armor.Variant = stateArmor.Variant; + } + } + } + + var stateStain = state.ModelData.Stain(slot); + if (armor.Stain.Value != stateStain.Value) + { + var oldActorStain = state.ActorData.Stain(slot); + if (oldActorStain.Value == actorArmor.Stain.Value) + { + armor.Stain = stateStain; + } + else + { + state.ActorData.SetStain(slot, actorArmor.Stain); + if (state[slot, true] is not StateChanged.Source.Fixed) + { + state.ModelData.SetStain(slot, actorArmor.Stain); + state[slot, true] = StateChanged.Source.Game; + } + else + { + armor.Stain = stateStain; + } + } + } + } + + private unsafe void ProtectRestrictedGear(nint equipDataPtr, Race race, Gender gender) + { + var idx = 0; + var ptr = (CharacterArmor*)equipDataPtr; + for (var end = ptr + 10; ptr < end; ++ptr) + { + var (_, newArmor) = + _items.RestrictedGear.ResolveRestricted(*ptr, EquipSlotExtensions.EqdpSlots[idx++], race, gender); + *ptr = newArmor; + } + } + + private void Subscribe() + { + _penumbra.CreatingCharacterBase += OnCreatingCharacterBase; + _slotUpdating.Subscribe(OnSlotUpdating, SlotUpdating.Priority.StateListener); + _weaponLoading.Subscribe(OnWeaponLoading, WeaponLoading.Priority.StateListener); + } + + private void Unsubscribe() + { + _penumbra.CreatingCharacterBase -= OnCreatingCharacterBase; + _slotUpdating.Unsubscribe(OnSlotUpdating); + _weaponLoading.Unsubscribe(OnWeaponLoading); + } +} diff --git a/Glamourer/State/StateManager.cs b/Glamourer/State/StateManager.cs index ad981c2..e1f76f2 100644 --- a/Glamourer/State/StateManager.cs +++ b/Glamourer/State/StateManager.cs @@ -17,7 +17,7 @@ using Penumbra.GameData.Structs; namespace Glamourer.State; -public class StateManager : IReadOnlyDictionary, IDisposable +public class StateManager : IReadOnlyDictionary { private readonly ActorService _actors; private readonly ItemManager _items; @@ -26,57 +26,20 @@ public class StateManager : IReadOnlyDictionary, ID private readonly StateChanged _event; private readonly PenumbraService _penumbra; - private readonly UpdatedSlot _updatedSlot; - + private readonly Dictionary _states = new(); + public StateManager(ActorService actors, ItemManager items, CustomizationService customizations, VisorService visor, StateChanged @event, - UpdatedSlot updatedSlot, PenumbraService penumbra) + PenumbraService penumbra) { _actors = actors; _items = items; _customizations = customizations; _visor = visor; _event = @event; - _updatedSlot = updatedSlot; _penumbra = penumbra; - _updatedSlot.Subscribe(OnSlotUpdated, UpdatedSlot.Priority.StateManager); - } - - public void Dispose() - { - _updatedSlot.Unsubscribe(OnSlotUpdated); - } - - private unsafe void OnSlotUpdated(Model model, EquipSlot slot, Ref armor, Ref returnValue) - { - var actor = _penumbra.GameObjectFromDrawObject(model); - var customize = model.GetCustomize(); - if (!actor.AsCharacter->DrawData.IsHatHidden && actor.Identifier(_actors.AwaitedService, out var identifier) && _states.TryGetValue(identifier, out var state)) - { - ref var armorState = ref state[slot, false]; - ref var stainState = ref state[slot, true]; - if (armorState != StateChanged.Source.Fixed) - { - armorState = StateChanged.Source.Game; - var current = state.Data.Item(slot); - if (current.ModelId.Value != armor.Value.Set.Value || current.Variant != armor.Value.Variant) - { - var item = _items.Identify(slot, armor.Value.Set, armor.Value.Variant); - state.Data.SetItem(slot, item); - } - } - - if (stainState != StateChanged.Source.Fixed) - { - stainState = StateChanged.Source.Game; - state.Data.SetStain(slot, armor.Value.Stain); - } - } - - var (replaced, replacedArmor) = _items.RestrictedGear.ResolveRestricted(armor, slot, customize.Race, customize.Gender); - if (replaced) - armor.Assign(replacedArmor); + } public bool GetOrCreate(Actor actor, [NotNullWhen(true)] out ActorState? state) @@ -90,7 +53,12 @@ public class StateManager : IReadOnlyDictionary, ID try { var designData = FromActor(actor); - _states.Add(identifier, new ActorState(identifier) { Data = designData }); + state = new ActorState(identifier) + { + ModelData = designData, + ActorData = designData + }; + _states.Add(identifier, state); return true; } catch (Exception ex) @@ -100,84 +68,68 @@ public class StateManager : IReadOnlyDictionary, ID } } - public unsafe void Update(ref DesignData data, Actor actor) + public void UpdateEquip(ActorState state, EquipSlot slot, CharacterArmor armor) + { + var current = state.ModelData.Item(slot); + if (armor.Set.Value != current.ModelId.Value || armor.Variant != current.Variant) + { + var item = _items.Identify(slot, armor.Set, armor.Variant); + state.ModelData.SetItem(slot, item); + } + + state.ModelData.SetStain(slot, armor.Stain); + } + + public void UpdateWeapon(ActorState state, EquipSlot slot, CharacterWeapon weapon) + { + var current = state.ModelData.Item(slot); + if (weapon.Set.Value != current.ModelId.Value || weapon.Variant != current.Variant || weapon.Type.Value != current.WeaponType.Value) + { + var item = _items.Identify(slot, weapon.Set, weapon.Type, (byte)weapon.Variant, + slot == EquipSlot.OffHand ? state.ModelData.Item(EquipSlot.MainHand).Type : FullEquipType.Unknown); + state.ModelData.SetItem(slot, item); + } + + state.ModelData.SetStain(slot, weapon.Stain); + } + + public unsafe void Update(ActorState state, Actor actor) { if (!actor.IsCharacter) return; - if (actor.AsCharacter->ModelCharaId != data.ModelId) + if (actor.AsCharacter->ModelCharaId != state.ModelData.ModelId) return; var model = actor.Model; - static bool EqualArmor(CharacterArmor armor, EquipItem item) - => armor.Set.Value == item.ModelId.Value && armor.Variant == item.Variant; + state.ModelData.SetHatVisible(!actor.AsCharacter->DrawData.IsHatHidden); + state.ModelData.SetIsWet(actor.AsCharacter->IsGPoseWet); + state.ModelData.SetWeaponVisible(!actor.AsCharacter->DrawData.IsWeaponHidden); - static bool EqualWeapon(CharacterWeapon weapon, EquipItem item) - => weapon.Set.Value == item.ModelId.Value && weapon.Type.Value == item.WeaponType.Value && weapon.Variant == item.Variant; - - data.SetHatVisible(!actor.AsCharacter->DrawData.IsHatHidden); - data.SetIsWet(actor.AsCharacter->IsGPoseWet); - data.SetWeaponVisible(!actor.AsCharacter->DrawData.IsWeaponHidden); - - CharacterWeapon main; - CharacterWeapon off; if (model.IsHuman) { - var head = data.IsHatVisible() ? model.GetArmor(EquipSlot.Head) : actor.GetArmor(EquipSlot.Head); - data.SetStain(EquipSlot.Head, head.Stain); - if (!EqualArmor(head, data.Item(EquipSlot.Head))) - { - var headItem = _items.Identify(EquipSlot.Head, head.Set, head.Variant); - data.SetItem(EquipSlot.Head, headItem); - } + var head = state.ModelData.IsHatVisible() ? model.GetArmor(EquipSlot.Head) : actor.GetArmor(EquipSlot.Head); + UpdateEquip(state, EquipSlot.Head, head); foreach (var slot in EquipSlotExtensions.EqdpSlots.Skip(1)) - { - var armor = model.GetArmor(slot); - data.SetStain(slot, armor.Stain); - if (EqualArmor(armor, data.Item(slot))) - continue; + UpdateEquip(state, slot, model.GetArmor(slot)); - var item = _items.Identify(slot, armor.Set, armor.Variant); - data.SetItem(slot, item); - } - - data.Customize = model.GetCustomize(); - (_, _, main, off) = model.GetWeapons(actor); - data.SetVisor(_visor.GetVisorState(model)); + state.ModelData.Customize = model.GetCustomize(); + var (_, _, main, off) = model.GetWeapons(actor); + UpdateWeapon(state, EquipSlot.MainHand, main); + UpdateWeapon(state, EquipSlot.OffHand, off); + state.ModelData.SetVisor(_visor.GetVisorState(model)); } else { foreach (var slot in EquipSlotExtensions.EqdpSlots) - { - var armor = actor.GetArmor(slot); - data.SetStain(slot, armor.Stain); - if (EqualArmor(armor, data.Item(slot))) - continue; + UpdateEquip(state, slot, actor.GetArmor(slot)); - var item = _items.Identify(slot, armor.Set, armor.Variant); - data.SetItem(slot, item); - } - - data.Customize = actor.GetCustomize(); - main = actor.GetMainhand(); - off = actor.GetOffhand(); - data.SetVisor(actor.AsCharacter->DrawData.IsVisorToggled); - } - - data.SetStain(EquipSlot.MainHand, main.Stain); - data.SetStain(EquipSlot.OffHand, off.Stain); - if (!EqualWeapon(main, data.Item(EquipSlot.MainHand))) - { - var mainItem = _items.Identify(EquipSlot.MainHand, main.Set, main.Type, (byte)main.Variant); - data.SetItem(EquipSlot.MainHand, mainItem); - } - - if (!EqualWeapon(off, data.Item(EquipSlot.OffHand))) - { - var offItem = _items.Identify(EquipSlot.OffHand, off.Set, off.Type, (byte)off.Variant, data.Item(EquipSlot.MainHand).Type); - data.SetItem(EquipSlot.OffHand, offItem); + state.ModelData.Customize = actor.GetCustomize(); + UpdateWeapon(state, EquipSlot.MainHand, actor.GetMainhand()); + UpdateWeapon(state, EquipSlot.OffHand, actor.GetOffhand()); + state.ModelData.SetVisor(actor.AsCharacter->DrawData.IsVisorToggled); } } @@ -281,11 +233,11 @@ public class StateManager : IReadOnlyDictionary, ID if (s is StateChanged.Source.Fixed && source is StateChanged.Source.Game) return; - var oldValue = state.Data.Customize[idx]; + var oldValue = state.ModelData.Customize[idx]; if (oldValue == value && !force) return; - state.Data.Customize[idx] = value; + state.ModelData.Customize[idx] = value; Glamourer.Log.Excessive( $"Changed customize {idx.ToDefaultName()} for {state.Identifier} ({string.Join(", ", data.Objects.Select(o => $"0x{o.Address}"))}) from {oldValue.Value} to {value.Value}.");