From e57538561fe623ffb743dd869a4557bea8eafb01 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 28 Jun 2023 01:39:53 +0200 Subject: [PATCH] . --- .../Customization/CustomizationOptions.cs | 25 +- Glamourer.GameData/Customization/Customize.cs | 4 +- .../Customization/CustomizeFlag.cs | 2 +- Glamourer/Automation/AutoDesign.cs | 78 +++ Glamourer/Automation/AutoDesignApplier.cs | 214 ++++++ Glamourer/Automation/AutoDesignManager.cs | 422 ++++++++++++ Glamourer/Automation/AutoDesignSet.cs | 40 ++ Glamourer/Automation/FixedDesignMigrator.cs | 111 +++ Glamourer/Configuration.cs | 5 +- Glamourer/Designs/Design.cs | 6 +- Glamourer/Designs/DesignManager.cs | 4 +- Glamourer/Events/AutomationChanged.cs | 66 ++ Glamourer/Glamourer.cs | 1 + .../CustomizationDrawer.GenderRace.cs | 10 +- .../Gui/Customization/CustomizationDrawer.cs | 5 +- Glamourer/Gui/Equipment/EquipmentDrawer.cs | 163 +++++ Glamourer/Gui/Equipment/ItemCombo.cs | 103 +++ Glamourer/Gui/Equipment/WeaponCombo.cs | 89 +++ Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs | 78 +-- Glamourer/Gui/Tabs/DebugTab.cs | 125 +++- Glamourer/Gui/Tabs/DesignTab/DesignPanel.cs | 57 +- Glamourer/Gui/Tabs/SettingsTab.cs | 37 +- Glamourer/Interop/JobService.cs | 49 ++ Glamourer/Interop/Structs/Model.cs | 2 +- Glamourer/Interop/WeaponService.cs | 13 +- Glamourer/Services/ConfigMigrationService.cs | 12 +- Glamourer/Services/CustomizationService.cs | 74 +- Glamourer/Services/FilenameService.cs | 2 + Glamourer/Services/PhrasingService.cs | 54 ++ Glamourer/Services/ServiceManager.cs | 13 +- Glamourer/State/ActorState.cs | 7 +- Glamourer/State/StateEditor.cs | 82 ++- Glamourer/State/StateListener.cs | 544 ++++++++------- Glamourer/State/StateManager.cs | 651 +++++++++--------- 34 files changed, 2428 insertions(+), 720 deletions(-) create mode 100644 Glamourer/Automation/AutoDesign.cs create mode 100644 Glamourer/Automation/AutoDesignApplier.cs create mode 100644 Glamourer/Automation/AutoDesignManager.cs create mode 100644 Glamourer/Automation/AutoDesignSet.cs create mode 100644 Glamourer/Automation/FixedDesignMigrator.cs create mode 100644 Glamourer/Events/AutomationChanged.cs create mode 100644 Glamourer/Gui/Equipment/EquipmentDrawer.cs create mode 100644 Glamourer/Gui/Equipment/ItemCombo.cs create mode 100644 Glamourer/Gui/Equipment/WeaponCombo.cs create mode 100644 Glamourer/Interop/JobService.cs create mode 100644 Glamourer/Services/PhrasingService.cs diff --git a/Glamourer.GameData/Customization/CustomizationOptions.cs b/Glamourer.GameData/Customization/CustomizationOptions.cs index 8f48c3a..98ab426 100644 --- a/Glamourer.GameData/Customization/CustomizationOptions.cs +++ b/Glamourer.GameData/Customization/CustomizationOptions.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Reflection; using Dalamud; using Dalamud.Data; +using Dalamud.Logging; using Dalamud.Plugin; using Dalamud.Utility; using Lumina.Excel; @@ -68,7 +69,7 @@ public partial class CustomizationOptions { var tmp = new TemporaryData(gameData, this); _icons = new IconStorage(pi, gameData, _customizationSets.Length * 50); - _valid = tmp.Valid; + _valid = tmp.Valid; SetNames(gameData, tmp); foreach (var race in Clans) { @@ -415,7 +416,7 @@ public partial class CustomizationOptions // Obtain available hairstyles via reflection from the Hair sheet for the given subrace and gender. private CustomizeData[] GetHairStyles(SubRace race, Gender gender) { - var row = _hairSheet.GetRow(((uint)race - 1) * 2 - 1 + (uint)gender)!; + var row = _hairSheet.GetRow(((uint)race - 1) * 2 - 1 + (uint)gender)!; // Unknown30 is the number of available hairstyles. var hairList = new List(row.Unknown30); // Hairstyles can be found starting at Unknown66. @@ -435,7 +436,8 @@ public partial class CustomizationOptions } else if (_options._icons.IconExists(hairRow.Icon)) { - hairList.Add(new CustomizeData(CustomizeIndex.Hairstyle, (CustomizeValue)hairRow.FeatureID, hairRow.Icon, (ushort)hairRow.RowId)); + hairList.Add(new CustomizeData(CustomizeIndex.Hairstyle, (CustomizeValue)hairRow.FeatureID, hairRow.Icon, + (ushort)hairRow.RowId)); } } @@ -462,9 +464,8 @@ public partial class CustomizationOptions // Get face paints from the hair sheet via reflection. private CustomizeData[] GetFacePaints(SubRace race, Gender gender) { - var row = _hairSheet.GetRow(((uint)race - 1) * 2 - 1 + (uint)gender)!; - var paintList = new List(row.Unknown37); - + var row = _hairSheet.GetRow(((uint)race - 1) * 2 - 1 + (uint)gender)!; + var paintList = new List(row.Unknown37); // Number of available face paints is at Unknown37. for (var i = 0; i < row.Unknown37; ++i) { @@ -478,12 +479,14 @@ public partial class CustomizationOptions var paintRow = _customizeSheet.GetRow(customizeIdx); // Facepaint Row from CustomizeSheet might not be set in case of unlockable facepaints. - paintList.Add(paintRow != null - ? new CustomizeData(CustomizeIndex.FacePaint, (CustomizeValue)paintRow.FeatureID, paintRow.Icon, - (ushort)paintRow.RowId) - : new CustomizeData(CustomizeIndex.FacePaint, (CustomizeValue)i, customizeIdx)); + if (paintRow != null) + { + paintList.Add(new CustomizeData(CustomizeIndex.FacePaint, (CustomizeValue)paintRow.FeatureID, paintRow.Icon, + (ushort)paintRow.RowId)); + } + else + paintList.Add(new CustomizeData(CustomizeIndex.FacePaint, (CustomizeValue)i, customizeIdx)); } - return paintList.ToArray(); } diff --git a/Glamourer.GameData/Customization/Customize.cs b/Glamourer.GameData/Customization/Customize.cs index e804f2e..aa3bfa7 100644 --- a/Glamourer.GameData/Customization/Customize.cs +++ b/Glamourer.GameData/Customization/Customize.cs @@ -8,7 +8,9 @@ public unsafe struct Customize public Penumbra.GameData.Structs.CustomizeData Data; public Customize(in Penumbra.GameData.Structs.CustomizeData data) - => Data = data; + { + Data = data.Clone(); + } public Race Race { diff --git a/Glamourer.GameData/Customization/CustomizeFlag.cs b/Glamourer.GameData/Customization/CustomizeFlag.cs index aae9de4..52964ca 100644 --- a/Glamourer.GameData/Customization/CustomizeFlag.cs +++ b/Glamourer.GameData/Customization/CustomizeFlag.cs @@ -48,7 +48,7 @@ public enum CustomizeFlag : ulong public static class CustomizeFlagExtensions { public const CustomizeFlag All = (CustomizeFlag)(((ulong)CustomizeFlag.FacePaintColor << 1) - 1ul); - public const CustomizeFlag RedrawRequired = CustomizeFlag.Race | CustomizeFlag.Clan | CustomizeFlag.Gender | CustomizeFlag.Face; + public const CustomizeFlag RedrawRequired = CustomizeFlag.Race | CustomizeFlag.Clan | CustomizeFlag.Gender | CustomizeFlag.Face | CustomizeFlag.BodyType; public static bool RequiresRedraw(this CustomizeFlag flags) => (flags & RedrawRequired) != 0; diff --git a/Glamourer/Automation/AutoDesign.cs b/Glamourer/Automation/AutoDesign.cs new file mode 100644 index 0000000..69d7afe --- /dev/null +++ b/Glamourer/Automation/AutoDesign.cs @@ -0,0 +1,78 @@ +using System; +using Glamourer.Customization; +using Glamourer.Designs; +using Glamourer.Interop.Structs; +using Glamourer.Structs; +using Newtonsoft.Json.Linq; + +namespace Glamourer.Automation; + +public class AutoDesign +{ + [Flags] + public enum Type : uint + { + Armor = 0x01, + Customizations = 0x02, + Meta = 0x04, + Weapons = 0x08, + Stains = 0x10, + Accessories = 0x20, + + All = Armor | Accessories | Customizations | Meta | Weapons | Stains, + } + + public Design Design; + public JobGroup Jobs; + public Type ApplicationType; + + public unsafe bool IsActive(Actor actor) + => actor.IsCharacter && Jobs.Fits(actor.AsCharacter->CharacterData.ClassJob); + + public JObject Serialize() + => new() + { + ["Design"] = Design.Identifier.ToString(), + ["ApplicationType"] = (uint)ApplicationType, + ["Conditions"] = CreateConditionObject(), + }; + + private JObject CreateConditionObject() + { + var ret = new JObject(); + if (Jobs.Id != 0) + ret["JobGroup"] = Jobs.Id; + return ret; + } + + public (EquipFlag Equip, CustomizeFlag Customize, bool ApplyHat, bool ApplyVisor, bool ApplyWeapon, bool ApplyWet) ApplyWhat() + { + var equipFlags = (ApplicationType.HasFlag(Type.Weapons) ? WeaponFlags : 0) + | (ApplicationType.HasFlag(Type.Armor) ? ArmorFlags : 0) + | (ApplicationType.HasFlag(Type.Accessories) ? AccessoryFlags : 0) + | (ApplicationType.HasFlag(Type.Stains) ? StainFlags : 0); + var customizeFlags = ApplicationType.HasFlag(Type.Customizations) ? CustomizeFlagExtensions.All : 0; + return (equipFlags & Design.ApplyEquip, customizeFlags & Design.ApplyCustomize, + ApplicationType.HasFlag(Type.Armor) && Design.DoApplyHatVisible(), + ApplicationType.HasFlag(Type.Armor) && Design.DoApplyVisorToggle(), + ApplicationType.HasFlag(Type.Weapons) && Design.DoApplyWeaponVisible(), + ApplicationType.HasFlag(Type.Customizations) && Design.DoApplyWetness()); + } + + public const EquipFlag WeaponFlags = EquipFlag.Mainhand | EquipFlag.Offhand; + public const EquipFlag ArmorFlags = EquipFlag.Head | EquipFlag.Body | EquipFlag.Hands | EquipFlag.Legs | EquipFlag.Feet; + public const EquipFlag AccessoryFlags = EquipFlag.Ears | EquipFlag.Neck | EquipFlag.Wrist | EquipFlag.RFinger | EquipFlag.LFinger; + + public const EquipFlag StainFlags = EquipFlag.MainhandStain + | EquipFlag.OffhandStain + | EquipFlag.HeadStain + | EquipFlag.BodyStain + | EquipFlag.HandsStain + | EquipFlag.LegsStain + | EquipFlag.FeetStain + | EquipFlag.EarsStain + | EquipFlag.NeckStain + | EquipFlag.WristStain + | EquipFlag.RFingerStain + | EquipFlag.LFingerStain; +} diff --git a/Glamourer/Automation/AutoDesignApplier.cs b/Glamourer/Automation/AutoDesignApplier.cs new file mode 100644 index 0000000..757c32c --- /dev/null +++ b/Glamourer/Automation/AutoDesignApplier.cs @@ -0,0 +1,214 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using Glamourer.Customization; +using Glamourer.Designs; +using Glamourer.Events; +using Glamourer.Interop; +using Glamourer.Interop.Structs; +using Glamourer.Services; +using Glamourer.State; +using Glamourer.Structs; +using Penumbra.GameData.Actors; +using Penumbra.GameData.Enums; + +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; + + public AutoDesignApplier(Configuration config, AutoDesignManager manager, PhrasingService phrasing, StateManager state, JobService jobs, + CustomizationService customizations, ActorService actors) + { + _config = config; + _manager = manager; + _phrasing = phrasing; + _state = state; + _jobs = jobs; + _customizations = customizations; + _actors = actors; + _jobs.JobChanged += OnJobChange; + } + + public void Dispose() + { + _jobs.JobChanged -= OnJobChange; + } + + private void OnJobChange(Actor actor, Job _) + { + if (!_config.EnableAutoDesigns || !actor.Identifier(_actors.AwaitedService, out var id)) + return; + + if (!_manager.EnabledSets.TryGetValue(id, out var set)) + return; + + if (!_state.GetOrCreate(id, actor, out var state)) + return; + + Reduce(actor, state, set); + _state.ReapplyState(actor); + } + + public void Reduce(Actor actor, ActorIdentifier identifier, ActorState state) + { + if (!_config.EnableAutoDesigns) + return; + + if (!GetPlayerSet(identifier, out var set)) + return; + Reduce(actor, state, set); + } + + private unsafe void Reduce(Actor actor, ActorState state, AutoDesignSet set) + { + EquipFlag totalEquipFlags = 0; + //var totalCustomizeFlags = _phrasing.Phrasing2 ? 0 : CustomizeFlagExtensions.RedrawRequired; + var totalCustomizeFlags = CustomizeFlagExtensions.RedrawRequired; + byte totalMetaFlags = 0; + foreach (var design in set.Designs) + { + if (!design.IsActive(actor)) + continue; + + if (design.ApplicationType is 0) + continue; + + if (actor.AsCharacter->CharacterData.ModelCharaId != design.Design.DesignData.ModelId) + continue; + + var (equipFlags, customizeFlags, applyHat, applyVisor, applyWeapon, applyWet) = design.ApplyWhat(); + Reduce(state, in design.Design.DesignData, equipFlags, ref totalEquipFlags); + Reduce(state, in design.Design.DesignData, customizeFlags, ref totalCustomizeFlags); + Reduce(state, in design.Design.DesignData, applyHat, applyVisor, applyWeapon, applyWet, ref totalMetaFlags); + } + } + + /// Get world-specific first and all-world afterwards. + private bool GetPlayerSet(ActorIdentifier identifier, [NotNullWhen(true)] out AutoDesignSet? set) + { + if (identifier.Type is not IdentifierType.Player) + return _manager.EnabledSets.TryGetValue(identifier, out set); + + if (_manager.EnabledSets.TryGetValue(identifier, out set)) + return true; + + identifier = _actors.AwaitedService.CreatePlayer(identifier.PlayerName, ushort.MaxValue); + return _manager.EnabledSets.TryGetValue(identifier, out set); + } + + private void Reduce(ActorState state, in DesignData design, EquipFlag equipFlags, ref EquipFlag totalEquipFlags) + { + equipFlags &= ~totalEquipFlags; + if (equipFlags == 0) + return; + + // TODO add item conditions + foreach (var slot in EquipSlotExtensions.EqdpSlots) + { + var flag = slot.ToFlag(); + if (equipFlags.HasFlag(flag)) + { + _state.ChangeItem(state, slot, design.Item(slot), StateChanged.Source.Fixed); + totalEquipFlags |= flag; + } + + var stainFlag = slot.ToStainFlag(); + if (equipFlags.HasFlag(stainFlag)) + { + _state.ChangeStain(state, slot, design.Stain(slot), StateChanged.Source.Fixed); + totalEquipFlags |= stainFlag; + } + } + + if (equipFlags.HasFlag(EquipFlag.Mainhand)) + { + var item = design.Item(EquipSlot.MainHand); + if (state.ModelData.Item(EquipSlot.MainHand).Type == item.Type) + { + _state.ChangeItem(state, EquipSlot.MainHand, item, StateChanged.Source.Fixed); + totalEquipFlags |= EquipFlag.Mainhand; + } + } + + if (equipFlags.HasFlag(EquipFlag.Offhand)) + { + var item = design.Item(EquipSlot.OffHand); + if (state.ModelData.Item(EquipSlot.OffHand).Type == item.Type) + { + _state.ChangeItem(state, EquipSlot.OffHand, item, StateChanged.Source.Fixed); + totalEquipFlags |= EquipFlag.Offhand; + } + } + + if (equipFlags.HasFlag(EquipFlag.MainhandStain)) + { + _state.ChangeStain(state, EquipSlot.MainHand, design.Stain(EquipSlot.MainHand), StateChanged.Source.Fixed); + totalEquipFlags |= EquipFlag.MainhandStain; + } + + if (equipFlags.HasFlag(EquipFlag.OffhandStain)) + { + _state.ChangeStain(state, EquipSlot.OffHand, design.Stain(EquipSlot.OffHand), StateChanged.Source.Fixed); + totalEquipFlags |= EquipFlag.OffhandStain; + } + } + + private void Reduce(ActorState state, in DesignData design, CustomizeFlag customizeFlags, ref CustomizeFlag totalCustomizeFlags) + { + customizeFlags &= ~totalCustomizeFlags; + if (customizeFlags == 0) + return; + + // TODO add race/gender handling + var set = _customizations.AwaitedService.GetList(state.ModelData.Customize.Clan, state.ModelData.Customize.Gender); + var face = state.ModelData.Customize.Face; + foreach (var index in Enum.GetValues()) + { + var flag = index.ToFlag(); + if (!customizeFlags.HasFlag(flag)) + continue; + + var value = design.Customize[index]; + if (CustomizationService.IsCustomizationValid(set, face, index, value)) + { + _state.ChangeCustomize(state, index, value, StateChanged.Source.Fixed); + totalCustomizeFlags |= flag; + } + } + } + + private void Reduce(ActorState state, in DesignData design, bool applyHat, bool applyVisor, bool applyWeapon, bool applyWet, + ref byte totalMetaFlags) + { + if (applyHat && (totalMetaFlags & 0x01) == 0) + { + _state.ChangeHatState(state, design.IsHatVisible(), StateChanged.Source.Fixed); + totalMetaFlags |= 0x01; + } + + if (applyVisor && (totalMetaFlags & 0x02) == 0) + { + _state.ChangeVisorState(state, design.IsVisorToggled(), StateChanged.Source.Fixed); + totalMetaFlags |= 0x02; + } + + if (applyWeapon && (totalMetaFlags & 0x04) == 0) + { + _state.ChangeWeaponState(state, design.IsWeaponVisible(), StateChanged.Source.Fixed); + totalMetaFlags |= 0x04; + } + + if (applyWet && (totalMetaFlags & 0x08) == 0) + { + _state.ChangeWetness(state, design.IsWet(), StateChanged.Source.Fixed); + totalMetaFlags |= 0x08; + } + } +} diff --git a/Glamourer/Automation/AutoDesignManager.cs b/Glamourer/Automation/AutoDesignManager.cs new file mode 100644 index 0000000..46ebcbd --- /dev/null +++ b/Glamourer/Automation/AutoDesignManager.cs @@ -0,0 +1,422 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Dalamud.Interface.Internal.Notifications; +using Dalamud.Utility; +using Glamourer.Designs; +using Glamourer.Events; +using Glamourer.Interop; +using Glamourer.Services; +using Glamourer.Structs; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using OtterGui.Filesystem; +using Penumbra.GameData.Actors; + +namespace Glamourer.Automation; + +public class AutoDesignManager : ISavable, IReadOnlyList +{ + public const int CurrentVersion = 1; + + private readonly SaveService _saveService; + + private readonly JobService _jobs; + private readonly DesignManager _designs; + private readonly ActorService _actors; + private readonly AutomationChanged _event; + + private readonly List _data = new(); + private readonly Dictionary _enabled = new(); + + public IReadOnlyDictionary EnabledSets + => _enabled; + + public AutoDesignManager(JobService jobs, ActorService actors, SaveService saveService, DesignManager designs, AutomationChanged @event, + FixedDesignMigrator migrator, DesignFileSystem fileSystem) + { + _jobs = jobs; + _actors = actors; + _saveService = saveService; + _designs = designs; + _event = @event; + Load(); + migrator.ConsumeMigratedData(_actors, fileSystem, this); + } + + public IEnumerator GetEnumerator() + => _data.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + public int Count + => _data.Count; + + public AutoDesignSet this[int index] + => _data[index]; + + public void AddDesignSet(string name, ActorIdentifier identifier) + { + if (!IdentifierValid(identifier) || name.Length == 0) + return; + + var newSet = new AutoDesignSet(name, identifier.CreatePermanent()) { Enabled = false }; + _data.Add(newSet); + Save(); + Glamourer.Log.Debug($"Created new design set for {identifier.Incognito(null)}."); + _event.Invoke(AutomationChanged.Type.AddedSet, newSet, (_data.Count - 1, name)); + } + + public void DeleteDesignSet(int whichSet) + { + if (whichSet >= _data.Count || whichSet < 0) + return; + + var set = _data[whichSet]; + if (set.Enabled) + { + set.Enabled = false; + _enabled.Remove(set.Identifier); + } + + Save(); + Glamourer.Log.Debug($"Deleted design set {whichSet + 1}."); + _event.Invoke(AutomationChanged.Type.DeletedSet, set, whichSet); + } + + public void Rename(int whichSet, string newName) + { + if (whichSet >= _data.Count || whichSet < 0 || newName.Length == 0) + return; + + var set = _data[whichSet]; + if (set.Name == newName) + return; + + var old = set.Name; + set.Name = newName; + Save(); + Glamourer.Log.Debug($"Renamed design set {whichSet + 1} from {old} to {newName}."); + _event.Invoke(AutomationChanged.Type.RenamedSet, set, (old, newName)); + } + + + public void MoveSet(int whichSet, int toWhichSet) + { + if (!_data.Move(whichSet, toWhichSet)) + return; + + Save(); + Glamourer.Log.Debug($"Moved design set {whichSet + 1} to position {toWhichSet + 1}."); + _event.Invoke(AutomationChanged.Type.MovedSet, _data[toWhichSet], (whichSet, toWhichSet)); + } + + public void ChangeIdentifier(int whichSet, ActorIdentifier to) + { + if (whichSet >= _data.Count || whichSet < 0 || !IdentifierValid(to)) + return; + + var set = _data[whichSet]; + if (set.Identifier == to) + return; + + var old = set.Identifier; + set.Identifier = to.CreatePermanent(); + AutoDesignSet? oldEnabled = null; + if (set.Enabled) + { + _enabled.Remove(old); + if (_enabled.Remove(to, out oldEnabled)) + oldEnabled.Enabled = false; + _enabled.Add(set.Identifier, set); + } + + Save(); + Glamourer.Log.Debug($"Changed Identifier of design set {whichSet + 1} from {old.Incognito(null)} to {to.Incognito(null)}."); + _event.Invoke(AutomationChanged.Type.ChangeIdentifier, set, (old, to, oldEnabled)); + } + + public void SetState(int whichSet, bool value) + { + if (whichSet >= _data.Count || whichSet < 0) + return; + + var set = _data[whichSet]; + if (set.Enabled == value) + return; + + AutoDesignSet? oldEnabled = null; + if (value) + { + if (_enabled.Remove(set.Identifier, out oldEnabled)) + oldEnabled.Enabled = false; + _enabled.Add(set.Identifier, set); + } + + Save(); + Glamourer.Log.Debug($"Changed enabled state of design set {whichSet + 1} to {value}."); + _event.Invoke(AutomationChanged.Type.ToggleSet, set, oldEnabled); + } + + public void AddDesign(AutoDesignSet set, Design design) + { + var newDesign = new AutoDesign() + { + Design = design, + ApplicationType = AutoDesign.Type.All, + Jobs = _jobs.JobGroups[1], + }; + set.Designs.Add(newDesign); + Save(); + Glamourer.Log.Debug($"Added new associated design {design.Identifier} as design {set.Designs.Count} to design set."); + _event.Invoke(AutomationChanged.Type.AddedDesign, set, set.Designs.Count - 1); + } + + public void DeleteDesign(AutoDesignSet set, int which) + { + if (which >= set.Designs.Count || which < 0) + return; + + set.Designs.RemoveAt(which); + Save(); + Glamourer.Log.Debug($"Removed associated design {which + 1} from design set."); + _event.Invoke(AutomationChanged.Type.DeletedDesign, set, which); + } + + public void MoveDesign(AutoDesignSet set, int from, int to) + { + if (!set.Designs.Move(from, to)) + return; + + Save(); + Glamourer.Log.Debug($"Moved design {from + 1} to {to + 1} in design set."); + _event.Invoke(AutomationChanged.Type.MovedDesign, set, (from, to)); + } + + public void ChangeDesign(AutoDesignSet set, int which, Design newDesign) + { + if (which >= set.Designs.Count || which < 0) + return; + + var design = set.Designs[which]; + if (design.Design.Identifier == newDesign.Identifier) + return; + + var old = design.Design; + design.Design = newDesign; + Save(); + Glamourer.Log.Debug( + $"Changed linked design from {old.Identifier} to {newDesign.Identifier} for associated design {which + 1} in design set."); + _event.Invoke(AutomationChanged.Type.ChangedDesign, set, (which, old, newDesign)); + } + + public void ChangeJobCondition(AutoDesignSet set, int which, JobGroup jobs) + { + if (which >= set.Designs.Count || which < 0) + return; + + var design = set.Designs[which]; + if (design.Jobs.Id == jobs.Id) + return; + + var old = design.Jobs; + design.Jobs = jobs; + Save(); + Glamourer.Log.Debug($"Changed job condition from {old.Id} to {jobs.Id} for associated design {which + 1} in design set."); + _event.Invoke(AutomationChanged.Type.ChangedConditions, set, (which, old, jobs)); + } + + public void ChangeApplicationType(AutoDesignSet set, int which, AutoDesign.Type type) + { + if (which >= set.Designs.Count || which < 0) + return; + + type &= AutoDesign.Type.All; + var design = set.Designs[which]; + if (design.ApplicationType == type) + return; + + var old = design.ApplicationType; + design.ApplicationType = type; + Save(); + Glamourer.Log.Debug($"Changed application type from {old} to {type} for associated design {which + 1} in design set."); + _event.Invoke(AutomationChanged.Type.ChangedType, set, (which, old, type)); + } + + public string ToFilename(FilenameService fileNames) + => fileNames.AutomationFile; + + public void Save(StreamWriter writer) + { + using var j = new JsonTextWriter(writer) + { + Formatting = Formatting.Indented, + }; + Serialize().WriteTo(j); + } + + private JObject Serialize() + { + var array = new JArray(); + foreach (var set in _data) + array.Add(set.Serialize()); + + return new JObject() + { + ["Version"] = CurrentVersion, + ["Data"] = array, + }; + } + + private void Load() + { + var file = _saveService.FileNames.AutomationFile; + _data.Clear(); + if (!File.Exists(file)) + return; + + try + { + var text = File.ReadAllText(file); + var obj = JObject.Parse(text); + var version = obj["Version"]?.ToObject() ?? 0; + + switch (version) + { + case < 1: + case > CurrentVersion: + Glamourer.Chat.NotificationMessage("Failure to load automated designs: No valid version available.", "Error", + NotificationType.Error); + break; + case 1: + LoadV1(obj["Data"]); + break; + } + } + catch (Exception ex) + { + Glamourer.Chat.NotificationMessage(ex, "Failure to load automated designs: Error during parsing.", + "Failure to load automated designs", "Error", NotificationType.Error); + } + } + + private void LoadV1(JToken? data) + { + if (data is not JArray array) + return; + + foreach (var obj in array) + { + var name = obj["Name"]?.ToObject() ?? string.Empty; + if (name.Length == 0) + { + Glamourer.Chat.NotificationMessage("Skipped loading Automation Set: No name provided.", "Warning", NotificationType.Warning); + continue; + } + + var id = _actors.AwaitedService.FromJson(obj["Identifier"] as JObject); + if (!IdentifierValid(id)) + { + Glamourer.Chat.NotificationMessage("Skipped loading Automation Set: Invalid Identifier.", "Warning", NotificationType.Warning); + continue; + } + + var set = new AutoDesignSet(name, id) + { + Enabled = obj["Enabled"]?.ToObject() ?? false, + }; + + if (set.Enabled) + if (!_enabled.TryAdd(set.Identifier, set)) + set.Enabled = false; + + _data.Add(set); + + if (obj["Designs"] is not JArray designArray) + continue; + + foreach (var designObj in designArray) + { + if (designObj is not JObject j) + { + Glamourer.Chat.NotificationMessage($"Skipped loading design in Automation Set for {set.Identifier}: Unknown design."); + continue; + } + + var design = ToDesignObject(j); + if (design != null) + set.Designs.Add(design); + } + } + } + + private AutoDesign? ToDesignObject(JObject jObj) + { + var designIdentifier = jObj["Design"]?.ToObject(); + if (designIdentifier.IsNullOrEmpty()) + { + Glamourer.Chat.NotificationMessage("Error parsing automatically applied design: No design specified."); + return null; + } + + if (!Guid.TryParse(designIdentifier, out var guid)) + { + Glamourer.Chat.NotificationMessage($"Error parsing automatically applied design: {designIdentifier} is not a valid GUID."); + + return null; + } + + var design = _designs.Designs.FirstOrDefault(d => d.Identifier == guid); + if (design == null) + { + Glamourer.Chat.NotificationMessage($"Error parsing automatically applied design: The specified design {guid} does not exist."); + return null; + } + + var applicationType = (AutoDesign.Type)(jObj["ApplicationType"]?.ToObject() ?? 0); + + + var ret = new AutoDesign() + { + Design = design, + ApplicationType = applicationType & AutoDesign.Type.All, + }; + + var conditions = jObj["Conditions"]; + if (conditions == null) + return ret; + + var jobs = conditions["JobGroup"]?.ToObject() ?? -1; + if (jobs >= 0) + { + if (!_jobs.JobGroups.TryGetValue((ushort)jobs, out var jobGroup)) + { + Glamourer.Chat.NotificationMessage($"Error parsing automatically applied design: The job condition {jobs} does not exist."); + return null; + } + + ret.Jobs = jobGroup; + } + + return ret; + } + + private void Save() + => _saveService.DelaySave(this); + + private static bool IdentifierValid(ActorIdentifier identifier) + { + if (!identifier.IsValid) + return false; + + return identifier.Type switch + { + IdentifierType.Player => true, + IdentifierType.Retainer => true, + _ => false, + }; + } +} diff --git a/Glamourer/Automation/AutoDesignSet.cs b/Glamourer/Automation/AutoDesignSet.cs new file mode 100644 index 0000000..fb76c99 --- /dev/null +++ b/Glamourer/Automation/AutoDesignSet.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; +using Newtonsoft.Json.Linq; +using Penumbra.GameData.Actors; + +namespace Glamourer.Automation; + +public class AutoDesignSet +{ + public readonly List Designs; + + public string Name; + public ActorIdentifier Identifier; + public bool Enabled; + + public JObject Serialize() + { + var list = new JArray(); + foreach (var design in Designs) + list.Add(design.Serialize()); + + return new JObject() + { + ["Name"] = Name, + ["Identifier"] = Identifier.ToJson(), + ["Enabled"] = Enabled, + ["Designs"] = list, + }; + } + + public AutoDesignSet(string name, ActorIdentifier identifier) + : this(name, identifier, new List()) + { } + + public AutoDesignSet(string name, ActorIdentifier identifier, List designs) + { + Name = name; + Identifier = identifier; + Designs = designs; + } +} diff --git a/Glamourer/Automation/FixedDesignMigrator.cs b/Glamourer/Automation/FixedDesignMigrator.cs new file mode 100644 index 0000000..3fd8218 --- /dev/null +++ b/Glamourer/Automation/FixedDesignMigrator.cs @@ -0,0 +1,111 @@ +using System.Collections.Generic; +using System.Linq; +using Dalamud.Interface.Internal.Notifications; +using Glamourer.Designs; +using Glamourer.Interop; +using Glamourer.Services; +using Glamourer.Structs; +using Newtonsoft.Json.Linq; +using Penumbra.GameData.Actors; +using Penumbra.String; + +namespace Glamourer.Automation; + +public class FixedDesignMigrator +{ + private readonly JobService _jobs; + private Dictionary)>? _migratedData; + + public FixedDesignMigrator(JobService jobs) + => _jobs = jobs; + + public void ConsumeMigratedData(ActorService actors, DesignFileSystem designFileSystem, AutoDesignManager autoManager) + { + if (_migratedData == null) + return; + + foreach (var data in _migratedData) + { + var enabled = data.Value.Item1; + var name = data.Key + (data.Value.Item1 ? " (Enabled)" : " (Disabled)"); + if (autoManager.Any(d => name == data.Key)) + continue; + + var id = ActorIdentifier.Invalid; + if (ByteString.FromString(data.Key, out var byteString, false)) + { + id = actors.AwaitedService.CreatePlayer(byteString, ushort.MaxValue); + if (!id.IsValid) + id = actors.AwaitedService.CreateRetainer(byteString, ActorIdentifier.RetainerType.Both); + } + + if (!id.IsValid) + { + byteString = ByteString.FromSpanUnsafe("Mig Ration"u8, true, false, true); + id = actors.AwaitedService.CreatePlayer(byteString, actors.AwaitedService.Data.Worlds.First().Key); + enabled = false; + if (!id.IsValid) + { + Glamourer.Chat.NotificationMessage($"Could not migrate fixed design {data.Key}.", "Error", NotificationType.Error); + continue; + } + } + + autoManager.AddDesignSet(name, id); + autoManager.SetState(autoManager.Count - 1, enabled); + var set = autoManager[^1]; + foreach (var design in data.Value.Item2) + { + if (!designFileSystem.Find(design.Item1, out var child) || child is not DesignFileSystem.Leaf leaf) + { + Glamourer.Chat.NotificationMessage($"Could not find design with path {design.Item1}, skipped fixed design.", "Warning", + NotificationType.Warning); + continue; + } + + autoManager.AddDesign(set, leaf.Value); + autoManager.ChangeJobCondition(set, set.Designs.Count - 1, design.Item2); + } + } + } + + public void Migrate(JToken? data) + { + if (data is not JArray array) + return; + + var list = new List<(string Name, string Path, JobGroup Group, bool Enabled)>(); + foreach (var obj in array) + { + var name = obj["Name"]?.ToObject() ?? string.Empty; + if (name.Length == 0) + { + Glamourer.Chat.NotificationMessage("Could not semi-migrate fixed design: No character name available.", "Warning", + NotificationType.Warning); + continue; + } + + var path = obj["Path"]?.ToObject() ?? string.Empty; + if (path.Length == 0) + { + Glamourer.Chat.NotificationMessage("Could not semi-migrate fixed design: No design path available.", "Warning", + NotificationType.Warning); + continue; + } + + var job = obj["JobGroups"]?.ToObject() ?? -1; + if (job < 0 || !_jobs.JobGroups.TryGetValue((ushort)job, out var group)) + { + Glamourer.Chat.NotificationMessage("Could not semi-migrate fixed design: Invalid job group specified.", "Warning", + NotificationType.Warning); + continue; + } + + var enabled = obj["Enabled"]?.ToObject() ?? false; + list.Add((name, path, group, enabled)); + } + + _migratedData = list.GroupBy(t => (t.Name, t.Enabled)) + .ToDictionary(kvp => kvp.Key.Name, kvp => (kvp.Key.Enabled, kvp.Select(k => (k.Path, k.Group)).ToList())); + } +} diff --git a/Glamourer/Configuration.cs b/Glamourer/Configuration.cs index 3d71fce..ec5f323 100644 --- a/Glamourer/Configuration.cs +++ b/Glamourer/Configuration.cs @@ -21,6 +21,7 @@ public class Configuration : IPluginConfiguration, ISavable public bool UseRestrictedGearProtection { get; set; } = true; public bool OpenFoldersByDefault { get; set; } = false; public bool AutoRedrawEquipOnChanges { get; set; } = false; + public bool EnableAutoDesigns { get; set; } = true; public MainWindow.TabType SelectedTab { get; set; } = MainWindow.TabType.Settings; public DoubleModifier DeleteDesignModifier { get; set; } = new(ModifierHotkey.Control, ModifierHotkey.Shift); @@ -28,11 +29,13 @@ 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; #if DEBUG public bool DebugMode { get; set; } = true; #else - public bool DebugMode { get; set; } = false; + public bool DebugMode { get; set; } = false; #endif public int Version { get; set; } = Constants.CurrentVersion; diff --git a/Glamourer/Designs/Design.cs b/Glamourer/Designs/Design.cs index ba4e162..274b9ce 100644 --- a/Glamourer/Designs/Design.cs +++ b/Glamourer/Designs/Design.cs @@ -205,7 +205,7 @@ public class Design : ISavable #region Serialization - public JObject JsonSerialize() + private JObject JsonSerialize() { var ret = new JObject { @@ -223,7 +223,7 @@ public class Design : ISavable return ret; } - public JObject SerializeEquipment() + private JObject SerializeEquipment() { static JObject Serialize(uint itemId, StainId stain, bool apply, bool applyStain) => new() @@ -250,7 +250,7 @@ public class Design : ISavable return ret; } - public JObject SerializeCustomize() + private JObject SerializeCustomize() { var ret = new JObject() { diff --git a/Glamourer/Designs/DesignManager.cs b/Glamourer/Designs/DesignManager.cs index df4c1ad..62ed43f 100644 --- a/Glamourer/Designs/DesignManager.cs +++ b/Glamourer/Designs/DesignManager.cs @@ -192,12 +192,12 @@ public class DesignManager Glamourer.Log.Error("Somehow race or body type was changed in a design. This should not happen."); return; case CustomizeIndex.Clan: - if (!_customizations.ChangeClan(ref design.DesignData.Customize, (SubRace)value.Value)) + if (_customizations.ChangeClan(ref design.DesignData.Customize, (SubRace)value.Value) == 0) return; break; case CustomizeIndex.Gender: - if (!_customizations.ChangeGender(ref design.DesignData.Customize, (Gender)(value.Value + 1))) + if (_customizations.ChangeGender(ref design.DesignData.Customize, (Gender)(value.Value + 1)) == 0) return; break; diff --git a/Glamourer/Events/AutomationChanged.cs b/Glamourer/Events/AutomationChanged.cs new file mode 100644 index 0000000..ff1208c --- /dev/null +++ b/Glamourer/Events/AutomationChanged.cs @@ -0,0 +1,66 @@ +using System; +using Glamourer.Automation; +using OtterGui.Classes; + +namespace Glamourer.Events; + +/// +/// Triggered when an automated design is changed in any way. +/// +/// Parameter is the type of the change +/// Parameter is the added or changed design set or null on deletion. +/// Parameter is additional data depending on the type of change. +/// +/// +public sealed class AutomationChanged : EventWrapper, + AutomationChanged.Priority> +{ + public enum Type + { + /// Add a new set. Names and identifiers do not have to be unique. It is not enabled by default. Additional data is the index it gets added at and the name [(int, string)]. + AddedSet, + + /// Delete a given set. Additional data is the index it got removed from [int]. + DeletedSet, + + /// Rename a given set. Names do not have to be unique. Additional data is the old name and the new name [(string, string)]. + RenamedSet, + + /// Move a given set to a different position. Additional data is the old index of the set and the new index of the set [(int, int)]. + MovedSet, + + /// Change the identifier a given set is associated with to another one. Additional data is the old identifier and the new one, and a potentially disabled other design set. [(ActorIdentifier, ActorIdentifier, AutoDesignSet?)]. + ChangeIdentifier, + + /// Toggle the enabled state of a given set. Additional data is the thus disabled other set, if any [AutoDesignSet?]. + ToggleSet, + + /// Add a new associated design to a given set. Additional data is the index it got added at [int]. + AddedDesign, + + /// Remove a given associated design from a given set. Additional data is the index it got removed from [int]. + DeletedDesign, + + /// Move a given associated design in the list of a given set. Additional data is the index that got moved and the index it got moved to [(int, int)]. + MovedDesign, + + /// Change the linked design in an associated design for a given set. Additional data is the index of the changed associated design, the old linked design and the new linked design [(int, Design, Design)]. + ChangedDesign, + + /// Change the job condition in an associated design for a given set. Additional data is the index of the changed associated design, the old job group and the new job group [(int, JobGroup, JobGroup)]. + ChangedConditions, + + /// Change the application type in an associated design for a given set. Additional data is the index of the changed associated design, the old type and the new type. [(int, AutoDesign.Type, AutoDesign.Type)]. + ChangedType, + } + + public enum Priority + { } + + public AutomationChanged() + : base(nameof(AutomationChanged)) + { } + + public void Invoke(Type type, AutoDesignSet? set, object? data) + => Invoke(this, type, set, data); +} diff --git a/Glamourer/Glamourer.cs b/Glamourer/Glamourer.cs index b2e1b49..754055b 100644 --- a/Glamourer/Glamourer.cs +++ b/Glamourer/Glamourer.cs @@ -3,6 +3,7 @@ using Dalamud.Plugin; using Glamourer.Gui; using Glamourer.Interop; using Glamourer.Services; +using Lumina.Excel.GeneratedSheets; using Microsoft.Extensions.DependencyInjection; using OtterGui.Classes; using OtterGui.Log; diff --git a/Glamourer/Gui/Customization/CustomizationDrawer.GenderRace.cs b/Glamourer/Gui/Customization/CustomizationDrawer.GenderRace.cs index 0ee09ff..1b305ff 100644 --- a/Glamourer/Gui/Customization/CustomizationDrawer.GenderRace.cs +++ b/Glamourer/Gui/Customization/CustomizationDrawer.GenderRace.cs @@ -24,20 +24,18 @@ public partial class CustomizationDrawer private void DrawGenderSelector() { - using var font = ImRaii.PushFont(UiBuilder.IconFont); var icon = _customize.Gender switch { Gender.Male when _customize.Race is Race.Hrothgar => FontAwesomeIcon.MarsDouble, Gender.Male => FontAwesomeIcon.Mars, Gender.Female => FontAwesomeIcon.Venus, - - _ => throw new Exception($"Gender value {_customize.Gender} is not a valid gender for a design."), + _ => FontAwesomeIcon.Question, }; - if (!ImGuiUtil.DrawDisabledButton(icon.ToIconString(), _framedIconSize, string.Empty, icon == FontAwesomeIcon.MarsDouble, true)) + if (!ImGuiUtil.DrawDisabledButton(icon.ToIconString(), _framedIconSize, string.Empty, icon is not FontAwesomeIcon.Mars and not FontAwesomeIcon.Venus, true)) return; - _service.ChangeGender(ref _customize, _customize.Gender is Gender.Male ? Gender.Female : Gender.Male); + Changed |= _service.ChangeGender(ref _customize, icon is FontAwesomeIcon.Mars ? Gender.Female : Gender.Male); } private void DrawRaceCombo() @@ -50,7 +48,7 @@ public partial class CustomizationDrawer foreach (var subRace in Enum.GetValues().Skip(1)) // Skip Unknown { if (ImGui.Selectable(_service.ClanName(subRace, _customize.Gender), subRace == _customize.Clan)) - _service.ChangeClan(ref _customize, subRace); + Changed |= _service.ChangeClan(ref _customize, subRace); } } } diff --git a/Glamourer/Gui/Customization/CustomizationDrawer.cs b/Glamourer/Gui/Customization/CustomizationDrawer.cs index d72800b..7f78b3d 100644 --- a/Glamourer/Gui/Customization/CustomizationDrawer.cs +++ b/Glamourer/Gui/Customization/CustomizationDrawer.cs @@ -20,7 +20,8 @@ public partial class CustomizationDrawer : IDisposable private Customize _customize; private CustomizationSet _set = null!; - public Customize Customize; + public Customize Customize + => _customize; public CustomizeFlag CurrentFlag { get; private set; } public CustomizeFlag Changed { get; private set; } @@ -41,7 +42,7 @@ public partial class CustomizationDrawer : IDisposable { _service = service; _legacyTattoo = GetLegacyTattooIcon(pi); - Customize = Customize.Default; + _customize = Customize.Default; } public void Dispose() diff --git a/Glamourer/Gui/Equipment/EquipmentDrawer.cs b/Glamourer/Gui/Equipment/EquipmentDrawer.cs new file mode 100644 index 0000000..4464b3c --- /dev/null +++ b/Glamourer/Gui/Equipment/EquipmentDrawer.cs @@ -0,0 +1,163 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using Dalamud.Data; +using Dalamud.Interface; +using Glamourer.Designs; +using Glamourer.Services; +using ImGuiNET; +using OtterGui; +using OtterGui.Widgets; +using Penumbra.GameData.Data; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; + +namespace Glamourer.Gui.Equipment; + +public class EquipmentDrawer +{ + private readonly ItemManager _items; + private readonly FilterComboColors _stainCombo; + private readonly StainData _stainData; + private readonly ItemCombo[] _itemCombo; + private readonly Dictionary _weaponCombo; + + public EquipmentDrawer(DataManager gameData, ItemManager items) + { + _items = items; + _stainData = items.Stains; + _stainCombo = new FilterComboColors(140, + _stainData.Data.Prepend(new KeyValuePair(0, ("None", 0, false)))); + _itemCombo = EquipSlotExtensions.EqdpSlots.Select(e => new ItemCombo(gameData, items, e)).ToArray(); + _weaponCombo = new Dictionary(FullEquipTypeExtensions.WeaponTypes.Count * 2); + foreach (var type in Enum.GetValues()) + { + if (type.ToSlot() is EquipSlot.MainHand) + _weaponCombo.TryAdd(type, new WeaponCombo(items, type)); + else if (type.ToSlot() is EquipSlot.OffHand) + _weaponCombo.TryAdd(type, new WeaponCombo(items, type)); + } + + _weaponCombo.Add(FullEquipType.Unknown, new WeaponCombo(items, FullEquipType.Unknown)); + } + + private string VerifyRestrictedGear(EquipItem gear, EquipSlot slot, Gender gender, Race race) + { + if (slot.IsAccessory()) + return gear.Name; + + var (changed, _) = _items.ResolveRestrictedGear(gear.Armor(), slot, race, gender); + if (changed) + return gear.Name + " (Restricted)"; + + return gear.Name; + } + + public bool DrawArmor(EquipItem current, EquipSlot slot, out EquipItem armor, Gender gender = Gender.Unknown, Race race = Race.Unknown) + { + Debug.Assert(slot.IsEquipment() || slot.IsAccessory(), $"Called {nameof(DrawArmor)} on {slot}."); + var combo = _itemCombo[slot.ToIndex()]; + armor = current; + var change = combo.Draw(VerifyRestrictedGear(armor, slot, gender, race), armor.Id, 320 * ImGuiHelpers.GlobalScale); + if (armor.ModelId.Value != 0) + { + ImGuiUtil.HoverTooltip("Right-click to clear."); + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + { + change = true; + armor = ItemManager.NothingItem(slot); + } + else if (change) + { + armor = combo.CurrentSelection; + } + } + else if (change) + { + armor = combo.CurrentSelection; + } + + return change; + } + + public bool DrawStain(StainId current, EquipSlot slot, out Stain stain) + { + var found = _stainData.TryGetValue(current, out stain); + var change = _stainCombo.Draw($"##stain{slot}", stain.RgbaColor, stain.Name, found); + ImGuiUtil.HoverTooltip("Right-click to clear."); + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + { + stain = Stain.None; + return true; + } + + return change && _stainData.TryGetValue(_stainCombo.CurrentSelection.Key, out stain); + } + + public bool DrawMainhand(EquipItem current, bool drawAll, out EquipItem weapon) + { + weapon = current; + if (!_weaponCombo.TryGetValue(drawAll ? FullEquipType.Unknown : current.Type, out var combo)) + return false; + + if (!combo.Draw(weapon.Name, weapon.Id, 320 * ImGuiHelpers.GlobalScale)) + return false; + + weapon = combo.CurrentSelection; + return true; + } + + public bool DrawOffhand(EquipItem current, FullEquipType mainType, out EquipItem weapon) + { + weapon = current; + var offType = mainType.Offhand(); + if (offType == FullEquipType.Unknown) + return false; + + if (!_weaponCombo.TryGetValue(offType, out var combo)) + return false; + + var change = combo.Draw(weapon.Name, weapon.Id, 320 * ImGuiHelpers.GlobalScale); + if (!offType.IsOffhandType() && weapon.ModelId.Value != 0) + { + ImGuiUtil.HoverTooltip("Right-click to clear."); + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + { + change = true; + weapon = ItemManager.NothingItem(offType); + } + } + else if (change) + { + weapon = combo.CurrentSelection; + } + + return change; + } + + public bool DrawApply(Design design, EquipSlot slot, out bool enabled) + => DrawCheckbox($"##apply{slot}", design.DoApplyEquip(slot), out enabled); + + public bool DrawApplyStain(Design design, EquipSlot slot, out bool enabled) + => DrawCheckbox($"##applyStain{slot}", design.DoApplyStain(slot), out enabled); + + private static bool DrawCheckbox(string label, bool value, out bool on) + { + var ret = ImGuiUtil.Checkbox(label, string.Empty, value, v => value = v); + on = value; + return ret; + } + + public bool DrawVisor(bool current, out bool on) + => DrawCheckbox("##visorToggled", current, out on); + + public bool DrawHat(bool current, out bool on) + => DrawCheckbox("##hatVisible", current, out on); + + public bool DrawWeapon(bool current, out bool on) + => DrawCheckbox("##weaponVisible", current, out on); + + public bool DrawWetness(bool current, out bool on) + => DrawCheckbox("##wetness", current, out on); +} diff --git a/Glamourer/Gui/Equipment/ItemCombo.cs b/Glamourer/Gui/Equipment/ItemCombo.cs new file mode 100644 index 0000000..e324656 --- /dev/null +++ b/Glamourer/Gui/Equipment/ItemCombo.cs @@ -0,0 +1,103 @@ +using System.Collections.Generic; +using System.Linq; +using Dalamud.Data; +using Glamourer.Services; +using ImGuiNET; +using Lumina.Excel.GeneratedSheets; +using OtterGui; +using OtterGui.Classes; +using OtterGui.Raii; +using OtterGui.Widgets; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; + +namespace Glamourer.Gui.Equipment; + +public sealed class ItemCombo : FilterComboCache +{ + public readonly string Label; + private uint _currentItem; + + public ItemCombo(DataManager gameData, ItemManager items, EquipSlot slot) + : base(() => GetItems(items, slot)) + { + Label = GetLabel(gameData, slot); + _currentItem = ItemManager.NothingId(slot); + } + + protected override void DrawList(float width, float itemHeight) + { + base.DrawList(width, itemHeight); + if (NewSelection != null && Items.Count > NewSelection.Value) + CurrentSelection = Items[NewSelection.Value]; + } + + protected override int UpdateCurrentSelected(int currentSelected) + { + if (CurrentSelection.Id == _currentItem) + return currentSelected; + + CurrentSelectionIdx = Items.IndexOf(i => i.Id == _currentItem); + CurrentSelection = CurrentSelectionIdx >= 0 ? Items[CurrentSelectionIdx] : default; + return base.UpdateCurrentSelected(CurrentSelectionIdx); + + } + + public bool Draw(string previewName, uint previewIdx, float width) + { + _currentItem = previewIdx; + return Draw(Label, previewName, string.Empty, width, ImGui.GetTextLineHeightWithSpacing()); + } + + protected override bool DrawSelectable(int globalIdx, bool selected) + { + var obj = Items[globalIdx]; + var name = ToString(obj); + var ret = ImGui.Selectable(name, selected); + ImGui.SameLine(); + using var color = ImRaii.PushColor(ImGuiCol.Text, 0xFF808080); + ImGuiUtil.RightAlign($"({obj.ModelId.Value}-{obj.Variant})"); + return ret; + } + + protected override bool IsVisible(int globalIndex, LowerString filter) + => base.IsVisible(globalIndex, filter) || filter.IsContained(Items[globalIndex].ModelId.Value.ToString()); + + protected override string ToString(EquipItem obj) + => obj.Name; + + private static string GetLabel(DataManager gameData, EquipSlot slot) + { + var sheet = gameData.GetExcelSheet()!; + + return slot switch + { + EquipSlot.Head => sheet.GetRow(740)?.Text.ToString() ?? "Head", + EquipSlot.Body => sheet.GetRow(741)?.Text.ToString() ?? "Body", + EquipSlot.Hands => sheet.GetRow(742)?.Text.ToString() ?? "Hands", + EquipSlot.Legs => sheet.GetRow(744)?.Text.ToString() ?? "Legs", + EquipSlot.Feet => sheet.GetRow(745)?.Text.ToString() ?? "Feet", + EquipSlot.Ears => sheet.GetRow(746)?.Text.ToString() ?? "Ears", + EquipSlot.Neck => sheet.GetRow(747)?.Text.ToString() ?? "Neck", + EquipSlot.Wrists => sheet.GetRow(748)?.Text.ToString() ?? "Wrists", + EquipSlot.RFinger => sheet.GetRow(749)?.Text.ToString() ?? "Right Ring", + EquipSlot.LFinger => sheet.GetRow(750)?.Text.ToString() ?? "Left Ring", + _ => string.Empty, + }; + } + + private static IReadOnlyList GetItems(ItemManager items, EquipSlot slot) + { + var nothing = ItemManager.NothingItem(slot); + if (!items.ItemService.AwaitedService.TryGetValue(slot.ToEquipType(), out var list)) + return new[] + { + nothing, + }; + + var enumerable = list.AsEnumerable(); + if (slot.IsEquipment()) + enumerable = enumerable.Append(ItemManager.SmallClothesItem(slot)); + return enumerable.OrderBy(i => i.Name).Prepend(nothing).ToList(); + } +} diff --git a/Glamourer/Gui/Equipment/WeaponCombo.cs b/Glamourer/Gui/Equipment/WeaponCombo.cs new file mode 100644 index 0000000..4fb112e --- /dev/null +++ b/Glamourer/Gui/Equipment/WeaponCombo.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Glamourer.Services; +using ImGuiNET; +using OtterGui; +using OtterGui.Classes; +using OtterGui.Raii; +using OtterGui.Widgets; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; + +namespace Glamourer.Gui.Equipment; + +public sealed class WeaponCombo : FilterComboCache +{ + public readonly string Label; + private uint _currentItemId; + + public WeaponCombo(ItemManager items, FullEquipType type) + : base(() => GetWeapons(items, type)) + => Label = GetLabel(type); + + protected override void DrawList(float width, float itemHeight) + { + base.DrawList(width, itemHeight); + if (NewSelection != null && Items.Count > NewSelection.Value) + CurrentSelection = Items[NewSelection.Value]; + } + + protected override int UpdateCurrentSelected(int currentSelected) + { + if (CurrentSelection.Id == _currentItemId) + return currentSelected; + + CurrentSelectionIdx = Items.IndexOf(i => i.Id == _currentItemId); + CurrentSelection = CurrentSelectionIdx >= 0 ? Items[CurrentSelectionIdx] : default; + return base.UpdateCurrentSelected(CurrentSelectionIdx); + } + + public bool Draw(string previewName, uint previewId, float width) + { + _currentItemId = previewId; + return Draw(Label, previewName, string.Empty, width, ImGui.GetTextLineHeightWithSpacing()); + } + + protected override bool DrawSelectable(int globalIdx, bool selected) + { + var obj = Items[globalIdx]; + var name = ToString(obj); + var ret = ImGui.Selectable(name, selected); + ImGui.SameLine(); + using var color = ImRaii.PushColor(ImGuiCol.Text, 0xFF808080); + ImGuiUtil.RightAlign($"({obj.ModelId.Value}-{obj.WeaponType.Value}-{obj.Variant})"); + return ret; + } + + protected override bool IsVisible(int globalIndex, LowerString filter) + => base.IsVisible(globalIndex, filter) || filter.IsContained(Items[globalIndex].ModelId.Value.ToString()); + + protected override string ToString(EquipItem obj) + => obj.Name; + + private static string GetLabel(FullEquipType type) + => type is FullEquipType.Unknown ? "Mainhand" : type.ToName(); + + private static IReadOnlyList GetWeapons(ItemManager items, FullEquipType type) + { + if (type is FullEquipType.Unknown) + { + var enumerable = Array.Empty().AsEnumerable(); + foreach (var t in Enum.GetValues().Where(e => e.ToSlot() is EquipSlot.MainHand)) + { + if (items.ItemService.AwaitedService.TryGetValue(t, out var l)) + enumerable = enumerable.Concat(l); + } + + return enumerable.OrderBy(e => e.Name).ToList(); + } + + if (!items.ItemService.AwaitedService.TryGetValue(type, out var list)) + return Array.Empty(); + + if (type.ToSlot() is EquipSlot.OffHand && !type.IsOffhandType()) + return list.OrderBy(e => e.Name).Prepend(ItemManager.NothingItem(type)).ToList(); + + return list.OrderBy(e => e.Name).ToList(); + } +} diff --git a/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs b/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs index b220ad7..52bd555 100644 --- a/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs +++ b/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs @@ -1,5 +1,8 @@ using System.Numerics; +using Glamourer.Customization; +using Glamourer.Events; using Glamourer.Gui.Customization; +using Glamourer.Gui.Equipment; using Glamourer.Interop.Structs; using Glamourer.State; using ImGuiNET; @@ -14,6 +17,7 @@ public class ActorPanel private readonly ActorSelector _selector; private readonly StateManager _stateManager; private readonly CustomizationDrawer _customizationDrawer; + private readonly EquipmentDrawer _equipmentDrawer; private ActorIdentifier _identifier; private string _actorName = string.Empty; @@ -21,11 +25,13 @@ public class ActorPanel private ActorData _data; private ActorState? _state; - public ActorPanel(ActorSelector selector, StateManager stateManager, CustomizationDrawer customizationDrawer) + public ActorPanel(ActorSelector selector, StateManager stateManager, CustomizationDrawer customizationDrawer, + EquipmentDrawer equipmentDrawer) { _selector = selector; _stateManager = stateManager; _customizationDrawer = customizationDrawer; + _equipmentDrawer = equipmentDrawer; } public void Draw() @@ -76,46 +82,40 @@ public class ActorPanel return; if (_customizationDrawer.Draw(_state.ModelData.Customize, false)) + _stateManager.ChangeCustomize(_state, _customizationDrawer.Customize, _customizationDrawer.Changed, StateChanged.Source.Manual); + + foreach (var slot in EquipSlotExtensions.EqdpSlots) { + var stain = _state.ModelData.Stain(slot); + if (_equipmentDrawer.DrawStain(stain, slot, out var newStain)) + _stateManager.ChangeStain(_state, slot, newStain.RowIndex, StateChanged.Source.Manual); + + ImGui.SameLine(); + var armor = _state.ModelData.Item(slot); + if (_equipmentDrawer.DrawArmor(armor, slot, out var newArmor, _state.ModelData.Customize.Gender, _state.ModelData.Customize.Race)) + _stateManager.ChangeEquip(_state, slot, newArmor, newStain.RowIndex, StateChanged.Source.Manual); + } + + var mhStain = _state.ModelData.Stain(EquipSlot.MainHand); + if (_equipmentDrawer.DrawStain(mhStain, EquipSlot.MainHand, out var newMhStain)) + _stateManager.ChangeStain(_state, EquipSlot.MainHand, newMhStain.RowIndex, StateChanged.Source.Manual); + + ImGui.SameLine(); + var mh = _state.ModelData.Item(EquipSlot.MainHand); + if (_equipmentDrawer.DrawMainhand(mh, false, out var newMh)) + _stateManager.ChangeEquip(_state, EquipSlot.MainHand, newMh, newMhStain.RowIndex, StateChanged.Source.Manual); + + if (newMh.Type.Offhand() is not FullEquipType.Unknown) + { + var ohStain = _state.ModelData.Stain(EquipSlot.OffHand); + if (_equipmentDrawer.DrawStain(ohStain, EquipSlot.OffHand, out var newOhStain)) + _stateManager.ChangeStain(_state, EquipSlot.OffHand, newOhStain.RowIndex, StateChanged.Source.Manual); + + ImGui.SameLine(); + var oh = _state.ModelData.Item(EquipSlot.OffHand); + if (_equipmentDrawer.DrawMainhand(oh, false, out var newOh)) + _stateManager.ChangeEquip(_state, EquipSlot.OffHand, newOh, newOhStain.RowIndex, StateChanged.Source.Manual); } - // if (_currentData.Valid) - // _currentSave.Initialize(_items, _currentData.Objects[0]); - // - // RevertButton(); - // ActorDebug.Draw(_currentSave.ModelData); - // return; - // - // if (_main._customizationDrawer.Draw(_currentSave.ModelData.Customize, _identifier.Type == IdentifierType.Special)) - // _activeDesigns.ChangeCustomize(_currentSave, _main._customizationDrawer.Changed, _main._customizationDrawer.Customize.Data, - // false); - // - // foreach (var slot in EquipSlotExtensions.EqdpSlots) - // { - // var current = _currentSave.Armor(slot); - // if (_main._equipmentDrawer.DrawStain(current.Stain, slot, out var stain)) - // _activeDesigns.ChangeStain(_currentSave, slot, stain.RowIndex, false); - // ImGui.SameLine(); - // if (_main._equipmentDrawer.DrawArmor(current, slot, out var armor, _currentSave.ModelData.Customize.Gender, - // _currentSave.ModelData.Customize.Race)) - // _activeDesigns.ChangeEquipment(_currentSave, slot, armor, false); - // } - // - // var currentMain = _currentSave.WeaponMain; - // if (_main._equipmentDrawer.DrawStain(currentMain.Stain, EquipSlot.MainHand, out var stainMain)) - // _activeDesigns.ChangeStain(_currentSave, EquipSlot.MainHand, stainMain.RowIndex, false); - // ImGui.SameLine(); - // _main._equipmentDrawer.DrawMainhand(currentMain, true, out var main); - // if (currentMain.Type.Offhand() != FullEquipType.Unknown) - // { - // var currentOff = _currentSave.WeaponOff; - // if (_main._equipmentDrawer.DrawStain(currentOff.Stain, EquipSlot.OffHand, out var stainOff)) - // _activeDesigns.ChangeStain(_currentSave, EquipSlot.OffHand, stainOff.RowIndex, false); - // ImGui.SameLine(); - // _main._equipmentDrawer.DrawOffhand(currentOff, main.Type, out var off); - // } - // - // if (_main._equipmentDrawer.DrawVisor(_currentSave, out var value)) - // _activeDesigns.ChangeVisor(_currentSave, value, false); } diff --git a/Glamourer/Gui/Tabs/DebugTab.cs b/Glamourer/Gui/Tabs/DebugTab.cs index 09157ba..03af378 100644 --- a/Glamourer/Gui/Tabs/DebugTab.cs +++ b/Glamourer/Gui/Tabs/DebugTab.cs @@ -9,6 +9,7 @@ using Dalamud.Interface; using Dalamud.Plugin; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using Glamourer.Api; +using Glamourer.Automation; using Glamourer.Customization; using Glamourer.Designs; using Glamourer.Events; @@ -40,13 +41,16 @@ public unsafe class DebugTab : ITab private readonly ObjectTable _objects; private readonly ObjectManager _objectManager; private readonly GlamourerIpc _ipc; + private readonly PhrasingService _phrasing; private readonly ItemManager _items; private readonly ActorService _actors; private readonly CustomizationService _customization; + private readonly JobService _jobs; - private readonly DesignManager _designManager; - private readonly DesignFileSystem _designFileSystem; + private readonly DesignManager _designManager; + private readonly DesignFileSystem _designFileSystem; + private readonly AutoDesignManager _autoDesignManager; private readonly PenumbraChangedItemTooltip _penumbraTooltip; @@ -61,7 +65,8 @@ public unsafe class DebugTab : ITab UpdateSlotService updateSlotService, WeaponService weaponService, PenumbraService penumbra, ActorService actors, ItemManager items, CustomizationService customization, ObjectManager objectManager, DesignFileSystem designFileSystem, DesignManager designManager, StateManager state, Configuration config, - PenumbraChangedItemTooltip penumbraTooltip, MetaService metaService, GlamourerIpc ipc, DalamudPluginInterface pluginInterface) + PenumbraChangedItemTooltip penumbraTooltip, MetaService metaService, GlamourerIpc ipc, DalamudPluginInterface pluginInterface, + AutoDesignManager autoDesignManager, JobService jobs, PhrasingService phrasing) { _changeCustomizeService = changeCustomizeService; _visorService = visorService; @@ -81,6 +86,9 @@ public unsafe class DebugTab : ITab _metaService = metaService; _ipc = ipc; _pluginInterface = pluginInterface; + _autoDesignManager = autoDesignManager; + _jobs = jobs; + _phrasing = phrasing; } public ReadOnlySpan Label @@ -97,6 +105,7 @@ public unsafe class DebugTab : ITab DrawPenumbraHeader(); DrawDesigns(); DrawState(); + DrawAutoDesigns(); DrawIpc(); } @@ -376,14 +385,22 @@ public unsafe class DebugTab : ITab if (ImGui.SmallButton("++")) { - modelCustomize.Set(type, (CustomizeValue)(modelCustomize[type].Value + 1)); + var value = modelCustomize[type].Value; + var (_, mask) = type.ToByteAndMask(); + var shift = BitOperations.TrailingZeroCount(mask); + var newValue = value + (1 << shift); + modelCustomize.Set(type, (CustomizeValue)newValue); _changeCustomizeService.UpdateCustomize(model, modelCustomize.Data); } ImGui.SameLine(); if (ImGui.SmallButton("--")) { - modelCustomize.Set(type, (CustomizeValue)(modelCustomize[type].Value - 1)); + var value = modelCustomize[type].Value; + var (_, mask) = type.ToByteAndMask(); + var shift = BitOperations.TrailingZeroCount(mask); + var newValue = value - (1 << shift); + modelCustomize.Set(type, (CustomizeValue)newValue); _changeCustomizeService.UpdateCustomize(model, modelCustomize.Data); } @@ -483,6 +500,44 @@ public unsafe class DebugTab : ITab DrawItemService(); DrawStainService(); DrawCustomizationService(); + DrawJobService(); + } + + private void DrawJobService() + { + using var tree = ImRaii.TreeNode("Job Service"); + if (!tree) + return; + + using (var t = ImRaii.TreeNode("Jobs")) + { + if (t) + { + using var table = ImRaii.Table("##jobs", 3, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); + if (table) + foreach (var (id, job) in _jobs.Jobs) + { + ImGuiUtil.DrawTableColumn(id.ToString("D2")); + ImGuiUtil.DrawTableColumn(job.Name); + ImGuiUtil.DrawTableColumn(job.Abbreviation); + } + } + } + + using (var t = ImRaii.TreeNode("Job Groups")) + { + if (t) + { + using var table = ImRaii.Table("##groups", 3, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); + if (table) + foreach (var (id, group) in _jobs.JobGroups) + { + ImGuiUtil.DrawTableColumn(id.ToString("D2")); + ImGuiUtil.DrawTableColumn(group.Name); + ImGuiUtil.DrawTableColumn(group.Count.ToString()); + } + } + } } private string _gamePath = string.Empty; @@ -1116,6 +1171,66 @@ public unsafe class DebugTab : ITab #endregion + #region Auto Designs + + private void DrawAutoDesigns() + { + if (!ImGui.CollapsingHeader("Auto Designs")) + return; + + DrawPhrasingService(); + + foreach (var (set, idx) in _autoDesignManager.WithIndex()) + { + using var id = ImRaii.PushId(idx); + using var tree = ImRaii.TreeNode(set.Name); + if (!tree) + continue; + + using var table = ImRaii.Table("##autoDesign", 2, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); + if (!table) + continue; + + ImGuiUtil.DrawTableColumn("Name"); + ImGuiUtil.DrawTableColumn(set.Name); + + ImGuiUtil.DrawTableColumn("Index"); + ImGuiUtil.DrawTableColumn(idx.ToString()); + + ImGuiUtil.DrawTableColumn("Enabled"); + ImGuiUtil.DrawTableColumn(set.Enabled.ToString()); + + ImGuiUtil.DrawTableColumn("Actor"); + ImGuiUtil.DrawTableColumn(set.Identifier.ToString()); + + foreach (var (design, designIdx) in set.Designs.WithIndex()) + { + ImGuiUtil.DrawTableColumn($"{design.Design.Name} ({designIdx})"); + ImGuiUtil.DrawTableColumn($"{design.ApplicationType} {design.Jobs.Name}"); + } + } + } + + 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 IPC private string _gameObjectName = string.Empty; diff --git a/Glamourer/Gui/Tabs/DesignTab/DesignPanel.cs b/Glamourer/Gui/Tabs/DesignTab/DesignPanel.cs index c7ad35e..3f1552b 100644 --- a/Glamourer/Gui/Tabs/DesignTab/DesignPanel.cs +++ b/Glamourer/Gui/Tabs/DesignTab/DesignPanel.cs @@ -2,6 +2,7 @@ using Glamourer.Customization; using Glamourer.Designs; using Glamourer.Gui.Customization; +using Glamourer.Gui.Equipment; using Glamourer.Interop; using Glamourer.Interop.Penumbra; using Glamourer.State; @@ -21,24 +22,17 @@ public class DesignPanel 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; + private readonly EquipmentDrawer _equipmentDrawer; public DesignPanel(DesignFileSystemSelector selector, CustomizationDrawer customizationDrawer, DesignManager manager, ObjectManager objects, - StateManager state, PenumbraService penumbra, ChangeCustomizeService changeCustomizeService, WeaponService weaponService, - UpdateSlotService updateSlot) + StateManager state, EquipmentDrawer equipmentDrawer) { - _selector = selector; - _customizationDrawer = customizationDrawer; - _manager = manager; - _objects = objects; - _state = state; - _penumbra = penumbra; - _changeCustomizeService = changeCustomizeService; - _weaponService = weaponService; - _updateSlot = updateSlot; + _selector = selector; + _customizationDrawer = customizationDrawer; + _manager = manager; + _objects = objects; + _state = state; + _equipmentDrawer = equipmentDrawer; } public void Draw() @@ -60,5 +54,38 @@ public class DesignPanel } _customizationDrawer.Draw(design.DesignData.Customize, design.WriteProtected()); + + foreach (var slot in EquipSlotExtensions.EqdpSlots) + { + var stain = design.DesignData.Stain(slot); + if (_equipmentDrawer.DrawStain(stain, slot, out var newStain)) + _manager.ChangeStain(design, slot, newStain.RowIndex); + + ImGui.SameLine(); + var armor = design.DesignData.Item(slot); + if (_equipmentDrawer.DrawArmor(armor, slot, out var newArmor, design.DesignData.Customize.Gender, design.DesignData.Customize.Race)) + _manager.ChangeEquip(design, slot, newArmor); + } + + var mhStain = design.DesignData.Stain(EquipSlot.MainHand); + if (_equipmentDrawer.DrawStain(mhStain, EquipSlot.MainHand, out var newMhStain)) + _manager.ChangeStain(design, EquipSlot.MainHand, newMhStain.RowIndex); + + ImGui.SameLine(); + var mh = design.DesignData.Item(EquipSlot.MainHand); + if (_equipmentDrawer.DrawMainhand(mh, true, out var newMh)) + _manager.ChangeWeapon(design, EquipSlot.MainHand, newMh); + + if (newMh.Type.Offhand() is not FullEquipType.Unknown) + { + var ohStain = design.DesignData.Stain(EquipSlot.OffHand); + if (_equipmentDrawer.DrawStain(ohStain, EquipSlot.OffHand, out var newOhStain)) + _manager.ChangeStain(design, EquipSlot.OffHand, newOhStain.RowIndex); + + ImGui.SameLine(); + var oh = design.DesignData.Item(EquipSlot.OffHand); + if (_equipmentDrawer.DrawMainhand(oh, false, out var newOh)) + _manager.ChangeWeapon(design, EquipSlot.OffHand, newOh); + } } } diff --git a/Glamourer/Gui/Tabs/SettingsTab.cs b/Glamourer/Gui/Tabs/SettingsTab.cs index e2d56a5..d8e00b4 100644 --- a/Glamourer/Gui/Tabs/SettingsTab.cs +++ b/Glamourer/Gui/Tabs/SettingsTab.cs @@ -3,6 +3,7 @@ using System.Runtime.CompilerServices; using Dalamud.Interface; using Glamourer.Gui.Tabs.DesignTab; using Glamourer.Interop.Penumbra; +using Glamourer.Services; using Glamourer.State; using ImGuiNET; using OtterGui; @@ -16,19 +17,25 @@ public class SettingsTab : ITab private readonly Configuration _config; private readonly DesignFileSystemSelector _selector; private readonly StateListener _stateListener; + private readonly PhrasingService _phrasingService; private readonly PenumbraAutoRedraw _autoRedraw; - public SettingsTab(Configuration config, DesignFileSystemSelector selector, StateListener stateListener, PenumbraAutoRedraw autoRedraw) + public SettingsTab(Configuration config, DesignFileSystemSelector selector, StateListener stateListener, + PhrasingService phrasingService, PenumbraAutoRedraw autoRedraw) { - _config = config; - _selector = selector; - _stateListener = stateListener; - _autoRedraw = autoRedraw; + _config = config; + _selector = selector; + _stateListener = stateListener; + _phrasingService = phrasingService; + _autoRedraw = autoRedraw; } public ReadOnlySpan Label => "Settings"u8; + private string? _tmpPhrasing1 = null; + private string? _tmpPhrasing2 = null; + public void DrawContent() { using var child = ImRaii.Child("MainWindowChild"); @@ -36,6 +43,8 @@ public class SettingsTab : ITab return; Checkbox("Enabled", "Enable main functionality of keeping and applying state.", _stateListener.Enabled, _stateListener.Enable); + Checkbox("Enable Auto Designs", "Enable the application of designs associated to characters to be applied automatically.", + _config.EnableAutoDesigns, v => _config.EnableAutoDesigns = v); 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); @@ -53,6 +62,24 @@ 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; + } + MainWindow.DrawSupportButtons(); } diff --git a/Glamourer/Interop/JobService.cs b/Glamourer/Interop/JobService.cs new file mode 100644 index 0000000..877f6a1 --- /dev/null +++ b/Glamourer/Interop/JobService.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using Dalamud.Data; +using Dalamud.Hooking; +using Dalamud.Utility.Signatures; +using FFXIVClientStructs.FFXIV.Client.Game.Character; +using Glamourer.Interop.Structs; +using Glamourer.Structs; + +namespace Glamourer.Interop; + +public class JobService : IDisposable +{ + private readonly nint _characterDataOffset; + + public readonly IReadOnlyDictionary Jobs; + public readonly IReadOnlyDictionary JobGroups; + + public event Action? JobChanged; + + public JobService(DataManager gameData) + { + SignatureHelper.Initialise(this); + _characterDataOffset = Marshal.OffsetOf(nameof(Character.CharacterData)); + Jobs = GameData.Jobs(gameData); + JobGroups = GameData.JobGroups(gameData); + _changeJobHook.Enable(); + } + + public void Dispose() + { + _changeJobHook.Dispose(); + } + + private delegate void ChangeJobDelegate(nint data, uint job); + + [Signature(Sigs.ChangeJob, DetourName = nameof(ChangeJobDetour))] + private readonly Hook _changeJobHook = null!; + + private void ChangeJobDetour(nint data, uint jobIndex) + { + _changeJobHook.Original(data, jobIndex); + var actor = (Actor)(data - _characterDataOffset); + var job = Jobs.TryGetValue((byte) jobIndex, out var j) ? j : Jobs[0]; + Glamourer.Log.Excessive($"{actor} changed job to {job}"); + JobChanged?.Invoke(actor, job); + } +} diff --git a/Glamourer/Interop/Structs/Model.cs b/Glamourer/Interop/Structs/Model.cs index 0ebda54..76d15a5 100644 --- a/Glamourer/Interop/Structs/Model.cs +++ b/Glamourer/Interop/Structs/Model.cs @@ -88,7 +88,7 @@ public readonly unsafe struct Model : IEquatable /// Only valid for humans. public CharacterArmor GetArmor(EquipSlot slot) - => ((CharacterArmor*)AsHuman->EquipSlotData)[slot.ToIndex()]; + => ((CharacterArmor*)&AsHuman->Head)[slot.ToIndex()]; public Customize GetCustomize() => *(Customize*)&AsHuman->Customize; diff --git a/Glamourer/Interop/WeaponService.cs b/Glamourer/Interop/WeaponService.cs index 96f25bb..70c386e 100644 --- a/Glamourer/Interop/WeaponService.cs +++ b/Glamourer/Interop/WeaponService.cs @@ -51,11 +51,14 @@ public unsafe class WeaponService : IDisposable _ => EquipSlot.Unknown, }; + var tmpWeapon = weapon; // First call the regular function. if (equipSlot is not EquipSlot.Unknown) - _event.Invoke(actor, equipSlot, ref weapon); + _event.Invoke(actor, equipSlot, ref tmpWeapon); _loadWeaponHook.Original(drawData, slot, weapon.Value, redrawOnEquality, unk2, skipGameObject, unk4); + if (tmpWeapon.Value != weapon.Value) + _loadWeaponHook.Original(drawData, slot, tmpWeapon.Value, 1, unk2, 1, unk4); Glamourer.Log.Excessive( $"Weapon reloaded for 0x{actor.Address:X} ({actor.Utf8Name}) with attributes {slot} {weapon.Value:X14}, {redrawOnEquality}, {unk2}, {skipGameObject}, {unk4}"); } @@ -88,8 +91,10 @@ public unsafe class WeaponService : IDisposable public void LoadStain(Actor character, EquipSlot slot, StainId stain) { - var value = slot == EquipSlot.OffHand ? character.AsCharacter->DrawData.OffHandModel : character.AsCharacter->DrawData.MainHandModel; - var weapon = new CharacterWeapon(value.Value) { Stain = stain.Value }; + var mdl = character.Model; + var (_, _, mh, oh) = mdl.GetWeapons(character); + var value = slot == EquipSlot.OffHand ? oh : mh; + var weapon = value.With(value.Set.Value == 0 ? 0 : stain); LoadWeapon(character, slot, weapon); } -} \ No newline at end of file +} diff --git a/Glamourer/Services/ConfigMigrationService.cs b/Glamourer/Services/ConfigMigrationService.cs index a37c67c..762dde9 100644 --- a/Glamourer/Services/ConfigMigrationService.cs +++ b/Glamourer/Services/ConfigMigrationService.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using Glamourer.Automation; using Glamourer.Gui; using Newtonsoft.Json.Linq; @@ -7,13 +8,17 @@ namespace Glamourer.Services; public class ConfigMigrationService { - private readonly SaveService _saveService; + private readonly SaveService _saveService; + private readonly FixedDesignMigrator _fixedDesignMigrator; private Configuration _config = null!; private JObject _data = null!; - public ConfigMigrationService(SaveService saveService) - => _saveService = saveService; + public ConfigMigrationService(SaveService saveService, FixedDesignMigrator fixedDesignMigrator) + { + _saveService = saveService; + _fixedDesignMigrator = fixedDesignMigrator; + } public void Migrate(Configuration config) { @@ -34,6 +39,7 @@ public class ConfigMigrationService if (_config.Version > 1) return; + _fixedDesignMigrator.Migrate(_data["FixedDesigns"]); _config.Version = 2; var customizationColor = _data["CustomizationColor"]?.ToObject() ?? ColorId.CustomizationDesign.Data().DefaultColor; _config.Colors[ColorId.CustomizationDesign] = customizationColor; diff --git a/Glamourer/Services/CustomizationService.cs b/Glamourer/Services/CustomizationService.cs index 41e316e..0e23120 100644 --- a/Glamourer/Services/CustomizationService.cs +++ b/Glamourer/Services/CustomizationService.cs @@ -14,6 +14,42 @@ public sealed class CustomizationService : AsyncServiceWrapper CustomizationManager.Create(pi, gameData)) { } + public (Customize NewValue, CustomizeFlag Applied) Combine(Customize oldValues, Customize newValues, CustomizeFlag applyWhich) + { + CustomizeFlag applied = 0; + Customize ret = default; + ret.Load(oldValues); + if (applyWhich.HasFlag(CustomizeFlag.Clan)) + { + ChangeClan(ref ret, newValues.Clan); + applied |= CustomizeFlag.Clan; + } + + if (applyWhich.HasFlag(CustomizeFlag.Gender)) + if (ret.Race is not Race.Hrothgar || newValues.Gender is not Gender.Female) + { + ChangeGender(ref ret, newValues.Gender); + applied |= CustomizeFlag.Gender; + } + + var set = AwaitedService.GetList(ret.Clan, ret.Gender); + foreach (var index in Enum.GetValues()) + { + var flag = index.ToFlag(); + if (!applyWhich.HasFlag(flag)) + continue; + + var value = newValues[index]; + if (IsCustomizationValid(set, ret.Face, index, value)) + { + ret[index] = value; + applied |= flag; + } + } + + return (ret, applied); + } + /// In languages other than english the actual clan name may depend on gender. public string ClanName(SubRace race, Gender gender) { @@ -175,53 +211,59 @@ public sealed class CustomizationService : AsyncServiceWrapper Change a clan while keeping all other customizations valid. - public bool ChangeClan(ref Customize customize, SubRace newClan) + public CustomizeFlag ChangeClan(ref Customize customize, SubRace newClan) { if (customize.Clan == newClan) - return false; + return 0; if (ValidateClan(newClan, newClan.ToRace(), out var newRace, out newClan).Length > 0) - return false; + return 0; + var flags = CustomizeFlag.Clan | CustomizeFlag.Race; customize.Race = newRace; customize.Clan = newClan; // TODO Female Hrothgar if (newRace == Race.Hrothgar) - customize.Gender = Gender.Male; + { + customize.Gender = Gender.Male; + flags |= CustomizeFlag.Gender; + } var set = AwaitedService.GetList(customize.Clan, customize.Gender); - FixValues(set, ref customize); - - return true; + return FixValues(set, ref customize) | flags; } /// Change a gender while keeping all other customizations valid. - public bool ChangeGender(ref Customize customize, Gender newGender) + public CustomizeFlag ChangeGender(ref Customize customize, Gender newGender) { if (customize.Gender == newGender) - return false; + return 0; // TODO Female Hrothgar if (customize.Race is Race.Hrothgar) - return false; + return 0; if (ValidateGender(customize.Race, newGender, out newGender).Length > 0) - return false; + return 0; customize.Gender = newGender; var set = AwaitedService.GetList(customize.Clan, customize.Gender); - FixValues(set, ref customize); - - return true; + return FixValues(set, ref customize) | CustomizeFlag.Gender; } - private static void FixValues(CustomizationSet set, ref Customize customize) + private static CustomizeFlag FixValues(CustomizationSet set, ref Customize customize) { + CustomizeFlag flags = 0; foreach (var idx in Enum.GetValues().Where(set.IsAvailable)) { if (ValidateCustomizeValue(set, customize.Face, idx, customize[idx], out var fixedValue).Length > 0) - customize[idx] = fixedValue; + { + customize[idx] = fixedValue; + flags |= idx.ToFlag(); + } } + + return flags; } } diff --git a/Glamourer/Services/FilenameService.cs b/Glamourer/Services/FilenameService.cs index 344bb6b..e5f21ba 100644 --- a/Glamourer/Services/FilenameService.cs +++ b/Glamourer/Services/FilenameService.cs @@ -12,11 +12,13 @@ public class FilenameService public readonly string DesignFileSystem; public readonly string MigrationDesignFile; public readonly string DesignDirectory; + public readonly string AutomationFile; public FilenameService(DalamudPluginInterface pi) { ConfigDirectory = pi.ConfigDirectory.FullName; ConfigFile = pi.ConfigFile.FullName; + AutomationFile = Path.Combine(ConfigDirectory, "automation.json"); DesignFileSystem = Path.Combine(ConfigDirectory, "sort_order.json"); MigrationDesignFile = Path.Combine(ConfigDirectory, "Designs.json"); DesignDirectory = Path.Combine(ConfigDirectory, "designs"); diff --git a/Glamourer/Services/PhrasingService.cs b/Glamourer/Services/PhrasingService.cs new file mode 100644 index 0000000..c411549 --- /dev/null +++ b/Glamourer/Services/PhrasingService.cs @@ -0,0 +1,54 @@ +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 7d6ad25..2e2d2c7 100644 --- a/Glamourer/Services/ServiceManager.cs +++ b/Glamourer/Services/ServiceManager.cs @@ -1,9 +1,11 @@ using Dalamud.Plugin; using Glamourer.Api; +using Glamourer.Automation; using Glamourer.Designs; using Glamourer.Events; using Glamourer.Gui; using Glamourer.Gui.Customization; +using Glamourer.Gui.Equipment; using Glamourer.Gui.Tabs; using Glamourer.Gui.Tabs.ActorTab; using Glamourer.Gui.Tabs.DesignTab; @@ -47,6 +49,7 @@ public static class ServiceManager .AddSingleton() .AddSingleton() .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton(); @@ -54,6 +57,7 @@ public static class ServiceManager => services.AddSingleton() .AddSingleton() .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() @@ -74,11 +78,15 @@ public static class ServiceManager .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton(); + .AddSingleton() + .AddSingleton(); private static IServiceCollection AddDesigns(this IServiceCollection services) => services.AddSingleton() - .AddSingleton(); + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton(); private static IServiceCollection AddState(this IServiceCollection services) => services.AddSingleton() @@ -94,6 +102,7 @@ public static class ServiceManager .AddSingleton() .AddSingleton() .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() diff --git a/Glamourer/State/ActorState.cs b/Glamourer/State/ActorState.cs index 1077f84..c5e79a9 100644 --- a/Glamourer/State/ActorState.cs +++ b/Glamourer/State/ActorState.cs @@ -1,4 +1,5 @@ -using Glamourer.Customization; +using System; +using Glamourer.Customization; using Glamourer.Designs; using Glamourer.Events; using Glamourer.Structs; @@ -20,7 +21,7 @@ public class ActorState ModelId, } - public ActorIdentifier Identifier { get; internal init; } + public readonly ActorIdentifier Identifier; /// This should always represent the unmodified state of the draw object. public DesignData BaseData; @@ -33,7 +34,7 @@ public class ActorState .Repeat(StateChanged.Source.Game, EquipFlagExtensions.NumEquipFlags + CustomizationExtensions.NumIndices + 5).ToArray(); internal ActorState(ActorIdentifier identifier) - => Identifier = identifier; + => Identifier = identifier.CreatePermanent(); public ref StateChanged.Source this[EquipSlot slot, bool stain] => ref _sources[slot.ToIndex() + (stain ? EquipFlagExtensions.NumEquipFlags / 2 : 0)]; diff --git a/Glamourer/State/StateEditor.cs b/Glamourer/State/StateEditor.cs index 8de89aa..2d79a00 100644 --- a/Glamourer/State/StateEditor.cs +++ b/Glamourer/State/StateEditor.cs @@ -1,15 +1,22 @@ using System.Linq; using Glamourer.Customization; using Glamourer.Interop; +using Glamourer.Interop.Penumbra; using Glamourer.Interop.Structs; using Glamourer.Services; +using Penumbra.Api.Enums; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; namespace Glamourer.State; +/// +/// This class applies changes made to state to actual objects in the game. +/// It handles applying those changes as well as redrawing the actor if necessary. +/// public class StateEditor { + private readonly PenumbraService _penumbra; private readonly UpdateSlotService _updateSlot; private readonly VisorService _visor; private readonly WeaponService _weapon; @@ -17,45 +24,63 @@ public class StateEditor private readonly ItemManager _items; public StateEditor(UpdateSlotService updateSlot, VisorService visor, WeaponService weapon, ChangeCustomizeService changeCustomize, - ItemManager items) + ItemManager items, PenumbraService penumbra) { _updateSlot = updateSlot; _visor = visor; _weapon = weapon; _changeCustomize = changeCustomize; _items = items; + _penumbra = penumbra; } + /// Changing the model ID simply requires guaranteed redrawing. + public void ChangeModelId(ActorData data, uint modelId) + { + foreach (var actor in data.Objects) + _penumbra.RedrawObject(actor, RedrawType.Redraw); + } + /// + /// Change the customization values of actors either by applying them via update or redrawing, + /// this depends on whether the changes include changes to Race, Gender, Body Type or Face. + /// public void ChangeCustomize(ActorData data, Customize customize) { foreach (var actor in data.Objects) - _changeCustomize.UpdateCustomize(actor, customize.Data); - } - - public void ChangeCustomize(ActorData data, CustomizeIndex idx, CustomizeValue value) - { - foreach (var actor in data.Objects.Where(a => a.IsCharacter)) { - var mdl = actor.Model; - var customize = mdl.GetCustomize(); - customize[idx] = value; - _changeCustomize.UpdateCustomize(mdl, customize.Data); + var mdl = actor.Model; + if (!mdl.IsHuman) + continue; + + var flags = Customize.Compare(mdl.GetCustomize(), customize); + if (!flags.RequiresRedraw()) + _changeCustomize.UpdateCustomize(mdl, customize.Data); + else + _penumbra.RedrawObject(actor, RedrawType.Redraw); } } - public void ChangeArmor(ActorState state, ActorData data, EquipSlot slot) + /// + /// Change a single piece of armor and/or stain depending on slot. + /// This uses the current customization of the model to potentially prevent restricted gear types from appearing. + /// This never requires redrawing. + /// + public void ChangeArmor(ActorData data, EquipSlot slot, CharacterArmor 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); + var (_, resolvedItem) = _items.ResolveRestrictedGear(armor, slot, customize.Race, customize.Gender); _updateSlot.UpdateSlot(actor.Model, slot, resolvedItem); } } + /// + /// Change the stain of a single piece of armor or weapon. + /// If the offhand is empty, the stain will be fixed to 0 to prevent crashes. + /// public void ChangeStain(ActorData data, EquipSlot slot, StainId stain) { var idx = slot.ToIndex(); @@ -76,18 +101,34 @@ public class StateEditor } } - public void ChangeMainhand(ActorData data, EquipItem weapon) + /// Apply a weapon to the appropriate slot. + public void ChangeWeapon(ActorData data, EquipSlot slot, EquipItem item, StainId stain) { - foreach (var actor in data.Objects.Where(a => a.IsCharacter)) - _weapon.LoadWeapon(actor, EquipSlot.MainHand, weapon.Weapon()); + if (slot is EquipSlot.MainHand) + ChangeMainhand(data, item, stain); + else + ChangeOffhand(data, item, stain); } - public void ChangeOffhand(ActorData data, EquipItem weapon) + /// + /// Apply a weapon to the mainhand. If the weapon type has no associated offhand type, apply both. + /// + public void ChangeMainhand(ActorData data, EquipItem weapon, StainId stain) { + var slot = weapon.Type.Offhand() == FullEquipType.Unknown ? EquipSlot.BothHand : EquipSlot.MainHand; foreach (var actor in data.Objects.Where(a => a.IsCharacter)) - _weapon.LoadWeapon(actor, EquipSlot.OffHand, weapon.Weapon()); + _weapon.LoadWeapon(actor, slot, weapon.Weapon().With(stain)); } + /// Apply a weapon to the offhand. + public void ChangeOffhand(ActorData data, EquipItem weapon, StainId stain) + { + stain = weapon.ModelId.Value == 0 ? 0 : stain; + foreach (var actor in data.Objects.Where(a => a.IsCharacter)) + _weapon.LoadWeapon(actor, EquipSlot.OffHand, weapon.Weapon().With(stain)); + } + + /// Change the visor state of actors only on the draw object. public void ChangeVisor(ActorData data, bool value) { foreach (var actor in data.Objects.Where(a => a.IsCharacter)) @@ -100,18 +141,21 @@ public class StateEditor } } + /// Change the forced wetness state on actors. public unsafe void ChangeWetness(ActorData data, bool value) { foreach (var actor in data.Objects.Where(a => a.IsCharacter)) actor.AsCharacter->IsGPoseWet = value; } + /// Change the hat-visibility state on actors. public unsafe void ChangeHatState(ActorData data, bool value) { foreach (var actor in data.Objects.Where(a => a.IsCharacter)) actor.AsCharacter->DrawData.HideHeadgear(0, !value); } + /// Change the weapon-visibility state on actors. public unsafe void ChangeWeaponState(ActorData data, bool value) { foreach (var actor in data.Objects.Where(a => a.IsCharacter)) diff --git a/Glamourer/State/StateListener.cs b/Glamourer/State/StateListener.cs index 2fa50c7..bb7fa1b 100644 --- a/Glamourer/State/StateListener.cs +++ b/Glamourer/State/StateListener.cs @@ -1,4 +1,5 @@ using System; +using Glamourer.Automation; using Glamourer.Customization; using Glamourer.Events; using Glamourer.Interop.Penumbra; @@ -10,6 +11,11 @@ using Penumbra.GameData.Structs; namespace Glamourer.State; +/// +/// This class handles all game events that could cause a drawn model to change, +/// it always updates the base state for existing states, +/// and either discards the changes or updates the model state too. +/// public class StateListener : IDisposable { private readonly Configuration _config; @@ -22,6 +28,7 @@ public class StateListener : IDisposable private readonly HeadGearVisibilityChanged _headGearVisibility; private readonly VisorStateChanged _visorState; private readonly WeaponVisibilityChanged _weaponVisibility; + private readonly AutoDesignApplier _autoDesignApplier; public bool Enabled { @@ -31,7 +38,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) + HeadGearVisibilityChanged headGearVisibility, AutoDesignApplier autoDesignApplier) { _manager = manager; _items = items; @@ -43,6 +50,7 @@ public class StateListener : IDisposable _visorState = visorState; _weaponVisibility = weaponVisibility; _headGearVisibility = headGearVisibility; + _autoDesignApplier = autoDesignApplier; if (Enabled) Subscribe(); @@ -68,48 +76,83 @@ public class StateListener : IDisposable Unsubscribe(); } + /// The result of updating the base state of an ActorState. private enum UpdateState { + /// The base state is the same as prior state. NoChange, + + /// The game requests an update to a state that does not agree with the actor state. Transformed, + + /// The base state changed compared to prior state. Change, } + /// + /// Invoked when a new draw object is created from a game object. + /// We need to update all state: Model ID, Customize and Equipment. + /// Weapons and meta flags are updated independently. + /// We also need to apply fixed designs here (TODO). + /// 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); var modelId = *(uint*)modelPtr; ref var customize = ref *(Customize*)customizePtr; if (_manager.TryGetValue(identifier, out var state)) + { + _autoDesignApplier.Reduce(actor, identifier, state); switch (UpdateBaseData(actor, state, modelId, customizePtr, equipDataPtr)) { case UpdateState.Change: break; case UpdateState.Transformed: break; case UpdateState.NoChange: - UpdateBaseData(actor, state, customize); + switch (UpdateBaseData(actor, state, customize)) + { + case UpdateState.Transformed: break; + case UpdateState.Change: break; + case UpdateState.NoChange: + customize = state.ModelData.Customize; + break; + } + + foreach (var slot in EquipSlotExtensions.EqdpSlots) + HandleEquipSlot(actor, state, slot, ref ((CharacterArmor*)equipDataPtr)[slot.ToIndex()]); + break; } + } - if (_config.UseRestrictedGearProtection && modelId == 0) + if (modelId == 0) ProtectRestrictedGear(equipDataPtr, customize.Race, customize.Gender); } + /// + /// A draw model loads a new equipment piece. + /// Update base data, apply or update model data, and protect against restricted gear. + /// private void OnSlotUpdating(Model model, EquipSlot slot, Ref armor, Ref returnValue) { - // TODO handle hat state - var actor = _penumbra.GameObjectFromDrawObject(model); - var customize = model.GetCustomize(); + var actor = _penumbra.GameObjectFromDrawObject(model); if (actor.Identifier(_actors.AwaitedService, out var identifier) && _manager.TryGetValue(identifier, out var state)) - ApplyEquipmentPiece(actor, state, slot, ref armor.Value); + HandleEquipSlot(actor, state, slot, ref armor.Value); - if (_config.UseRestrictedGearProtection) - (_, armor.Value) = _items.RestrictedGear.ResolveRestricted(armor, slot, customize.Race, customize.Gender); + if (!_config.UseRestrictedGearProtection) + return; + + var customize = model.GetCustomize(); + (_, armor.Value) = _items.RestrictedGear.ResolveRestricted(armor, slot, customize.Race, customize.Gender); } + /// + /// A game object loads a new weapon. + /// Update base data, apply or update model data. + /// Verify consistent weapon types. + /// private void OnWeaponLoading(Actor actor, EquipSlot slot, Ref weapon) { if (!actor.Identifier(_actors.AwaitedService, out var identifier) @@ -117,134 +160,283 @@ public class StateListener : IDisposable 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 baseType = state.BaseData.Item(slot).Type; + var apply = false; + switch (UpdateBaseData(actor, state, slot, actorWeapon)) { - var oldActorItem = state.BaseData.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.BaseData.Item(EquipSlot.MainHand).Type : FullEquipType.Unknown); - state.BaseData.SetItem(slot, identified); + // Do nothing. But this usually can not happen because the hooked function also writes to game objects later. + case UpdateState.Transformed: break; + case UpdateState.Change: if (state[slot, false] is not StateChanged.Source.Fixed) { - state.ModelData.SetItem(slot, identified); + state.ModelData.SetItem(slot, state.BaseData.Item(slot)); state[slot, false] = StateChanged.Source.Game; } else { - actorWeapon.Set = stateItem.ModelId; - actorWeapon.Type = stateItem.Variant; - actorWeapon.Variant = stateItem.Variant; + apply = true; } - } - } - var stateStain = state.ModelData.Stain(slot); - if (actorWeapon.Stain.Value != stateStain.Value) - { - var oldActorStain = state.BaseData.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.BaseData.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) + if (state[slot, false] is not StateChanged.Source.Fixed) { - state.ModelData.Customize[idx] = value; - state[idx] = StateChanged.Source.Game; + state.ModelData.SetStain(slot, state.BaseData.Stain(slot)); + state[slot, true] = StateChanged.Source.Game; } else { - customize[idx] = stateValue; + apply = true; } - } + + break; + case UpdateState.NoChange: + apply = true; + break; + } + + if (apply) + { + // Only allow overwriting identical weapons + var newWeapon = state.ModelData.Weapon(slot); + if (baseType is FullEquipType.Unknown || baseType == state.ModelData.Item(slot).Type) + actorWeapon = newWeapon; + else if (actorWeapon.Set.Value != 0) + actorWeapon = actorWeapon.With(newWeapon.Stain); } } - private unsafe void ApplyEquipment(Actor actor, ActorState state, CharacterArmor* equipData) + /// Update base data for a single changed equipment slot. + private UpdateState UpdateBaseData(Actor actor, ActorState state, EquipSlot slot, CharacterArmor armor) { - // TODO: Handle hat state - foreach (var slot in EquipSlotExtensions.EqdpSlots) - ApplyEquipmentPiece(actor, state, slot, ref *equipData++); + var actorArmor = actor.GetArmor(slot); + // The actor armor does not correspond to the model armor, thus the actor is transformed. + // This also prevents it from changing values due to hat state. + if (actorArmor.Value != armor.Value) + return UpdateState.Transformed; + + 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 void ApplyEquipmentPiece(Actor actor, ActorState state, EquipSlot slot, ref CharacterArmor armor) + /// Handle a full equip slot update for base data and model data. + private void HandleEquipSlot(Actor actor, ActorState state, EquipSlot slot, ref CharacterArmor armor) { - var changeState = UpdateBaseData(actor, state, slot, armor); - if (changeState is UpdateState.Transformed) + switch (UpdateBaseData(actor, state, slot, armor)) + { + // Transformed also handles invisible hat state. + case UpdateState.Transformed: break; + // Base data changed equipment while actors were not there. + // Update model state if not on fixed design. + case UpdateState.Change: + var apply = false; + if (state[slot, false] is not StateChanged.Source.Fixed) + { + state.ModelData.SetItem(slot, state.BaseData.Item(slot)); + state[slot, false] = StateChanged.Source.Game; + } + else + { + apply = true; + } + + if (state[slot, true] is not StateChanged.Source.Fixed) + { + state.ModelData.SetStain(slot, state.BaseData.Stain(slot)); + state[slot, true] = StateChanged.Source.Game; + } + else + { + apply = true; + } + + if (apply) + armor = state.ModelData.Armor(slot); + + break; + // Use current model data. + case UpdateState.NoChange: + armor = state.ModelData.Armor(slot); + break; + } + } + + /// Update base data for a single changed weapon slot. + private UpdateState UpdateBaseData(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; + } + + /// + /// Update the base data starting with the model id. + /// If the model id changed, and is not a transformation, we need to reload the entire base state from scratch. + /// Non-Humans are handled differently than humans. + /// + private unsafe UpdateState UpdateBaseData(Actor actor, ActorState state, uint modelId, nint customizeData, nint equipData) + { + // Model ID does not agree between game object and new draw object => Transformation. + if (modelId != (uint)actor.AsCharacter->CharacterData.ModelCharaId) + return UpdateState.Transformed; + + // Model ID did not change to stored state. + if (modelId == state.BaseData.ModelId) + return UpdateState.NoChange; + + // Model ID did change, reload entire state accordingly. + if (modelId == 0) + state.BaseData.LoadNonHuman(modelId, *(Customize*)customizeData, (byte*)equipData); + else + state.BaseData = _manager.FromActor(actor); + + return UpdateState.Change; + } + + /// + /// Update the customize base data of a state. + /// This should rarely result in changes, + /// only if we kept track of state of someone who went to the aesthetician, + /// or if they used other tools to change things. + /// + private UpdateState UpdateBaseData(Actor actor, ActorState state, Customize customize) + { + // Customize array does not agree between game object and draw object => transformation. + if (!actor.GetCustomize().Equals(customize)) + return UpdateState.Transformed; + + // Customize array did not change to stored state. + if (state.BaseData.Customize.Equals(customize)) + return UpdateState.NoChange; + + // Update customize base state. + state.BaseData.Customize.Load(customize); + return UpdateState.Change; + } + + /// Handle visor state changes made by the game. + private void OnVisorChange(Model model, Ref value) + { + // Find appropriate actor and state. + // We do not need to handle fixed designs, + // since a fixed design would already have established state-tracking. + var actor = _penumbra.GameObjectFromDrawObject(model); + if (!actor.Identifier(_actors.AwaitedService, out var identifier)) return; - if (changeState is UpdateState.NoChange) + if (!_manager.TryGetValue(identifier, out var state)) + return; + + // Update visor base state. + if (state.BaseData.SetVisor(value)) { - armor = state.ModelData.Armor(slot); + // if base state changed, either overwrite the actual value if we have fixed values, + // or overwrite the stored model state with the new one. + if (state[ActorState.MetaFlag.VisorState] is StateChanged.Source.Fixed) + value.Value = state.ModelData.IsVisorToggled(); + else + _manager.ChangeVisorState(state, value, StateChanged.Source.Game); } else { - var modelArmor = state.ModelData.Armor(slot); - if (armor.Value == modelArmor.Value) - return; - - if (state[slot, false] is StateChanged.Source.Fixed) - { - armor.Set = modelArmor.Set; - armor.Variant = modelArmor.Variant; - } - else - { - _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); + // if base state did not change, overwrite the value with the model state one. + value.Value = state.ModelData.IsVisorToggled(); } } + /// Handle Hat Visibility changes. These act on the game object. + private void OnHeadGearVisibilityChange(Actor actor, Ref value) + { + // Find appropriate state. + // We do not need to handle fixed designs, + // if there is no model that caused a fixed design to exist yet, + // we also do not care about the invisible model. + if (!actor.Identifier(_actors.AwaitedService, out var identifier)) + return; + + if (!_manager.TryGetValue(identifier, out var state)) + return; + + // Update hat visibility state. + if (state.BaseData.SetHatVisible(value)) + { + // if base state changed, either overwrite the actual value if we have fixed values, + // or overwrite the stored model state with the new one. + if (state[ActorState.MetaFlag.HatState] is StateChanged.Source.Fixed) + value.Value = state.ModelData.IsHatVisible(); + else + _manager.ChangeHatState(state, value, StateChanged.Source.Game); + } + else + { + // if base state did not change, overwrite the value with the model state one. + value.Value = state.ModelData.IsHatVisible(); + } + } + + /// Handle Weapon Visibility changes. These act on the game object. + private void OnWeaponVisibilityChange(Actor actor, Ref value) + { + // Find appropriate state. + // We do not need to handle fixed designs, + // if there is no model that caused a fixed design to exist yet, + // we also do not care about the invisible model. + if (!actor.Identifier(_actors.AwaitedService, out var identifier)) + return; + + if (!_manager.TryGetValue(identifier, out var state)) + return; + + // Update weapon visibility state. + if (state.BaseData.SetWeaponVisible(value)) + { + // if base state changed, either overwrite the actual value if we have fixed values, + // or overwrite the stored model state with the new one. + if (state[ActorState.MetaFlag.WeaponState] is StateChanged.Source.Fixed) + value.Value = state.ModelData.IsWeaponVisible(); + else + _manager.ChangeWeaponState(state, value, StateChanged.Source.Game); + } + else + { + // if base state did not change, overwrite the value with the model state one. + value.Value = state.ModelData.IsWeaponVisible(); + } + } + + /// Protect a given equipment data array against restricted gear if enabled. private unsafe void ProtectRestrictedGear(nint equipDataPtr, Race race, Gender gender) { + if (!_config.UseRestrictedGearProtection) + return; + var idx = 0; var ptr = (CharacterArmor*)equipDataPtr; for (var end = ptr + 10; ptr < end; ++ptr) @@ -274,144 +466,4 @@ public class StateListener : IDisposable _headGearVisibility.Unsubscribe(OnHeadGearVisibilityChange); _weaponVisibility.Unsubscribe(OnWeaponVisibilityChange); } - - 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. - // This also prevents it from changing values due to hat state. - if (actorArmor.Value != armor.Value) - return UpdateState.Transformed; - - 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; - } - - private void OnVisorChange(Model model, Ref value) - { - var actor = _penumbra.GameObjectFromDrawObject(model); - if (!actor.Identifier(_actors.AwaitedService, out var identifier)) - return; - - if (!_manager.TryGetValue(identifier, out var state)) - return; - - if (state.BaseData.SetVisor(value)) - { - if (state[ActorState.MetaFlag.VisorState] is StateChanged.Source.Fixed) - value.Value = state.ModelData.IsVisorToggled(); - else - _manager.ChangeVisorState(state, value, StateChanged.Source.Game); - } - else - { - value.Value = state.ModelData.IsVisorToggled(); - } - } - - private void OnHeadGearVisibilityChange(Actor actor, Ref value) - { - if (!actor.Identifier(_actors.AwaitedService, out var identifier)) - return; - - if (!_manager.TryGetValue(identifier, out var state)) - return; - - if (state.BaseData.SetHatVisible(value)) - { - if (state[ActorState.MetaFlag.HatState] is StateChanged.Source.Fixed) - value.Value = state.ModelData.IsHatVisible(); - else - _manager.ChangeHatState(state, value, StateChanged.Source.Game); - } - else - { - value.Value = state.ModelData.IsHatVisible(); - } - } - - private void OnWeaponVisibilityChange(Actor actor, Ref value) - { - if (!actor.Identifier(_actors.AwaitedService, out var identifier)) - return; - - if (!_manager.TryGetValue(identifier, out var state)) - return; - - if (state.BaseData.SetWeaponVisible(value)) - { - if (state[ActorState.MetaFlag.WeaponState] is StateChanged.Source.Fixed) - value.Value = state.ModelData.IsWeaponVisible(); - else - _manager.ChangeWeaponState(state, value, StateChanged.Source.Game); - } - else - { - value.Value = state.ModelData.IsWeaponVisible(); - } - } } diff --git a/Glamourer/State/StateManager.cs b/Glamourer/State/StateManager.cs index 819541f..a87a21b 100644 --- a/Glamourer/State/StateManager.cs +++ b/Glamourer/State/StateManager.cs @@ -3,17 +3,12 @@ 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; using Glamourer.Interop; -using Glamourer.Interop.Penumbra; using Glamourer.Interop.Structs; using Glamourer.Services; -using Glamourer.Structs; -using Lumina.Excel.GeneratedSheets; -using OtterGui.Log; using Penumbra.GameData.Actors; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; @@ -30,114 +25,20 @@ public class StateManager : IReadOnlyDictionary 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, ObjectManager objects, StateEditor editor) + ObjectManager objects, StateEditor editor) { _actors = actors; _items = items; _customizations = customizations; _visor = visor; _event = @event; - _penumbra = penumbra; _objects = objects; _editor = editor; } - public bool GetOrCreate(Actor actor, [NotNullWhen(true)] out ActorState? state) - => GetOrCreate(actor.GetIdentifier(_actors.AwaitedService), actor, out state); - - public bool GetOrCreate(ActorIdentifier identifier, Actor actor, [NotNullWhen(true)] out ActorState? state) - { - if (TryGetValue(identifier, out state)) - return true; - - try - { - var designData = FromActor(actor); - state = new ActorState(identifier) - { - ModelData = designData, - BaseData = designData, - }; - _states.Add(identifier, state); - return true; - } - catch (Exception ex) - { - Glamourer.Log.Error($"Could not create new actor data for {identifier}:\n{ex}"); - return false; - } - } - - 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 != state.ModelData.ModelId) - return; - - var model = actor.Model; - - state.ModelData.SetHatVisible(!actor.AsCharacter->DrawData.IsHatHidden); - state.ModelData.SetIsWet(actor.AsCharacter->IsGPoseWet); - state.ModelData.SetWeaponVisible(!actor.AsCharacter->DrawData.IsWeaponHidden); - - if (model.IsHuman) - { - 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)) - UpdateEquip(state, slot, model.GetArmor(slot)); - - 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) - UpdateEquip(state, slot, actor.GetArmor(slot)); - - state.ModelData.Customize = actor.GetCustomize(); - UpdateWeapon(state, EquipSlot.MainHand, actor.GetMainhand()); - UpdateWeapon(state, EquipSlot.OffHand, actor.GetOffhand()); - state.ModelData.SetVisor(actor.AsCharacter->DrawData.IsVisorToggled); - } - } - public IEnumerator> GetEnumerator() => _states.GetEnumerator(); @@ -162,15 +63,54 @@ public class StateManager : IReadOnlyDictionary public IEnumerable Values => _states.Values; + /// + public bool GetOrCreate(Actor actor, [NotNullWhen(true)] out ActorState? state) + => GetOrCreate(actor.GetIdentifier(_actors.AwaitedService), actor, out state); + + /// Try to obtain or create a new state for an existing actor. Returns false if no state could be created. + public bool GetOrCreate(ActorIdentifier identifier, Actor actor, [NotNullWhen(true)] out ActorState? state) + { + if (TryGetValue(identifier, out state)) + return true; + + try + { + var designData = FromActor(actor); + // Initial Creation has identical base and model data. + state = new ActorState(identifier) + { + ModelData = designData, + BaseData = designData, + }; + // state.Identifier is owned. + _states.Add(state.Identifier, state); + return true; + } + catch (Exception ex) + { + Glamourer.Log.Error($"Could not create new actor data for {identifier}:\n{ex}"); + return false; + } + } + + /// + /// Create DesignData from a given actor. + /// This uses the draw object if available and where possible, + /// and the game object where necessary. + /// public unsafe DesignData FromActor(Actor actor) { var ret = new DesignData(); + // If the given actor is not a character, just return a default character. if (!actor.IsCharacter) { ret.SetDefaultEquipment(_items); return ret; } + // Model ID is only unambiguously contained in the game object. + // The draw object only has the object type. + // TODO do this right. if (actor.AsCharacter->CharacterData.ModelCharaId != 0) { ret.LoadNonHuman((uint)actor.AsCharacter->CharacterData.ModelCharaId, *(Customize*)&actor.AsCharacter->DrawData.CustomizeData, @@ -182,14 +122,23 @@ public class StateManager : IReadOnlyDictionary CharacterWeapon main; CharacterWeapon off; + // Hat visibility is only unambiguously contained in the game object. + // Set it first to know where to get head slot data from. ret.SetHatVisible(!actor.AsCharacter->DrawData.IsHatHidden); + + // Use the draw object if it is a human. if (model.IsHuman) { + // Customize can be obtained from the draw object. + ret.Customize = model.GetCustomize(); + + // We can not use the head slot data from the draw object if the hat is hidden. var head = ret.IsHatVisible() ? model.GetArmor(EquipSlot.Head) : actor.GetArmor(EquipSlot.Head); var headItem = _items.Identify(EquipSlot.Head, head.Set, head.Variant); ret.SetItem(EquipSlot.Head, headItem); ret.SetStain(EquipSlot.Head, head.Stain); + // The other slots can be used from the draw object. foreach (var slot in EquipSlotExtensions.EqdpSlots.Skip(1)) { var armor = model.GetArmor(slot); @@ -198,12 +147,17 @@ public class StateManager : IReadOnlyDictionary ret.SetStain(slot, armor.Stain); } - ret.Customize = model.GetCustomize(); + // Weapons use the draw objects of the weapons, but require the game object either way. (_, _, main, off) = model.GetWeapons(actor); + + // Visor state is a flag on the game object, but we can see the actual state on the draw object. ret.SetVisor(_visor.GetVisorState(model)); } else { + // Obtain all data from the game object. + ret.Customize = actor.GetCustomize(); + foreach (var slot in EquipSlotExtensions.EqdpSlots) { var armor = actor.GetArmor(slot); @@ -212,12 +166,12 @@ public class StateManager : IReadOnlyDictionary ret.SetStain(slot, armor.Stain); } - ret.Customize = actor.GetCustomize(); - main = actor.GetMainhand(); - off = actor.GetOffhand(); + main = actor.GetMainhand(); + off = actor.GetOffhand(); ret.SetVisor(actor.AsCharacter->DrawData.IsVisorToggled); } + // Set the weapons regardless of source. var mainItem = _items.Identify(EquipSlot.MainHand, main.Set, main.Type, (byte)main.Variant); var offItem = _items.Identify(EquipSlot.OffHand, off.Set, off.Type, (byte)off.Variant, mainItem.Type); ret.SetItem(EquipSlot.MainHand, mainItem); @@ -225,39 +179,232 @@ public class StateManager : IReadOnlyDictionary ret.SetItem(EquipSlot.OffHand, offItem); ret.SetStain(EquipSlot.OffHand, off.Stain); + // Wetness can technically only be set in GPose or via external tools. + // It is only available in the game object. ret.SetIsWet(actor.AsCharacter->IsGPoseWet); + + // Weapon visibility could technically be inferred from the weapon draw objects, + // but since we use hat visibility from the game object we can also use weapon visibility from it. ret.SetWeaponVisible(!actor.AsCharacter->DrawData.IsWeaponHidden); return ret; } + #region Change Values + /// Change a customization value. - public void ChangeCustomize(ActorState state, ActorData data, CustomizeIndex idx, CustomizeValue value, StateChanged.Source source, - bool force) + public void ChangeCustomize(ActorState state, CustomizeIndex idx, CustomizeValue value, StateChanged.Source source) { - ref var s = ref state[idx]; - if (s is StateChanged.Source.Fixed && source is StateChanged.Source.Game) - return; - - var oldValue = state.ModelData.Customize[idx]; - if (oldValue == value && !force) - return; - + // Update state data. + var old = state.ModelData.Customize[idx]; state.ModelData.Customize[idx] = value; + state[idx] = source; - 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}."); - _event.Invoke(StateChanged.Type.Customize, source, state, data, (oldValue, value, idx)); + // Update draw objects. + _objects.Update(); + var objects = _objects.TryGetValue(state.Identifier, out var d) ? d : ActorData.Invalid; + if (source is StateChanged.Source.Manual) + _editor.ChangeCustomize(objects, state.ModelData.Customize); + + // Meta. + Glamourer.Log.Verbose( + $"Set {idx.ToDefaultName()} customizations in state {state.Identifier} from {old.Value} to {value.Value}. [Affecting {objects.ToLazyString("nothing")}.]"); + _event.Invoke(StateChanged.Type.Customize, source, state, objects, (old, value, idx)); } + /// Change an entire customization array according to flags. + public void ChangeCustomize(ActorState state, in Customize customizeInput, CustomizeFlag apply, StateChanged.Source source) + { + // Update state data. + var old = state.ModelData.Customize; + var (customize, applied) = _customizations.Combine(state.ModelData.Customize, customizeInput, apply); + if (applied == 0) + return; + + state.ModelData.Customize = customize; + foreach (var type in Enum.GetValues()) + { + var flag = type.ToFlag(); + if (applied.HasFlag(flag)) + state[type] = source; + } + + // Update draw objects. + _objects.Update(); + var objects = _objects.TryGetValue(state.Identifier, out var d) ? d : ActorData.Invalid; + if (source is StateChanged.Source.Manual) + _editor.ChangeCustomize(objects, state.ModelData.Customize); + + // Meta. + Glamourer.Log.Verbose( + $"Set {applied} customizations in state {state.Identifier} from {old} to {customize}. [Affecting {objects.ToLazyString("nothing")}.]"); + _event.Invoke(StateChanged.Type.Customize, source, state, objects, (old, customize, applied)); + } + + /// Change a single piece of equipment without stain. + /// Do not use this in the same frame as ChangeStain, use instead. + public void ChangeItem(ActorState state, EquipSlot slot, EquipItem item, StateChanged.Source source) + { + // Update state data. + var old = state.ModelData.Item(slot); + state.ModelData.SetItem(slot, item); + state[slot, false] = source; + var type = slot is EquipSlot.MainHand or EquipSlot.OffHand ? StateChanged.Type.Weapon : StateChanged.Type.Equip; + + // Update draw objects. + _objects.Update(); + var objects = _objects.TryGetValue(state.Identifier, out var d) ? d : ActorData.Invalid; + if (source is StateChanged.Source.Manual) + if (type == StateChanged.Type.Equip) + _editor.ChangeArmor(objects, slot, state.ModelData.Armor(slot)); + else + _editor.ChangeWeapon(objects, slot, state.ModelData.Item(slot), state.ModelData.Stain(slot)); + + // Meta. + Glamourer.Log.Verbose( + $"Set {slot.ToName()} in state {state.Identifier} from {old.Name} ({old.Id}) to {item.Name} ({item.Id}). [Affecting {objects.ToLazyString("nothing")}.]"); + _event.Invoke(type, source, state, objects, (old, item, slot)); + } + + /// Change a single piece of equipment including stain. + public void ChangeEquip(ActorState state, EquipSlot slot, EquipItem item, StainId stain, StateChanged.Source source) + { + // Update state data. + 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; + var type = slot is EquipSlot.MainHand or EquipSlot.OffHand ? StateChanged.Type.Weapon : StateChanged.Type.Equip; + + // Update draw objects. + _objects.Update(); + var objects = _objects.TryGetValue(state.Identifier, out var d) ? d : ActorData.Invalid; + if (source is StateChanged.Source.Manual) + if (type == StateChanged.Type.Equip) + _editor.ChangeArmor(objects, slot, state.ModelData.Armor(slot)); + else + _editor.ChangeWeapon(objects, slot, state.ModelData.Item(slot), state.ModelData.Stain(slot)); + + // Meta. + Glamourer.Log.Verbose( + $"Set {slot.ToName()} 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(type, source, state, objects, (old, item, slot)); + _event.Invoke(StateChanged.Type.Stain, source, state, objects, (oldStain, stain, slot)); + } + + /// Change only the stain of an equipment piece. + /// + /// Do not use this in the same frame as ChangeEquip, use instead. + public void ChangeStain(ActorState state, EquipSlot slot, StainId stain, StateChanged.Source source) + { + // Update state data. + var old = state.ModelData.Stain(slot); + state.ModelData.SetStain(slot, stain); + state[slot, true] = source; + + // Update draw objects. + _objects.Update(); + var objects = _objects.TryGetValue(state.Identifier, out var d) ? d : ActorData.Invalid; + if (source is StateChanged.Source.Manual) + _editor.ChangeStain(objects, slot, stain); + + // Meta. + 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 hat visibility. + public void ChangeHatState(ActorState state, bool value, StateChanged.Source source) + { + // Update state data. + var old = state.ModelData.IsHatVisible(); + state.ModelData.SetHatVisible(value); + state[ActorState.MetaFlag.HatState] = source; + + // Update draw objects / game objects. + _objects.Update(); + var objects = _objects.TryGetValue(state.Identifier, out var d) ? d : ActorData.Invalid; + if (source is StateChanged.Source.Manual) + _editor.ChangeHatState(objects, value); + + // Meta. + Glamourer.Log.Verbose( + $"Set Head Gear Visibility in state {state.Identifier} from {old} to {value}. [Affecting {objects.ToLazyString("nothing")}.]"); + _event.Invoke(StateChanged.Type.Other, source, state, objects, (old, value, ActorState.MetaFlag.HatState)); + } + + /// Change weapon visibility. + public void ChangeWeaponState(ActorState state, bool value, StateChanged.Source source) + { + // Update state data. + var old = state.ModelData.IsWeaponVisible(); + state.ModelData.SetWeaponVisible(value); + state[ActorState.MetaFlag.WeaponState] = source; + + // Update draw objects / game objects. + _objects.Update(); + var objects = _objects.TryGetValue(state.Identifier, out var d) ? d : ActorData.Invalid; + if (source is StateChanged.Source.Manual) + _editor.ChangeWeaponState(objects, value); + + // Meta. + Glamourer.Log.Verbose( + $"Set Weapon Visibility in state {state.Identifier} from {old} to {value}. [Affecting {objects.ToLazyString("nothing")}.]"); + _event.Invoke(StateChanged.Type.Other, source, state, objects, (old, value, ActorState.MetaFlag.WeaponState)); + } + + /// Change visor state. + public void ChangeVisorState(ActorState state, bool value, StateChanged.Source source) + { + // Update state data. + var old = state.ModelData.IsVisorToggled(); + state.ModelData.SetVisor(value); + state[ActorState.MetaFlag.VisorState] = source; + + // Update draw objects. + _objects.Update(); + var objects = _objects.TryGetValue(state.Identifier, out var d) ? d : ActorData.Invalid; + if (source is StateChanged.Source.Manual) + _editor.ChangeVisor(objects, value); + + // Meta. + Glamourer.Log.Verbose( + $"Set Visor State in state {state.Identifier} from {old} to {value}. [Affecting {objects.ToLazyString("nothing")}.]"); + _event.Invoke(StateChanged.Type.Other, source, state, objects, (old, value, ActorState.MetaFlag.VisorState)); + } + + /// Set GPose Wetness. + public void ChangeWetness(ActorState state, bool value, StateChanged.Source source) + { + // Update state data. + var old = state.ModelData.IsWet(); + state.ModelData.SetIsWet(value); + state[ActorState.MetaFlag.Wetness] = source; + + // Update draw objects / game objects. + _objects.Update(); + var objects = _objects.TryGetValue(state.Identifier, out var d) ? d : ActorData.Invalid; + _editor.ChangeWetness(objects, value); + + // Meta. + Glamourer.Log.Verbose( + $"Set Wetness in state {state.Identifier} from {old} to {value}. [Affecting {objects.ToLazyString("nothing")}.]"); + _event.Invoke(StateChanged.Type.Other, state[ActorState.MetaFlag.Wetness], state, objects, (old, value, ActorState.MetaFlag.Wetness)); + } + + #endregion + public void ApplyDesign(Design design, ActorState state) { - foreach (var slot in EquipSlotExtensions.EqdpSlots) + void HandleEquip(EquipSlot slot, bool applyPiece, bool applyStain) { - switch (design.DoApplyEquip(slot), design.DoApplyStain(slot)) + switch (applyPiece, applyStain) { - case (false, false): continue; + case (false, false): break; case (true, false): - ChangeEquip(state, slot, design.DesignData.Item(slot), StateChanged.Source.Manual); + ChangeItem(state, slot, design.DesignData.Item(slot), StateChanged.Source.Manual); break; case (false, true): ChangeStain(state, slot, design.DesignData.Stain(slot), StateChanged.Source.Manual); @@ -267,6 +414,19 @@ public class StateManager : IReadOnlyDictionary break; } } + + foreach (var slot in EquipSlotExtensions.EqdpSlots) + HandleEquip(slot, design.DoApplyEquip(slot), design.DoApplyStain(slot)); + + HandleEquip(EquipSlot.MainHand, + design.DoApplyEquip(EquipSlot.MainHand) + && design.DesignData.Item(EquipSlot.MainHand).Type == state.BaseData.Item(EquipSlot.MainHand).Type, + design.DoApplyStain(EquipSlot.MainHand)); + HandleEquip(EquipSlot.OffHand, + design.DoApplyEquip(EquipSlot.OffHand) + && design.DesignData.Item(EquipSlot.OffHand).Type == state.BaseData.Item(EquipSlot.OffHand).Type, + design.DoApplyStain(EquipSlot.OffHand)); + if (design.DoApplyHatVisible()) ChangeHatState(state, design.DesignData.IsHatVisible(), StateChanged.Source.Manual); if (design.DoApplyWeaponVisible()) @@ -274,17 +434,29 @@ public class StateManager : IReadOnlyDictionary if (design.DoApplyVisorToggle()) ChangeVisorState(state, design.DesignData.IsVisorToggled(), StateChanged.Source.Manual); if (design.DoApplyWetness()) - ChangeWetness(state, design.DesignData.IsWet()); + ChangeWetness(state, design.DesignData.IsWet(), StateChanged.Source.Manual); + ChangeCustomize(state, design.DesignData.Customize, design.ApplyCustomize, StateChanged.Source.Manual); } 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); - } + + ChangeEquip(state, EquipSlot.MainHand, state.BaseData.Item(EquipSlot.MainHand), state.BaseData.Stain(EquipSlot.MainHand), + StateChanged.Source.Game); + ChangeEquip(state, EquipSlot.OffHand, state.BaseData.Item(EquipSlot.OffHand), state.BaseData.Stain(EquipSlot.OffHand), + StateChanged.Source.Game); + ChangeHatState(state, state.BaseData.IsHatVisible(), StateChanged.Source.Game); + ChangeVisorState(state, state.BaseData.IsVisorToggled(), StateChanged.Source.Game); + ChangeWeaponState(state, state.BaseData.IsWeaponVisible(), StateChanged.Source.Game); + ChangeWetness(state, false, StateChanged.Source.Game); + ChangeCustomize(state, state.BaseData.Customize, CustomizeFlagExtensions.All, StateChanged.Source.Game); + + _objects.Update(); + var objects = _objects.TryGetValue(state.Identifier, out var d) ? d : ActorData.Invalid; + foreach (var actor in objects.Objects) + ReapplyState(actor); } public void ReapplyState(Actor actor) @@ -292,215 +464,24 @@ public class StateManager : IReadOnlyDictionary if (!GetOrCreate(actor, out var state)) return; - _objects.Update(); - var objects = _objects.TryGetValue(state.Identifier, out var d) ? d : ActorData.Invalid; + var mdl = actor.Model; + if (!mdl.IsHuman) + return; + + var data = new ActorData(actor, string.Empty); + var customizeFlags = Customize.Compare(mdl.GetCustomize(), state.ModelData.Customize); + + _editor.ChangeCustomize(data, state.ModelData.Customize); + if (customizeFlags.RequiresRedraw()) + return; + foreach (var slot in EquipSlotExtensions.EqdpSlots) - _editor.ChangeArmor(state, objects, slot); + _editor.ChangeArmor(data, slot, state.ModelData.Armor(slot)); + _editor.ChangeMainhand(data, state.ModelData.Item(EquipSlot.MainHand), state.ModelData.Stain(EquipSlot.MainHand)); + _editor.ChangeOffhand(data, state.ModelData.Item(EquipSlot.OffHand), state.ModelData.Stain(EquipSlot.OffHand)); + _editor.ChangeWetness(data, false); + _editor.ChangeWeaponState(data, state.ModelData.IsWeaponVisible()); + _editor.ChangeHatState(data, state.ModelData.IsHatVisible()); + _editor.ChangeVisor(data, state.ModelData.IsVisorToggled()); } - - 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)); - } - - public void ChangeHatState(ActorState state, bool value, StateChanged.Source source) - { - var old = state.ModelData.IsHatVisible(); - state.ModelData.SetHatVisible(value); - state[ActorState.MetaFlag.HatState] = source; - _objects.Update(); - var objects = _objects.TryGetValue(state.Identifier, out var d) ? d : ActorData.Invalid; - if (source is StateChanged.Source.Manual) - _editor.ChangeHatState(objects, value); - Glamourer.Log.Verbose( - $"Set Head Gear Visibility in state {state.Identifier} from {old} to {value}. [Affecting {objects.ToLazyString("nothing")}.]"); - _event.Invoke(StateChanged.Type.Other, source, state, objects, (old, value, ActorState.MetaFlag.HatState)); - } - - public void ChangeWeaponState(ActorState state, bool value, StateChanged.Source source) - { - var old = state.ModelData.IsWeaponVisible(); - state.ModelData.SetWeaponVisible(value); - state[ActorState.MetaFlag.WeaponState] = source; - _objects.Update(); - var objects = _objects.TryGetValue(state.Identifier, out var d) ? d : ActorData.Invalid; - if (source is StateChanged.Source.Manual) - _editor.ChangeWeaponState(objects, value); - Glamourer.Log.Verbose( - $"Set Weapon Visibility in state {state.Identifier} from {old} to {value}. [Affecting {objects.ToLazyString("nothing")}.]"); - _event.Invoke(StateChanged.Type.Other, source, state, objects, (old, value, ActorState.MetaFlag.WeaponState)); - } - - public void ChangeVisorState(ActorState state, bool value, StateChanged.Source source) - { - var old = state.ModelData.IsVisorToggled(); - state.ModelData.SetVisor(value); - state[ActorState.MetaFlag.VisorState] = source; - _objects.Update(); - var objects = _objects.TryGetValue(state.Identifier, out var d) ? d : ActorData.Invalid; - if (source is StateChanged.Source.Manual) - _editor.ChangeVisor(objects, value); - Glamourer.Log.Verbose( - $"Set Visor State in state {state.Identifier} from {old} to {value}. [Affecting {objects.ToLazyString("nothing")}.]"); - _event.Invoke(StateChanged.Type.Other, source, state, objects, (old, value, ActorState.MetaFlag.VisorState)); - } - - public void ChangeWetness(ActorState state, bool value) - { - var old = state.ModelData.IsWet(); - state.ModelData.SetIsWet(value); - state[ActorState.MetaFlag.Wetness] = value ? StateChanged.Source.Manual : StateChanged.Source.Game; - _objects.Update(); - var objects = _objects.TryGetValue(state.Identifier, out var d) ? d : ActorData.Invalid; - _editor.ChangeWetness(objects, value); - Glamourer.Log.Verbose( - $"Set Wetness in state {state.Identifier} from {old} to {value}. [Affecting {objects.ToLazyString("nothing")}.]"); - _event.Invoke(StateChanged.Type.Other, state[ActorState.MetaFlag.Wetness], state, objects, (old, value, ActorState.MetaFlag.Wetness)); - } - - // - ///// Change whether to apply a specific customize value. - //public void ChangeApplyCustomize(Design design, CustomizeIndex idx, bool value) - //{ - // if (!design.SetApplyCustomize(idx, value)) - // return; - // - // design.LastEdit = DateTimeOffset.UtcNow; - // _saveService.QueueSave(design); - // Glamourer.Log.Debug($"Set applying of customization {idx.ToDefaultName()} to {value}."); - // _event.Invoke(DesignChanged.Type.ApplyCustomize, design, idx); - //} - // - - // - ///// Change a weapon. - //public void ChangeWeapon(Design design, EquipSlot slot, EquipItem item) - //{ - // var currentMain = design.DesignData.Item(EquipSlot.MainHand); - // var currentOff = design.DesignData.Item(EquipSlot.OffHand); - // switch (slot) - // { - // case EquipSlot.MainHand: - // var newOff = currentOff; - // if (item.Type == currentMain.Type) - // { - // if (_items.ValidateWeapons(item.Id, currentOff.Id, out _, out _).Length != 0) - // return; - // } - // else - // { - // var newOffId = FullEquipTypeExtensions.OffhandTypes.Contains(item.Type) - // ? item.Id - // : ItemManager.NothingId(item.Type.Offhand()); - // if (_items.ValidateWeapons(item.Id, newOffId, out _, out newOff).Length != 0) - // return; - // } - // - // design.DesignData.SetItem(EquipSlot.MainHand, item); - // design.DesignData.SetItem(EquipSlot.OffHand, newOff); - // design.LastEdit = DateTimeOffset.UtcNow; - // _saveService.QueueSave(design); - // Glamourer.Log.Debug( - // $"Set {EquipSlot.MainHand.ToName()} weapon in design {design.Identifier} from {currentMain.Name} ({currentMain.Id}) to {item.Name} ({item.Id})."); - // _event.Invoke(DesignChanged.Type.Weapon, design, (currentMain, currentOff, item, newOff)); - // return; - // case EquipSlot.OffHand: - // if (item.Type != currentOff.Type) - // return; - // if (_items.ValidateWeapons(currentMain.Id, item.Id, out _, out _).Length > 0) - // return; - // - // if (!design.DesignData.SetItem(EquipSlot.OffHand, item)) - // return; - // - // design.LastEdit = DateTimeOffset.UtcNow; - // _saveService.QueueSave(design); - // Glamourer.Log.Debug( - // $"Set {EquipSlot.OffHand.ToName()} weapon in design {design.Identifier} from {currentOff.Name} ({currentOff.Id}) to {item.Name} ({item.Id})."); - // _event.Invoke(DesignChanged.Type.Weapon, design, (currentMain, currentOff, currentMain, item)); - // return; - // default: return; - // } - //} - // - ///// Change whether to apply a specific equipment piece. - //public void ChangeApplyEquip(Design design, EquipSlot slot, bool value) - //{ - // if (!design.SetApplyEquip(slot, value)) - // return; - // - // design.LastEdit = DateTimeOffset.UtcNow; - // _saveService.QueueSave(design); - // Glamourer.Log.Debug($"Set applying of {slot} equipment piece to {value}."); - // _event.Invoke(DesignChanged.Type.ApplyEquip, design, slot); - //} - // - ///// Change the stain for any equipment piece. - //public void ChangeStain(Design design, EquipSlot slot, StainId stain) - //{ - // if (_items.ValidateStain(stain, out _).Length > 0) - // return; - // - // var oldStain = design.DesignData.Stain(slot); - // if (!design.DesignData.SetStain(slot, stain)) - // return; - // - // design.LastEdit = DateTimeOffset.UtcNow; - // _saveService.QueueSave(design); - // Glamourer.Log.Debug($"Set stain of {slot} equipment piece to {stain.Value}."); - // _event.Invoke(DesignChanged.Type.Stain, design, (oldStain, stain, slot)); - //} - // - ///// Change whether to apply a specific stain. - //public void ChangeApplyStain(Design design, EquipSlot slot, bool value) - //{ - // if (!design.SetApplyStain(slot, value)) - // return; - // - // design.LastEdit = DateTimeOffset.UtcNow; - // _saveService.QueueSave(design); - // Glamourer.Log.Debug($"Set applying of stain of {slot} equipment piece to {value}."); - // _event.Invoke(DesignChanged.Type.ApplyStain, design, slot); - //} }