From 27f151c55a84e1afffa89d65b0e7bccd7ce82028 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 16 Jun 2023 16:13:26 +0200 Subject: [PATCH] , --- Glamourer.GameData/Customization/Customize.cs | 4 +- Glamourer.GameData/Structs/EquipFlag.cs | 74 +++ Glamourer/Configuration.cs | 80 +++ Glamourer/Designs/Design.cs | 549 ++++++++++++++++++ Glamourer/Designs/DesignBase64Migration.cs | 150 +++++ Glamourer/Designs/DesignData.cs | 179 ++++++ Glamourer/Designs/IDesign.cs | 46 ++ Glamourer/Glamourer.cs | 4 +- Glamourer/Glamourer.csproj | 4 - Glamourer/Gui/Colors.cs | 34 ++ Glamourer/Gui/MainWindow.cs | 4 +- Glamourer/Gui/Tabs/DebugTab.cs | 280 ++++++++- Glamourer/Interop/ChangeCustomizeService.cs | 2 +- Glamourer/Interop/ObjectManager.cs | 139 +++++ Glamourer/Interop/Penumbra/PenumbraService.cs | 6 +- Glamourer/Interop/Structs/Actor.cs | 8 + Glamourer/Interop/Structs/ActorData.cs | 3 + Glamourer/Interop/VisorService.cs | 4 +- Glamourer/Interop/WeaponService.cs | 2 +- Glamourer/Services/ConfigMigrationService.cs | 56 ++ Glamourer/Services/FilenameService.cs | 4 + Glamourer/Services/ItemManager.cs | 26 +- Glamourer/Services/SaveService.cs | 17 + Glamourer/Services/ServiceManager.cs | 12 +- Glamourer/Services/ServiceWrapper.cs | 4 +- GlamourerOld/Configuration.cs | 97 +++- GlamourerOld/Gui/Interface.SettingsTab.cs | 6 +- GlamourerOld/Gui/Interface.cs | 4 +- GlamourerOld/Interop/Actor.cs | 2 +- GlamourerOld/Services/ItemManager.cs | 4 +- GlamourerOld/Services/SaveService.cs | 89 +-- GlamourerOld/Services/ServiceManager.cs | 2 +- 32 files changed, 1744 insertions(+), 151 deletions(-) create mode 100644 Glamourer.GameData/Structs/EquipFlag.cs create mode 100644 Glamourer/Configuration.cs create mode 100644 Glamourer/Designs/Design.cs create mode 100644 Glamourer/Designs/DesignBase64Migration.cs create mode 100644 Glamourer/Designs/DesignData.cs create mode 100644 Glamourer/Designs/IDesign.cs create mode 100644 Glamourer/Gui/Colors.cs create mode 100644 Glamourer/Interop/ObjectManager.cs create mode 100644 Glamourer/Services/ConfigMigrationService.cs create mode 100644 Glamourer/Services/SaveService.cs diff --git a/Glamourer.GameData/Customization/Customize.cs b/Glamourer.GameData/Customization/Customize.cs index 7c90088..e804f2e 100644 --- a/Glamourer.GameData/Customization/Customize.cs +++ b/Glamourer.GameData/Customization/Customize.cs @@ -93,13 +93,13 @@ public unsafe struct Customize public void Load(Customize other) => Data.Read(&other.Data); - public void Write(nint target) + public readonly void Write(nint target) => Data.Write((void*)target); public bool LoadBase64(string data) => Data.LoadBase64(data); - public string WriteBase64() + public readonly string WriteBase64() => Data.WriteBase64(); public static CustomizeFlag Compare(Customize lhs, Customize rhs) diff --git a/Glamourer.GameData/Structs/EquipFlag.cs b/Glamourer.GameData/Structs/EquipFlag.cs new file mode 100644 index 0000000..52a6f48 --- /dev/null +++ b/Glamourer.GameData/Structs/EquipFlag.cs @@ -0,0 +1,74 @@ +using System; +using Penumbra.GameData.Enums; + +namespace Glamourer.Structs; + +[Flags] +public enum EquipFlag : uint +{ + Head = 0x00000001, + Body = 0x00000002, + Hands = 0x00000004, + Legs = 0x00000008, + Feet = 0x00000010, + Ears = 0x00000020, + Neck = 0x00000040, + Wrist = 0x00000080, + RFinger = 0x00000100, + LFinger = 0x00000200, + Mainhand = 0x00000400, + Offhand = 0x00000800, + HeadStain = 0x00001000, + BodyStain = 0x00002000, + HandsStain = 0x00004000, + LegsStain = 0x00008000, + FeetStain = 0x00010000, + EarsStain = 0x00020000, + NeckStain = 0x00040000, + WristStain = 0x00080000, + RFingerStain = 0x00100000, + LFingerStain = 0x00200000, + MainhandStain = 0x00400000, + OffhandStain = 0x00800000, +} + +public static class EquipFlagExtensions +{ + public const EquipFlag All = (EquipFlag)(((uint)EquipFlag.OffhandStain << 1) - 1); + + public static EquipFlag ToFlag(this EquipSlot slot) + => slot switch + { + EquipSlot.MainHand => EquipFlag.Mainhand, + EquipSlot.OffHand => EquipFlag.Offhand, + EquipSlot.Head => EquipFlag.Head, + EquipSlot.Body => EquipFlag.Body, + EquipSlot.Hands => EquipFlag.Hands, + EquipSlot.Legs => EquipFlag.Legs, + EquipSlot.Feet => EquipFlag.Feet, + EquipSlot.Ears => EquipFlag.Ears, + EquipSlot.Neck => EquipFlag.Neck, + EquipSlot.Wrists => EquipFlag.Wrist, + EquipSlot.RFinger => EquipFlag.RFinger, + EquipSlot.LFinger => EquipFlag.LFinger, + _ => 0, + }; + + public static EquipFlag ToStainFlag(this EquipSlot slot) + => slot switch + { + EquipSlot.MainHand => EquipFlag.MainhandStain, + EquipSlot.OffHand => EquipFlag.OffhandStain, + EquipSlot.Head => EquipFlag.HeadStain, + EquipSlot.Body => EquipFlag.BodyStain, + EquipSlot.Hands => EquipFlag.HandsStain, + EquipSlot.Legs => EquipFlag.LegsStain, + EquipSlot.Feet => EquipFlag.FeetStain, + EquipSlot.Ears => EquipFlag.EarsStain, + EquipSlot.Neck => EquipFlag.NeckStain, + EquipSlot.Wrists => EquipFlag.WristStain, + EquipSlot.RFinger => EquipFlag.RFingerStain, + EquipSlot.LFinger => EquipFlag.LFingerStain, + _ => 0, + }; +} diff --git a/Glamourer/Configuration.cs b/Glamourer/Configuration.cs new file mode 100644 index 0000000..485e8aa --- /dev/null +++ b/Glamourer/Configuration.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Dalamud.Configuration; +using Dalamud.Interface.Internal.Notifications; +using Glamourer.Gui; +using Glamourer.Services; +using Newtonsoft.Json; +using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs; + +namespace Glamourer; + +public class Configuration : IPluginConfiguration, ISavable +{ + public bool UseRestrictedGearProtection = true; + + public int Version { get; set; } = Constants.CurrentVersion; + + public Dictionary Colors { get; private set; } + = Enum.GetValues().ToDictionary(c => c, c => c.Data().DefaultColor); + + [JsonIgnore] + private readonly SaveService _saveService; + + public Configuration(SaveService saveService, ConfigMigrationService migrator) + { + _saveService = saveService; + Load(migrator); + } + + public void Save() + => _saveService.QueueSave(this); + + public void Load(ConfigMigrationService migrator) + { + static void HandleDeserializationError(object? sender, ErrorEventArgs errorArgs) + { + Glamourer.Log.Error( + $"Error parsing Configuration at {errorArgs.ErrorContext.Path}, using default or migrating:\n{errorArgs.ErrorContext.Error}"); + errorArgs.ErrorContext.Handled = true; + } + + if (!File.Exists(_saveService.FileNames.ConfigFile)) + return; + + if (File.Exists(_saveService.FileNames.ConfigFile)) + try + { + var text = File.ReadAllText(_saveService.FileNames.ConfigFile); + JsonConvert.PopulateObject(text, this, new JsonSerializerSettings + { + Error = HandleDeserializationError, + }); + } + catch (Exception ex) + { + Glamourer.Chat.NotificationMessage(ex, + "Error reading Configuration, reverting to default.\nYou may be able to restore your configuration using the rolling backups in the XIVLauncher/backups/Glamourer directory.", + "Error reading Configuration", "Error", NotificationType.Error); + } + + migrator.Migrate(this); + } + + public string ToFilename(FilenameService fileNames) + => fileNames.ConfigFile; + + public void Save(StreamWriter writer) + { + using var jWriter = new JsonTextWriter(writer) { Formatting = Formatting.Indented }; + var serializer = new JsonSerializer { Formatting = Formatting.Indented }; + serializer.Serialize(jWriter, this); + } + + public static class Constants + { + public const int CurrentVersion = 2; + } +} \ No newline at end of file diff --git a/Glamourer/Designs/Design.cs b/Glamourer/Designs/Design.cs new file mode 100644 index 0000000..77a0dec --- /dev/null +++ b/Glamourer/Designs/Design.cs @@ -0,0 +1,549 @@ +using System; +using System.IO; +using System.Linq; +using Dalamud.Interface.Internal.Notifications; +using Glamourer.Customization; +using Glamourer.Services; +using Glamourer.Structs; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using OtterGui.Classes; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; + +namespace Glamourer.Designs; + +public class Design : ISavable +{ + internal Design(ItemManager items) + { } + + // Metadata + public const int FileVersion = 1; + + public Guid Identifier { get; internal init; } + public DateTimeOffset CreationDate { get; internal init; } + public DateTimeOffset LastEdit { get; internal set; } + public LowerString Name { get; internal set; } = LowerString.Empty; + public string Description { get; internal set; } = string.Empty; + public string[] Tags { get; internal set; } = Array.Empty(); + public int Index { get; internal set; } + + internal DesignData DesignData; + + #region Application Data + + [Flags] + private enum DesignFlags : byte + { + ApplyHatVisible = 0x01, + ApplyVisorState = 0x02, + ApplyWeaponVisible = 0x04, + WriteProtected = 0x08, + } + + private CustomizeFlag _applyCustomize; + private EquipFlag _applyEquip; + private DesignFlags _designFlags; + + public bool DoApplyHatVisible() + => _designFlags.HasFlag(DesignFlags.ApplyHatVisible); + + public bool DoApplyVisorToggle() + => _designFlags.HasFlag(DesignFlags.ApplyVisorState); + + public bool DoApplyWeaponVisible() + => _designFlags.HasFlag(DesignFlags.ApplyWeaponVisible); + + public bool WriteProtected() + => _designFlags.HasFlag(DesignFlags.WriteProtected); + + public bool SetApplyHatVisible(bool value) + { + var newFlag = value ? _designFlags | DesignFlags.ApplyHatVisible : _designFlags & ~DesignFlags.ApplyHatVisible; + if (newFlag == _designFlags) + return false; + + _designFlags = newFlag; + return true; + } + + public bool SetApplyVisorToggle(bool value) + { + var newFlag = value ? _designFlags | DesignFlags.ApplyVisorState : _designFlags & ~DesignFlags.ApplyVisorState; + if (newFlag == _designFlags) + return false; + + _designFlags = newFlag; + return true; + } + + public bool SetApplyWeaponVisible(bool value) + { + var newFlag = value ? _designFlags | DesignFlags.ApplyWeaponVisible : _designFlags & ~DesignFlags.ApplyWeaponVisible; + if (newFlag == _designFlags) + return false; + + _designFlags = newFlag; + return true; + } + + public bool SetWriteProtected(bool value) + { + var newFlag = value ? _designFlags | DesignFlags.WriteProtected : _designFlags & ~DesignFlags.WriteProtected; + if (newFlag == _designFlags) + return false; + + _designFlags = newFlag; + return true; + } + + + public bool DoApplyEquip(EquipSlot slot) + => _applyEquip.HasFlag(slot.ToFlag()); + + public bool DoApplyStain(EquipSlot slot) + => _applyEquip.HasFlag(slot.ToStainFlag()); + + public bool DoApplyCustomize(CustomizeIndex idx) + => _applyCustomize.HasFlag(idx.ToFlag()); + + internal bool SetApplyEquip(EquipSlot slot, bool value) + { + var newValue = value ? _applyEquip | slot.ToFlag() : _applyEquip & ~slot.ToFlag(); + if (newValue == _applyEquip) + return false; + + _applyEquip = newValue; + return true; + } + + internal bool SetApplyStain(EquipSlot slot, bool value) + { + var newValue = value ? _applyEquip | slot.ToStainFlag() : _applyEquip & ~slot.ToStainFlag(); + if (newValue == _applyEquip) + return false; + + _applyEquip = newValue; + return true; + } + + internal bool SetApplyCustomize(CustomizeIndex idx, bool value) + { + var newValue = value ? _applyCustomize | idx.ToFlag() : _applyCustomize & ~idx.ToFlag(); + if (newValue == _applyCustomize) + return false; + + _applyCustomize = newValue; + return true; + } + + #endregion + + #region ISavable + + public JObject JsonSerialize() + { + var ret = new JObject + { + ["FileVersion"] = FileVersion, + ["Identifier"] = Identifier, + ["CreationDate"] = CreationDate, + ["LastEdit"] = LastEdit, + ["Name"] = Name.Text, + ["Description"] = Description, + ["Tags"] = JArray.FromObject(Tags), + ["WriteProtected"] = WriteProtected(), + ["Equipment"] = SerializeEquipment(), + ["Customize"] = SerializeCustomize(), + }; + return ret; + } + + public JObject SerializeEquipment() + { + static JObject Serialize(uint itemId, StainId stain, bool apply, bool applyStain) + => new() + { + ["ItemId"] = itemId, + ["Stain"] = stain.Value, + ["Apply"] = apply, + ["ApplyStain"] = applyStain, + }; + + var ret = new JObject(); + + foreach (var slot in EquipSlotExtensions.EqdpSlots.Prepend(EquipSlot.OffHand).Prepend(EquipSlot.MainHand)) + { + var item = DesignData.Item(slot); + var stain = DesignData.Stain(slot); + ret[slot.ToString()] = Serialize(item.Id, stain, DoApplyEquip(slot), DoApplyStain(slot)); + } + + ret["Hat"] = new QuadBool(DesignData.IsHatVisible(), DoApplyHatVisible()).ToJObject("Show", "Apply"); + ret["Visor"] = new QuadBool(DesignData.IsVisorToggled(), DoApplyVisorToggle()).ToJObject("IsToggled", "Apply"); + ret["Weapon"] = new QuadBool(DesignData.IsWeaponVisible(), DoApplyWeaponVisible()).ToJObject("Show", "Apply"); + + return ret; + } + + public JObject SerializeCustomize() + { + var ret = new JObject() + { + ["ModelId"] = DesignData.ModelId, + }; + var customize = DesignData.Customize; + foreach (var idx in Enum.GetValues()) + { + ret[idx.ToString()] = new JObject() + { + ["Value"] = customize[idx].Value, + ["Apply"] = DoApplyCustomize(idx), + }; + } + + ret["IsWet"] = DesignData.IsWet(); + return ret; + } + + public static Design LoadDesign(CustomizationManager customizeManager, ItemManager items, JObject json, out bool changes) + { + var version = json["FileVersion"]?.ToObject() ?? 0; + return version switch + { + 1 => LoadDesignV1(customizeManager, items, json, out changes), + _ => throw new Exception("The design to be loaded has no valid Version."), + }; + } + + private static Design LoadDesignV1(CustomizationManager customizeManager, ItemManager items, JObject json, out bool changes) + { + static string[] ParseTags(JObject json) + { + var tags = json["Tags"]?.ToObject() ?? Array.Empty(); + return tags.OrderBy(t => t).Distinct().ToArray(); + } + + var creationDate = json["CreationDate"]?.ToObject() ?? throw new ArgumentNullException("CreationDate"); + + var design = new Design(items) + { + CreationDate = creationDate, + Identifier = json["Identifier"]?.ToObject() ?? throw new ArgumentNullException("Identifier"), + Name = new LowerString(json["Name"]?.ToObject() ?? throw new ArgumentNullException("Name")), + Description = json["Description"]?.ToObject() ?? string.Empty, + Tags = ParseTags(json), + LastEdit = json["LastEdit"]?.ToObject() ?? creationDate, + }; + + changes = LoadEquip(items, json["Equipment"], design); + changes |= LoadCustomize(customizeManager, json["Customize"], design); + return design; + } + + private static bool ValidateItem(ItemManager items, EquipSlot slot, uint itemId, out EquipItem item) + { + item = items.Resolve(slot, itemId); + if (item.Valid) + return true; + + Glamourer.Chat.NotificationMessage($"The {slot.ToName()} item {itemId} does not exist, reset to Nothing.", "Warning", + NotificationType.Warning); + item = ItemManager.NothingItem(slot); + return false; + } + + private static bool ValidateStain(ItemManager items, StainId stain, out StainId ret) + { + if (stain.Value != 0 && !items.Stains.ContainsKey(stain)) + { + ret = 0; + Glamourer.Chat.NotificationMessage($"The Stain {stain} does not exist, reset to unstained."); + return false; + } + + ret = stain; + return true; + } + + private static bool ValidateWeapons(ItemManager items, uint mainId, uint offId, out EquipItem main, out EquipItem off) + { + var ret = true; + main = items.Resolve(EquipSlot.MainHand, mainId); + if (!main.Valid) + { + Glamourer.Chat.NotificationMessage($"The mainhand weapon {mainId} does not exist, reset to default sword.", "Warning", + NotificationType.Warning); + main = items.DefaultSword; + ret = false; + } + + off = items.Resolve(main.Type.Offhand(), offId); + if (off.Valid) + return ret; + + ret = false; + off = items.Resolve(main.Type.Offhand(), mainId); + if (off.Valid) + { + Glamourer.Chat.NotificationMessage($"The offhand weapon {offId} does not exist, reset to implied offhand.", "Warning", + NotificationType.Warning); + } + else + { + off = ItemManager.NothingItem(FullEquipType.Shield); + if (main.Type.Offhand() == FullEquipType.Shield) + { + Glamourer.Chat.NotificationMessage($"The offhand weapon {offId} does not exist, reset to no offhand.", "Warning", + NotificationType.Warning); + } + else + { + main = items.DefaultSword; + Glamourer.Chat.NotificationMessage( + $"The offhand weapon {offId} does not exist, but no default could be restored, reset mainhand to default sword and offhand to nothing.", + "Warning", + NotificationType.Warning); + } + } + + return ret; + } + + private static bool LoadEquip(ItemManager items, JToken? equip, Design design) + { + if (equip == null) + return true; + + static (uint, StainId, bool, bool) ParseItem(EquipSlot slot, JToken? item) + { + var id = item?["ItemId"]?.ToObject() ?? ItemManager.NothingId(slot); + var stain = (StainId)(item?["Stain"]?.ToObject() ?? 0); + var apply = item?["Apply"]?.ToObject() ?? false; + var applyStain = item?["ApplyStain"]?.ToObject() ?? false; + return (id, stain, apply, applyStain); + } + + var changes = false; + foreach (var slot in EquipSlotExtensions.EqdpSlots) + { + var (id, stain, apply, applyStain) = ParseItem(slot, equip[slot.ToString()]); + changes |= !ValidateItem(items, slot, id, out var item); + changes |= !ValidateStain(items, stain, out stain); + design.DesignData.SetItem(item); + design.DesignData.SetStain(slot, stain); + design.SetApplyEquip(slot, apply); + design.SetApplyStain(slot, applyStain); + } + + { + var (id, stain, apply, applyStain) = ParseItem(EquipSlot.MainHand, equip[EquipSlot.MainHand.ToString()]); + if (id == ItemManager.NothingId(EquipSlot.MainHand)) + id = items.DefaultSword.Id; + var (idOff, stainOff, applyOff, applyStainOff) = ParseItem(EquipSlot.OffHand, equip[EquipSlot.OffHand.ToString()]); + if (id == ItemManager.NothingId(EquipSlot.OffHand)) + id = ItemManager.NothingId(FullEquipType.Shield); + changes |= ValidateWeapons(items, id, idOff, out var main, out var off); + changes |= ValidateStain(items, stain, out stain); + changes |= ValidateStain(items, stainOff, out stainOff); + design.DesignData.SetItem(main); + design.DesignData.SetItem(off); + design.DesignData.SetStain(EquipSlot.MainHand, stain); + design.DesignData.SetStain(EquipSlot.OffHand, stainOff); + design.SetApplyEquip(EquipSlot.MainHand, apply); + design.SetApplyEquip(EquipSlot.OffHand, applyOff); + design.SetApplyStain(EquipSlot.MainHand, applyStain); + design.SetApplyStain(EquipSlot.OffHand, applyStainOff); + } + var metaValue = QuadBool.FromJObject(equip["Hat"], "Show", "Apply", QuadBool.NullFalse); + design.SetApplyHatVisible(metaValue.Enabled); + design.DesignData.SetHatVisible(metaValue.ForcedValue); + + metaValue = QuadBool.FromJObject(equip["Weapon"], "Show", "Apply", QuadBool.NullFalse); + design.SetApplyWeaponVisible(metaValue.Enabled); + design.DesignData.SetWeaponVisible(metaValue.ForcedValue); + + metaValue = QuadBool.FromJObject(equip["Visor"], "IsToggled", "Apply", QuadBool.NullFalse); + design.SetApplyVisorToggle(metaValue.Enabled); + design.DesignData.SetVisor(metaValue.ForcedValue); + + return changes; + } + + private static bool ValidateCustomize(CustomizationManager manager, ref Customize customize) + { + var ret = true; + if (!manager.Races.Contains(customize.Race)) + { + ret = false; + if (manager.Clans.Contains(customize.Clan)) + { + Glamourer.Chat.NotificationMessage( + $"The race {customize.Race.ToName()} is unknown, reset to {customize.Clan.ToRace().ToName()} from Clan.", "Warning", + NotificationType.Warning); + customize.Race = customize.Clan.ToRace(); + } + else + { + Glamourer.Chat.NotificationMessage( + $"The race {customize.Race.ToName()} is unknown, reset to {Race.Hyur.ToName()} {SubRace.Midlander.ToName()}.", "Warning", + NotificationType.Warning); + customize.Race = Race.Hyur; + customize.Clan = SubRace.Midlander; + } + } + + if (!manager.Clans.Contains(customize.Clan)) + { + ret = false; + var oldClan = customize.Clan; + customize.Clan = (SubRace)((byte)customize.Race * 2 - 1); + if (manager.Clans.Contains(customize.Clan)) + { + Glamourer.Chat.NotificationMessage($"The clan {oldClan.ToName()} is unknown, reset to {customize.Clan.ToName()} from race.", + "Warning", NotificationType.Warning); + } + else + { + customize.Race = Race.Hyur; + customize.Clan = SubRace.Midlander; + Glamourer.Chat.NotificationMessage( + $"The clan {oldClan.ToName()} is unknown, reset to {customize.Race.ToName()} {customize.Clan.ToName()}.", "Warning", + NotificationType.Warning); + } + } + + if (!manager.Genders.Contains(customize.Gender)) + { + ret = false; + Glamourer.Chat.NotificationMessage($"The gender {customize.Gender} is unknown, reset to {Gender.Male.ToName()}.", "Warning", + NotificationType.Warning); + customize.Gender = Gender.Male; + } + + // TODO: Female Hrothgar + if (customize.Gender == Gender.Female && customize.Race == Race.Hrothgar) + { + ret = false; + Glamourer.Chat.NotificationMessage($"Hrothgar do not currently support female characters, reset to male.", "Warning", + NotificationType.Warning); + customize.Gender = Gender.Male; + } + + var list = manager.GetList(customize.Clan, customize.Gender); + + // Face is handled first automatically so it should not conflict with other customizations when corrupt. + foreach (var index in Enum.GetValues().Where(list.IsAvailable)) + { + var value = customize.Get(index); + var count = list.Count(index, customize.Face); + var idx = list.DataByValue(index, value, out var data, customize.Face); + if (idx >= 0 && idx < count) + continue; + + ret = false; + var name = list.Option(index); + var newValue = list.Data(index, 0, customize.Face); + Glamourer.Chat.NotificationMessage( + $"Customization {name} for {customize.Race.ToName()} {customize.Gender.ToName()}s does not support value {value.Value}, reset to {newValue.Value.Value}"); + customize.Set(index, newValue.Value); + } + + return ret; + } + + private static bool ValidateModelId(ref uint modelId) + { + if (modelId != 0) + { + Glamourer.Chat.NotificationMessage($"Model IDs different from 0 are not currently allowed, reset {modelId} to 0.", "Warning", + NotificationType.Warning); + modelId = 0; + return false; + } + + return true; + } + + private static bool LoadCustomize(CustomizationManager manager, JToken? json, Design design) + { + if (json == null) + return true; + + design.DesignData.ModelId = json["ModelId"]?.ToObject() ?? 0; + var ret = !ValidateModelId(ref design.DesignData.ModelId); + + foreach (var idx in Enum.GetValues()) + { + var tok = json[idx.ToString()]; + var data = (CustomizeValue)(tok?["Value"]?.ToObject() ?? 0); + var apply = tok?["Apply"]?.ToObject() ?? false; + design.DesignData.Customize[idx] = data; + design.SetApplyCustomize(idx, apply); + } + + design.DesignData.SetIsWet(json["IsWet"]?.ToObject() ?? false); + ret |= !ValidateCustomize(manager, ref design.DesignData.Customize); + + return ret; + } + + //public void MigrateBase64(ItemManager items, string base64) + //{ + // var data = DesignBase64Migration.MigrateBase64(items, base64, out var applyEquip, out var applyCustomize, out var writeProtected, out var wet, + // out var hat, + // out var visor, out var weapon); + // UpdateMainhand(items, data.MainHand); + // UpdateOffhand(items, data.OffHand); + // foreach (var slot in EquipSlotExtensions.EqdpSlots) + // UpdateArmor(items, slot, data.Armor(slot), true); + // ModelData.Customize = data.Customize; + // _applyEquip = applyEquip; + // _applyCustomize = applyCustomize; + // WriteProtected = writeProtected; + // Wetness = wet; + // Hat = hat; + // Visor = visor; + // Weapon = weapon; + //} + // + //public static Design CreateTemporaryFromBase64(ItemManager items, string base64, bool customize, bool equip) + //{ + // var ret = new Design(items); + // ret.MigrateBase64(items, base64); + // if (!customize) + // ret._applyCustomize = 0; + // if (!equip) + // ret._applyEquip = 0; + // ret.Wetness = ret.Wetness.SetEnabled(customize); + // ret.Visor = ret.Visor.SetEnabled(equip); + // ret.Hat = ret.Hat.SetEnabled(equip); + // ret.Weapon = ret.Weapon.SetEnabled(equip); + // return ret; + //} + + // Outdated. + //public string CreateOldBase64() + // => DesignBase64Migration.CreateOldBase64(in ModelData, _applyEquip, _applyCustomize, Wetness == QuadBool.True, Hat.ForcedValue, + // Hat.Enabled, + // Visor.ForcedValue, Visor.Enabled, Weapon.ForcedValue, Weapon.Enabled, WriteProtected, 1f); + + public string ToFilename(FilenameService fileNames) + => fileNames.DesignFile(this); + + public void Save(StreamWriter writer) + { + using var j = new JsonTextWriter(writer) + { + Formatting = Formatting.Indented, + }; + var obj = JsonSerialize(); + obj.WriteTo(j); + } + + public string LogName(string fileName) + => Path.GetFileNameWithoutExtension(fileName); + + #endregion +} diff --git a/Glamourer/Designs/DesignBase64Migration.cs b/Glamourer/Designs/DesignBase64Migration.cs new file mode 100644 index 0000000..6634b01 --- /dev/null +++ b/Glamourer/Designs/DesignBase64Migration.cs @@ -0,0 +1,150 @@ +using System; +using Glamourer.Customization; +using Glamourer.Services; +using Glamourer.Structs; +using OtterGui; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; + +namespace Glamourer.Designs; + +public static class DesignBase64Migration +{ + public const int Base64Size = 91; + + public static DesignData MigrateBase64(ItemManager items, string base64, out EquipFlag equipFlags, out CustomizeFlag customizeFlags, + out bool writeProtected, out bool applyHat, out bool applyVisor, out bool applyWeapon) + { + static void CheckSize(int length, int requiredLength) + { + if (length != requiredLength) + throw new Exception( + $"Can not parse Base64 string into CharacterSave:\n\tInvalid size {length} instead of {requiredLength}."); + } + + byte applicationFlags; + ushort equipFlagsS; + var bytes = Convert.FromBase64String(base64); + applyHat = false; + applyVisor = false; + applyWeapon = false; + var data = new DesignData(); + switch (bytes[0]) + { + case 1: + { + CheckSize(bytes.Length, 86); + applicationFlags = bytes[1]; + equipFlagsS = BitConverter.ToUInt16(bytes, 2); + break; + } + case 2: + { + CheckSize(bytes.Length, Base64Size); + applicationFlags = bytes[1]; + equipFlagsS = BitConverter.ToUInt16(bytes, 2); + data.SetHatVisible((bytes[90] & 0x01) == 0); + data.SetVisor((bytes[90] & 0x10) != 0); + data.SetWeaponVisible((bytes[90] & 0x02) == 0); + break; + } + default: throw new Exception($"Can not parse Base64 string into design for migration:\n\tInvalid Version {bytes[0]}."); + } + + customizeFlags = (applicationFlags & 0x01) != 0 ? CustomizeFlagExtensions.All : 0; + data.SetIsWet((applicationFlags & 0x02) != 0); + applyHat = (applicationFlags & 0x04) != 0; + applyWeapon = (applicationFlags & 0x08) != 0; + applyVisor = (applicationFlags & 0x10) != 0; + writeProtected = (applicationFlags & 0x20) != 0; + + equipFlags = 0; + equipFlags |= (equipFlagsS & 0x0001) != 0 ? EquipFlag.Mainhand | EquipFlag.MainhandStain : 0; + equipFlags |= (equipFlagsS & 0x0002) != 0 ? EquipFlag.Offhand | EquipFlag.OffhandStain : 0; + var flag = 0x0002u; + foreach (var slot in EquipSlotExtensions.EqdpSlots) + { + flag <<= 1; + equipFlags |= (equipFlagsS & flag) != 0 ? slot.ToFlag() | slot.ToStainFlag() : 0; + } + + unsafe + { + fixed (byte* ptr = bytes) + { + data.Customize.Load(*(Customize*)(ptr + 4)); + var cur = (CharacterWeapon*)(ptr + 30); + var main = items.Identify(EquipSlot.MainHand, cur[0].Set, cur[0].Type, (byte)cur[0].Variant); + if (!main.Valid) + throw new Exception($"Base64 string invalid, weapon could not be identified."); + + data.SetItem(main); + data.SetStain(EquipSlot.MainHand, cur[0].Stain); + var off = items.Identify(EquipSlot.OffHand, cur[1].Set, cur[1].Type, (byte)cur[1].Variant, main.Type); + if (!off.Valid) + throw new Exception($"Base64 string invalid, weapon could not be identified."); + + data.SetItem(off); + data.SetStain(EquipSlot.OffHand, cur[1].Stain); + + var eq = (CharacterArmor*)(ptr + 46); + foreach (var (slot, idx) in EquipSlotExtensions.EqdpSlots.WithIndex()) + { + var mdl = eq[idx]; + var item = items.Identify(slot, mdl.Set, mdl.Variant); + if (!item.Valid) + throw new Exception($"Base64 string invalid, item could not be identified."); + + data.SetItem(item); + data.SetStain(slot, mdl.Stain); + } + } + } + + return data; + } + + public static unsafe string CreateOldBase64(in DesignData save, EquipFlag equipFlags, CustomizeFlag customizeFlags, + bool setHat, bool setVisor, bool setWeapon, bool writeProtected, float alpha = 1.0f) + { + var data = stackalloc byte[Base64Size]; + data[0] = 2; + data[1] = (byte)((customizeFlags == CustomizeFlagExtensions.All ? 0x01 : 0) + | (save.IsWet() ? 0x02 : 0) + | (setHat ? 0x04 : 0) + | (setWeapon ? 0x08 : 0) + | (setVisor ? 0x10 : 0) + | (writeProtected ? 0x20 : 0)); + data[2] = (byte)((equipFlags.HasFlag(EquipFlag.Mainhand) ? 0x01 : 0) + | (equipFlags.HasFlag(EquipFlag.Offhand) ? 0x02 : 0) + | (equipFlags.HasFlag(EquipFlag.Head) ? 0x04 : 0) + | (equipFlags.HasFlag(EquipFlag.Body) ? 0x08 : 0) + | (equipFlags.HasFlag(EquipFlag.Hands) ? 0x10 : 0) + | (equipFlags.HasFlag(EquipFlag.Legs) ? 0x20 : 0) + | (equipFlags.HasFlag(EquipFlag.Feet) ? 0x40 : 0) + | (equipFlags.HasFlag(EquipFlag.Ears) ? 0x80 : 0)); + data[3] = (byte)((equipFlags.HasFlag(EquipFlag.Neck) ? 0x01 : 0) + | (equipFlags.HasFlag(EquipFlag.Wrist) ? 0x02 : 0) + | (equipFlags.HasFlag(EquipFlag.RFinger) ? 0x04 : 0) + | (equipFlags.HasFlag(EquipFlag.LFinger) ? 0x08 : 0)); + save.Customize.Write((nint)data + 4); + ((CharacterWeapon*)(data + 30))[0] = save.Item(EquipSlot.MainHand).Weapon(save.Stain(EquipSlot.MainHand)); + ((CharacterWeapon*)(data + 30))[1] = save.Item(EquipSlot.OffHand).Weapon(save.Stain(EquipSlot.OffHand)); + ((CharacterArmor*)(data + 46))[0] = save.Item(EquipSlot.Head).Armor(save.Stain(EquipSlot.Head)); + ((CharacterArmor*)(data + 46))[1] = save.Item(EquipSlot.Body).Armor(save.Stain(EquipSlot.Body)); + ((CharacterArmor*)(data + 46))[2] = save.Item(EquipSlot.Hands).Armor(save.Stain(EquipSlot.Hands)); + ((CharacterArmor*)(data + 46))[3] = save.Item(EquipSlot.Legs).Armor(save.Stain(EquipSlot.Legs)); + ((CharacterArmor*)(data + 46))[4] = save.Item(EquipSlot.Feet).Armor(save.Stain(EquipSlot.Feet)); + ((CharacterArmor*)(data + 46))[5] = save.Item(EquipSlot.Ears).Armor(save.Stain(EquipSlot.Ears)); + ((CharacterArmor*)(data + 46))[6] = save.Item(EquipSlot.Neck).Armor(save.Stain(EquipSlot.Neck)); + ((CharacterArmor*)(data + 46))[7] = save.Item(EquipSlot.Wrists).Armor(save.Stain(EquipSlot.Wrists)); + ((CharacterArmor*)(data + 46))[8] = save.Item(EquipSlot.RFinger).Armor(save.Stain(EquipSlot.RFinger)); + ((CharacterArmor*)(data + 46))[9] = save.Item(EquipSlot.LFinger).Armor(save.Stain(EquipSlot.LFinger)); + *(float*)(data + 86) = 1f; + data[90] = (byte)((save.IsHatVisible() ? 0x01 : 0) + | (save.IsVisorToggled() ? 0x10 : 0) + | (save.IsWeaponVisible() ? 0x02 : 0)); + + return Convert.ToBase64String(new Span(data, Base64Size)); + } +} diff --git a/Glamourer/Designs/DesignData.cs b/Glamourer/Designs/DesignData.cs new file mode 100644 index 0000000..73df918 --- /dev/null +++ b/Glamourer/Designs/DesignData.cs @@ -0,0 +1,179 @@ +using System; +using System.Runtime.CompilerServices; +using Glamourer.Customization; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; + +namespace Glamourer.Designs; + +public unsafe struct DesignData +{ + private string _nameHead = string.Empty; + private string _nameBody = string.Empty; + private string _nameHands = string.Empty; + private string _nameLegs = string.Empty; + private string _nameFeet = string.Empty; + private string _nameEars = string.Empty; + private string _nameNeck = string.Empty; + private string _nameWrists = string.Empty; + private string _nameRFinger = string.Empty; + private string _nameLFinger = string.Empty; + private string _nameMainhand = string.Empty; + private string _nameOffhand = string.Empty; + private fixed uint _itemIds[12]; + private fixed ushort _iconIds[12]; + private fixed byte _equipmentBytes[48]; + public Customize Customize = Customize.Default; + public uint ModelId; + private WeaponType _secondaryMainhand; + private WeaponType _secondaryOffhand; + private FullEquipType _typeMainhand; + private FullEquipType _typeOffhand; + private byte _states; + + public DesignData() + {} + + public readonly StainId Stain(EquipSlot slot) + { + var index = slot.ToIndex(); + return index > 11 ? (StainId)0 : _equipmentBytes[4 * index + 3]; + } + + public readonly EquipItem Item(EquipSlot slot) + => slot.ToIndex() switch + { + // @formatter:off + 0 => new EquipItem(_nameHead, _itemIds[ 0], _iconIds[ 0], (SetId)(_equipmentBytes[ 0] | (_equipmentBytes[ 1] << 8)), (WeaponType)0, _equipmentBytes[ 2], FullEquipType.Head ), + 1 => new EquipItem(_nameBody, _itemIds[ 1], _iconIds[ 1], (SetId)(_equipmentBytes[ 4] | (_equipmentBytes[ 5] << 8)), (WeaponType)0, _equipmentBytes[ 6], FullEquipType.Body ), + 2 => new EquipItem(_nameHands, _itemIds[ 2], _iconIds[ 2], (SetId)(_equipmentBytes[ 8] | (_equipmentBytes[ 9] << 8)), (WeaponType)0, _equipmentBytes[10], FullEquipType.Hands ), + 3 => new EquipItem(_nameLegs, _itemIds[ 3], _iconIds[ 3], (SetId)(_equipmentBytes[12] | (_equipmentBytes[13] << 8)), (WeaponType)0, _equipmentBytes[14], FullEquipType.Legs ), + 4 => new EquipItem(_nameFeet, _itemIds[ 4], _iconIds[ 4], (SetId)(_equipmentBytes[16] | (_equipmentBytes[17] << 8)), (WeaponType)0, _equipmentBytes[18], FullEquipType.Feet ), + 5 => new EquipItem(_nameEars, _itemIds[ 5], _iconIds[ 5], (SetId)(_equipmentBytes[20] | (_equipmentBytes[21] << 8)), (WeaponType)0, _equipmentBytes[22], FullEquipType.Ears ), + 6 => new EquipItem(_nameNeck, _itemIds[ 6], _iconIds[ 6], (SetId)(_equipmentBytes[24] | (_equipmentBytes[25] << 8)), (WeaponType)0, _equipmentBytes[26], FullEquipType.Neck ), + 7 => new EquipItem(_nameWrists, _itemIds[ 7], _iconIds[ 7], (SetId)(_equipmentBytes[28] | (_equipmentBytes[29] << 8)), (WeaponType)0, _equipmentBytes[30], FullEquipType.Wrists ), + 8 => new EquipItem(_nameRFinger, _itemIds[ 8], _iconIds[ 8], (SetId)(_equipmentBytes[32] | (_equipmentBytes[33] << 8)), (WeaponType)0, _equipmentBytes[34], FullEquipType.Finger ), + 9 => new EquipItem(_nameLFinger, _itemIds[ 9], _iconIds[ 9], (SetId)(_equipmentBytes[36] | (_equipmentBytes[37] << 8)), (WeaponType)0, _equipmentBytes[38], FullEquipType.Finger ), + 10 => new EquipItem(_nameMainhand, _itemIds[10], _iconIds[10], (SetId)(_equipmentBytes[40] | (_equipmentBytes[41] << 8)), _secondaryMainhand, _equipmentBytes[42], _typeMainhand ), + 11 => new EquipItem(_nameOffhand, _itemIds[11], _iconIds[11], (SetId)(_equipmentBytes[44] | (_equipmentBytes[45] << 8)), _secondaryOffhand, _equipmentBytes[46], _typeOffhand ), + _ => new EquipItem(), + // @formatter:on + }; + + public bool SetItem(EquipItem item) + { + var index = item.Type.ToSlot().ToIndex(); + if (index > 11 || _itemIds[index] == item.Id) + return false; + + _itemIds[index] = item.Id; + _iconIds[index] = item.IconId; + _equipmentBytes[4 * index + 0] = (byte)item.ModelId; + _equipmentBytes[4 * index + 1] = (byte)(item.ModelId.Value >> 8); + _equipmentBytes[4 * index + 2] = item.Variant; + switch (index) + { + // @formatter:off + case 0: _nameHead = item.Name; return true; + case 1: _nameBody = item.Name; return true; + case 2: _nameHands = item.Name; return true; + case 3: _nameLegs = item.Name; return true; + case 4: _nameFeet = item.Name; return true; + case 5: _nameEars = item.Name; return true; + case 6: _nameNeck = item.Name; return true; + case 7: _nameWrists = item.Name; return true; + case 8: _nameRFinger = item.Name; return true; + case 9: _nameLFinger = item.Name; return true; + // @formatter:on + case 10: + _nameMainhand = item.Name; + _secondaryMainhand = item.WeaponType; + _typeMainhand = item.Type; + return true; + case 11: + _nameOffhand = item.Name; + _secondaryOffhand = item.WeaponType; + _typeOffhand = item.Type; + return true; + } + + return true; + } + + public bool SetStain(EquipSlot slot, StainId stain) + => slot.ToIndex() switch + { + 0 => SetIfDifferent(ref _equipmentBytes[3], stain.Value), + 1 => SetIfDifferent(ref _equipmentBytes[7], stain.Value), + 2 => SetIfDifferent(ref _equipmentBytes[11], stain.Value), + 3 => SetIfDifferent(ref _equipmentBytes[15], stain.Value), + 4 => SetIfDifferent(ref _equipmentBytes[19], stain.Value), + 5 => SetIfDifferent(ref _equipmentBytes[23], stain.Value), + 6 => SetIfDifferent(ref _equipmentBytes[27], stain.Value), + 7 => SetIfDifferent(ref _equipmentBytes[31], stain.Value), + 8 => SetIfDifferent(ref _equipmentBytes[35], stain.Value), + 9 => SetIfDifferent(ref _equipmentBytes[39], stain.Value), + 10 => SetIfDifferent(ref _equipmentBytes[43], stain.Value), + 11 => SetIfDifferent(ref _equipmentBytes[47], stain.Value), + _ => false, + }; + + public readonly bool IsWet() + => (_states & 0x01) == 0x01; + + public bool SetIsWet(bool value) + { + if (value == IsWet()) + return false; + + _states = (byte)(value ? _states | 0x01 : _states & ~0x01); + return true; + } + + + public readonly bool IsVisorToggled() + => (_states & 0x02) == 0x02; + + public bool SetVisor(bool value) + { + if (value == IsVisorToggled()) + return false; + + _states = (byte)(value ? _states | 0x02 : _states & ~0x02); + return true; + } + + public readonly bool IsHatVisible() + => (_states & 0x04) == 0x04; + + public bool SetHatVisible(bool value) + { + if (value == IsHatVisible()) + return false; + + _states = (byte)(value ? _states | 0x04 : _states & ~0x04); + return true; + } + + public readonly bool IsWeaponVisible() + => (_states & 0x08) == 0x09; + + public bool SetWeaponVisible(bool value) + { + if (value == IsWeaponVisible()) + return false; + + _states = (byte)(value ? _states | 0x08 : _states & ~0x08); + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static bool SetIfDifferent(ref T old, T value) where T : IEquatable + { + if (old.Equals(value)) + return false; + + old = value; + return true; + } +} \ No newline at end of file diff --git a/Glamourer/Designs/IDesign.cs b/Glamourer/Designs/IDesign.cs new file mode 100644 index 0000000..8544e69 --- /dev/null +++ b/Glamourer/Designs/IDesign.cs @@ -0,0 +1,46 @@ +using Glamourer.Customization; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; + +namespace Glamourer.Designs; + +public interface IDesign +{ + public uint GetModelId(); + public bool SetModelId(uint modelId); + + public EquipItem GetEquipItem(EquipSlot slot); + public bool SetEquipItem(EquipItem item); + + public StainId GetStain(EquipSlot slot); + public bool SetStain(EquipSlot slot, StainId stain); + + public CustomizeValue GetCustomizeValue(CustomizeIndex type); + public bool SetCustomizeValue(CustomizeIndex type); + + public bool DoApplyEquip(EquipSlot slot); + public bool DoApplyStain(EquipSlot slot); + public bool DoApplyCustomize(CustomizeIndex index); + + public bool SetApplyEquip(EquipSlot slot, bool value); + public bool SetApplyStain(EquipSlot slot, bool value); + public bool SetApplyCustomize(CustomizeIndex slot, bool value); + + public bool IsWet(); + public bool SetIsWet(bool value); + + public bool IsHatVisible(); + public bool DoApplyHatVisible(); + public bool SetHatVisible(bool value); + public bool SetApplyHatVisible(bool value); + + public bool IsVisorToggled(); + public bool DoApplyVisorToggle(); + public bool SetVisorToggle(bool value); + public bool SetApplyVisorToggle(bool value); + + public bool IsWeaponVisible(); + public bool DoApplyWeaponVisible(); + public bool SetWeaponVisible(bool value); + public bool SetApplyWeaponVisible(bool value); +} diff --git a/Glamourer/Glamourer.cs b/Glamourer/Glamourer.cs index 9e9b4f4..b2e1b49 100644 --- a/Glamourer/Glamourer.cs +++ b/Glamourer/Glamourer.cs @@ -9,7 +9,7 @@ using OtterGui.Log; namespace Glamourer; -public class Item : IDalamudPlugin +public class Glamourer : IDalamudPlugin { public string Name => "Glamourer"; @@ -26,7 +26,7 @@ public class Item : IDalamudPlugin private readonly ServiceProvider _services; - public Item(DalamudPluginInterface pluginInterface) + public Glamourer(DalamudPluginInterface pluginInterface) { try { diff --git a/Glamourer/Glamourer.csproj b/Glamourer/Glamourer.csproj index cea86c5..5c7886a 100644 --- a/Glamourer/Glamourer.csproj +++ b/Glamourer/Glamourer.csproj @@ -120,10 +120,6 @@ PreserveNewest - - - - diff --git a/Glamourer/Gui/Colors.cs b/Glamourer/Gui/Colors.cs new file mode 100644 index 0000000..a5ce52f --- /dev/null +++ b/Glamourer/Gui/Colors.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; + +namespace Glamourer.Gui; + +public enum ColorId +{ + CustomizationDesign, + StateDesign, + EquipmentDesign, +} + +public static class Colors +{ + public static (uint DefaultColor, string Name, string Description) Data(this ColorId color) + => color switch + { + // @formatter:off + ColorId.CustomizationDesign => (0xFFC000C0, "Customization Design", "A design that only changes customizations on a character." ), + ColorId.StateDesign => (0xFF00C0C0, "State Design", "A design that only changes meta state on a character." ), + ColorId.EquipmentDesign => (0xFF00C000, "Equipment Design", "A design that only changes equipment on a character." ), + _ => (0x00000000, string.Empty, string.Empty ), + // @formatter:on + }; + + private static IReadOnlyDictionary _colors = new Dictionary(); + + /// Obtain the configured value for a color. + public static uint Value(this ColorId color) + => _colors.TryGetValue(color, out var value) ? value : color.Data().DefaultColor; + + /// Set the configurable colors dictionary to a value. + public static void SetColors(Configuration config) + => _colors = config.Colors; +} diff --git a/Glamourer/Gui/MainWindow.cs b/Glamourer/Gui/MainWindow.cs index 9e4c4fc..0eace1b 100644 --- a/Glamourer/Gui/MainWindow.cs +++ b/Glamourer/Gui/MainWindow.cs @@ -33,7 +33,7 @@ public class MainWindow : Window } private static string GetLabel() - => Item.Version.Length == 0 + => Glamourer.Version.Length == 0 ? "Glamourer###GlamourerMainWindow" - : $"Glamourer v{Item.Version}###GlamourerMainWindow"; + : $"Glamourer v{Glamourer.Version}###GlamourerMainWindow"; } diff --git a/Glamourer/Gui/Tabs/DebugTab.cs b/Glamourer/Gui/Tabs/DebugTab.cs index 849ae54..2b76348 100644 --- a/Glamourer/Gui/Tabs/DebugTab.cs +++ b/Glamourer/Gui/Tabs/DebugTab.cs @@ -1,11 +1,13 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Numerics; using Dalamud.Game.ClientState.Objects; using Dalamud.Interface; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using Glamourer.Customization; +using Glamourer.Designs; using Glamourer.Interop; using Glamourer.Interop.Penumbra; using Glamourer.Interop.Structs; @@ -17,6 +19,7 @@ using OtterGui.Widgets; using Penumbra.Api.Enums; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; +using static OtterGui.Raii.ImRaii; namespace Glamourer.Gui.Tabs; @@ -28,6 +31,7 @@ public unsafe class DebugTab : ITab private readonly WeaponService _weaponService; private readonly PenumbraService _penumbra; private readonly ObjectTable _objects; + private readonly ObjectManager _objectManager; private readonly ItemManager _items; private readonly ActorService _actors; @@ -37,7 +41,7 @@ public unsafe class DebugTab : ITab public DebugTab(ChangeCustomizeService changeCustomizeService, VisorService visorService, ObjectTable objects, UpdateSlotService updateSlotService, WeaponService weaponService, PenumbraService penumbra, IdentifierService identifier, - ActorService actors, ItemManager items, CustomizationService customization) + ActorService actors, ItemManager items, CustomizationService customization, ObjectManager objectManager) { _changeCustomizeService = changeCustomizeService; _visorService = visorService; @@ -48,6 +52,7 @@ public unsafe class DebugTab : ITab _actors = actors; _items = items; _customization = customization; + _objectManager = objectManager; } public ReadOnlySpan Label @@ -58,6 +63,7 @@ public unsafe class DebugTab : ITab DrawInteropHeader(); DrawGameDataHeader(); DrawPenumbraHeader(); + DrawDesignManager(); } #region Interop @@ -67,10 +73,20 @@ public unsafe class DebugTab : ITab if (!ImGui.CollapsingHeader("Interop")) return; + DrawModelEvaluation(); + DrawObjectManager(); + } + + private void DrawModelEvaluation() + { + using var tree = TreeNode("Model Evaluation"); + if (!tree) + return; + ImGui.InputInt("Game Object Index", ref _gameObjectIndex, 0, 0); var actor = (Actor)_objects.GetObjectAddress(_gameObjectIndex); var model = actor.Model; - using var table = ImRaii.Table("##interopTable", 4, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); + using var table = Table("##evaluationTable", 4, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); ImGui.TableNextColumn(); ImGui.TableNextColumn(); ImGui.TableHeader("Actor"); @@ -107,9 +123,79 @@ public unsafe class DebugTab : ITab DrawCustomize(actor, model); } + private string _objectFilter = string.Empty; + + private void DrawObjectManager() + { + using var tree = TreeNode("Object Manager"); + if (!tree) + return; + + _objectManager.Update(); + + using (var table = Table("##data", 3, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit)) + { + if (!table) + return; + + ImGuiUtil.DrawTableColumn("Last Update"); + ImGuiUtil.DrawTableColumn(_objectManager.LastUpdate.ToString(CultureInfo.InvariantCulture)); + ImGui.TableNextColumn(); + + ImGuiUtil.DrawTableColumn("World"); + ImGuiUtil.DrawTableColumn(_actors.Valid ? _actors.AwaitedService.Data.ToWorldName(_objectManager.World) : "Service Missing"); + ImGuiUtil.DrawTableColumn(_objectManager.World.ToString()); + + ImGuiUtil.DrawTableColumn("Player Character"); + ImGuiUtil.DrawTableColumn($"{_objectManager.Player.Utf8Name} ({_objectManager.Player.Index})"); + ImGui.TableNextColumn(); + ImGuiUtil.CopyOnClickSelectable(_objectManager.Player.ToString()); + + ImGuiUtil.DrawTableColumn("In GPose"); + ImGuiUtil.DrawTableColumn(_objectManager.IsInGPose.ToString()); + ImGui.TableNextColumn(); + + if (_objectManager.IsInGPose) + { + ImGuiUtil.DrawTableColumn("GPose Player"); + ImGuiUtil.DrawTableColumn($"{_objectManager.GPosePlayer.Utf8Name} ({_objectManager.GPosePlayer.Index})"); + ImGui.TableNextColumn(); + ImGuiUtil.CopyOnClickSelectable(_objectManager.GPosePlayer.ToString()); + } + + ImGuiUtil.DrawTableColumn("Number of Players"); + ImGuiUtil.DrawTableColumn(_objectManager.Count.ToString()); + ImGui.TableNextColumn(); + } + + var filterChanged = ImGui.InputTextWithHint("##Filter", "Filter...", ref _objectFilter, 64); + using var table2 = Table("##data2", 3, + ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersOuter | ImGuiTableFlags.ScrollY, + new Vector2(-1, 20 * ImGui.GetTextLineHeightWithSpacing())); + if (!table2) + return; + + if (filterChanged) + ImGui.SetScrollY(0); + + ImGui.TableNextColumn(); + var skips = ImGuiClip.GetNecessarySkips(ImGui.GetTextLineHeightWithSpacing()); + ImGui.TableNextRow(); + + var remainder = ImGuiClip.FilteredClippedDraw(_objectManager, skips, + p => p.Value.Label.Contains(_objectFilter, StringComparison.OrdinalIgnoreCase), p + => + { + ImGuiUtil.DrawTableColumn(p.Key.ToString()); + ImGuiUtil.DrawTableColumn(p.Value.Label); + ImGuiUtil.DrawTableColumn(string.Join(", ", p.Value.Objects.OrderBy(a => a.Index).Select(a => a.Index.ToString()))); + }); + ImGuiClip.DrawEndDummy(remainder, ImGui.GetTextLineHeightWithSpacing()); + } + private void DrawVisor(Actor actor, Model model) { - using var id = ImRaii.PushId("Visor"); + using var id = PushId("Visor"); ImGuiUtil.DrawTableColumn("Visor State"); ImGuiUtil.DrawTableColumn(actor.IsCharacter ? actor.AsCharacter->DrawData.IsVisorToggled.ToString() : "No Character"); ImGuiUtil.DrawTableColumn(model.IsHuman ? _visorService.GetVisorState(model).ToString() : "No Human"); @@ -129,7 +215,7 @@ public unsafe class DebugTab : ITab private void DrawHatState(Actor actor, Model model) { - using var id = ImRaii.PushId("HatState"); + using var id = PushId("HatState"); ImGuiUtil.DrawTableColumn("Hat State"); ImGuiUtil.DrawTableColumn(actor.IsCharacter ? actor.AsCharacter->DrawData.IsHatHidden ? "Hidden" : actor.GetArmor(EquipSlot.Head).ToString() @@ -154,7 +240,7 @@ public unsafe class DebugTab : ITab private void DrawWeaponState(Actor actor, Model model) { - using var id = ImRaii.PushId("WeaponState"); + using var id = PushId("WeaponState"); ImGuiUtil.DrawTableColumn("Weapon State"); ImGuiUtil.DrawTableColumn(actor.IsCharacter ? actor.AsCharacter->DrawData.IsWeaponHidden ? "Hidden" : "Visible" @@ -186,7 +272,7 @@ public unsafe class DebugTab : ITab private void DrawWetness(Actor actor, Model model) { - using var id = ImRaii.PushId("Wetness"); + using var id = PushId("Wetness"); ImGuiUtil.DrawTableColumn("Wetness"); ImGuiUtil.DrawTableColumn(actor.IsCharacter ? actor.AsCharacter->IsGPoseWet ? "GPose" : "None" : "No Character"); var modelString = model.IsCharacterBase @@ -212,10 +298,10 @@ public unsafe class DebugTab : ITab private void DrawEquip(Actor actor, Model model) { - using var id = ImRaii.PushId("Equipment"); + using var id = PushId("Equipment"); foreach (var slot in EquipSlotExtensions.EqdpSlots) { - using var id2 = ImRaii.PushId((int)slot); + using var id2 = PushId((int)slot); ImGuiUtil.DrawTableColumn(slot.ToName()); ImGuiUtil.DrawTableColumn(actor.IsCharacter ? actor.GetArmor(slot).ToString() : "No Character"); ImGuiUtil.DrawTableColumn(model.IsHuman ? model.GetArmor(slot).ToString() : "No Human"); @@ -237,7 +323,7 @@ public unsafe class DebugTab : ITab private void DrawCustomize(Actor actor, Model model) { - using var id = ImRaii.PushId("Customize"); + using var id = PushId("Customize"); var actorCustomize = new Customize(actor.IsCharacter ? *(Penumbra.GameData.Structs.CustomizeData*)&actor.AsCharacter->DrawData.CustomizeData : new Penumbra.GameData.Structs.CustomizeData()); @@ -246,7 +332,7 @@ public unsafe class DebugTab : ITab : new Penumbra.GameData.Structs.CustomizeData()); foreach (var type in Enum.GetValues()) { - using var id2 = ImRaii.PushId((int)type); + using var id2 = PushId((int)type); ImGuiUtil.DrawTableColumn(type.ToDefaultName()); ImGuiUtil.DrawTableColumn(actor.IsCharacter ? actorCustomize[type].Value.ToString("X2") : "No Character"); ImGuiUtil.DrawTableColumn(model.IsHuman ? modelCustomize[type].Value.ToString("X2") : "No Human"); @@ -287,7 +373,7 @@ public unsafe class DebugTab : ITab if (!ImGui.CollapsingHeader("Penumbra")) return; - using var table = ImRaii.Table("##PenumbraTable", 3, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); + using var table = Table("##PenumbraTable", 3, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); if (!table) return; @@ -324,7 +410,7 @@ public unsafe class DebugTab : ITab ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale); ImGui.InputInt("##redrawObject", ref _gameObjectIndex, 0, 0); ImGui.TableNextColumn(); - using (var disabled = ImRaii.Disabled(!_penumbra.Available)) + using (var disabled = Disabled(!_penumbra.Available)) { if (ImGui.SmallButton("Redraw")) _penumbra.RedrawObject(_objects.GetObjectAddress(_gameObjectIndex), RedrawType.Redraw); @@ -355,8 +441,8 @@ public unsafe class DebugTab : ITab private void DrawIdentifierService() { - using var disabled = ImRaii.Disabled(!_items.IdentifierService.Valid); - using var tree = ImRaii.TreeNode("Identifier Service"); + using var disabled = Disabled(!_items.IdentifierService.Valid); + using var tree = TreeNode("Identifier Service"); if (!tree || !_items.IdentifierService.Valid) return; @@ -400,7 +486,7 @@ public unsafe class DebugTab : ITab private void DrawRestrictedGear() { - using var tree = ImRaii.TreeNode("Restricted Gear Service"); + using var tree = TreeNode("Restricted Gear Service"); if (!tree) return; @@ -451,8 +537,8 @@ public unsafe class DebugTab : ITab private void DrawActorService() { - using var disabled = ImRaii.Disabled(!_actors.Valid); - using var tree = ImRaii.TreeNode("Actor Service"); + using var disabled = Disabled(!_actors.Valid); + using var tree = TreeNode("Actor Service"); if (!tree || !_actors.Valid) return; @@ -468,14 +554,14 @@ public unsafe class DebugTab : ITab private static void DrawNameTable(string label, ref string filter, IEnumerable<(uint, string)> names) { - using var _ = ImRaii.PushId(label); - using var tree = ImRaii.TreeNode(label); + using var _ = PushId(label); + using var tree = TreeNode(label); if (!tree) return; var resetScroll = ImGui.InputTextWithHint("##filter", "Filter...", ref filter, 256); var height = ImGui.GetTextLineHeightWithSpacing() + 2 * ImGui.GetStyle().CellPadding.Y; - using var table = ImRaii.Table("##table", 2, ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY | ImGuiTableFlags.BordersOuter, + using var table = Table("##table", 2, ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY | ImGuiTableFlags.BordersOuter, new Vector2(-1, 10 * height)); if (!table) return; @@ -502,13 +588,13 @@ public unsafe class DebugTab : ITab private void DrawItemService() { - using var disabled = ImRaii.Disabled(!_items.ItemService.Valid); - using var tree = ImRaii.TreeNode("Item Manager"); + using var disabled = Disabled(!_items.ItemService.Valid); + using var tree = TreeNode("Item Manager"); if (!tree || !_items.ItemService.Valid) return; disabled.Dispose(); - ImRaii.TreeNode($"Default Sword: {_items.DefaultSword.Name} ({_items.DefaultSword.Id}) ({_items.DefaultSword.Weapon()})", + TreeNode($"Default Sword: {_items.DefaultSword.Name} ({_items.DefaultSword.Id}) ({_items.DefaultSword.Weapon()})", ImGuiTreeNodeFlags.Leaf).Dispose(); DrawNameTable("All Items (Main)", ref _itemFilter, _items.ItemService.AwaitedService.AllItems(true).Select(p => (p.Item1, @@ -530,13 +616,13 @@ public unsafe class DebugTab : ITab private void DrawStainService() { - using var tree = ImRaii.TreeNode("Stain Service"); + using var tree = TreeNode("Stain Service"); if (!tree) return; var resetScroll = ImGui.InputTextWithHint("##filter", "Filter...", ref _stainFilter, 256); var height = ImGui.GetTextLineHeightWithSpacing() + 2 * ImGui.GetStyle().CellPadding.Y; - using var table = ImRaii.Table("##table", 4, + using var table = Table("##table", 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY | ImGuiTableFlags.BordersOuter | ImGuiTableFlags.SizingFixedFit, new Vector2(-1, 10 * height)); if (!table) @@ -566,8 +652,8 @@ public unsafe class DebugTab : ITab private void DrawCustomizationService() { - using var disabled = ImRaii.Disabled(!_customization.Valid); - using var tree = ImRaii.TreeNode("Customization Service"); + using var disabled = Disabled(!_customization.Valid); + using var tree = TreeNode("Customization Service"); if (!tree || !_customization.Valid) return; @@ -582,11 +668,11 @@ public unsafe class DebugTab : ITab private void DrawCustomizationInfo(CustomizationSet set) { - using var tree = ImRaii.TreeNode($"{_customization.ClanName(set.Clan, set.Gender)} {set.Gender}"); + using var tree = TreeNode($"{_customization.ClanName(set.Clan, set.Gender)} {set.Gender}"); if (!tree) return; - using var table = ImRaii.Table("data", 5, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); + using var table = Table("data", 5, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); if (!table) return; @@ -601,4 +687,140 @@ public unsafe class DebugTab : ITab } #endregion + + #region Designs + + private string _base64 = string.Empty; + private string _restore = string.Empty; + private byte[] _base64Bytes = Array.Empty(); + private byte[] _restoreBytes = Array.Empty(); + private DesignData _parse64 = new(); + private Exception? _parse64Failure; + + private void DrawDesignManager() + { + if (!ImGui.CollapsingHeader("Designs")) + return; + + ImGui.InputTextWithHint("##base64", "Base 64 input...", ref _base64, 2048); + if (ImGui.IsItemDeactivatedAfterEdit()) + { + try + { + _base64Bytes = Convert.FromBase64String(_base64); + _parse64Failure = null; + } + catch (Exception ex) + { + _base64Bytes = Array.Empty(); + _parse64Failure = ex; + } + + if (_parse64Failure == null) + try + { + _parse64 = DesignBase64Migration.MigrateBase64(_items, _base64, out var ef, out var cf, out var wp, out var ah, out var av, + out var aw); + _restore = DesignBase64Migration.CreateOldBase64(in _parse64, ef, cf, ah, av, wp, aw); + _restoreBytes = Convert.FromBase64String(_restore); + } + catch (Exception ex) + { + _parse64Failure = ex; + _restore = string.Empty; + } + } + + if (_parse64Failure != null) + { + ImGuiUtil.TextWrapped(_parse64Failure.ToString()); + } + else if (_restore.Length > 0) + { + DrawDesignData(_parse64); + using var font = PushFont(UiBuilder.MonoFont); + ImGui.TextUnformatted(_base64); + using (var style = PushStyle(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing with { X = 0 })) + { + foreach (var (c1, c2) in _restore.Zip(_base64)) + { + using var color = PushColor(ImGuiCol.Text, 0xFF4040D0, c1 != c2); + ImGui.TextUnformatted(c1.ToString()); + ImGui.SameLine(); + } + } + + ImGui.NewLine(); + + foreach (var ((b1, b2), idx) in _base64Bytes.Zip(_restoreBytes).WithIndex()) + { + using (var group = Group()) + { + ImGui.TextUnformatted(idx.ToString("D2")); + ImGui.TextUnformatted(b1.ToString("X2")); + using var color = PushColor(ImGuiCol.Text, 0xFF4040D0, b1 != b2); + ImGui.TextUnformatted(b2.ToString("X2")); + } + + ImGui.SameLine(); + } + } + + if (_parse64Failure != null && _base64Bytes.Length > 0) + { + using var font = PushFont(UiBuilder.MonoFont); + foreach (var (b, idx) in _base64Bytes.WithIndex()) + { + using (var group = Group()) + { + ImGui.TextUnformatted(idx.ToString("D2")); + ImGui.TextUnformatted(b.ToString("X2")); + } + + ImGui.SameLine(); + } + } + } + + private static void DrawDesignData(in DesignData data) + { + using var table = Table("##equip", 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit); + foreach (var slot in EquipSlotExtensions.EqdpSlots.Prepend(EquipSlot.OffHand).Prepend(EquipSlot.MainHand)) + { + var item = data.Item(slot); + var stain = data.Stain(slot); + ImGuiUtil.DrawTableColumn(slot.ToName()); + ImGuiUtil.DrawTableColumn(item.Name); + ImGuiUtil.DrawTableColumn(item.Id.ToString()); + ImGuiUtil.DrawTableColumn(stain.ToString()); + } + + ImGuiUtil.DrawTableColumn("Hat Visible"); + ImGuiUtil.DrawTableColumn(data.IsHatVisible().ToString()); + ImGui.TableNextRow(); + ImGuiUtil.DrawTableColumn("Visor Toggled"); + ImGuiUtil.DrawTableColumn(data.IsVisorToggled().ToString()); + ImGui.TableNextRow(); + ImGuiUtil.DrawTableColumn("Weapon Visible"); + ImGuiUtil.DrawTableColumn(data.IsWeaponVisible().ToString()); + ImGui.TableNextRow(); + + ImGuiUtil.DrawTableColumn("Model ID"); + ImGuiUtil.DrawTableColumn(data.ModelId.ToString()); + ImGui.TableNextRow(); + + foreach (var index in Enum.GetValues()) + { + var value = data.Customize[index]; + ImGuiUtil.DrawTableColumn(index.ToDefaultName()); + ImGuiUtil.DrawTableColumn(value.Value.ToString()); + ImGui.TableNextRow(); + } + + ImGuiUtil.DrawTableColumn("Is Wet"); + ImGuiUtil.DrawTableColumn(data.IsWet().ToString()); + ImGui.TableNextRow(); + } + + #endregion } diff --git a/Glamourer/Interop/ChangeCustomizeService.cs b/Glamourer/Interop/ChangeCustomizeService.cs index da81490..9995214 100644 --- a/Glamourer/Interop/ChangeCustomizeService.cs +++ b/Glamourer/Interop/ChangeCustomizeService.cs @@ -25,7 +25,7 @@ public unsafe class ChangeCustomizeService if (!model.IsHuman) return false; - Item.Log.Verbose($"[ChangeCustomize] Invoked on 0x{model.Address:X} with {customize}."); + Glamourer.Log.Verbose($"[ChangeCustomize] Invoked on 0x{model.Address:X} with {customize}."); return _changeCustomize(model.AsHuman, customize.Data, 1); } diff --git a/Glamourer/Interop/ObjectManager.cs b/Glamourer/Interop/ObjectManager.cs new file mode 100644 index 0000000..17295d6 --- /dev/null +++ b/Glamourer/Interop/ObjectManager.cs @@ -0,0 +1,139 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using Dalamud.Game; +using Dalamud.Game.ClientState; +using Dalamud.Game.ClientState.Objects; +using Glamourer.Interop.Structs; +using Glamourer.Services; +using Penumbra.GameData.Actors; + +namespace Glamourer.Interop; + +public class ObjectManager : IReadOnlyDictionary +{ + private readonly Framework _framework; + private readonly ClientState _clientState; + private readonly ObjectTable _objects; + private readonly ActorService _actors; + + public ObjectManager(Framework framework, ClientState clientState, ObjectTable objects, ActorService actors) + { + _framework = framework; + _clientState = clientState; + _objects = objects; + _actors = actors; + } + + public DateTime LastUpdate { get; private set; } + + public bool IsInGPose { get; private set; } + public ushort World { get; private set; } + + private readonly Dictionary _identifiers = new(200); + + public IReadOnlyDictionary Identifiers + => _identifiers; + + public void Update() + { + var lastUpdate = _framework.LastUpdate; + if (lastUpdate <= LastUpdate) + return; + + LastUpdate = lastUpdate; + World = (ushort)(_clientState.LocalPlayer?.CurrentWorld.Id ?? 0u); + _identifiers.Clear(); + + for (var i = 0; i < (int)ScreenActor.CutsceneStart; ++i) + { + Actor character = _objects.GetObjectAddress(i); + if (character.Identifier(_actors.AwaitedService, out var identifier)) + HandleIdentifier(identifier, character); + } + + for (var i = (int)ScreenActor.CutsceneStart; i < (int)ScreenActor.CutsceneEnd; ++i) + { + Actor character = _objects.GetObjectAddress(i); + if (!character.Valid) + break; + + HandleIdentifier(character.GetIdentifier(_actors.AwaitedService), character); + } + + void AddSpecial(ScreenActor idx, string label) + { + Actor actor = _objects.GetObjectAddress((int)idx); + if (actor.Identifier(_actors.AwaitedService, out var ident)) + { + var data = new ActorData(actor, label); + _identifiers.Add(ident, data); + } + } + + AddSpecial(ScreenActor.CharacterScreen, "Character Screen Actor"); + AddSpecial(ScreenActor.ExamineScreen, "Examine Screen Actor"); + AddSpecial(ScreenActor.FittingRoom, "Fitting Room Actor"); + AddSpecial(ScreenActor.DyePreview, "Dye Preview Actor"); + AddSpecial(ScreenActor.Portrait, "Portrait Actor"); + AddSpecial(ScreenActor.Card6, "Card Actor 6"); + AddSpecial(ScreenActor.Card7, "Card Actor 7"); + AddSpecial(ScreenActor.Card8, "Card Actor 8"); + + for (var i = (int)ScreenActor.ScreenEnd; i < _objects.Length; ++i) + { + Actor character = _objects.GetObjectAddress(i); + if (character.Identifier(_actors.AwaitedService, out var identifier)) + HandleIdentifier(identifier, character); + } + + var gPose = GPosePlayer; + IsInGPose = gPose.Utf8Name.Length > 0; + } + + private void HandleIdentifier(ActorIdentifier identifier, Actor character) + { + if (!character.Model || !identifier.IsValid) + return; + + if (!_identifiers.TryGetValue(identifier, out var data)) + { + data = new ActorData(character, identifier.ToString()); + _identifiers[identifier] = data; + } + else + { + data.Objects.Add(character); + } + } + + public Actor GPosePlayer + => _objects.GetObjectAddress((int)ScreenActor.GPosePlayer); + + public Actor Player + => _objects.GetObjectAddress(0); + + public IEnumerator> GetEnumerator() + => Identifiers.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + public int Count + => Identifiers.Count; + + public bool ContainsKey(ActorIdentifier key) + => Identifiers.ContainsKey(key); + + public bool TryGetValue(ActorIdentifier key, out ActorData value) + => Identifiers.TryGetValue(key, out value); + + public ActorData this[ActorIdentifier key] + => Identifiers[key]; + + public IEnumerable Keys + => Identifiers.Keys; + + public IEnumerable Values + => Identifiers.Values; +} diff --git a/Glamourer/Interop/Penumbra/PenumbraService.cs b/Glamourer/Interop/Penumbra/PenumbraService.cs index d547a7c..b092233 100644 --- a/Glamourer/Interop/Penumbra/PenumbraService.cs +++ b/Glamourer/Interop/Penumbra/PenumbraService.cs @@ -107,11 +107,11 @@ public unsafe class PenumbraService : IDisposable _cutsceneParent = Ipc.GetCutsceneParentIndex.Subscriber(_pluginInterface); _redrawSubscriber = Ipc.RedrawObjectByIndex.Subscriber(_pluginInterface); Available = true; - Item.Log.Debug("Glamourer attached to Penumbra."); + Glamourer.Log.Debug("Glamourer attached to Penumbra."); } catch (Exception e) { - Item.Log.Debug($"Could not attach to Penumbra:\n{e}"); + Glamourer.Log.Debug($"Could not attach to Penumbra:\n{e}"); } } @@ -125,7 +125,7 @@ public unsafe class PenumbraService : IDisposable if (Available) { Available = false; - Item.Log.Debug("Glamourer detached from Penumbra."); + Glamourer.Log.Debug("Glamourer detached from Penumbra."); } } diff --git a/Glamourer/Interop/Structs/Actor.cs b/Glamourer/Interop/Structs/Actor.cs index 5285e14..2f28367 100644 --- a/Glamourer/Interop/Structs/Actor.cs +++ b/Glamourer/Interop/Structs/Actor.cs @@ -2,8 +2,10 @@ using System; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Object; +using FFXIVClientStructs.FFXIV.Client.System.String; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; +using Penumbra.String; namespace Glamourer.Interop.Structs; @@ -43,6 +45,9 @@ public readonly unsafe struct Actor : IEquatable public ActorIdentifier GetIdentifier(ActorManager actors) => actors.FromObject(AsObject, out _, true, true, false); + public ByteString Utf8Name + => Valid ? new ByteString(AsObject->Name) : ByteString.Empty; + public bool Identifier(ActorManager actors, out ActorIdentifier ident) { if (Valid) @@ -55,6 +60,9 @@ public readonly unsafe struct Actor : IEquatable return false; } + public int Index + => Valid ? AsObject->ObjectIndex : -1; + public Model Model => Valid ? AsObject->DrawObject : null; diff --git a/Glamourer/Interop/Structs/ActorData.cs b/Glamourer/Interop/Structs/ActorData.cs index 953260b..e7b4194 100644 --- a/Glamourer/Interop/Structs/ActorData.cs +++ b/Glamourer/Interop/Structs/ActorData.cs @@ -2,6 +2,9 @@ namespace Glamourer.Interop.Structs; +/// +/// A single actor with its label and the list of associated game objects. +/// public readonly struct ActorData { public readonly List Objects; diff --git a/Glamourer/Interop/VisorService.cs b/Glamourer/Interop/VisorService.cs index 3679b66..226ee00 100644 --- a/Glamourer/Interop/VisorService.cs +++ b/Glamourer/Interop/VisorService.cs @@ -41,7 +41,7 @@ public class VisorService : IDisposable return false; var oldState = GetVisorState(human); - Item.Log.Verbose($"[SetVisorState] Invoked manually on 0x{human.Address:X} switching from {oldState} to {on}."); + Glamourer.Log.Verbose($"[SetVisorState] Invoked manually on 0x{human.Address:X} switching from {oldState} to {on}."); if (oldState == on) return false; @@ -63,7 +63,7 @@ public class VisorService : IDisposable // and also control whether the function should be called at all. Event.Invoke(human, ref on, ref callOriginal); - Item.Log.Excessive( + Glamourer.Log.Excessive( $"[SetVisorState] Invoked from game on 0x{human:X} switching to {on} (original {originalOn}, call original {callOriginal})."); if (callOriginal) diff --git a/Glamourer/Interop/WeaponService.cs b/Glamourer/Interop/WeaponService.cs index cece053..273813b 100644 --- a/Glamourer/Interop/WeaponService.cs +++ b/Glamourer/Interop/WeaponService.cs @@ -34,7 +34,7 @@ public unsafe class WeaponService : IDisposable // First call the regular function. _loadWeaponHook.Original(drawData, slot, weapon, redrawOnEquality, unk2, skipGameObject, unk4); - Item.Log.Information( + Glamourer.Log.Excessive( $"Weapon reloaded for 0x{actor.Address:X} with attributes {slot} {weapon:X14}, {redrawOnEquality}, {unk2}, {skipGameObject}, {unk4}"); } diff --git a/Glamourer/Services/ConfigMigrationService.cs b/Glamourer/Services/ConfigMigrationService.cs new file mode 100644 index 0000000..a37c67c --- /dev/null +++ b/Glamourer/Services/ConfigMigrationService.cs @@ -0,0 +1,56 @@ +using System; +using System.IO; +using Glamourer.Gui; +using Newtonsoft.Json.Linq; + +namespace Glamourer.Services; + +public class ConfigMigrationService +{ + private readonly SaveService _saveService; + + private Configuration _config = null!; + private JObject _data = null!; + + public ConfigMigrationService(SaveService saveService) + => _saveService = saveService; + + public void Migrate(Configuration config) + { + _config = config; + if (config.Version >= Configuration.Constants.CurrentVersion || !File.Exists(_saveService.FileNames.ConfigFile)) + { + AddColors(config, false); + return; + } + + _data = JObject.Parse(File.ReadAllText(_saveService.FileNames.ConfigFile)); + MigrateV1To2(); + AddColors(config, true); + } + + private void MigrateV1To2() + { + if (_config.Version > 1) + return; + + _config.Version = 2; + var customizationColor = _data["CustomizationColor"]?.ToObject() ?? ColorId.CustomizationDesign.Data().DefaultColor; + _config.Colors[ColorId.CustomizationDesign] = customizationColor; + var stateColor = _data["StateColor"]?.ToObject() ?? ColorId.StateDesign.Data().DefaultColor; + _config.Colors[ColorId.StateDesign] = stateColor; + var equipmentColor = _data["EquipmentColor"]?.ToObject() ?? ColorId.EquipmentDesign.Data().DefaultColor; + _config.Colors[ColorId.EquipmentDesign] = equipmentColor; + } + + private static void AddColors(Configuration config, bool forceSave) + { + var save = false; + foreach (var color in Enum.GetValues()) + save |= config.Colors.TryAdd(color, color.Data().DefaultColor); + + if (save || forceSave) + config.Save(); + Colors.SetColors(config); + } +} diff --git a/Glamourer/Services/FilenameService.cs b/Glamourer/Services/FilenameService.cs index e67a531..48c75a4 100644 --- a/Glamourer/Services/FilenameService.cs +++ b/Glamourer/Services/FilenameService.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.IO; using Dalamud.Plugin; +using Glamourer.Designs; namespace Glamourer.Services; @@ -32,4 +33,7 @@ public class FilenameService public string DesignFile(string identifier) => Path.Combine(DesignDirectory, $"{identifier}.json"); + + public string DesignFile(Design design) + => DesignFile(design.Identifier.ToString()); } diff --git a/Glamourer/Services/ItemManager.cs b/Glamourer/Services/ItemManager.cs index bc24ff9..afe0765 100644 --- a/Glamourer/Services/ItemManager.cs +++ b/Glamourer/Services/ItemManager.cs @@ -16,6 +16,8 @@ public class ItemManager : IDisposable public const string SmallClothesNpc = "Smallclothes (NPC)"; public const ushort SmallClothesNpcModel = 9903; + private readonly Configuration _config; + public readonly IdentifierService IdentifierService; public readonly ExcelSheet ItemSheet; public readonly StainData Stains; @@ -24,8 +26,10 @@ public class ItemManager : IDisposable public readonly EquipItem DefaultSword; - public ItemManager(DalamudPluginInterface pi, DataManager gameData, IdentifierService identifierService, ItemService itemService) + public ItemManager(Configuration config, DalamudPluginInterface pi, DataManager gameData, IdentifierService identifierService, + ItemService itemService) { + _config = config; ItemSheet = gameData.GetExcelSheet()!; IdentifierService = identifierService; Stains = new StainData(pi, gameData, gameData.Language); @@ -42,10 +46,8 @@ public class ItemManager : IDisposable public (bool, CharacterArmor) ResolveRestrictedGear(CharacterArmor armor, EquipSlot slot, Race race, Gender gender) - // TODO - //if (_config.UseRestrictedGearProtection) - => RestrictedGear.ResolveRestricted(armor, slot, race, gender); - //return (false, armor); + => _config.UseRestrictedGearProtection ? RestrictedGear.ResolveRestricted(armor, slot, race, gender) : (false, armor); + public static uint NothingId(EquipSlot slot) => uint.MaxValue - 128 - (uint)slot.ToSlot(); @@ -82,6 +84,20 @@ public class ItemManager : IDisposable return item; } + public EquipItem Resolve(FullEquipType type, uint itemId) + { + if (itemId == NothingId(type)) + return NothingItem(type); + + if (!ItemService.AwaitedService.TryGetValue(itemId, false, out var item)) + return new EquipItem(string.Intern($"Unknown #{itemId}"), itemId, 0, 0, 0, 0, 0); + + if (item.Type != type) + return new EquipItem(string.Intern($"Invalid #{itemId}"), itemId, item.IconId, item.ModelId, item.WeaponType, item.Variant, 0); + + return item; + } + public EquipItem Identify(EquipSlot slot, SetId id, byte variant) { slot = slot.ToSlot(); diff --git a/Glamourer/Services/SaveService.cs b/Glamourer/Services/SaveService.cs new file mode 100644 index 0000000..2a96755 --- /dev/null +++ b/Glamourer/Services/SaveService.cs @@ -0,0 +1,17 @@ +using OtterGui.Classes; +using OtterGui.Log; + +namespace Glamourer.Services; + +/// +/// Any file type that we want to save via SaveService. +/// +public interface ISavable : ISavable +{ } + +public sealed class SaveService : SaveServiceBase +{ + public SaveService(Logger log, FrameworkManager framework, FilenameService fileNames) + : base(log, framework, fileNames) + { } +} diff --git a/Glamourer/Services/ServiceManager.cs b/Glamourer/Services/ServiceManager.cs index 89c50f4..763034c 100644 --- a/Glamourer/Services/ServiceManager.cs +++ b/Glamourer/Services/ServiceManager.cs @@ -36,7 +36,11 @@ public static class ServiceManager private static IServiceCollection AddMeta(this IServiceCollection services) => services.AddSingleton() .AddSingleton() - .AddSingleton(); + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton(); private static IServiceCollection AddEvents(this IServiceCollection services) => services.AddSingleton() @@ -54,11 +58,11 @@ public static class ServiceManager .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton(); + .AddSingleton() + .AddSingleton(); private static IServiceCollection AddUi(this IServiceCollection services) - => services - .AddSingleton() + => services.AddSingleton() .AddSingleton() .AddSingleton(); diff --git a/Glamourer/Services/ServiceWrapper.cs b/Glamourer/Services/ServiceWrapper.cs index 70610c0..22656de 100644 --- a/Glamourer/Services/ServiceWrapper.cs +++ b/Glamourer/Services/ServiceWrapper.cs @@ -51,7 +51,7 @@ public abstract class AsyncServiceWrapper : IDisposable else { Service = service; - Item.Log.Verbose($"[{Name}] Created."); + Glamourer.Log.Verbose($"[{Name}] Created."); _task = null; } }); @@ -71,7 +71,7 @@ public abstract class AsyncServiceWrapper : IDisposable _task = null; if (Service is IDisposable d) d.Dispose(); - Item.Log.Verbose($"[{Name}] Disposed."); + Glamourer.Log.Verbose($"[{Name}] Disposed."); } } diff --git a/GlamourerOld/Configuration.cs b/GlamourerOld/Configuration.cs index e7a024c..f0f9596 100644 --- a/GlamourerOld/Configuration.cs +++ b/GlamourerOld/Configuration.cs @@ -1,13 +1,106 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.IO; using Dalamud.Configuration; +using Dalamud.Interface.Internal.Notifications; using Glamourer.Services; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs; namespace Glamourer; public class Configuration : IPluginConfiguration, ISavable +{ + public bool UseRestrictedGearProtection = true; + + public int Version { get; set; } = 2; + + public Dictionary Colors { get; set; } + = Enum.GetValues().ToDictionary(c => c, c => c.Data().DefaultColor); + + [JsonIgnore] + private readonly SaveService _saveService; + + public Configuration(SaveService saveService, ConfigMigrationService migrator) + { + _saveService = saveService; + Load(migrator); + } + + public void Save() + => _saveService.QueueSave(this); + + public void Load(ConfigMigrationService migrator) + { + static void HandleDeserializationError(object? sender, ErrorEventArgs errorArgs) + { + Glamourer.Log.Error( + $"Error parsing Configuration at {errorArgs.ErrorContext.Path}, using default or migrating:\n{errorArgs.ErrorContext.Error}"); + errorArgs.ErrorContext.Handled = true; + } + + if (!File.Exists(_saveService.FileNames.ConfigFile)) + return; + + if (File.Exists(_saveService.FileNames.ConfigFile)) + try + { + var text = File.ReadAllText(_saveService.FileNames.ConfigFile); + JsonConvert.PopulateObject(text, this, new JsonSerializerSettings + { + Error = HandleDeserializationError, + }); + } + catch (Exception ex) + { + Glamourer.ChatService.NotificationMessage(ex, + "Error reading Configuration, reverting to default.\nYou may be able to restore your configuration using the rolling backups in the XIVLauncher/backups/Glamourer directory.", + "Error reading Configuration", "Error", NotificationType.Error); + } + + migrator.Migrate(this); + } + + public string ToFilename(FilenameService fileNames) + => fileNames.ConfigFile; + + public void Save(StreamWriter writer) + { + using var jWriter = new JsonTextWriter(writer) { Formatting = Formatting.Indented }; + var serializer = new JsonSerializer { Formatting = Formatting.Indented }; + serializer.Serialize(jWriter, this); + } +} + +public class ConfigMigrationService +{ + private readonly SaveService _saveService; + + private Configuration _config = null!; + private JObject _data = null!; + + public ConfigMigrationService(SaveService saveService) + => _saveService = saveService; + + public void Migrate(Configuration config) + { + _config = config; + _data = JObject.Parse(File.ReadAllText(_saveService.FileNames.ConfigFile)); + MigrateV1To2(); + } + + private void MigrateV1To2() + { + if (_config.Version > 1) + return; + + _config.Version = 2; + + } +} + +public class ConfigurationOld : IPluginConfiguration, ISavable { [JsonIgnore] private readonly SaveService _saveService; @@ -42,7 +135,7 @@ public class Configuration : IPluginConfiguration, ISavable public void Save() => _saveService.QueueSave(this); - public Configuration(SaveService saveService) + public ConfigurationOld(SaveService saveService) { _saveService = saveService; Load(); diff --git a/GlamourerOld/Gui/Interface.SettingsTab.cs b/GlamourerOld/Gui/Interface.SettingsTab.cs index b1c1276..701bfb1 100644 --- a/GlamourerOld/Gui/Interface.SettingsTab.cs +++ b/GlamourerOld/Gui/Interface.SettingsTab.cs @@ -75,10 +75,10 @@ public partial class Interface ImGui.Dummy(_spacing); DrawColorPicker("Customization Color", "The color for designs that only apply their character customization.", - _config.CustomizationColor, Configuration.DefaultCustomizationColor, c => _config.CustomizationColor = c); + _config.CustomizationColor, ConfigurationOld.DefaultCustomizationColor, c => _config.CustomizationColor = c); DrawColorPicker("Equipment Color", "The color for designs that only apply some or all of their equipment slots and stains.", - _config.EquipmentColor, Configuration.DefaultEquipmentColor, c => _config.EquipmentColor = c); + _config.EquipmentColor, ConfigurationOld.DefaultEquipmentColor, c => _config.EquipmentColor = c); DrawColorPicker("State Color", "The color for designs that only apply some state modification.", - _config.StateColor, Configuration.DefaultStateColor, c => _config.StateColor = c); + _config.StateColor, ConfigurationOld.DefaultStateColor, c => _config.StateColor = c); } } diff --git a/GlamourerOld/Gui/Interface.cs b/GlamourerOld/Gui/Interface.cs index 243a38a..09a5f98 100644 --- a/GlamourerOld/Gui/Interface.cs +++ b/GlamourerOld/Gui/Interface.cs @@ -23,14 +23,14 @@ public partial class Interface : Window, IDisposable private readonly EquipmentDrawer _equipmentDrawer; private readonly CustomizationDrawer _customizationDrawer; - private readonly Configuration _config; + private readonly ConfigurationOld _config; private readonly ActorTab _actorTab; private readonly DesignTab _designTab; private readonly DebugStateTab _debugStateTab; private readonly DebugDataTab _debugDataTab; public Interface(DalamudPluginInterface pi, ItemManager items, ActiveDesign.Manager activeDesigns, DesignManager designManager, - DesignFileSystem fileSystem, ObjectManager objects, CustomizationService customization, Configuration config, DataManager gameData, TargetManager targets, ActorService actors, KeyState keyState) + DesignFileSystem fileSystem, ObjectManager objects, CustomizationService customization, ConfigurationOld config, DataManager gameData, TargetManager targets, ActorService actors, KeyState keyState) : base(GetLabel()) { _pi = pi; diff --git a/GlamourerOld/Interop/Actor.cs b/GlamourerOld/Interop/Actor.cs index 3e1676a..36ba4eb 100644 --- a/GlamourerOld/Interop/Actor.cs +++ b/GlamourerOld/Interop/Actor.cs @@ -67,7 +67,7 @@ public unsafe partial struct Actor : IEquatable, IDesignable => Pointer != null; public int Index - => Pointer->GameObject.ObjectIndex; + => Valid ? Pointer->GameObject.ObjectIndex : -1; public uint ModelId { diff --git a/GlamourerOld/Services/ItemManager.cs b/GlamourerOld/Services/ItemManager.cs index 65f4b13..3a37b4a 100644 --- a/GlamourerOld/Services/ItemManager.cs +++ b/GlamourerOld/Services/ItemManager.cs @@ -20,14 +20,14 @@ public class ItemManager : IDisposable public const string SmallClothesNpc = "Smallclothes (NPC)"; public const ushort SmallClothesNpcModel = 9903; - private readonly Configuration _config; + private readonly ConfigurationOld _config; public readonly IdentifierService IdentifierService; public readonly ExcelSheet ItemSheet; public readonly StainData Stains; public readonly ItemService ItemService; public readonly RestrictedGear RestrictedGear; - public ItemManager(DalamudPluginInterface pi, DataManager gameData, IdentifierService identifierService, ItemService itemService, Configuration config) + public ItemManager(DalamudPluginInterface pi, DataManager gameData, IdentifierService identifierService, ItemService itemService, ConfigurationOld config) { _config = config; ItemSheet = gameData.GetExcelSheet()!; diff --git a/GlamourerOld/Services/SaveService.cs b/GlamourerOld/Services/SaveService.cs index 6638888..8e192fc 100644 --- a/GlamourerOld/Services/SaveService.cs +++ b/GlamourerOld/Services/SaveService.cs @@ -9,89 +9,12 @@ namespace Glamourer.Services; /// /// Any file type that we want to save via SaveService. /// -public interface ISavable +public interface ISavable : ISavable +{ } + +public sealed class SaveService : SaveServiceBase { - /// The full file name of a given object. - public string ToFilename(FilenameService fileNames); - - /// Write the objects data to the given stream writer. - public void Save(StreamWriter writer); - - /// An arbitrary message printed to Debug before saving. - public string LogName(string fileName) - => fileName; - - public string TypeName - => GetType().Name; -} - -public class SaveService -{ - private readonly Logger _log; - private readonly FrameworkManager _framework; - - public readonly FilenameService FileNames; - public SaveService(Logger log, FrameworkManager framework, FilenameService fileNames) - { - _log = log; - _framework = framework; - FileNames = fileNames; - } - - /// Queue a save for the next framework tick. - public void QueueSave(ISavable value) - { - var file = value.ToFilename(FileNames); - _framework.RegisterOnTick(value.GetType().Name + file, () => - { - ImmediateSave(value); - }); - } - - /// Immediately trigger a save. - public void ImmediateSave(ISavable value) - { - var name = value.ToFilename(FileNames); - try - { - if (name.Length == 0) - { - throw new Exception("Invalid object returned empty filename."); - } - - _log.Debug($"Saving {value.TypeName} {value.LogName(name)}..."); - var file = new FileInfo(name); - file.Directory?.Create(); - using var s = file.Exists ? file.Open(FileMode.Truncate) : file.Open(FileMode.CreateNew); - using var w = new StreamWriter(s, Encoding.UTF8); - value.Save(w); - } - catch (Exception ex) - { - _log.Error($"Could not save {value.GetType().Name} {value.LogName(name)}:\n{ex}"); - } - } - - public void ImmediateDelete(ISavable value) - { - var name = value.ToFilename(FileNames); - try - { - if (name.Length == 0) - { - throw new Exception("Invalid object returned empty filename."); - } - - if (!File.Exists(name)) - return; - - _log.Information($"Deleting {value.GetType().Name} {value.LogName(name)}..."); - File.Delete(name); - } - catch (Exception ex) - { - _log.Error($"Could not delete {value.GetType().Name} {value.LogName(name)}:\n{ex}"); - } - } + : base(log, framework, fileNames) + { } } diff --git a/GlamourerOld/Services/ServiceManager.cs b/GlamourerOld/Services/ServiceManager.cs index 0baacef..8569d90 100644 --- a/GlamourerOld/Services/ServiceManager.cs +++ b/GlamourerOld/Services/ServiceManager.cs @@ -42,7 +42,7 @@ public static class ServiceManager .AddSingleton(); private static IServiceCollection AddConfig(this IServiceCollection services) - => services.AddSingleton() + => services.AddSingleton() .AddSingleton(); private static IServiceCollection AddPenumbra(this IServiceCollection services)