From 803fd1b247bd238e3d4eafd3cde9f3a65ba26a6a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 21 Jun 2023 18:09:50 +0200 Subject: [PATCH] . --- Glamourer.GameData/Structs/EquipFlag.cs | 18 ++ Glamourer/Configuration.cs | 1 + Glamourer/Designs/Design.cs | 11 +- Glamourer/Designs/DesignData.cs | 18 ++ Glamourer/Gui/Tabs/DebugTab.cs | 80 +++++++- Glamourer/Gui/Tabs/DesignTab/DesignPanel.cs | 39 +++- Glamourer/Gui/Tabs/SettingsTab.cs | 13 +- .../Interop/Penumbra/PenumbraAutoRedraw.cs | 67 +++++++ Glamourer/Interop/Penumbra/PenumbraService.cs | 39 +++- Glamourer/Interop/Structs/ActorData.cs | 16 +- Glamourer/Interop/UpdateSlotService.cs | 19 +- Glamourer/Services/ServiceManager.cs | 4 +- Glamourer/State/ActorState.cs | 12 +- Glamourer/State/StateEditor.cs | 11 +- Glamourer/State/StateListener.cs | 182 ++++++++++++------ Glamourer/State/StateManager.cs | 123 +++++++++--- 16 files changed, 521 insertions(+), 132 deletions(-) create mode 100644 Glamourer/Interop/Penumbra/PenumbraAutoRedraw.cs diff --git a/Glamourer.GameData/Structs/EquipFlag.cs b/Glamourer.GameData/Structs/EquipFlag.cs index 81590f0..eaacbac 100644 --- a/Glamourer.GameData/Structs/EquipFlag.cs +++ b/Glamourer.GameData/Structs/EquipFlag.cs @@ -72,4 +72,22 @@ public static class EquipFlagExtensions EquipSlot.LFinger => EquipFlag.LFingerStain, _ => 0, }; + + public static EquipFlag ToBothFlags(this EquipSlot slot) + => slot switch + { + EquipSlot.MainHand => EquipFlag.Mainhand | EquipFlag.MainhandStain, + EquipSlot.OffHand => EquipFlag.Offhand | EquipFlag.OffhandStain, + EquipSlot.Head => EquipFlag.Head | EquipFlag.HeadStain, + EquipSlot.Body => EquipFlag.Body | EquipFlag.BodyStain, + EquipSlot.Hands => EquipFlag.Hands | EquipFlag.HandsStain, + EquipSlot.Legs => EquipFlag.Legs | EquipFlag.LegsStain, + EquipSlot.Feet => EquipFlag.Feet | EquipFlag.FeetStain, + EquipSlot.Ears => EquipFlag.Ears | EquipFlag.EarsStain, + EquipSlot.Neck => EquipFlag.Neck | EquipFlag.NeckStain, + EquipSlot.Wrists => EquipFlag.Wrist | EquipFlag.WristStain, + EquipSlot.RFinger => EquipFlag.RFinger | EquipFlag.RFingerStain, + EquipSlot.LFinger => EquipFlag.LFinger | EquipFlag.LFingerStain, + _ => 0, + }; } diff --git a/Glamourer/Configuration.cs b/Glamourer/Configuration.cs index a023a83..3d71fce 100644 --- a/Glamourer/Configuration.cs +++ b/Glamourer/Configuration.cs @@ -20,6 +20,7 @@ public class Configuration : IPluginConfiguration, ISavable public bool Enabled { get; set; } = true; public bool UseRestrictedGearProtection { get; set; } = true; public bool OpenFoldersByDefault { get; set; } = false; + public bool AutoRedrawEquipOnChanges { get; set; } = false; public MainWindow.TabType SelectedTab { get; set; } = MainWindow.TabType.Settings; public DoubleModifier DeleteDesignModifier { get; set; } = new(ModifierHotkey.Control, ModifierHotkey.Shift); diff --git a/Glamourer/Designs/Design.cs b/Glamourer/Designs/Design.cs index aacf603..ba4e162 100644 --- a/Glamourer/Designs/Design.cs +++ b/Glamourer/Designs/Design.cs @@ -37,7 +37,7 @@ public class Design : ISavable /// Unconditionally apply a design to a designdata. /// Whether a redraw is required for the changes to take effect. - public bool ApplyDesign(ref DesignData data) + public (bool, CustomizeFlag, EquipFlag) ApplyDesign(ref DesignData data) { var modelChanged = data.ModelId != DesignData.ModelId; data.ModelId = DesignData.ModelId; @@ -52,13 +52,16 @@ public class Design : ISavable customizeFlags |= index.ToFlag(); } + EquipFlag equipFlags = 0; foreach (var slot in EquipSlotExtensions.EqdpSlots.Append(EquipSlot.MainHand).Append(EquipSlot.OffHand)) { if (DoApplyEquip(slot)) - data.SetItem(slot, DesignData.Item(slot)); + if (data.SetItem(slot, DesignData.Item(slot))) + equipFlags |= slot.ToFlag(); if (DoApplyStain(slot)) - data.SetStain(slot, DesignData.Stain(slot)); + if (data.SetStain(slot, DesignData.Stain(slot))) + equipFlags |= slot.ToStainFlag(); } if (DoApplyHatVisible()) @@ -72,7 +75,7 @@ public class Design : ISavable if (DoApplyWetness()) data.SetIsWet(DesignData.IsWet()); - return modelChanged || customizeFlags.RequiresRedraw(); + return (modelChanged, customizeFlags, equipFlags); } #endregion diff --git a/Glamourer/Designs/DesignData.cs b/Glamourer/Designs/DesignData.cs index e09be27..9bd9447 100644 --- a/Glamourer/Designs/DesignData.cs +++ b/Glamourer/Designs/DesignData.cs @@ -63,6 +63,24 @@ public unsafe struct DesignData // @formatter:on }; + public readonly CharacterArmor Armor(EquipSlot slot) + { + fixed (byte* ptr = _equipmentBytes) + { + var armorPtr = (CharacterArmor*)ptr; + return armorPtr[slot.ToIndex()]; + } + } + + public readonly CharacterWeapon Weapon(EquipSlot slot) + { + fixed (byte* ptr = _equipmentBytes) + { + var armorPtr = (CharacterArmor*)ptr; + return armorPtr[slot is EquipSlot.MainHand ? 10 : 11].ToWeapon(_secondaryMainhand); + } + } + public bool SetItem(EquipSlot slot, EquipItem item) { var index = slot.ToIndex(); diff --git a/Glamourer/Gui/Tabs/DebugTab.cs b/Glamourer/Gui/Tabs/DebugTab.cs index 143e0c8..b88b1cb 100644 --- a/Glamourer/Gui/Tabs/DebugTab.cs +++ b/Glamourer/Gui/Tabs/DebugTab.cs @@ -3,11 +3,13 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Numerics; +using System.Runtime.CompilerServices; using Dalamud.Game.ClientState.Objects; using Dalamud.Interface; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using Glamourer.Customization; using Glamourer.Designs; +using Glamourer.Events; using Glamourer.Interop; using Glamourer.Interop.Penumbra; using Glamourer.Interop.Structs; @@ -854,11 +856,81 @@ public unsafe class DebugTab : ITab } } + public void DrawState(ActorData data, ActorState state) + { + using var table = ImRaii.Table("##state", 7, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit); + if (!table) + return; + + ImGuiUtil.DrawTableColumn("Name"); + ImGuiUtil.DrawTableColumn(state.Identifier.ToString()); + ImGui.TableNextColumn(); + if (ImGui.Button("Reset")) + _state.ResetState(state); + + ImGui.TableNextRow(); + + static void PrintRow(string label, T actor, T model, StateChanged.Source source) where T : notnull + { + ImGuiUtil.DrawTableColumn(label); + ImGuiUtil.DrawTableColumn(actor.ToString()!); + ImGuiUtil.DrawTableColumn(model.ToString()!); + ImGuiUtil.DrawTableColumn(source.ToString()); + } + + static string ItemString(in DesignData data, EquipSlot slot) + { + var item = data.Item(slot); + return $"{item.Name} ({item.ModelId.Value}{(item.WeaponType != 0 ? $"-{item.WeaponType.Value}" : string.Empty)}-{item.Variant})"; + } + + PrintRow("Model ID", state.BaseData.ModelId, state.ModelData.ModelId, state[ActorState.MetaFlag.ModelId]); + ImGui.TableNextRow(); + PrintRow("Wetness", state.BaseData.IsWet(), state.ModelData.IsWet(), state[ActorState.MetaFlag.Wetness]); + ImGui.TableNextRow(); + + if (state.BaseData.ModelId == 0 && state.ModelData.ModelId == 0) + { + PrintRow("Hat Visible", state.BaseData.IsHatVisible(), state.ModelData.IsHatVisible(), state[ActorState.MetaFlag.HatState]); + ImGui.TableNextRow(); + PrintRow("Visor Toggled", state.BaseData.IsVisorToggled(), state.ModelData.IsVisorToggled(), + state[ActorState.MetaFlag.VisorState]); + ImGui.TableNextRow(); + PrintRow("Weapon Visible", state.BaseData.IsWeaponVisible(), state.ModelData.IsWeaponVisible(), + state[ActorState.MetaFlag.WeaponState]); + ImGui.TableNextRow(); + foreach (var slot in EquipSlotExtensions.EqdpSlots.Prepend(EquipSlot.OffHand).Prepend(EquipSlot.MainHand)) + { + PrintRow(slot.ToName(), ItemString(state.BaseData, slot), ItemString(state.ModelData, slot), state[slot, false]); + ImGuiUtil.DrawTableColumn(state.BaseData.Stain(slot).Value.ToString()); + ImGuiUtil.DrawTableColumn(state.ModelData.Stain(slot).Value.ToString()); + ImGuiUtil.DrawTableColumn(state[slot, true].ToString()); + } + + foreach (var type in Enum.GetValues()) + { + PrintRow(type.ToDefaultName(), state.BaseData.Customize[type].Value, state.ModelData.Customize[type].Value, state[type]); + ImGui.TableNextRow(); + } + } + else + { + ImGuiUtil.DrawTableColumn(string.Join(" ", state.BaseData.GetCustomizeBytes().Select(b => b.ToString("X2")))); + ImGuiUtil.DrawTableColumn(string.Join(" ", state.ModelData.GetCustomizeBytes().Select(b => b.ToString("X2")))); + ImGui.TableNextRow(); + ImGuiUtil.DrawTableColumn(string.Join(" ", state.BaseData.GetEquipmentBytes().Select(b => b.ToString("X2")))); + ImGuiUtil.DrawTableColumn(string.Join(" ", state.ModelData.GetEquipmentBytes().Select(b => b.ToString("X2")))); + } + } + public static void DrawDesignData(in DesignData data) { if (data.ModelId == 0) { using var table = ImRaii.Table("##equip", 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit); + if (!table) + return; + foreach (var slot in EquipSlotExtensions.EqdpSlots.Prepend(EquipSlot.OffHand).Prepend(EquipSlot.MainHand)) { var item = data.Item(slot); @@ -1011,7 +1083,7 @@ public unsafe class DebugTab : ITab continue; if (_state.GetOrCreate(identifier, actors.Objects[0], out var state)) - DrawDesignData(state.ModelData); + DrawState(actors, state); else ImGui.TextUnformatted("Invalid actor."); } @@ -1026,8 +1098,10 @@ public unsafe class DebugTab : ITab foreach (var (identifier, state) in _state.Where(kvp => !_objectManager.ContainsKey(kvp.Key))) { using var t = ImRaii.TreeNode(identifier.ToString()); - if (t) - DrawDesignData(state.ModelData); + if (!t) + return; + + DrawState(ActorData.Invalid, state); } } diff --git a/Glamourer/Gui/Tabs/DesignTab/DesignPanel.cs b/Glamourer/Gui/Tabs/DesignTab/DesignPanel.cs index df624e9..c7ad35e 100644 --- a/Glamourer/Gui/Tabs/DesignTab/DesignPanel.cs +++ b/Glamourer/Gui/Tabs/DesignTab/DesignPanel.cs @@ -1,21 +1,44 @@ using System.Numerics; +using Glamourer.Customization; using Glamourer.Designs; using Glamourer.Gui.Customization; +using Glamourer.Interop; +using Glamourer.Interop.Penumbra; +using Glamourer.State; +using Glamourer.Structs; +using ImGuiNET; using OtterGui.Raii; +using Penumbra.Api.Enums; +using Penumbra.Api.Helpers; +using Penumbra.GameData.Enums; namespace Glamourer.Gui.Tabs.DesignTab; public class DesignPanel { + private readonly ObjectManager _objects; private readonly DesignFileSystemSelector _selector; private readonly DesignManager _manager; private readonly CustomizationDrawer _customizationDrawer; + private readonly StateManager _state; + private readonly PenumbraService _penumbra; + private readonly UpdateSlotService _updateSlot; + private readonly WeaponService _weaponService; + private readonly ChangeCustomizeService _changeCustomizeService; - public DesignPanel(DesignFileSystemSelector selector, CustomizationDrawer customizationDrawer, DesignManager manager) + public DesignPanel(DesignFileSystemSelector selector, CustomizationDrawer customizationDrawer, DesignManager manager, ObjectManager objects, + StateManager state, PenumbraService penumbra, ChangeCustomizeService changeCustomizeService, WeaponService weaponService, + UpdateSlotService updateSlot) { - _selector = selector; - _customizationDrawer = customizationDrawer; - _manager = manager; + _selector = selector; + _customizationDrawer = customizationDrawer; + _manager = manager; + _objects = objects; + _state = state; + _penumbra = penumbra; + _changeCustomizeService = changeCustomizeService; + _weaponService = weaponService; + _updateSlot = updateSlot; } public void Draw() @@ -28,6 +51,14 @@ public class DesignPanel if (!child) return; + if (ImGui.Button("TEST")) + { + var (id, data) = _objects.PlayerData; + + if (data.Valid && _state.GetOrCreate(id, data.Objects[0], out var state)) + _state.ApplyDesign(design, state); + } + _customizationDrawer.Draw(design.DesignData.Customize, design.WriteProtected()); } } diff --git a/Glamourer/Gui/Tabs/SettingsTab.cs b/Glamourer/Gui/Tabs/SettingsTab.cs index 904f446..e2d56a5 100644 --- a/Glamourer/Gui/Tabs/SettingsTab.cs +++ b/Glamourer/Gui/Tabs/SettingsTab.cs @@ -2,6 +2,7 @@ using System.Runtime.CompilerServices; using Dalamud.Interface; using Glamourer.Gui.Tabs.DesignTab; +using Glamourer.Interop.Penumbra; using Glamourer.State; using ImGuiNET; using OtterGui; @@ -15,12 +16,14 @@ public class SettingsTab : ITab private readonly Configuration _config; private readonly DesignFileSystemSelector _selector; private readonly StateListener _stateListener; + private readonly PenumbraAutoRedraw _autoRedraw; - public SettingsTab(Configuration config, DesignFileSystemSelector selector, StateListener stateListener) + public SettingsTab(Configuration config, DesignFileSystemSelector selector, StateListener stateListener, PenumbraAutoRedraw autoRedraw) { - _config = config; - _selector = selector; + _config = config; + _selector = selector; _stateListener = stateListener; + _autoRedraw = autoRedraw; } public ReadOnlySpan Label @@ -31,10 +34,14 @@ 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); + Checkbox("Auto-Reload Gear", + "Automatically reload equipment pieces on your own character when changing any mod options in Penumbra in their associated collection.", + _config.AutoRedrawEquipOnChanges, _autoRedraw.SetState); if (Widget.DoubleModifierSelector("Design Deletion Modifier", "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)) diff --git a/Glamourer/Interop/Penumbra/PenumbraAutoRedraw.cs b/Glamourer/Interop/Penumbra/PenumbraAutoRedraw.cs new file mode 100644 index 0000000..ce4f99e --- /dev/null +++ b/Glamourer/Interop/Penumbra/PenumbraAutoRedraw.cs @@ -0,0 +1,67 @@ +using System; +using Glamourer.State; +using Penumbra.Api.Enums; + +namespace Glamourer.Interop.Penumbra; + +public class PenumbraAutoRedraw : IDisposable +{ + private readonly Configuration _config; + private readonly PenumbraService _penumbra; + private readonly StateManager _state; + private readonly ObjectManager _objects; + private bool _enabled; + + public PenumbraAutoRedraw(PenumbraService penumbra, Configuration config, StateManager state, ObjectManager objects) + { + _penumbra = penumbra; + _config = config; + _state = state; + _objects = objects; + if (_config.AutoRedrawEquipOnChanges) + Enable(); + } + + public void SetState(bool value) + { + if (value == _config.AutoRedrawEquipOnChanges) + return; + + _config.AutoRedrawEquipOnChanges = value; + _config.Save(); + if (value) + Enable(); + else + Disable(); + } + + public void Enable() + { + if (_enabled) + return; + + _penumbra.ModSettingChanged += OnModSettingChange; + _enabled = true; + } + + public void Disable() + { + if (!_enabled) + return; + + _penumbra.ModSettingChanged -= OnModSettingChange; + _enabled = false; + } + + public void Dispose() + { + Disable(); + } + + private void OnModSettingChange(ModSettingChange type, string name, string mod, bool inherited) + { + var playerName = _penumbra.GetCurrentPlayerCollection(); + if (playerName == name) + _state.ReapplyState(_objects.Player); + } +} diff --git a/Glamourer/Interop/Penumbra/PenumbraService.cs b/Glamourer/Interop/Penumbra/PenumbraService.cs index b092233..124363c 100644 --- a/Glamourer/Interop/Penumbra/PenumbraService.cs +++ b/Glamourer/Interop/Penumbra/PenumbraService.cs @@ -13,14 +13,16 @@ public unsafe class PenumbraService : IDisposable public const int RequiredPenumbraBreakingVersion = 4; public const int RequiredPenumbraFeatureVersion = 15; - private readonly DalamudPluginInterface _pluginInterface; - private readonly EventSubscriber _tooltipSubscriber; - private readonly EventSubscriber _clickSubscriber; - private readonly EventSubscriber _creatingCharacterBase; - private readonly EventSubscriber _createdCharacterBase; - private ActionSubscriber _redrawSubscriber; - private FuncSubscriber _drawObjectInfo; - private FuncSubscriber _cutsceneParent; + private readonly DalamudPluginInterface _pluginInterface; + private readonly EventSubscriber _tooltipSubscriber; + private readonly EventSubscriber _clickSubscriber; + private readonly EventSubscriber _creatingCharacterBase; + private readonly EventSubscriber _createdCharacterBase; + private readonly EventSubscriber _modSettingChanged; + private ActionSubscriber _redrawSubscriber; + private FuncSubscriber _drawObjectInfo; + private FuncSubscriber _cutsceneParent; + private FuncSubscriber _objectCollection; private readonly EventSubscriber _initializedEvent; private readonly EventSubscriber _disposedEvent; @@ -35,6 +37,7 @@ public unsafe class PenumbraService : IDisposable _clickSubscriber = Ipc.ChangedItemClick.Subscriber(pi); _createdCharacterBase = Ipc.CreatedCharacterBase.Subscriber(pi); _creatingCharacterBase = Ipc.CreatingCharacterBase.Subscriber(pi); + _modSettingChanged = Ipc.ModSettingChanged.Subscriber(pi); Reattach(); } @@ -63,6 +66,22 @@ public unsafe class PenumbraService : IDisposable remove => _createdCharacterBase.Event -= value; } + public event Action ModSettingChanged + { + add => _modSettingChanged.Event += value; + remove => _modSettingChanged.Event -= value; + } + + /// Obtain the name of the collection currently assigned to the player. + public string GetCurrentPlayerCollection() + { + if (!Available) + return string.Empty; + + var (valid, _, name) = _objectCollection.Invoke(0); + return valid ? name : string.Empty; + } + /// Obtain the game object corresponding to a draw object. public Actor GameObjectFromDrawObject(Model drawObject) => Available ? _drawObjectInfo.Invoke(drawObject.Address).Item1 : Actor.Null; @@ -103,9 +122,11 @@ public unsafe class PenumbraService : IDisposable _clickSubscriber.Enable(); _creatingCharacterBase.Enable(); _createdCharacterBase.Enable(); + _modSettingChanged.Enable(); _drawObjectInfo = Ipc.GetDrawObjectInfo.Subscriber(_pluginInterface); _cutsceneParent = Ipc.GetCutsceneParentIndex.Subscriber(_pluginInterface); _redrawSubscriber = Ipc.RedrawObjectByIndex.Subscriber(_pluginInterface); + _objectCollection = Ipc.GetCollectionForObject.Subscriber(_pluginInterface); Available = true; Glamourer.Log.Debug("Glamourer attached to Penumbra."); } @@ -122,6 +143,7 @@ public unsafe class PenumbraService : IDisposable _clickSubscriber.Disable(); _creatingCharacterBase.Disable(); _createdCharacterBase.Disable(); + _modSettingChanged.Disable(); if (Available) { Available = false; @@ -138,5 +160,6 @@ public unsafe class PenumbraService : IDisposable _createdCharacterBase.Dispose(); _initializedEvent.Dispose(); _disposedEvent.Dispose(); + _modSettingChanged.Dispose(); } } diff --git a/Glamourer/Interop/Structs/ActorData.cs b/Glamourer/Interop/Structs/ActorData.cs index e7b4194..933cf0c 100644 --- a/Glamourer/Interop/Structs/ActorData.cs +++ b/Glamourer/Interop/Structs/ActorData.cs @@ -1,4 +1,6 @@ using System.Collections.Generic; +using System.Linq; +using OtterGui.Log; namespace Glamourer.Interop.Structs; @@ -8,7 +10,7 @@ namespace Glamourer.Interop.Structs; public readonly struct ActorData { public readonly List Objects; - public readonly string Label; + public readonly string Label; public bool Valid => Objects.Count > 0; @@ -16,7 +18,7 @@ public readonly struct ActorData public ActorData(Actor actor, string label) { Objects = new List { actor }; - Label = label; + Label = label; } public static readonly ActorData Invalid = new(false); @@ -24,6 +26,14 @@ public readonly struct ActorData private ActorData(bool _) { Objects = new List(0); - Label = string.Empty; + Label = string.Empty; + } + + public LazyString ToLazyString(string invalid) + { + var objects = Objects; + return Valid + ? new LazyString(() => string.Join(", ", objects.Select(o => o.ToString()))) + : new LazyString(() => invalid); } } diff --git a/Glamourer/Interop/UpdateSlotService.cs b/Glamourer/Interop/UpdateSlotService.cs index f515944..e13b4ac 100644 --- a/Glamourer/Interop/UpdateSlotService.cs +++ b/Glamourer/Interop/UpdateSlotService.cs @@ -5,6 +5,7 @@ using Glamourer.Events; using Glamourer.Interop.Structs; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; +using static System.Runtime.InteropServices.JavaScript.JSType; namespace Glamourer.Interop; @@ -31,24 +32,18 @@ public unsafe class UpdateSlotService : IDisposable { if (!drawObject.IsCharacterBase) return; + FlagSlotForUpdateInterop(drawObject, slot, data); } - public void UpdateArmor(Model drawObject, EquipSlot slot, CharacterArmor data) - { - if (!drawObject.IsCharacterBase) - return; + public void UpdateArmor(Model drawObject, EquipSlot slot, CharacterArmor armor, StainId stain) + => UpdateSlot(drawObject, slot, armor.With(stain)); - FlagSlotForUpdateInterop(drawObject, slot, data.With(drawObject.GetArmor(slot).Stain)); - } + public void UpdateArmor(Model drawObject, EquipSlot slot, CharacterArmor armor) + => UpdateArmor(drawObject, slot, armor, drawObject.GetArmor(slot).Stain); public void UpdateStain(Model drawObject, EquipSlot slot, StainId stain) - { - if (!drawObject.IsHuman) - return; - - FlagSlotForUpdateInterop(drawObject, slot, drawObject.GetArmor(slot).With(stain)); - } + => UpdateArmor(drawObject, slot, drawObject.GetArmor(slot), stain); private ulong FlagSlotForUpdateDetour(nint drawObject, uint slotIdx, CharacterArmor* data) { diff --git a/Glamourer/Services/ServiceManager.cs b/Glamourer/Services/ServiceManager.cs index 687ccd9..14dbc3e 100644 --- a/Glamourer/Services/ServiceManager.cs +++ b/Glamourer/Services/ServiceManager.cs @@ -69,7 +69,8 @@ public static class ServiceManager .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton(); + .AddSingleton() + .AddSingleton(); private static IServiceCollection AddDesigns(this IServiceCollection services) => services.AddSingleton() @@ -77,6 +78,7 @@ public static class ServiceManager private static IServiceCollection AddState(this IServiceCollection services) => services.AddSingleton() + .AddSingleton() .AddSingleton(); private static IServiceCollection AddUi(this IServiceCollection services) diff --git a/Glamourer/State/ActorState.cs b/Glamourer/State/ActorState.cs index 2d963f4..1077f84 100644 --- a/Glamourer/State/ActorState.cs +++ b/Glamourer/State/ActorState.cs @@ -17,14 +17,20 @@ public class ActorState HatState, VisorState, WeaponState, + ModelId, } public ActorIdentifier Identifier { get; internal init; } - public DesignData ActorData; - public DesignData ModelData; + /// This should always represent the unmodified state of the draw object. + public DesignData BaseData; + + /// This should be the desired state of the draw object. + public DesignData ModelData; + + /// This contains whether a change to the base data was made by the game, the user via manual input or through automatic application. private readonly StateChanged.Source[] _sources = Enumerable - .Repeat(StateChanged.Source.Game, EquipFlagExtensions.NumEquipFlags + CustomizationExtensions.NumIndices + 4).ToArray(); + .Repeat(StateChanged.Source.Game, EquipFlagExtensions.NumEquipFlags + CustomizationExtensions.NumIndices + 5).ToArray(); internal ActorState(ActorIdentifier identifier) => Identifier = identifier; diff --git a/Glamourer/State/StateEditor.cs b/Glamourer/State/StateEditor.cs index 9ce9516..8de89aa 100644 --- a/Glamourer/State/StateEditor.cs +++ b/Glamourer/State/StateEditor.cs @@ -26,6 +26,7 @@ public class StateEditor _items = items; } + public void ChangeCustomize(ActorData data, Customize customize) { foreach (var actor in data.Objects) @@ -43,19 +44,15 @@ public class StateEditor } } - public void ChangeArmor(ActorData data, EquipSlot slot, EquipItem item) + public void ChangeArmor(ActorState state, ActorData data, EquipSlot slot) { - var idx = slot.ToIndex(); - if (idx >= 10) - return; - - var armor = item.Armor(); + var armor = state.ModelData.Armor(slot); foreach (var actor in data.Objects.Where(a => a.IsCharacter)) { var mdl = actor.Model; var customize = mdl.IsHuman ? mdl.GetCustomize() : actor.GetCustomize(); var (_, resolvedItem) = _items.RestrictedGear.ResolveRestricted(armor, slot, customize.Race, customize.Gender); - _updateSlot.UpdateArmor(actor.Model, slot, resolvedItem); + _updateSlot.UpdateSlot(actor.Model, slot, resolvedItem); } } diff --git a/Glamourer/State/StateListener.cs b/Glamourer/State/StateListener.cs index e146df2..012dde6 100644 --- a/Glamourer/State/StateListener.cs +++ b/Glamourer/State/StateListener.cs @@ -61,42 +61,46 @@ public class StateListener : IDisposable Unsubscribe(); } + private enum UpdateState + { + NoChange, + Transformed, + Change, + } + 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; - + var modelId = *(uint*)modelPtr; 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) - { + switch (UpdateBaseData(actor, state, modelId, customizePtr, equipDataPtr)) + { + case UpdateState.Change: break; + case UpdateState.Transformed: break; + case UpdateState.NoChange: + UpdateBaseData(actor, state, customize); + break; + } + + if (_config.UseRestrictedGearProtection && modelId == 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); + if (_config.UseRestrictedGearProtection) + (_, armor.Value) = _items.RestrictedGear.ResolveRestricted(armor, slot, customize.Race, customize.Gender); } private void OnWeaponLoading(Actor actor, EquipSlot slot, Ref weapon) @@ -111,7 +115,7 @@ public class StateListener : IDisposable || actorWeapon.Type.Value != stateItem.WeaponType || actorWeapon.Variant != stateItem.Variant) { - var oldActorItem = state.ActorData.Item(slot); + var oldActorItem = state.BaseData.Item(slot); if (oldActorItem.ModelId.Value == actorWeapon.Set.Value && oldActorItem.WeaponType.Value == actorWeapon.Type.Value && oldActorItem.Variant == actorWeapon.Variant) @@ -123,8 +127,8 @@ public class StateListener : IDisposable 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); + slot == EquipSlot.OffHand ? state.BaseData.Item(EquipSlot.MainHand).Type : FullEquipType.Unknown); + state.BaseData.SetItem(slot, identified); if (state[slot, false] is not StateChanged.Source.Fixed) { state.ModelData.SetItem(slot, identified); @@ -142,7 +146,7 @@ public class StateListener : IDisposable var stateStain = state.ModelData.Stain(slot); if (actorWeapon.Stain.Value != stateStain.Value) { - var oldActorStain = state.ActorData.Stain(slot); + var oldActorStain = state.BaseData.Stain(slot); if (state[slot, true] is not StateChanged.Source.Fixed) { state.ModelData.SetStain(slot, actorWeapon.Stain); @@ -159,7 +163,7 @@ public class StateListener : IDisposable private void ApplyCustomize(Actor actor, ActorState state, ref Customize customize) { var actorCustomize = actor.GetCustomize(); - ref var oldActorCustomize = ref state.ActorData.Customize; + ref var oldActorCustomize = ref state.BaseData.Customize; ref var stateCustomize = ref state.ModelData.Customize; foreach (var idx in Enum.GetValues()) { @@ -201,57 +205,34 @@ public class StateListener : IDisposable private void ApplyEquipmentPiece(Actor actor, ActorState state, EquipSlot slot, ref CharacterArmor armor) { - var actorArmor = actor.GetArmor(slot); - if (armor.Value != actorArmor.Value) + var changeState = UpdateBaseData(actor, state, slot, armor); + if (changeState is UpdateState.Transformed) return; - var stateArmor = state.ModelData.Item(slot); - if (armor.Set.Value != stateArmor.ModelId.Value || armor.Variant != stateArmor.Variant) + if (changeState is UpdateState.NoChange) { - 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; - } - } + armor = state.ModelData.Armor(slot); } - - var stateStain = state.ModelData.Stain(slot); - if (armor.Stain.Value != stateStain.Value) + else { - var oldActorStain = state.ActorData.Stain(slot); - if (oldActorStain.Value == actorArmor.Stain.Value) + var modelArmor = state.ModelData.Armor(slot); + if (armor.Value == modelArmor.Value) + return; + + if (state[slot, false] is StateChanged.Source.Fixed) { - armor.Stain = stateStain; + armor.Set = modelArmor.Set; + armor.Variant = modelArmor.Variant; } 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; - } + _manager.ChangeEquip(state, slot, state.BaseData.Item(slot), StateChanged.Source.Game); } + + if (state[slot, true] is StateChanged.Source.Fixed) + armor.Stain = modelArmor.Stain; + else + _manager.ChangeStain(state, slot, state.BaseData.Stain(slot), StateChanged.Source.Game); } } @@ -280,4 +261,81 @@ public class StateListener : IDisposable _slotUpdating.Unsubscribe(OnSlotUpdating); _weaponLoading.Unsubscribe(OnWeaponLoading); } + + private UpdateState UpdateBaseData(Actor actor, ActorState state, EquipSlot slot, CharacterArmor armor) + { + var actorArmor = actor.GetArmor(slot); + // The actor armor does not correspond to the model armor, thus the actor is transformed. + if (actorArmor.Value != armor.Value) + return UpdateState.Transformed; + + // TODO: Hat State. + + var baseData = state.BaseData.Armor(slot); + var change = UpdateState.NoChange; + if (baseData.Stain != armor.Stain) + { + state.BaseData.SetStain(slot, armor.Stain); + change = UpdateState.Change; + } + + if (baseData.Set.Value != armor.Set.Value || baseData.Variant != armor.Variant) + { + var item = _items.Identify(slot, armor.Set, armor.Variant); + state.BaseData.SetItem(slot, item); + change = UpdateState.Change; + } + + return change; + } + + private UpdateState UpdateBaseData(Actor actor, ActorState state, EquipSlot slot, CharacterWeapon weapon) + { + var baseData = state.BaseData.Weapon(slot); + var change = UpdateState.NoChange; + + if (baseData.Stain != weapon.Stain) + { + state.BaseData.SetStain(slot, weapon.Stain); + change = UpdateState.Change; + } + + if (baseData.Set.Value != weapon.Set.Value || baseData.Type.Value != weapon.Type.Value || baseData.Variant != weapon.Variant) + { + var item = _items.Identify(slot, weapon.Set, weapon.Type, (byte)weapon.Variant, + slot is EquipSlot.OffHand ? state.BaseData.Item(EquipSlot.MainHand).Type : FullEquipType.Unknown); + state.BaseData.SetItem(slot, item); + change = UpdateState.Change; + } + + return change; + } + + private unsafe UpdateState UpdateBaseData(Actor actor, ActorState state, uint modelId, nint customizeData, nint equipData) + { + if (modelId != (uint)actor.AsCharacter->CharacterData.ModelCharaId) + return UpdateState.Transformed; + + if (modelId == state.BaseData.ModelId) + return UpdateState.NoChange; + + if (modelId == 0) + state.BaseData.LoadNonHuman(modelId, *(Customize*)customizeData, (byte*)equipData); + else + state.BaseData = _manager.FromActor(actor); + + return UpdateState.Change; + } + + private UpdateState UpdateBaseData(Actor actor, ActorState state, Customize customize) + { + if (!actor.GetCustomize().Equals(customize)) + return UpdateState.Transformed; + + if (state.BaseData.Customize.Equals(customize)) + return UpdateState.NoChange; + + state.BaseData.Customize.Load(customize); + return UpdateState.Change; + } } diff --git a/Glamourer/State/StateManager.cs b/Glamourer/State/StateManager.cs index c20b47c..d3bb9f6 100644 --- a/Glamourer/State/StateManager.cs +++ b/Glamourer/State/StateManager.cs @@ -3,6 +3,7 @@ using System.Collections; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Security.Cryptography; using Glamourer.Customization; using Glamourer.Designs; using Glamourer.Events; @@ -11,6 +12,7 @@ using Glamourer.Interop.Penumbra; using Glamourer.Interop.Structs; using Glamourer.Services; using Glamourer.Structs; +using OtterGui.Log; using Penumbra.GameData.Actors; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; @@ -24,13 +26,15 @@ public class StateManager : IReadOnlyDictionary private readonly CustomizationService _customizations; private readonly VisorService _visor; private readonly StateChanged _event; + private readonly ObjectManager _objects; + private readonly StateEditor _editor; private readonly PenumbraService _penumbra; - + private readonly Dictionary _states = new(); public StateManager(ActorService actors, ItemManager items, CustomizationService customizations, VisorService visor, StateChanged @event, - PenumbraService penumbra) + PenumbraService penumbra, ObjectManager objects, StateEditor editor) { _actors = actors; _items = items; @@ -38,7 +42,8 @@ public class StateManager : IReadOnlyDictionary _visor = visor; _event = @event; _penumbra = penumbra; - + _objects = objects; + _editor = editor; } public bool GetOrCreate(Actor actor, [NotNullWhen(true)] out ActorState? state) @@ -55,7 +60,7 @@ public class StateManager : IReadOnlyDictionary state = new ActorState(identifier) { ModelData = designData, - ActorData = designData + BaseData = designData, }; _states.Add(identifier, state); return true; @@ -115,7 +120,7 @@ public class StateManager : IReadOnlyDictionary UpdateEquip(state, slot, model.GetArmor(slot)); state.ModelData.Customize = model.GetCustomize(); - var (_, _, main, off) = model.GetWeapons(actor); + var (_, _, main, off) = model.GetWeapons(actor); UpdateWeapon(state, EquipSlot.MainHand, main); UpdateWeapon(state, EquipSlot.OffHand, off); state.ModelData.SetVisor(_visor.GetVisorState(model)); @@ -165,9 +170,9 @@ public class StateManager : IReadOnlyDictionary return ret; } - if (actor.AsCharacter->ModelCharaId != 0) + if (actor.AsCharacter->CharacterData.ModelCharaId != 0) { - ret.LoadNonHuman((uint)actor.AsCharacter->ModelCharaId, *(Customize*)&actor.AsCharacter->DrawData.CustomizeData, + ret.LoadNonHuman((uint)actor.AsCharacter->CharacterData.ModelCharaId, *(Customize*)&actor.AsCharacter->DrawData.CustomizeData, (byte*)&actor.AsCharacter->DrawData.Head); return ret; } @@ -242,6 +247,94 @@ public class StateManager : IReadOnlyDictionary $"Changed customize {idx.ToDefaultName()} for {state.Identifier} ({string.Join(", ", data.Objects.Select(o => $"0x{o.Address}"))}) from {oldValue.Value} to {value.Value}."); _event.Invoke(StateChanged.Type.Customize, source, state, data, (oldValue, value, idx)); } + + public void ApplyDesign(Design design, ActorState state) + { + foreach (var slot in EquipSlotExtensions.EqdpSlots) + { + switch (design.DoApplyEquip(slot), design.DoApplyStain(slot)) + { + case (false, false): continue; + case (true, false): + ChangeEquip(state, slot, design.DesignData.Item(slot), StateChanged.Source.Manual); + break; + case (false, true): + ChangeStain(state, slot, design.DesignData.Stain(slot), StateChanged.Source.Manual); + break; + case (true, true): + ChangeEquip(state, slot, design.DesignData.Item(slot), design.DesignData.Stain(slot), StateChanged.Source.Manual); + break; + } + } + } + + public void ResetState(ActorState state) + { + var objects = _objects.TryGetValue(state.Identifier, out var d) ? d : ActorData.Invalid; + foreach (var slot in EquipSlotExtensions.EqdpSlots) + { + ChangeEquip(state, slot, state.BaseData.Item(slot), state.BaseData.Stain(slot), StateChanged.Source.Game); + _editor.ChangeArmor(state, objects, slot); + } + } + + public void ReapplyState(Actor actor) + { + if (!GetOrCreate(actor, out var state)) + return; + + _objects.Update(); + var objects = _objects.TryGetValue(state.Identifier, out var d) ? d : ActorData.Invalid; + foreach (var slot in EquipSlotExtensions.EqdpSlots) + _editor.ChangeArmor(state, objects, slot); + } + + public void ChangeEquip(ActorState state, EquipSlot slot, EquipItem item, StateChanged.Source source) + { + var old = state.ModelData.Item(slot); + state.ModelData.SetItem(slot, item); + state[slot, false] = source; + _objects.Update(); + var objects = _objects.TryGetValue(state.Identifier, out var d) ? d : ActorData.Invalid; + if (source is StateChanged.Source.Manual) + _editor.ChangeArmor(state, objects, slot); + Glamourer.Log.Verbose( + $"Set {slot.ToName()} equipment piece in state {state.Identifier} from {old.Name} ({old.Id}) to {item.Name} ({item.Id}). [Affecting {objects.ToLazyString("nothing")}.]"); + _event.Invoke(StateChanged.Type.Equip, source, state, objects, (old, item, slot)); + } + + public void ChangeEquip(ActorState state, EquipSlot slot, EquipItem item, StainId stain, StateChanged.Source source) + { + var old = state.ModelData.Item(slot); + var oldStain = state.ModelData.Stain(slot); + state.ModelData.SetItem(slot, item); + state.ModelData.SetStain(slot, stain); + state[slot, false] = source; + state[slot, true] = source; + _objects.Update(); + var objects = _objects.TryGetValue(state.Identifier, out var d) ? d : ActorData.Invalid; + if (source is StateChanged.Source.Manual) + _editor.ChangeArmor(state, objects, slot); + Glamourer.Log.Verbose( + $"Set {slot.ToName()} equipment piece in state {state.Identifier} from {old.Name} ({old.Id}) to {item.Name} ({item.Id}) and its stain from {oldStain.Value} to {stain.Value}. [Affecting {objects.ToLazyString("nothing")}.]"); + _event.Invoke(StateChanged.Type.Equip, source, state, objects, (old, item, slot)); + _event.Invoke(StateChanged.Type.Stain, source, state, objects, (oldStain, stain, slot)); + } + + public void ChangeStain(ActorState state, EquipSlot slot, StainId stain, StateChanged.Source source) + { + var old = state.ModelData.Stain(slot); + state.ModelData.SetStain(slot, stain); + state[slot, true] = source; + _objects.Update(); + var objects = _objects.TryGetValue(state.Identifier, out var d) ? d : ActorData.Invalid; + if (source is StateChanged.Source.Manual) + _editor.ChangeArmor(state, objects, slot); + Glamourer.Log.Verbose( + $"Set {slot.ToName()} stain in state {state.Identifier} from {old.Value} to {stain.Value}. [Affecting {objects.ToLazyString("nothing")}.]"); + _event.Invoke(StateChanged.Type.Stain, source, state, objects, (old, stain, slot)); + } + // ///// Change whether to apply a specific customize value. //public void ChangeApplyCustomize(Design design, CustomizeIndex idx, bool value) @@ -255,21 +348,7 @@ public class StateManager : IReadOnlyDictionary // _event.Invoke(DesignChanged.Type.ApplyCustomize, design, idx); //} // - ///// Change a non-weapon equipment piece. - //public void ChangeEquip(Design design, EquipSlot slot, EquipItem item) - //{ - // if (_items.ValidateItem(slot, item.Id, out item).Length > 0) - // return; - // - // var old = design.DesignData.Item(slot); - // if (!design.DesignData.SetItem(slot, item)) - // return; - // - // Glamourer.Log.Debug( - // $"Set {slot.ToName()} equipment piece in design {design.Identifier} from {old.Name} ({old.Id}) to {item.Name} ({item.Id})."); - // _saveService.QueueSave(design); - // _event.Invoke(DesignChanged.Type.Equip, design, (old, item, slot)); - //} + // ///// Change a weapon. //public void ChangeWeapon(Design design, EquipSlot slot, EquipItem item)