diff --git a/Glamourer.GameData/ActorEquipExtensions.cs b/Glamourer.GameData/ActorEquipExtensions.cs deleted file mode 100644 index acec01f..0000000 --- a/Glamourer.GameData/ActorEquipExtensions.cs +++ /dev/null @@ -1,168 +0,0 @@ -using System; -using System.ComponentModel; -using Penumbra.GameData.Enums; -using Penumbra.GameData.Structs; - -namespace Glamourer -{ - public static class WriteExtensions - { - private static unsafe void Write(IntPtr actorPtr, EquipSlot slot, SetId? id, WeaponType? type, ushort? variant, StainId? stain) - { - void WriteWeapon(int offset) - { - var address = (byte*) actorPtr + offset; - if (id.HasValue) - *(ushort*) address = (ushort) id.Value; - - if (type.HasValue) - *(ushort*) (address + 2) = (ushort) type.Value; - - if (variant.HasValue) - *(ushort*) (address + 4) = variant.Value; - - if (stain.HasValue) - *(address + 6) = (byte) stain.Value; - } - - void WriteEquip(int offset) - { - var address = (byte*) actorPtr + offset; - if (id.HasValue) - *(ushort*) address = (ushort) id.Value; - - if (variant < byte.MaxValue) - *(address + 2) = (byte) variant.Value; - - if (stain.HasValue) - *(address + 3) = (byte) stain.Value; - } - - switch (slot) - { - case EquipSlot.MainHand: - WriteWeapon(ActorEquipment.MainWeaponOffset); - break; - case EquipSlot.OffHand: - WriteWeapon(ActorEquipment.OffWeaponOffset); - break; - case EquipSlot.Head: - WriteEquip(ActorEquipment.EquipmentOffset); - break; - case EquipSlot.Body: - WriteEquip(ActorEquipment.EquipmentOffset + 4); - break; - case EquipSlot.Hands: - WriteEquip(ActorEquipment.EquipmentOffset + 8); - break; - case EquipSlot.Legs: - WriteEquip(ActorEquipment.EquipmentOffset + 12); - break; - case EquipSlot.Feet: - WriteEquip(ActorEquipment.EquipmentOffset + 16); - break; - case EquipSlot.Ears: - WriteEquip(ActorEquipment.EquipmentOffset + 20); - break; - case EquipSlot.Neck: - WriteEquip(ActorEquipment.EquipmentOffset + 24); - break; - case EquipSlot.Wrists: - WriteEquip(ActorEquipment.EquipmentOffset + 28); - break; - case EquipSlot.RFinger: - WriteEquip(ActorEquipment.EquipmentOffset + 32); - break; - case EquipSlot.LFinger: - WriteEquip(ActorEquipment.EquipmentOffset + 36); - break; - default: throw new InvalidEnumArgumentException(); - } - } - - public static void Write(this Stain stain, IntPtr actorPtr, EquipSlot slot) - => Write(actorPtr, slot, null, null, null, stain.RowIndex); - - public static void Write(this Item item, IntPtr actorAddress) - { - var (id, type, variant) = item.MainModel; - Write(actorAddress, item.EquippableTo, id, type, variant, null); - if (item.EquippableTo == EquipSlot.MainHand && item.HasSubModel) - { - var (subId, subType, subVariant) = item.SubModel; - Write(actorAddress, EquipSlot.OffHand, subId, subType, subVariant, null); - } - } - - public static void Write(this ActorArmor armor, IntPtr actorAddress, EquipSlot slot) - => Write(actorAddress, slot, armor.Set, null, armor.Variant, armor.Stain); - - public static void Write(this ActorWeapon weapon, IntPtr actorAddress, EquipSlot slot) - => Write(actorAddress, slot, weapon.Set, weapon.Type, weapon.Variant, weapon.Stain); - - public static unsafe void Write(this ActorEquipment equip, IntPtr actorAddress) - { - if (equip.IsSet == 0) - return; - - Write(actorAddress, EquipSlot.MainHand, equip.MainHand.Set, equip.MainHand.Type, equip.MainHand.Variant, equip.MainHand.Stain); - Write(actorAddress, EquipSlot.OffHand, equip.OffHand.Set, equip.OffHand.Type, equip.OffHand.Variant, equip.OffHand.Stain); - - fixed (ActorArmor* equipment = &equip.Head) - { - Buffer.MemoryCopy(equipment, (byte*) actorAddress + ActorEquipment.EquipmentOffset, - ActorEquipment.EquipmentSlots * sizeof(ActorArmor), ActorEquipment.EquipmentSlots * sizeof(ActorArmor)); - } - } - - public static void Write(this ActorEquipment equip, IntPtr actorAddress, ActorEquipMask models, ActorEquipMask stains) - { - if (models == ActorEquipMask.All && stains == ActorEquipMask.All) - { - equip.Write(actorAddress); - return; - } - - if (models.HasFlag(ActorEquipMask.MainHand)) - Write(actorAddress, EquipSlot.MainHand, equip.MainHand.Set, equip.MainHand.Type, equip.MainHand.Variant, null); - if (stains.HasFlag(ActorEquipMask.MainHand)) - Write(actorAddress, EquipSlot.MainHand, null, null, null, equip.MainHand.Stain); - if (models.HasFlag(ActorEquipMask.OffHand)) - Write(actorAddress, EquipSlot.OffHand, equip.OffHand.Set, equip.OffHand.Type, equip.OffHand.Variant, null); - if (stains.HasFlag(ActorEquipMask.OffHand)) - Write(actorAddress, EquipSlot.OffHand, null, null, null, equip.OffHand.Stain); - - if (models.HasFlag(ActorEquipMask.Head)) - Write(actorAddress, EquipSlot.Head, equip.Head.Set, null, equip.Head.Variant, null); - if (stains.HasFlag(ActorEquipMask.Head)) - Write(actorAddress, EquipSlot.Head, null, null, null, equip.Head.Stain); - if (models.HasFlag(ActorEquipMask.Body)) - Write(actorAddress, EquipSlot.Body, equip.Body.Set, null, equip.Body.Variant, null); - if (stains.HasFlag(ActorEquipMask.Body)) - Write(actorAddress, EquipSlot.Body, null, null, null, equip.Body.Stain); - if (models.HasFlag(ActorEquipMask.Hands)) - Write(actorAddress, EquipSlot.Hands, equip.Hands.Set, null, equip.Hands.Variant, null); - if (stains.HasFlag(ActorEquipMask.Hands)) - Write(actorAddress, EquipSlot.Hands, null, null, null, equip.Hands.Stain); - if (models.HasFlag(ActorEquipMask.Legs)) - Write(actorAddress, EquipSlot.Legs, equip.Legs.Set, null, equip.Legs.Variant, null); - if (stains.HasFlag(ActorEquipMask.Legs)) - Write(actorAddress, EquipSlot.Legs, null, null, null, equip.Legs.Stain); - if (models.HasFlag(ActorEquipMask.Feet)) - Write(actorAddress, EquipSlot.Feet, equip.Feet.Set, null, equip.Feet.Variant, null); - if (stains.HasFlag(ActorEquipMask.Feet)) - Write(actorAddress, EquipSlot.Feet, null, null, null, equip.Feet.Stain); - - if (models.HasFlag(ActorEquipMask.Ears)) - Write(actorAddress, EquipSlot.Ears, equip.Ears.Set, null, equip.Ears.Variant, null); - if (models.HasFlag(ActorEquipMask.Neck)) - Write(actorAddress, EquipSlot.Neck, equip.Neck.Set, null, equip.Neck.Variant, null); - if (models.HasFlag(ActorEquipMask.Wrists)) - Write(actorAddress, EquipSlot.Wrists, equip.Wrists.Set, null, equip.Wrists.Variant, null); - if (models.HasFlag(ActorEquipMask.LFinger)) - Write(actorAddress, EquipSlot.LFinger, equip.LFinger.Set, null, equip.LFinger.Variant, null); - if (models.HasFlag(ActorEquipMask.RFinger)) - Write(actorAddress, EquipSlot.RFinger, equip.RFinger.Set, null, equip.RFinger.Variant, null); - } - } -} diff --git a/Glamourer.GameData/CharacterEquipExtensions.cs b/Glamourer.GameData/CharacterEquipExtensions.cs new file mode 100644 index 0000000..37bd606 --- /dev/null +++ b/Glamourer.GameData/CharacterEquipExtensions.cs @@ -0,0 +1,168 @@ +using System; +using System.ComponentModel; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; + +namespace Glamourer +{ + public static class WriteExtensions + { + private static unsafe void Write(IntPtr characterPtr, EquipSlot slot, SetId? id, WeaponType? type, ushort? variant, StainId? stain) + { + void WriteWeapon(int offset) + { + var address = (byte*) characterPtr + offset; + if (id.HasValue) + *(ushort*) address = (ushort) id.Value; + + if (type.HasValue) + *(ushort*) (address + 2) = (ushort) type.Value; + + if (variant.HasValue) + *(ushort*) (address + 4) = variant.Value; + + if (stain.HasValue) + *(address + 6) = (byte) stain.Value; + } + + void WriteEquip(int offset) + { + var address = (byte*) characterPtr + offset; + if (id.HasValue) + *(ushort*) address = (ushort) id.Value; + + if (variant < byte.MaxValue) + *(address + 2) = (byte) variant.Value; + + if (stain.HasValue) + *(address + 3) = (byte) stain.Value; + } + + switch (slot) + { + case EquipSlot.MainHand: + WriteWeapon(CharacterEquipment.MainWeaponOffset); + break; + case EquipSlot.OffHand: + WriteWeapon(CharacterEquipment.OffWeaponOffset); + break; + case EquipSlot.Head: + WriteEquip(CharacterEquipment.EquipmentOffset); + break; + case EquipSlot.Body: + WriteEquip(CharacterEquipment.EquipmentOffset + 4); + break; + case EquipSlot.Hands: + WriteEquip(CharacterEquipment.EquipmentOffset + 8); + break; + case EquipSlot.Legs: + WriteEquip(CharacterEquipment.EquipmentOffset + 12); + break; + case EquipSlot.Feet: + WriteEquip(CharacterEquipment.EquipmentOffset + 16); + break; + case EquipSlot.Ears: + WriteEquip(CharacterEquipment.EquipmentOffset + 20); + break; + case EquipSlot.Neck: + WriteEquip(CharacterEquipment.EquipmentOffset + 24); + break; + case EquipSlot.Wrists: + WriteEquip(CharacterEquipment.EquipmentOffset + 28); + break; + case EquipSlot.RFinger: + WriteEquip(CharacterEquipment.EquipmentOffset + 32); + break; + case EquipSlot.LFinger: + WriteEquip(CharacterEquipment.EquipmentOffset + 36); + break; + default: throw new InvalidEnumArgumentException(); + } + } + + public static void Write(this Stain stain, IntPtr characterPtr, EquipSlot slot) + => Write(characterPtr, slot, null, null, null, stain.RowIndex); + + public static void Write(this Item item, IntPtr characterAddress) + { + var (id, type, variant) = item.MainModel; + Write(characterAddress, item.EquippableTo, id, type, variant, null); + if (item.EquippableTo == EquipSlot.MainHand && item.HasSubModel) + { + var (subId, subType, subVariant) = item.SubModel; + Write(characterAddress, EquipSlot.OffHand, subId, subType, subVariant, null); + } + } + + public static void Write(this CharacterArmor armor, IntPtr characterAddress, EquipSlot slot) + => Write(characterAddress, slot, armor.Set, null, armor.Variant, armor.Stain); + + public static void Write(this CharacterWeapon weapon, IntPtr characterAddress, EquipSlot slot) + => Write(characterAddress, slot, weapon.Set, weapon.Type, weapon.Variant, weapon.Stain); + + public static unsafe void Write(this CharacterEquipment equip, IntPtr characterAddress) + { + if (equip.IsSet == 0) + return; + + Write(characterAddress, EquipSlot.MainHand, equip.MainHand.Set, equip.MainHand.Type, equip.MainHand.Variant, equip.MainHand.Stain); + Write(characterAddress, EquipSlot.OffHand, equip.OffHand.Set, equip.OffHand.Type, equip.OffHand.Variant, equip.OffHand.Stain); + + fixed (CharacterArmor* equipment = &equip.Head) + { + Buffer.MemoryCopy(equipment, (byte*) characterAddress + CharacterEquipment.EquipmentOffset, + CharacterEquipment.EquipmentSlots * sizeof(CharacterArmor), CharacterEquipment.EquipmentSlots * sizeof(CharacterArmor)); + } + } + + public static void Write(this CharacterEquipment equip, IntPtr characterAddress, CharacterEquipMask models, CharacterEquipMask stains) + { + if (models == CharacterEquipMask.All && stains == CharacterEquipMask.All) + { + equip.Write(characterAddress); + return; + } + + if (models.HasFlag(CharacterEquipMask.MainHand)) + Write(characterAddress, EquipSlot.MainHand, equip.MainHand.Set, equip.MainHand.Type, equip.MainHand.Variant, null); + if (stains.HasFlag(CharacterEquipMask.MainHand)) + Write(characterAddress, EquipSlot.MainHand, null, null, null, equip.MainHand.Stain); + if (models.HasFlag(CharacterEquipMask.OffHand)) + Write(characterAddress, EquipSlot.OffHand, equip.OffHand.Set, equip.OffHand.Type, equip.OffHand.Variant, null); + if (stains.HasFlag(CharacterEquipMask.OffHand)) + Write(characterAddress, EquipSlot.OffHand, null, null, null, equip.OffHand.Stain); + + if (models.HasFlag(CharacterEquipMask.Head)) + Write(characterAddress, EquipSlot.Head, equip.Head.Set, null, equip.Head.Variant, null); + if (stains.HasFlag(CharacterEquipMask.Head)) + Write(characterAddress, EquipSlot.Head, null, null, null, equip.Head.Stain); + if (models.HasFlag(CharacterEquipMask.Body)) + Write(characterAddress, EquipSlot.Body, equip.Body.Set, null, equip.Body.Variant, null); + if (stains.HasFlag(CharacterEquipMask.Body)) + Write(characterAddress, EquipSlot.Body, null, null, null, equip.Body.Stain); + if (models.HasFlag(CharacterEquipMask.Hands)) + Write(characterAddress, EquipSlot.Hands, equip.Hands.Set, null, equip.Hands.Variant, null); + if (stains.HasFlag(CharacterEquipMask.Hands)) + Write(characterAddress, EquipSlot.Hands, null, null, null, equip.Hands.Stain); + if (models.HasFlag(CharacterEquipMask.Legs)) + Write(characterAddress, EquipSlot.Legs, equip.Legs.Set, null, equip.Legs.Variant, null); + if (stains.HasFlag(CharacterEquipMask.Legs)) + Write(characterAddress, EquipSlot.Legs, null, null, null, equip.Legs.Stain); + if (models.HasFlag(CharacterEquipMask.Feet)) + Write(characterAddress, EquipSlot.Feet, equip.Feet.Set, null, equip.Feet.Variant, null); + if (stains.HasFlag(CharacterEquipMask.Feet)) + Write(characterAddress, EquipSlot.Feet, null, null, null, equip.Feet.Stain); + + if (models.HasFlag(CharacterEquipMask.Ears)) + Write(characterAddress, EquipSlot.Ears, equip.Ears.Set, null, equip.Ears.Variant, null); + if (models.HasFlag(CharacterEquipMask.Neck)) + Write(characterAddress, EquipSlot.Neck, equip.Neck.Set, null, equip.Neck.Variant, null); + if (models.HasFlag(CharacterEquipMask.Wrists)) + Write(characterAddress, EquipSlot.Wrists, equip.Wrists.Set, null, equip.Wrists.Variant, null); + if (models.HasFlag(CharacterEquipMask.LFinger)) + Write(characterAddress, EquipSlot.LFinger, equip.LFinger.Set, null, equip.LFinger.Variant, null); + if (models.HasFlag(CharacterEquipMask.RFinger)) + Write(characterAddress, EquipSlot.RFinger, equip.RFinger.Set, null, equip.RFinger.Variant, null); + } + } +} diff --git a/Glamourer.GameData/ActorEquipMask.cs b/Glamourer.GameData/CharacterEquipMask.cs similarity index 92% rename from Glamourer.GameData/ActorEquipMask.cs rename to Glamourer.GameData/CharacterEquipMask.cs index 24ad254..c5a881b 100644 --- a/Glamourer.GameData/ActorEquipMask.cs +++ b/Glamourer.GameData/CharacterEquipMask.cs @@ -3,7 +3,7 @@ using System; namespace Glamourer { [Flags] - public enum ActorEquipMask : ushort + public enum CharacterEquipMask : ushort { None = 0, MainHand = 0b000000000001, diff --git a/Glamourer.GameData/Customization/ActorCustomization.cs b/Glamourer.GameData/Customization/CharacterCustomization.cs similarity index 89% rename from Glamourer.GameData/Customization/ActorCustomization.cs rename to Glamourer.GameData/Customization/CharacterCustomization.cs index cbfd3ac..4376b35 100644 --- a/Glamourer.GameData/Customization/ActorCustomization.cs +++ b/Glamourer.GameData/Customization/CharacterCustomization.cs @@ -1,32 +1,32 @@ using System; using System.Runtime.InteropServices; -using Dalamud.Game.ClientState.Actors.Types; +using Dalamud.Game.ClientState.Objects.Types; using Penumbra.GameData.Enums; namespace Glamourer.Customization { public unsafe struct LazyCustomization { - public ActorCustomization* Address; + public CharacterCustomization* Address; - public LazyCustomization(IntPtr actorPtr) - => Address = (ActorCustomization*) (actorPtr + ActorCustomization.CustomizationOffset); + public LazyCustomization(IntPtr characterPtr) + => Address = (CharacterCustomization*) (characterPtr + CharacterCustomization.CustomizationOffset); - public ref ActorCustomization Value + public ref CharacterCustomization Value => ref *Address; - public LazyCustomization(ActorCustomization data) + public LazyCustomization(CharacterCustomization data) => Address = &data; } [StructLayout(LayoutKind.Sequential, Pack = 1)] - public struct ActorCustomization + public struct CharacterCustomization { public const int CustomizationOffset = 0x1898; public const int CustomizationBytes = 26; - public static ActorCustomization Default = new() + public static CharacterCustomization Default = new() { Race = Race.Hyur, Gender = Gender.Male, @@ -150,13 +150,13 @@ namespace Glamourer.Customization } } - public void Read(Actor actor) - => Read(actor.Address + CustomizationOffset); + public void Read(Character character) + => Read(character.Address + CustomizationOffset); - public ActorCustomization(Actor actor) + public CharacterCustomization(Character character) : this() { - Read(actor.Address + CustomizationOffset); + Read(character.Address + CustomizationOffset); } public byte this[CustomizationId id] @@ -278,11 +278,11 @@ namespace Glamourer.Customization } } - public unsafe void Write(IntPtr actorAddress) + public unsafe void Write(IntPtr characterAddress) { fixed (Race* ptr = &Race) { - Buffer.MemoryCopy(ptr, (byte*) actorAddress + CustomizationOffset, CustomizationBytes, CustomizationBytes); + Buffer.MemoryCopy(ptr, (byte*) characterAddress + CustomizationOffset, CustomizationBytes, CustomizationBytes); } } diff --git a/Glamourer.GameData/Customization/CmpFile.cs b/Glamourer.GameData/Customization/CmpFile.cs index e320ab2..c709a0e 100644 --- a/Glamourer.GameData/Customization/CmpFile.cs +++ b/Glamourer.GameData/Customization/CmpFile.cs @@ -1,4 +1,5 @@ -using Dalamud.Plugin; +using Dalamud.Data; +using Dalamud.Plugin; namespace Glamourer { @@ -7,9 +8,9 @@ namespace Glamourer public readonly Lumina.Data.FileResource File; public readonly uint[] RgbaColors; - public CmpFile(DalamudPluginInterface pi) + public CmpFile(DataManager gameData) { - File = pi.Data.GetFile("chara/xls/charamake/human.cmp"); + File = gameData.GetFile("chara/xls/charamake/human.cmp")!; RgbaColors = new uint[File.Data.Length >> 2]; for (var i = 0; i < File.Data.Length; i += 4) { diff --git a/Glamourer.GameData/Customization/CustomName.cs b/Glamourer.GameData/Customization/CustomName.cs index 6476b85..2d96f76 100644 --- a/Glamourer.GameData/Customization/CustomName.cs +++ b/Glamourer.GameData/Customization/CustomName.cs @@ -8,6 +8,7 @@ OddEyes, IrisSmall, IrisLarge, + IrisSize, MidlanderM, HighlanderM, WildwoodM, diff --git a/Glamourer.GameData/Customization/CustomizationManager.cs b/Glamourer.GameData/Customization/CustomizationManager.cs index 086903c..3d1a4f0 100644 --- a/Glamourer.GameData/Customization/CustomizationManager.cs +++ b/Glamourer.GameData/Customization/CustomizationManager.cs @@ -1,4 +1,6 @@ using System.Collections.Generic; +using Dalamud; +using Dalamud.Data; using Dalamud.Plugin; using Penumbra.GameData.Enums; @@ -11,9 +13,9 @@ namespace Glamourer.Customization private CustomizationManager() { } - public static ICustomizationManager Create(DalamudPluginInterface pi) + public static ICustomizationManager Create(DalamudPluginInterface pi, DataManager gameData, ClientLanguage language) { - _options ??= new CustomizationOptions(pi); + _options ??= new CustomizationOptions(pi, gameData, language); return new CustomizationManager(); } diff --git a/Glamourer.GameData/Customization/CustomizationOptions.cs b/Glamourer.GameData/Customization/CustomizationOptions.cs index a95b459..6cc6529 100644 --- a/Glamourer.GameData/Customization/CustomizationOptions.cs +++ b/Glamourer.GameData/Customization/CustomizationOptions.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; using Dalamud; +using Dalamud.Data; using Dalamud.Plugin; using Glamourer.Util; using Lumina.Data; @@ -50,7 +51,7 @@ namespace Glamourer.Customization private Customization[] 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)!; var hairList = new List(row.Unknown30); for (var i = 0; i < row.Unknown30; ++i) { @@ -129,7 +130,7 @@ namespace Glamourer.Customization private CustomizationSet GetSet(SubRace race, Gender gender) { var (skin, hair) = GetColors(race, gender); - var row = _listSheet.GetRow(((uint) race - 1) * 2 - 1 + (uint) gender); + var row = _listSheet.GetRow(((uint) race - 1) * 2 - 1 + (uint) gender)!; var set = new CustomizationSet(race, gender) { HairStyles = race.ToRace() == Race.Hrothgar ? HrothgarFaces(row) : GetHairStyles(race, gender), @@ -190,7 +191,7 @@ namespace Glamourer.Customization set.FeaturesTattoos = featureDict; - set.OptionName = ((CustomizationId[]) Enum.GetValues(typeof(CustomizationId))).Select(c => + var nameArray = ((CustomizationId[]) Enum.GetValues(typeof(CustomizationId))).Select(c => { var menu = row.Menus .Cast() @@ -210,14 +211,19 @@ namespace Glamourer.Customization var textRow = _lobby.GetRow(menu.Value.Id); return textRow?.Text.ToString() ?? c.ToDefaultName(); }).ToArray(); + nameArray[(int) CustomizationId.EyeColorL] = nameArray[(int) CustomizationId.EyeColorR]; + nameArray[(int) CustomizationId.EyeColorR] = GetName(CustomName.OddEyes); + set.OptionName = nameArray; - set._types = ((CustomizationId[]) Enum.GetValues(typeof(CustomizationId))).Select(c => + set.Types = ((CustomizationId[]) Enum.GetValues(typeof(CustomizationId))).Select(c => { - if (c == CustomizationId.HighlightColor) - return CharaMakeParams.MenuType.ColorPicker; - - if (c == CustomizationId.EyeColorL) - return CharaMakeParams.MenuType.ColorPicker; + switch (c) + { + case CustomizationId.HighlightColor: + case CustomizationId.EyeColorL: + case CustomizationId.EyeColorR: + return CharaMakeParams.MenuType.ColorPicker; + } var menu = row.Menus .Cast() @@ -255,21 +261,21 @@ namespace Glamourer.Customization _ => Language.English, }; - internal CustomizationOptions(DalamudPluginInterface pi) + internal CustomizationOptions(DalamudPluginInterface pi, DataManager gameData, ClientLanguage language) { - _cmpFile = new CmpFile(pi); - _customizeSheet = pi.Data.GetExcelSheet(); - _lobby = pi.Data.GetExcelSheet(); - var tmp = pi.Data.Excel.GetType()!.GetMethod("GetSheet", BindingFlags.Instance | BindingFlags.NonPublic)! - .MakeGenericMethod(typeof(CharaMakeParams))!.Invoke(pi.Data.Excel, new object?[] + _cmpFile = new CmpFile(gameData); + _customizeSheet = gameData.GetExcelSheet()!; + _lobby = gameData.GetExcelSheet()!; + var tmp = gameData.Excel.GetType()!.GetMethod("GetSheet", BindingFlags.Instance | BindingFlags.NonPublic)! + .MakeGenericMethod(typeof(CharaMakeParams))!.Invoke(gameData.Excel, new object?[] { "charamaketype", - FromClientLanguage(pi.ClientState.ClientLanguage), + FromClientLanguage(language), null, }) as ExcelSheet; _listSheet = tmp!; - _hairSheet = pi.Data.GetExcelSheet(); - SetNames(pi); + _hairSheet = gameData.GetExcelSheet()!; + SetNames(gameData); _highlightPicker = CreateColorPicker(CustomizationId.HighlightColor, 256, 192); _lipColorPickerDark = CreateColorPicker(CustomizationId.LipColor, 512, 96); @@ -279,7 +285,7 @@ namespace Glamourer.Customization _facePaintColorPickerLight = CreateColorPicker(CustomizationId.FacePaintColor, 1152, 96, true); _tattooColorPicker = CreateColorPicker(CustomizationId.TattooColor, 0, 192); - _icons = new IconStorage(pi, _list.Length * 50); + _icons = new IconStorage(pi, gameData, _list.Length * 50); foreach (var race in Clans) { foreach (var gender in Genders) @@ -287,15 +293,16 @@ namespace Glamourer.Customization } } - public void SetNames(DalamudPluginInterface pi) + private void SetNames(DataManager gameData) { - var subRace = pi.Data.GetExcelSheet(); + var subRace = gameData.GetExcelSheet()!; _names[(int) CustomName.Clan] = _lobby.GetRow(102)?.Text ?? "Clan"; _names[(int) CustomName.Gender] = _lobby.GetRow(103)?.Text ?? "Gender"; _names[(int) CustomName.Reverse] = _lobby.GetRow(2135)?.Text ?? "Reverse"; _names[(int) CustomName.OddEyes] = _lobby.GetRow(2125)?.Text ?? "Odd Eyes"; _names[(int) CustomName.IrisSmall] = _lobby.GetRow(1076)?.Text ?? "Small"; _names[(int) CustomName.IrisLarge] = _lobby.GetRow(1075)?.Text ?? "Large"; + _names[(int) CustomName.IrisSize] = _lobby.GetRow(244)?.Text ?? "Iris Size"; _names[(int) CustomName.MidlanderM] = subRace.GetRow((int) SubRace.Midlander)?.Masculine.ToString() ?? SubRace.Midlander.ToName(); _names[(int) CustomName.MidlanderF] = subRace.GetRow((int) SubRace.Midlander)?.Feminine.ToString() ?? SubRace.Midlander.ToName(); _names[(int) CustomName.HighlanderM] = diff --git a/Glamourer.GameData/Customization/CustomizationSet.cs b/Glamourer.GameData/Customization/CustomizationSet.cs index 035c956..8e6017d 100644 --- a/Glamourer.GameData/Customization/CustomizationSet.cs +++ b/Glamourer.GameData/Customization/CustomizationSet.cs @@ -69,7 +69,7 @@ namespace Glamourer.Customization public IReadOnlyList LipColorsLight { get; internal set; } = null!; public IReadOnlyList LipColorsDark { get; internal set; } = null!; - public IReadOnlyList _types { get; internal set; } = null!; + public IReadOnlyList Types { get; internal set; } = null!; public string Option(CustomizationId id) => OptionName[(int) id]; @@ -154,7 +154,7 @@ namespace Glamourer.Customization } public CharaMakeParams.MenuType Type(CustomizationId id) - => _types[(int) id]; + => Types[(int) id]; public int Count(CustomizationId id) diff --git a/Glamourer.GameData/Main.cs b/Glamourer.GameData/GameData.cs similarity index 71% rename from Glamourer.GameData/Main.cs rename to Glamourer.GameData/GameData.cs index 40c64ae..9d255f1 100644 --- a/Glamourer.GameData/Main.cs +++ b/Glamourer.GameData/GameData.cs @@ -1,7 +1,8 @@ using System; using System.Collections.Generic; using System.Linq; -using Dalamud.Plugin; +using Dalamud.Data; +using Lumina.Excel.GeneratedSheets; using Penumbra.GameData.Enums; namespace Glamourer @@ -10,23 +11,37 @@ namespace Glamourer { private static Dictionary? _stains; private static Dictionary>? _itemsBySlot; + private static SortedList? _models; - public static IReadOnlyDictionary Stains(DalamudPluginInterface pi) + public static IReadOnlyDictionary Models(DataManager dataManager) + { + if (_models != null) + return _models; + + var sheet = dataManager.GetExcelSheet()!; + + _models = new SortedList((int) sheet.RowCount); + foreach (var model in sheet.Where(m => m.Type != 0)) + _models.Add(model.RowId, model); + return _models; + } + + public static IReadOnlyDictionary Stains(DataManager dataManager) { if (_stains != null) return _stains; - var sheet = pi.Data.GetExcelSheet(); + var sheet = dataManager.GetExcelSheet()!; _stains = sheet.Where(s => s.Color != 0).ToDictionary(s => (byte) s.RowId, s => new Stain((byte) s.RowId, s)); return _stains; } - public static IReadOnlyDictionary> ItemsBySlot(DalamudPluginInterface pi) + public static IReadOnlyDictionary> ItemsBySlot(DataManager dataManager) { if (_itemsBySlot != null) return _itemsBySlot; - var sheet = pi.Data.GetExcelSheet(); + var sheet = dataManager.GetExcelSheet()!; Item EmptySlot(EquipSlot slot) => new(sheet.First(), "Nothing", slot); @@ -40,7 +55,7 @@ namespace Glamourer [EquipSlot.Feet] = new(200) { EmptySlot(EquipSlot.Feet) }, [EquipSlot.RFinger] = new(200) { EmptySlot(EquipSlot.RFinger) }, [EquipSlot.Neck] = new(200) { EmptySlot(EquipSlot.Neck) }, - [EquipSlot.MainHand] = new(200) { EmptySlot(EquipSlot.MainHand) }, + [EquipSlot.MainHand] = new(1000) { EmptySlot(EquipSlot.MainHand) }, [EquipSlot.OffHand] = new(200) { EmptySlot(EquipSlot.OffHand) }, [EquipSlot.Wrists] = new(200) { EmptySlot(EquipSlot.Wrists) }, [EquipSlot.Ears] = new(200) { EmptySlot(EquipSlot.Ears) }, diff --git a/Glamourer.GameData/Glamourer.GameData.csproj b/Glamourer.GameData/Glamourer.GameData.csproj index 7c1be7c..560f5bf 100644 --- a/Glamourer.GameData/Glamourer.GameData.csproj +++ b/Glamourer.GameData/Glamourer.GameData.csproj @@ -1,7 +1,8 @@ - net472 - preview + net5.0-windows + preview + x64 Glamourer Glamourer.GameData 1.0.0.0 @@ -55,7 +56,7 @@ False - ..\..\Penumbra\Penumbra\bin\$(Configuration)\net472\Penumbra.GameData.dll + ..\..\Penumbra\Penumbra\bin\$(Configuration)\$(TargetFramework)\Penumbra.GameData.dll diff --git a/Glamourer.GameData/Util/IconStorage.cs b/Glamourer.GameData/Util/IconStorage.cs index 24aa63a..0d6d37e 100644 --- a/Glamourer.GameData/Util/IconStorage.cs +++ b/Glamourer.GameData/Util/IconStorage.cs @@ -1,7 +1,8 @@ using System; using System.Collections.Generic; -using Dalamud.Data.LuminaExtensions; +using Dalamud.Data; using Dalamud.Plugin; +using Dalamud.Utility; using ImGuiScene; using Lumina.Data.Files; @@ -9,33 +10,35 @@ namespace Glamourer.Util { public class IconStorage : IDisposable { - private readonly DalamudPluginInterface _pi; - private readonly Dictionary _icons; + private readonly DalamudPluginInterface _pi; + private readonly DataManager _gameData; + private readonly Dictionary _icons; - public IconStorage(DalamudPluginInterface pi, int size = 0) + public IconStorage(DalamudPluginInterface pi, DataManager gameData, int size = 0) { - _pi = pi; - _icons = new Dictionary(size); + _pi = pi; + _gameData = gameData; + _icons = new Dictionary(size); } public TextureWrap this[int id] => LoadIcon(id); - private TexFile? LoadIconHq(int id) + private TexFile? LoadIconHq(uint id) { var path = $"ui/icon/{id / 1000 * 1000:000000}/{id:000000}_hr1.tex"; - return _pi.Data.GetFile(path); + return _gameData.GetFile(path); } - public TextureWrap LoadIcon(uint id) - => LoadIcon((int) id); - public TextureWrap LoadIcon(int id) + => LoadIcon((uint) id); + + public TextureWrap LoadIcon(uint id) { if (_icons.TryGetValue(id, out var ret)) return ret; - var icon = LoadIconHq(id) ?? _pi.Data.GetIcon(id); + var icon = LoadIconHq(id) ?? _gameData.GetIcon(id)!; var iconData = icon.GetRgbaImageData(); ret = _pi.UiBuilder.LoadImageRaw(iconData, icon.Header.Width, icon.Header.Height, 4); diff --git a/Glamourer.zip b/Glamourer.zip index 84df484..88eba45 100644 Binary files a/Glamourer.zip and b/Glamourer.zip differ diff --git a/Glamourer/ActorExtensions.cs b/Glamourer/CharacterExtensions.cs similarity index 71% rename from Glamourer/ActorExtensions.cs rename to Glamourer/CharacterExtensions.cs index 158c0c2..a0f21e5 100644 --- a/Glamourer/ActorExtensions.cs +++ b/Glamourer/CharacterExtensions.cs @@ -1,8 +1,8 @@ -using Dalamud.Game.ClientState.Actors.Types; +using Dalamud.Game.ClientState.Objects.Types; namespace Glamourer { - public static class ActorExtensions + public static class CharacterExtensions { public const int WetnessOffset = 0x19A5; public const byte WetnessFlag = 0x10; @@ -13,10 +13,10 @@ namespace Glamourer public const int WeaponHiddenOffset = 0xF64; public const byte WeaponHiddenFlag = 0x02; - public static unsafe bool IsWet(this Actor a) + public static unsafe bool IsWet(this Character a) => (*((byte*) a.Address + WetnessOffset) & WetnessFlag) != 0; - public static unsafe bool SetWetness(this Actor a, bool value) + public static unsafe bool SetWetness(this Character a, bool value) { var current = a.IsWet(); if (current == value) @@ -29,10 +29,10 @@ namespace Glamourer return true; } - public static unsafe ref byte StateFlags(this Actor a) + public static unsafe ref byte StateFlags(this Character a) => ref *((byte*) a.Address + StateFlagsOffset); - public static bool SetStateFlag(this Actor a, bool value, byte flag) + public static bool SetStateFlag(this Character a, bool value, byte flag) { var current = a.StateFlags(); var previousValue = (current & flag) != 0; @@ -46,20 +46,20 @@ namespace Glamourer return true; } - public static bool IsHatHidden(this Actor a) + public static bool IsHatHidden(this Character a) => (a.StateFlags() & HatHiddenFlag) != 0; - public static unsafe bool IsWeaponHidden(this Actor a) + public static unsafe bool IsWeaponHidden(this Character a) => (a.StateFlags() & WeaponHiddenFlag) != 0 && (*((byte*) a.Address + WeaponHiddenOffset) & WeaponHiddenFlag) != 0; - public static bool IsVisorToggled(this Actor a) + public static bool IsVisorToggled(this Character a) => (a.StateFlags() & VisorToggledFlag) != 0; - public static bool SetHatHidden(this Actor a, bool value) + public static bool SetHatHidden(this Character a, bool value) => SetStateFlag(a, value, HatHiddenFlag); - public static unsafe bool SetWeaponHidden(this Actor a, bool value) + public static unsafe bool SetWeaponHidden(this Character a, bool value) { var ret = SetStateFlag(a, value, WeaponHiddenFlag); var val = *((byte*) a.Address + WeaponHiddenOffset); @@ -70,10 +70,10 @@ namespace Glamourer return ret || (val & WeaponHiddenFlag) != 0 != value; } - public static bool SetVisorToggled(this Actor a, bool value) + public static bool SetVisorToggled(this Character a, bool value) => SetStateFlag(a, value, VisorToggledFlag); - public static unsafe ref float Alpha(this Actor a) + public static unsafe ref float Alpha(this Character a) => ref *(float*) ((byte*) a.Address + AlphaOffset); } } diff --git a/Glamourer/CharacterSave.cs b/Glamourer/CharacterSave.cs index 2107caf..86163f5 100644 --- a/Glamourer/CharacterSave.cs +++ b/Glamourer/CharacterSave.cs @@ -1,8 +1,11 @@ using System; -using Dalamud.Game.ClientState.Actors.Types; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using Dalamud.Game.ClientState.Objects.Types; using Glamourer.Customization; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; namespace Glamourer @@ -36,8 +39,8 @@ namespace Glamourer public class CharacterSave { public const byte CurrentVersion = 2; - public const byte TotalSizeVersion1 = 1 + 1 + 2 + 56 + ActorCustomization.CustomizationBytes; - public const byte TotalSizeVersion2 = 1 + 1 + 2 + 56 + ActorCustomization.CustomizationBytes + 4 + 1; + public const byte TotalSizeVersion1 = 1 + 1 + 2 + 56 + CharacterCustomization.CustomizationBytes; + public const byte TotalSizeVersion2 = 1 + 1 + 2 + 56 + CharacterCustomization.CustomizationBytes + 4 + 1; public const byte TotalSize = TotalSizeVersion2; @@ -97,8 +100,8 @@ namespace Glamourer public byte StateFlags { - get => _bytes[64 + ActorCustomization.CustomizationBytes]; - set => _bytes[64 + ActorCustomization.CustomizationBytes] = value; + get => _bytes[64 + CharacterCustomization.CustomizationBytes]; + set => _bytes[64 + CharacterCustomization.CustomizationBytes] = value; } public bool HatState @@ -119,9 +122,9 @@ namespace Glamourer set => StateFlags = (byte) (value ? StateFlags & ~0x02 : StateFlags | 0x02); } - public ActorEquipMask WriteEquipment + public CharacterEquipMask WriteEquipment { - get => (ActorEquipMask) ((ushort) _bytes[2] | ((ushort) _bytes[3] << 8)); + get => (CharacterEquipMask) (_bytes[2] | (_bytes[3] << 8)); set { _bytes[2] = (byte) ((ushort) value & 0xFF); @@ -129,11 +132,104 @@ namespace Glamourer } } + private static Dictionary Offsets() + { + var stainOffsetWeapon = (int) Marshal.OffsetOf("Stain"); + var stainOffsetEquip = (int) Marshal.OffsetOf("Stain"); + + (int, int, bool) ToOffsets(IntPtr offset, bool weapon) + { + var off = 4 + CharacterCustomization.CustomizationBytes + (int) offset; + return weapon ? (off, off + stainOffsetWeapon, weapon) : (off, off + stainOffsetEquip, weapon); + } + + return new Dictionary(12) + { + [EquipSlot.MainHand] = ToOffsets(Marshal.OffsetOf("MainHand"), true), + [EquipSlot.OffHand] = ToOffsets(Marshal.OffsetOf("OffHand"), true), + [EquipSlot.Head] = ToOffsets(Marshal.OffsetOf("Head"), false), + [EquipSlot.Body] = ToOffsets(Marshal.OffsetOf("Body"), false), + [EquipSlot.Hands] = ToOffsets(Marshal.OffsetOf("Hands"), false), + [EquipSlot.Legs] = ToOffsets(Marshal.OffsetOf("Legs"), false), + [EquipSlot.Feet] = ToOffsets(Marshal.OffsetOf("Feet"), false), + [EquipSlot.Ears] = ToOffsets(Marshal.OffsetOf("Ears"), false), + [EquipSlot.Neck] = ToOffsets(Marshal.OffsetOf("Neck"), false), + [EquipSlot.Wrists] = ToOffsets(Marshal.OffsetOf("Wrists"), false), + [EquipSlot.RFinger] = ToOffsets(Marshal.OffsetOf("RFinger"), false), + [EquipSlot.LFinger] = ToOffsets(Marshal.OffsetOf("LFinger"), false), + }; + } + + private static readonly IReadOnlyDictionary FieldOffsets = Offsets(); + + public bool WriteStain(EquipSlot slot, StainId stainId) + { + if (WriteProtected) + return false; + + var (_, stainOffset, _) = FieldOffsets[slot]; + if (_bytes[stainOffset] == (byte) stainId) + return false; + + _bytes[stainOffset] = stainId.Value; + return true; + } + + private bool WriteItem(int offset, SetId id, WeaponType type, ushort variant, bool weapon) + { + var idBytes = BitConverter.GetBytes(id.Value); + + static bool WriteIfDifferent(ref byte x, byte y) + { + if (x == y) + return false; + + x = y; + return true; + } + + var ret = WriteIfDifferent(ref _bytes[offset], idBytes[0]); + ret |= WriteIfDifferent(ref _bytes[offset + 1], idBytes[1]); + if (weapon) + { + var typeBytes = BitConverter.GetBytes(type.Value); + var variantBytes = BitConverter.GetBytes(variant); + ret |= WriteIfDifferent(ref _bytes[offset + 2], typeBytes[0]); + ret |= WriteIfDifferent(ref _bytes[offset + 3], typeBytes[1]); + ret |= WriteIfDifferent(ref _bytes[offset + 4], variantBytes[0]); + ret |= WriteIfDifferent(ref _bytes[offset + 5], variantBytes[1]); + } + else + { + ret |= WriteIfDifferent(ref _bytes[offset + 2], (byte) variant); + } + + return ret; + } + + public bool WriteItem(Item item) + { + if (WriteProtected) + return false; + + var (itemOffset, _, isWeapon) = FieldOffsets[item.EquippableTo]; + var (id, type, variant) = item.MainModel; + var ret = WriteItem(itemOffset, id, type, variant, isWeapon); + if (item.EquippableTo == EquipSlot.MainHand && item.HasSubModel) + { + var (subOffset, _, _) = FieldOffsets[EquipSlot.OffHand]; + var (subId, subType, subVariant) = item.SubModel; + ret |= WriteItem(subOffset, subId, subType, subVariant, true); + } + + return ret; + } + public unsafe float Alpha { get { - fixed (byte* ptr = &_bytes[60 + ActorCustomization.CustomizationBytes]) + fixed (byte* ptr = &_bytes[60 + CharacterCustomization.CustomizationBytes]) { return *(float*) ptr; } @@ -142,24 +238,24 @@ namespace Glamourer { fixed (byte* ptr = _bytes) { - *(ptr + 60 + ActorCustomization.CustomizationBytes + 0) = *((byte*) &value + 0); - *(ptr + 60 + ActorCustomization.CustomizationBytes + 1) = *((byte*) &value + 1); - *(ptr + 60 + ActorCustomization.CustomizationBytes + 2) = *((byte*) &value + 2); - *(ptr + 60 + ActorCustomization.CustomizationBytes + 3) = *((byte*) &value + 3); + *(ptr + 60 + CharacterCustomization.CustomizationBytes + 0) = *((byte*) &value + 0); + *(ptr + 60 + CharacterCustomization.CustomizationBytes + 1) = *((byte*) &value + 1); + *(ptr + 60 + CharacterCustomization.CustomizationBytes + 2) = *((byte*) &value + 2); + *(ptr + 60 + CharacterCustomization.CustomizationBytes + 3) = *((byte*) &value + 3); } } } - public void Load(ActorCustomization customization) + public void Load(CharacterCustomization customization) { WriteCustomizations = true; customization.WriteBytes(_bytes, 4); } - public void Load(ActorEquipment equipment, ActorEquipMask mask = ActorEquipMask.All) + public void Load(CharacterEquipment equipment, CharacterEquipMask mask = CharacterEquipMask.All) { WriteEquipment = mask; - equipment.WriteBytes(_bytes, 4 + ActorCustomization.CustomizationBytes); + equipment.WriteBytes(_bytes, 4 + CharacterCustomization.CustomizationBytes); } public string ToBase64() @@ -179,19 +275,19 @@ namespace Glamourer $"Can not parse Base64 string into CharacterSave:\n\tInvalid value {value} in byte {idx}, should be in [{min},{max}]."); } - private static void CheckActorMask(byte val1, byte val2) + private static void CheckCharacterMask(byte val1, byte val2) { - var mask = (ActorEquipMask) ((ushort) val1 | ((ushort) val2 << 8)); - if (mask > ActorEquipMask.All) + var mask = (CharacterEquipMask) (val1 | (val2 << 8)); + if (mask > CharacterEquipMask.All) throw new Exception($"Can not parse Base64 string into CharacterSave:\n\tInvalid value {mask} in byte 3 and 4."); } - public void LoadActor(Actor a) + public void LoadCharacter(Character a) { WriteCustomizations = true; - Load(new ActorCustomization(a)); + Load(new CharacterCustomization(a)); - Load(new ActorEquipment(a), ActorEquipMask.All); + Load(new CharacterEquipment(a)); SetHatState = true; SetVisorState = true; @@ -202,11 +298,13 @@ namespace Glamourer Alpha = a.Alpha(); } - public void Apply(Actor a) + public void Apply(Character a) { + Glamourer.RevertableDesigns.Add(a); + if (WriteCustomizations) Customizations.Write(a.Address); - if (WriteEquipment != ActorEquipMask.None) + if (WriteEquipment != CharacterEquipMask.None) Equipment.Write(a.Address, WriteEquipment, WriteEquipment); a.SetWetness(IsWet); a.Alpha() = Alpha; @@ -243,7 +341,7 @@ namespace Glamourer default: throw new Exception($"Can not parse Base64 string into CharacterSave:\n\tInvalid Version {bytes[0]}."); } - CheckActorMask(bytes[2], bytes[3]); + CheckCharacterMask(bytes[2], bytes[3]); bytes.CopyTo(_bytes, 0); } @@ -254,23 +352,23 @@ namespace Glamourer return ret; } - public unsafe ref ActorCustomization Customizations + public unsafe ref CharacterCustomization Customizations { get { fixed (byte* ptr = _bytes) { - return ref *(ActorCustomization*) (ptr + 4); + return ref *(CharacterCustomization*) (ptr + 4); } } } - public ActorEquipment Equipment + public CharacterEquipment Equipment { get { - var ret = new ActorEquipment(); - ret.FromBytes(_bytes, 4 + ActorCustomization.CustomizationBytes); + var ret = new CharacterEquipment(); + ret.FromBytes(_bytes, 4 + CharacterCustomization.CustomizationBytes); return ret; } } diff --git a/Glamourer/CmpFile.cs b/Glamourer/CmpFile.cs deleted file mode 100644 index e320ab2..0000000 --- a/Glamourer/CmpFile.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Dalamud.Plugin; - -namespace Glamourer -{ - public class CmpFile - { - public readonly Lumina.Data.FileResource File; - public readonly uint[] RgbaColors; - - public CmpFile(DalamudPluginInterface pi) - { - File = pi.Data.GetFile("chara/xls/charamake/human.cmp"); - RgbaColors = new uint[File.Data.Length >> 2]; - for (var i = 0; i < File.Data.Length; i += 4) - { - RgbaColors[i >> 2] = File.Data[i] - | (uint) (File.Data[i + 1] << 8) - | (uint) (File.Data[i + 2] << 16) - | (uint) (File.Data[i + 3] << 24); - } - } - } -} diff --git a/Glamourer/Dalamud.cs b/Glamourer/Dalamud.cs new file mode 100644 index 0000000..80ac1a4 --- /dev/null +++ b/Glamourer/Dalamud.cs @@ -0,0 +1,56 @@ +using Dalamud.Data; +using Dalamud.Game; +using Dalamud.Game.ClientState; +using Dalamud.Game.ClientState.Buddy; +using Dalamud.Game.ClientState.Conditions; +using Dalamud.Game.ClientState.Fates; +using Dalamud.Game.ClientState.JobGauge; +using Dalamud.Game.ClientState.Keys; +using Dalamud.Game.ClientState.Objects; +using Dalamud.Game.ClientState.Party; +using Dalamud.Game.Command; +using Dalamud.Game.Gui; +using Dalamud.Game.Gui.FlyText; +using Dalamud.Game.Gui.PartyFinder; +using Dalamud.Game.Gui.Toast; +using Dalamud.Game.Libc; +using Dalamud.Game.Network; +using Dalamud.Game.Text.SeStringHandling; +using Dalamud.IoC; +using Dalamud.Plugin; +// ReSharper disable AutoPropertyCanBeMadeGetOnly.Local + +namespace Glamourer +{ + public class Dalamud + { + public static void Initialize(DalamudPluginInterface pluginInterface) + => pluginInterface.Create(); + + // @formatter:off + [PluginService][RequiredVersion("1.0")] public static DalamudPluginInterface PluginInterface { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public static CommandManager Commands { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public static SigScanner SigScanner { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public static DataManager GameData { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public static ClientState ClientState { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public static ChatGui Chat { get; private set; } = null!; + //[PluginService][RequiredVersion("1.0")] public static SeStringManager SeStrings { get; private set; } = null!; + //[PluginService][RequiredVersion("1.0")] public static ChatHandlers ChatHandlers { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public static Framework Framework { get; private set; } = null!; + //[PluginService][RequiredVersion("1.0")] public static GameNetwork Network { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public static Condition Conditions { get; private set; } = null!; + //[PluginService][RequiredVersion("1.0")] public static KeyState Keys { get; private set; } = null!; + //[PluginService][RequiredVersion("1.0")] public static GameGui GameGui { get; private set; } = null!; + //[PluginService][RequiredVersion("1.0")] public static FlyTextGui FlyTexts { get; private set; } = null!; + //[PluginService][RequiredVersion("1.0")] public static ToastGui Toasts { get; private set; } = null!; + //[PluginService][RequiredVersion("1.0")] public static JobGauges Gauges { get; private set; } = null!; + //[PluginService][RequiredVersion("1.0")] public static PartyFinderGui PartyFinder { get; private set; } = null!; + //[PluginService][RequiredVersion("1.0")] public static BuddyList Buddies { get; private set; } = null!; + //[PluginService][RequiredVersion("1.0")] public static PartyList Party { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public static TargetManager Targets { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public static ObjectTable Objects { get; private set; } = null!; + //[PluginService][RequiredVersion("1.0")] public static FateTable Fates { get; private set; } = null!; + //[PluginService][RequiredVersion("1.0")] public static LibcFunction LibC { get; private set; } = null!; + // @formatter:on + } +} diff --git a/Glamourer/Designs/DesignManager.cs b/Glamourer/Designs/DesignManager.cs index 73d9a96..31fefcf 100644 --- a/Glamourer/Designs/DesignManager.cs +++ b/Glamourer/Designs/DesignManager.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using Dalamud.Plugin; +using Dalamud.Logging; using Glamourer.FileSystem; using Newtonsoft.Json; @@ -16,9 +16,9 @@ namespace Glamourer.Designs public SortedList Designs = null!; public FileSystem.FileSystem FileSystem { get; } = new(); - public DesignManager(DalamudPluginInterface pi) + public DesignManager() { - var saveFolder = new DirectoryInfo(pi.GetPluginConfigDirectory()); + var saveFolder = new DirectoryInfo(Dalamud.PluginInterface.GetPluginConfigDirectory()); if (!saveFolder.Exists) Directory.CreateDirectory(saveFolder.FullName); @@ -31,24 +31,21 @@ namespace Glamourer.Designs { FileSystem.Clear(); var anyChanges = false; - foreach (var kvp in Designs.ToArray()) + foreach (var (path, save) in Designs.ToArray()) { - var path = kvp.Key; - var save = kvp.Value; - try { var (folder, name) = FileSystem.CreateAllFolders(path); var design = new Design(folder, name) { Data = save }; folder.FindOrAddChild(design); var fixedPath = design.FullName(); - if (!string.Equals(fixedPath, path, StringComparison.InvariantCultureIgnoreCase)) - { - Designs.Remove(path); - Designs[fixedPath] = save; - anyChanges = true; - PluginLog.Debug($"Problem loading saved designs, {path} was renamed to {fixedPath}."); - } + if (string.Equals(fixedPath, path, StringComparison.InvariantCultureIgnoreCase)) + continue; + + Designs.Remove(path); + Designs[fixedPath] = save; + anyChanges = true; + PluginLog.Debug($"Problem loading saved designs, {path} was renamed to {fixedPath}."); } catch (Exception e) { diff --git a/Glamourer/Designs/FixedDesigns.cs b/Glamourer/Designs/FixedDesigns.cs new file mode 100644 index 0000000..0a45a5c --- /dev/null +++ b/Glamourer/Designs/FixedDesigns.cs @@ -0,0 +1,162 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Dalamud.Game.ClientState.Objects.Enums; +using Dalamud.Game.ClientState.Objects.Types; +using Dalamud.Logging; +using Glamourer.FileSystem; +using Penumbra.GameData.Enums; + +namespace Glamourer.Designs +{ + public class FixedDesigns : IDisposable + { + public class FixedDesign + { + public string Name; + public Design Design; + public bool Enabled; + + public GlamourerConfig.FixedDesign ToSave() + => new() + { + Name = Name, + Path = Design.FullName(), + Enabled = Enabled, + }; + + public FixedDesign(string name, Design design, bool enabled) + { + Name = name; + Design = design; + Enabled = enabled; + } + } + + public List Data; + public Dictionary EnabledDesigns; + + public bool EnableDesign(FixedDesign design) + { + var changes = !design.Enabled; + if (EnabledDesigns.TryGetValue(design.Name, out var oldDesign)) + { + oldDesign.Enabled = false; + changes = true; + } + else + { + Glamourer.PlayerWatcher.AddPlayerToWatch(design.Name); + } + + EnabledDesigns[design.Name] = design; + design.Enabled = true; + if (Dalamud.Objects.FirstOrDefault(o => o.ObjectKind == ObjectKind.Player && o.Name.ToString() == design.Name) + is Character character) + OnPlayerChange(character); + return changes; + } + + public bool DisableDesign(FixedDesign design) + { + if (!design.Enabled) + return false; + + design.Enabled = false; + EnabledDesigns.Remove(design.Name); + Glamourer.PlayerWatcher.RemovePlayerFromWatch(design.Name); + return true; + } + + public FixedDesigns(DesignManager designs) + { + Data = new List(Glamourer.Config.FixedDesigns.Count); + EnabledDesigns = new Dictionary(Glamourer.Config.FixedDesigns.Count); + Glamourer.PlayerWatcher.PlayerChanged += OnPlayerChange; + var changes = false; + for (var i = 0; i < Glamourer.Config.FixedDesigns.Count; ++i) + { + var save = Glamourer.Config.FixedDesigns[i]; + if (designs.FileSystem.Find(save.Path, out var d) && d is Design design) + { + Data.Add(new FixedDesign(save.Name, design, save.Enabled)); + if (save.Enabled) + changes |= EnableDesign(Data.Last()); + } + else + { + PluginLog.Warning($"{save.Path} does not exist anymore, removing {save.Name} from fixed designs."); + Glamourer.Config.FixedDesigns.RemoveAt(i--); + changes = true; + } + } + + if (changes) + Glamourer.Config.Save(); + } + + private void OnPlayerChange(Character character) + { + var name = character.Name.ToString(); + if (EnabledDesigns.TryGetValue(name, out var design)) + { + PluginLog.Debug("Redrawing {CharacterName} with {DesignName}.", name, design.Design.FullName()); + design.Design.Data.Apply(character); + Glamourer.PlayerWatcher.UpdatePlayerWithoutEvent(character); + Glamourer.Penumbra.RedrawObject(character, RedrawType.WithSettings, false); + } + } + + public void Add(string name, Design design, bool enabled = false) + { + Data.Add(new FixedDesign(name, design, enabled)); + Glamourer.Config.FixedDesigns.Add(Data.Last().ToSave()); + + if (enabled) + EnableDesign(Data.Last()); + + Glamourer.Config.Save(); + } + + public void Remove(FixedDesign design) + { + var idx = Data.IndexOf(design); + if (idx < 0) + return; + + Data.RemoveAt(idx); + Glamourer.Config.FixedDesigns.RemoveAt(idx); + if (design.Enabled) + { + EnabledDesigns.Remove(design.Name); + Glamourer.PlayerWatcher.RemovePlayerFromWatch(design.Name); + } + + Glamourer.Config.Save(); + } + + public void Move(FixedDesign design, int newIdx) + { + if (newIdx < 0) + newIdx = 0; + if (newIdx >= Data.Count) + newIdx = Data.Count - 1; + + var idx = Data.IndexOf(design); + if (idx < 0 || idx == newIdx) + return; + + Data.RemoveAt(idx); + Data.Insert(newIdx, design); + Glamourer.Config.FixedDesigns.RemoveAt(idx); + Glamourer.Config.FixedDesigns.Insert(newIdx, design.ToSave()); + Glamourer.Config.Save(); + } + + public void Dispose() + { + Glamourer.Config.FixedDesigns = Data.Select(d => d.ToSave()).ToList(); + Glamourer.Config.Save(); + } + } +} diff --git a/Glamourer/Designs/RevertableDesigns.cs b/Glamourer/Designs/RevertableDesigns.cs new file mode 100644 index 0000000..99d9e9b --- /dev/null +++ b/Glamourer/Designs/RevertableDesigns.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using Dalamud.Game.ClientState.Objects.Types; + +namespace Glamourer.Designs +{ + public class RevertableDesigns + { + public readonly Dictionary Saves = new(); + + public bool Add(Character actor) + { + var name = actor.Name.ToString(); + if (Saves.TryGetValue(name, out var save)) + return false; + + save = new CharacterSave(); + save.LoadCharacter(actor); + Saves[name] = save; + return true; + } + + public bool Revert(Character actor) + { + if (!Saves.TryGetValue(actor.Name.ToString(), out var save)) + return false; + + save.Apply(actor); + return true; + } + } +} diff --git a/Glamourer/FileSystem/FileSystemImGui.cs b/Glamourer/FileSystem/FileSystemImGui.cs index a358be2..8c70368 100644 --- a/Glamourer/FileSystem/FileSystemImGui.cs +++ b/Glamourer/FileSystem/FileSystemImGui.cs @@ -1,6 +1,5 @@ using System; -using System.Linq; -using Dalamud.Plugin; +using Dalamud.Logging; using ImGuiNET; namespace Glamourer.FileSystem @@ -12,7 +11,7 @@ namespace Glamourer.FileSystem private static unsafe bool IsDropping(string name) => ImGui.AcceptDragDropPayload(name).NativePtr != null; - private static IFileSystemBase? _draggedObject = null; + private static IFileSystemBase? _draggedObject; public static bool DragDropTarget(FileSystem fs, IFileSystemBase child, out string oldPath, out IFileSystemBase? draggedChild) { diff --git a/Glamourer/FileSystem/IFolderStructure.cs b/Glamourer/FileSystem/IFolderStructure.cs index f4bf01b..fc0cabd 100644 --- a/Glamourer/FileSystem/IFolderStructure.cs +++ b/Glamourer/FileSystem/IFolderStructure.cs @@ -1,17 +1,16 @@ using System; using System.Collections.Generic; using System.Linq; -using Glamourer.Designs; namespace Glamourer.FileSystem { internal class FolderStructureComparer : IComparer { // Compare only the direct folder names since this is only used inside an enumeration of children of one folder. - public static int Cmp(IFileSystemBase x, IFileSystemBase y) - => ReferenceEquals(x, y) ? 0 : string.Compare(x.Name, y.Name, StringComparison.InvariantCultureIgnoreCase); + public static int Cmp(IFileSystemBase? x, IFileSystemBase? y) + => ReferenceEquals(x, y) ? 0 : string.Compare(x?.Name, y?.Name, StringComparison.InvariantCultureIgnoreCase); - public int Compare(IFileSystemBase x, IFileSystemBase y) + public int Compare(IFileSystemBase? x, IFileSystemBase? y) => Cmp(x, y); internal static readonly FolderStructureComparer Default = new(); diff --git a/Glamourer/Glamourer.cs b/Glamourer/Glamourer.cs new file mode 100644 index 0000000..c411068 --- /dev/null +++ b/Glamourer/Glamourer.cs @@ -0,0 +1,207 @@ +using System; +using System.Linq; +using System.Reflection; +using Dalamud.Game.ClientState.Objects.Types; +using Dalamud.Game.Command; +using Dalamud.Plugin; +using Glamourer.Customization; +using Glamourer.Designs; +using Glamourer.FileSystem; +using Glamourer.Gui; +using ImGuiNET; +using Penumbra.PlayerWatch; + +namespace Glamourer +{ + public class Glamourer : IDalamudPlugin + { + private const string HelpString = "[Copy|Apply|Save],[Name or PlaceHolder],"; + + public string Name + => "Glamourer"; + + public static GlamourerConfig Config = null!; + public static IPlayerWatcher PlayerWatcher = null!; + public static ICustomizationManager Customization = null!; + private readonly Interface _interface; + public readonly DesignManager Designs; + public readonly FixedDesigns FixedDesigns; + public static RevertableDesigns RevertableDesigns = new(); + + + public static string Version = string.Empty; + public static PenumbraAttach Penumbra = null!; + + public Glamourer(DalamudPluginInterface pluginInterface) + { + Dalamud.Initialize(pluginInterface); + Version = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? ""; + Config = GlamourerConfig.Load(); + Customization = CustomizationManager.Create(Dalamud.PluginInterface, Dalamud.GameData, Dalamud.ClientState.ClientLanguage); + Designs = new DesignManager(); + Penumbra = new PenumbraAttach(Config.AttachToPenumbra); + PlayerWatcher = PlayerWatchFactory.Create(Dalamud.Framework, Dalamud.ClientState, Dalamud.Objects); + FixedDesigns = new FixedDesigns(Designs); + + if (Config.ApplyFixedDesigns) + PlayerWatcher.Enable(); + + Dalamud.Commands.AddHandler("/glamourer", new CommandInfo(OnGlamourer) + { + HelpMessage = "Open or close the Glamourer window.", + }); + Dalamud.Commands.AddHandler("/glamour", new CommandInfo(OnGlamour) + { + HelpMessage = $"Use Glamourer Functions: {HelpString}", + }); + + _interface = new Interface(this); + } + + public void OnGlamourer(string command, string arguments) + => _interface.ToggleVisibility(); + + private static GameObject? GetPlayer(string name) + { + var lowerName = name.ToLowerInvariant(); + return lowerName switch + { + "" => null, + "" => Dalamud.Objects[Interface.GPoseObjectId] ?? Dalamud.ClientState.LocalPlayer, + "self" => Dalamud.Objects[Interface.GPoseObjectId] ?? Dalamud.ClientState.LocalPlayer, + "" => Dalamud.Targets.Target, + "target" => Dalamud.Targets.Target, + "" => Dalamud.Targets.FocusTarget, + "focus" => Dalamud.Targets.FocusTarget, + "" => Dalamud.Targets.MouseOverTarget, + "mouseover" => Dalamud.Targets.MouseOverTarget, + _ => Dalamud.Objects.LastOrDefault( + a => string.Equals(a.Name.ToString(), lowerName, StringComparison.InvariantCultureIgnoreCase)), + }; + } + + public void CopyToClipboard(Character player) + { + var save = new CharacterSave(); + save.LoadCharacter(player); + ImGui.SetClipboardText(save.ToBase64()); + } + + public void ApplyCommand(Character player, string target) + { + CharacterSave? save = null; + if (target.ToLowerInvariant() == "clipboard") + try + { + save = CharacterSave.FromString(ImGui.GetClipboardText()); + } + catch (Exception) + { + Dalamud.Chat.PrintError("Clipboard does not contain a valid customization string."); + } + else if (!Designs.FileSystem.Find(target, out var child) || child is not Design d) + Dalamud.Chat.PrintError("The given path to a saved design does not exist or does not point to a design."); + else + save = d.Data; + + save?.Apply(player); + Penumbra.UpdateCharacters(player); + } + + public void SaveCommand(Character player, string path) + { + var save = new CharacterSave(); + save.LoadCharacter(player); + try + { + var (folder, name) = Designs.FileSystem.CreateAllFolders(path); + var design = new Design(folder, name) { Data = save }; + folder.FindOrAddChild(design); + Designs.Designs.Add(design.FullName(), design.Data); + Designs.SaveToFile(); + } + catch (Exception e) + { + Dalamud.Chat.PrintError("Could not save file:"); + Dalamud.Chat.PrintError($" {e.Message}"); + } + } + + public void OnGlamour(string command, string arguments) + { + static void PrintHelp() + { + Dalamud.Chat.Print("Usage:"); + Dalamud.Chat.Print($" {HelpString}"); + } + + arguments = arguments.Trim(); + if (!arguments.Any()) + { + PrintHelp(); + return; + } + + var split = arguments.Split(new[] + { + ',', + }, 3, StringSplitOptions.RemoveEmptyEntries); + + if (split.Length < 2) + { + PrintHelp(); + return; + } + + var player = GetPlayer(split[1]) as Character; + if (player == null) + { + Dalamud.Chat.Print($"Could not find object for {split[1]} or it was not a Character."); + return; + } + + switch (split[0].ToLowerInvariant()) + { + case "copy": + CopyToClipboard(player); + return; + case "apply": + { + if (split.Length < 3) + { + Dalamud.Chat.Print("Applying requires a name for the save to be applied or 'clipboard'."); + return; + } + + ApplyCommand(player, split[2]); + + return; + } + case "save": + { + if (split.Length < 3) + { + Dalamud.Chat.Print("Saving requires a name for the save."); + return; + } + + SaveCommand(player, split[2]); + return; + } + default: + PrintHelp(); + return; + } + } + + public void Dispose() + { + FixedDesigns.Dispose(); + Penumbra.Dispose(); + PlayerWatcher.Dispose(); + _interface.Dispose(); + Dalamud.Commands.RemoveHandler("/glamour"); + Dalamud.Commands.RemoveHandler("/glamourer"); + } + } +} diff --git a/Glamourer/Glamourer.csproj b/Glamourer/Glamourer.csproj index 9249764..fd5a69a 100644 --- a/Glamourer/Glamourer.csproj +++ b/Glamourer/Glamourer.csproj @@ -1,11 +1,12 @@  - net472 + net5.0-windows preview + x64 Glamourer Glamourer - 0.0.3.0 - 0.0.3.0 + 0.0.5.4 + 0.0.5.4 SoftOtter Glamourer Copyright © 2020 @@ -46,47 +47,40 @@ $(appdata)\XIVLauncher\addon\Hooks\dev\Dalamud.dll + False $(appdata)\XIVLauncher\addon\Hooks\dev\ImGui.NET.dll + False $(appdata)\XIVLauncher\addon\Hooks\dev\ImGuiScene.dll + False $(appdata)\XIVLauncher\addon\Hooks\dev\SDL2-CS.dll + False $(appdata)\XIVLauncher\addon\Hooks\dev\Lumina.dll + False $(appdata)\XIVLauncher\addon\Hooks\dev\Lumina.Excel.dll + False - ..\..\Penumbra\Penumbra\bin\$(Configuration)\net472\Penumbra.GameData.dll - - - ..\..\Penumbra\Penumbra\bin\$(Configuration)\net472\Penumbra.Api.dll + ..\..\Penumbra\Penumbra\bin\$(Configuration)\$(TargetFramework)\Penumbra.GameData.dll + True - ..\..\Penumbra\Penumbra\bin\$(Configuration)\net472\Penumbra.PlayerWatch.dll + ..\..\Penumbra\Penumbra\bin\$(Configuration)\$(TargetFramework)\Penumbra.PlayerWatch.dll + True - - - - - - - - - - - + - - 12.0.3 - + @@ -100,7 +94,7 @@ - + \ No newline at end of file diff --git a/Glamourer/Glamourer.json b/Glamourer/Glamourer.json index 6556e2d..19bd0ee 100644 --- a/Glamourer/Glamourer.json +++ b/Glamourer/Glamourer.json @@ -1,11 +1,15 @@ -{ - "Author": "Ottermandias", - "Name": "Glamourer", - "Description": "Adds functionality to change appearance of actors. Requires Penumbra to be installed and activated to work.", - "InternalName": "Glamourer", - "AssemblyVersion": "0.0.3.0", - "RepoUrl": "https://github.com/Ottermandias/Glamourer", - "ApplicableVersion": "any", - "DalamudApiLevel": 3, - "LoadPriority": -100 +{ + "Author": "Ottermandias", + "Name": "Glamourer", + "Punchline": "Change and save appearance of players.", + "Description": "Adds functionality to change and store appearance of players, customization and equip. Requires Penumbra to be installed and activated to work. Can also add preview options to the Changed Items tab for Penumbra.", + "Tags": [ "Appearance", "Glamour", "Race", "Outfit", "Armor", "Clothes", "Skins", "Customization", "Design", "Character" ], + "InternalName": "Glamourer", + "AssemblyVersion": "0.0.5.4", + "RepoUrl": "https://github.com/Ottermandias/Glamourer", + "ApplicableVersion": "any", + "DalamudApiLevel": 4, + "LoadPriority": -100, + "ImageUrls": null, + "IconUrl": "https://raw.githubusercontent.com/Ottermandias/Glamourer/master/images/icon.png" } \ No newline at end of file diff --git a/Glamourer/GlamourerConfig.cs b/Glamourer/GlamourerConfig.cs index 2fa5da4..0e28efd 100644 --- a/Glamourer/GlamourerConfig.cs +++ b/Glamourer/GlamourerConfig.cs @@ -1,36 +1,45 @@ -using Dalamud.Configuration; +using System.Collections.Generic; +using Dalamud.Configuration; namespace Glamourer { public class GlamourerConfig : IPluginConfiguration { + public struct FixedDesign + { + public string Name; + public string Path; + public bool Enabled; + } + public int Version { get; set; } = 1; public const uint DefaultCustomizationColor = 0xFFC000C0; public const uint DefaultStateColor = 0xFF00C0C0; public const uint DefaultEquipmentColor = 0xFF00C000; - public bool FoldersFirst { get; set; } = false; - public bool ColorDesigns { get; set; } = true; - public bool ShowLocks { get; set; } = true; - public bool AttachToPenumbra { get; set; } = true; + public bool FoldersFirst { get; set; } = false; + public bool ColorDesigns { get; set; } = true; + public bool ShowLocks { get; set; } = true; + public bool AttachToPenumbra { get; set; } = true; + public bool ApplyFixedDesigns { get; set; } = true; public uint CustomizationColor { get; set; } = DefaultCustomizationColor; public uint StateColor { get; set; } = DefaultStateColor; public uint EquipmentColor { get; set; } = DefaultEquipmentColor; + public List FixedDesigns { get; set; } = new(); + public void Save() - => Glamourer.PluginInterface.SavePluginConfig(this); + => Dalamud.PluginInterface.SavePluginConfig(this); - public static GlamourerConfig Create() + public static GlamourerConfig Load() { - var config = Glamourer.PluginInterface.GetPluginConfig() as GlamourerConfig; - if (config == null) - { - config = new GlamourerConfig(); - Glamourer.PluginInterface.SavePluginConfig(config); - } + if (Dalamud.PluginInterface.GetPluginConfig() is GlamourerConfig config) + return config; + config = new GlamourerConfig(); + config.Save(); return config; } } diff --git a/Glamourer/Gui/ComboWithFilter.cs b/Glamourer/Gui/ComboWithFilter.cs index 69f4fdd..9a63425 100644 --- a/Glamourer/Gui/ComboWithFilter.cs +++ b/Glamourer/Gui/ComboWithFilter.cs @@ -8,30 +8,48 @@ namespace Glamourer.Gui { public class ComboWithFilter { - private readonly string _label; - private readonly string _filterLabel; - private readonly string _listLabel; - private string _currentFilter = string.Empty; - private string _currentFilterLower = string.Empty; - private bool _focus; - private readonly float _size; - private float _previewSize; - private readonly IReadOnlyList _items; - private readonly IReadOnlyList _itemNamesLower; - private readonly Func _itemToName; + private readonly string _label; + private readonly string _filterLabel; + private readonly string _listLabel; + private string _currentFilter = string.Empty; + private string _currentFilterLower = string.Empty; + private bool _focus; + private readonly float _size; + private float _previewSize; + private readonly IReadOnlyList _items; + private readonly IReadOnlyList<(string, int)> _itemNamesLower; + private readonly Func _itemToName; + private IReadOnlyList<(string, int)> _currentItemNames; + private bool _needsClear; - public Action? PrePreview = null; - public Action? PostPreview = null; - public Func? CreateSelectable = null; - public Action? PreList = null; - public Action? PostList = null; - public float? HeightPerItem = null; + public Action? PrePreview; + public Action? PostPreview; + public Func? CreateSelectable; + public Action? PreList; + public Action? PostList; + public float? HeightPerItem; private float _heightPerItem; public ImGuiComboFlags Flags { get; set; } = ImGuiComboFlags.None; public int ItemsAtOnce { get; set; } = 12; + private void UpdateFilter(string newFilter) + { + if (newFilter == _currentFilter) + return; + + var lower = newFilter.ToLowerInvariant(); + if (_currentFilterLower.Any() && lower.Contains(_currentFilterLower)) + _currentItemNames = _currentItemNames.Where(p => p.Item1.Contains(lower)).ToArray(); + else if (lower.Any()) + _currentItemNames = _itemNamesLower.Where(p => p.Item1.Contains(lower)).ToArray(); + else + _currentItemNames = _itemNamesLower; + _currentFilter = newFilter; + _currentFilterLower = lower; + } + public ComboWithFilter(string label, float size, float previewSize, IReadOnlyList items, Func itemToName) { _label = label; @@ -42,26 +60,28 @@ namespace Glamourer.Gui _size = size; _previewSize = previewSize; - _itemNamesLower = _items.Select(i => _itemToName(i).ToLowerInvariant()).ToList(); + _itemNamesLower = _items.Select((i, idx) => (_itemToName(i).ToLowerInvariant(), idx)).ToArray(); + _currentItemNames = _itemNamesLower; } public ComboWithFilter(string label, ComboWithFilter other) { - _label = label; - _filterLabel = $"##_{label}_filter"; - _listLabel = $"##_{label}_list"; - _itemToName = other._itemToName; - _items = other._items; - _itemNamesLower = other._itemNamesLower; - _size = other._size; - _previewSize = other._previewSize; - PrePreview = other.PrePreview; - PostPreview = other.PostPreview; - CreateSelectable = other.CreateSelectable; - PreList = other.PreList; - PostList = other.PostList; - HeightPerItem = other.HeightPerItem; - Flags = other.Flags; + _label = label; + _filterLabel = $"##_{label}_filter"; + _listLabel = $"##_{label}_list"; + _itemToName = other._itemToName; + _items = other._items; + _itemNamesLower = other._itemNamesLower; + _currentItemNames = other._currentItemNames; + _size = other._size; + _previewSize = other._previewSize; + PrePreview = other.PrePreview; + PostPreview = other.PostPreview; + CreateSelectable = other.CreateSelectable; + PreList = other.PreList; + PostList = other.PostList; + HeightPerItem = other.HeightPerItem; + Flags = other.Flags; } private bool DrawList(string currentName, out int numItems, out int nodeIdx, ref T? value) @@ -69,7 +89,10 @@ namespace Glamourer.Gui numItems = ItemsAtOnce; nodeIdx = -1; if (!ImGui.BeginChild(_listLabel, new Vector2(_size, ItemsAtOnce * _heightPerItem))) + { + ImGui.EndChild(); return false; + } var ret = false; try @@ -80,7 +103,6 @@ namespace Glamourer.Gui _focus = true; } - var scrollY = Math.Max((int) (ImGui.GetScrollY() / _heightPerItem) - 1, 0); var restHeight = scrollY * _heightPerItem; numItems = 0; @@ -89,38 +111,34 @@ namespace Glamourer.Gui if (restHeight > 0) ImGui.Dummy(Vector2.UnitY * restHeight); - for (var i = scrollY; i < _items.Count; ++i) + for (var i = scrollY; i < _currentItemNames.Count; ++i) { - if (!_itemNamesLower[i].Contains(_currentFilterLower)) + if (++numItems > ItemsAtOnce + 2) continue; - ++numItems; - if (numItems <= ItemsAtOnce + 2) + nodeIdx = _currentItemNames[i].Item2; + var item = _items[nodeIdx]!; + bool success; + if (CreateSelectable != null) { - nodeIdx = i; - var item = _items[i]!; - var success = false; - if (CreateSelectable != null) - { - success = CreateSelectable(item); - } - else - { - var name = _itemToName(item); - success = ImGui.Selectable(name, name == currentName); - } + success = CreateSelectable(item); + } + else + { + var name = _itemToName(item); + success = ImGui.Selectable(name, name == currentName); + } - if (success) - { - value = item; - ImGui.CloseCurrentPopup(); - ret = true; - } + if (success) + { + value = item; + ImGui.CloseCurrentPopup(); + ret = true; } } - if (numItems > ItemsAtOnce + 2) - ImGui.Dummy(Vector2.UnitY * (numItems - ItemsAtOnce - 2) * _heightPerItem); + if (_currentItemNames.Count > ItemsAtOnce + 2) + ImGui.Dummy(Vector2.UnitY * (_currentItemNames.Count - ItemsAtOnce - 2 - scrollY) * _heightPerItem); } finally { @@ -140,23 +158,29 @@ namespace Glamourer.Gui PrePreview?.Invoke(); if (!ImGui.BeginCombo(_label, currentName, Flags)) { - _focus = false; - _currentFilter = string.Empty; - _currentFilterLower = string.Empty; + if (_needsClear) + { + _needsClear = false; + _focus = false; + UpdateFilter(string.Empty); + } + PostPreview?.Invoke(); return false; } + _needsClear = true; PostPreview?.Invoke(); _heightPerItem = HeightPerItem ?? ImGui.GetTextLineHeightWithSpacing(); - var ret = false; + bool ret; try { ImGui.SetNextItemWidth(-1); - if (ImGui.InputTextWithHint(_filterLabel, "Filter...", ref _currentFilter, 255)) - _currentFilterLower = _currentFilter.ToLowerInvariant(); + var tmp = _currentFilter; + if (ImGui.InputTextWithHint(_filterLabel, "Filter...", ref tmp, 255)) + UpdateFilter(tmp); var isFocused = ImGui.IsItemActive(); if (!_focus) diff --git a/Glamourer/Gui/ImGuiRaii.cs b/Glamourer/Gui/ImGuiRaii.cs index 8aba17b..5704be9 100644 --- a/Glamourer/Gui/ImGuiRaii.cs +++ b/Glamourer/Gui/ImGuiRaii.cs @@ -7,15 +7,12 @@ namespace Glamourer.Gui { public sealed class ImGuiRaii : IDisposable { - private int _colorStack = 0; - private int _fontStack = 0; - private int _styleStack = 0; - private float _indentation = 0f; + private int _colorStack; + private int _fontStack; + private int _styleStack; + private float _indentation; - private Stack? _onDispose = null; - - public ImGuiRaii() - { } + private Stack? _onDispose; public static ImGuiRaii NewGroup() => new ImGuiRaii().Group(); @@ -51,6 +48,7 @@ namespace Glamourer.Gui ImGui.PopStyleColor(actualN); _colorStack -= actualN; } + return this; } @@ -76,6 +74,7 @@ namespace Glamourer.Gui ImGui.PopStyleVar(actualN); _styleStack -= actualN; } + return this; } @@ -95,6 +94,7 @@ namespace Glamourer.Gui ImGui.PopFont(); --_fontStack; } + return this; } @@ -105,6 +105,7 @@ namespace Glamourer.Gui ImGui.Indent(width); _indentation += width; } + return this; } @@ -134,7 +135,7 @@ namespace Glamourer.Gui public void End(int n = 1) { var actualN = Math.Min(n, _onDispose?.Count ?? 0); - while(actualN-- > 0) + while (actualN-- > 0) _onDispose!.Pop()(); } diff --git a/Glamourer/Gui/Interface.cs b/Glamourer/Gui/Interface.cs index 02c9c96..139b938 100644 --- a/Glamourer/Gui/Interface.cs +++ b/Glamourer/Gui/Interface.cs @@ -2,9 +2,11 @@ using System.Collections.Generic; using System.Linq; using System.Numerics; -using Dalamud.Game.ClientState.Actors; +using System.Reflection; +using Dalamud.Game.ClientState.Objects.Types; using Glamourer.Designs; using ImGuiNET; +using Lumina.Excel.GeneratedSheets; using Penumbra.GameData; using Penumbra.GameData.Enums; @@ -14,12 +16,12 @@ namespace Glamourer.Gui { public const float SelectorWidth = 200; public const float MinWindowWidth = 675; - public const int GPoseActorId = 201; + public const int GPoseObjectId = 201; private const string PluginName = "Glamourer"; private readonly string _glamourerHeader; private readonly IReadOnlyDictionary _stains; - private readonly ActorTable _actors; + private readonly IReadOnlyDictionary _models; private readonly IObjectIdentifier _identifier; private readonly Dictionary, ComboWithFilter)> _combos; private readonly ImGuiScene.TextureWrap? _legacyTattooIcon; @@ -27,8 +29,8 @@ namespace Glamourer.Gui private readonly DesignManager _designs; private readonly Glamourer _plugin; - private bool _visible = false; - private bool _inGPose = false; + private bool _visible; + private bool _inGPose; public Interface(Glamourer plugin) { @@ -37,31 +39,37 @@ namespace Glamourer.Gui _glamourerHeader = Glamourer.Version.Length > 0 ? $"{PluginName} v{Glamourer.Version}###{PluginName}Main" : $"{PluginName}###{PluginName}Main"; - Glamourer.PluginInterface.UiBuilder.DisableGposeUiHide = true; - Glamourer.PluginInterface.UiBuilder.OnBuildUi += Draw; - Glamourer.PluginInterface.UiBuilder.OnOpenConfigUi += ToggleVisibility; + Dalamud.PluginInterface.UiBuilder.DisableGposeUiHide = true; + Dalamud.PluginInterface.UiBuilder.Draw += Draw; + Dalamud.PluginInterface.UiBuilder.OpenConfigUi += ToggleVisibility; + + _characterConstructor = typeof(Character).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, new[] + { + typeof(IntPtr), + }, null)!; _equipSlotNames = GetEquipSlotNames(); - _stains = GameData.Stains(Glamourer.PluginInterface); - _identifier = Penumbra.GameData.GameData.GetIdentifier(Glamourer.PluginInterface); - _actors = Glamourer.PluginInterface.ClientState.Actors; + _stains = GameData.Stains(Dalamud.GameData); + _models = GameData.Models(Dalamud.GameData); + _identifier = Penumbra.GameData.GameData.GetIdentifier(Dalamud.GameData, Dalamud.ClientState.ClientLanguage); + var stainCombo = CreateDefaultStainCombo(_stains.Values.ToArray()); - var equip = GameData.ItemsBySlot(Glamourer.PluginInterface); + var equip = GameData.ItemsBySlot(Dalamud.GameData); _combos = equip.ToDictionary(kvp => kvp.Key, kvp => CreateCombos(kvp.Key, kvp.Value, stainCombo)); _legacyTattooIcon = GetLegacyTattooIcon(); } - public void ToggleVisibility(object _, object _2) + public void ToggleVisibility() => _visible = !_visible; public void Dispose() { _legacyTattooIcon?.Dispose(); - Glamourer.PluginInterface.UiBuilder.OnBuildUi -= Draw; - Glamourer.PluginInterface.UiBuilder.OnOpenConfigUi -= ToggleVisibility; + Dalamud.PluginInterface.UiBuilder.Draw -= Draw; + Dalamud.PluginInterface.UiBuilder.OpenConfigUi -= ToggleVisibility; } private void Draw() @@ -72,7 +80,10 @@ namespace Glamourer.Gui ImGui.SetNextWindowSizeConstraints(Vector2.One * MinWindowWidth * ImGui.GetIO().FontGlobalScale, Vector2.One * 5000 * ImGui.GetIO().FontGlobalScale); if (!ImGui.Begin(_glamourerHeader, ref _visible)) + { + ImGui.End(); return; + } try { @@ -80,7 +91,7 @@ namespace Glamourer.Gui if (!raii.Begin(() => ImGui.BeginTabBar("##tabBar"), ImGui.EndTabBar)) return; - _inGPose = _actors[GPoseActorId] != null; + _inGPose = Dalamud.Objects[GPoseObjectId] != null; _iconSize = Vector2.One * ImGui.GetTextLineHeightWithSpacing() * 2; _actualIconSize = _iconSize + 2 * ImGui.GetStyle().FramePadding; _comboSelectorSize = 4 * _actualIconSize.X + 3 * ImGui.GetStyle().ItemSpacing.X; @@ -89,9 +100,11 @@ namespace Glamourer.Gui _raceSelectorWidth = _inputIntSize + _percentageSize - _actualIconSize.X; _itemComboWidth = 6 * _actualIconSize.X + 4 * ImGui.GetStyle().ItemSpacing.X - ColorButtonWidth + 1; - DrawActorTab(); + DrawPlayerTab(); DrawSaves(); + DrawFixedDesignsTab(); DrawConfigTab(); + DrawRevertablesTab(); } finally { diff --git a/Glamourer/Gui/InterfaceActorPanel.cs b/Glamourer/Gui/InterfaceActorPanel.cs index fac7506..3c873f4 100644 --- a/Glamourer/Gui/InterfaceActorPanel.cs +++ b/Glamourer/Gui/InterfaceActorPanel.cs @@ -1,9 +1,13 @@ using System; using System.Linq; using System.Numerics; -using System.Windows.Forms; +using System.Reflection; +using Dalamud.Game.ClientState.Objects.Enums; +using Dalamud.Game.ClientState.Objects.SubKinds; +using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Interface; -using Dalamud.Plugin; +using Dalamud.Logging; +using Glamourer.Customization; using Glamourer.Designs; using Glamourer.FileSystem; using ImGuiNET; @@ -12,14 +16,15 @@ namespace Glamourer.Gui { internal partial class Interface { - private readonly CharacterSave _currentSave = new(); - private string _newDesignName = string.Empty; - private bool _keyboardFocus = false; - private const string DesignNamePopupLabel = "Save Design As..."; - private const uint RedHeaderColor = 0xFF1818C0; - private const uint GreenHeaderColor = 0xFF18C018; + private readonly CharacterSave _currentSave = new(); + private string _newDesignName = string.Empty; + private bool _keyboardFocus; + private const string DesignNamePopupLabel = "Save Design As..."; + private const uint RedHeaderColor = 0xFF1818C0; + private const uint GreenHeaderColor = 0xFF18C018; + private readonly ConstructorInfo _characterConstructor; - private void DrawActorHeader() + private void DrawPlayerHeader() { var color = _player == null ? RedHeaderColor : GreenHeaderColor; var buttonColor = ImGui.GetColorU32(ImGuiCol.FrameBg); @@ -30,14 +35,14 @@ namespace Glamourer.Gui .PushColor(ImGuiCol.ButtonActive, buttonColor) .PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero) .PushStyle(ImGuiStyleVar.FrameRounding, 0); - ImGui.Button($"{_currentActorName}##actorHeader", -Vector2.UnitX * 0.0001f); + ImGui.Button($"{_currentLabel}##playerHeader", -Vector2.UnitX * 0.0001f); } private static void DrawCopyClipboardButton(CharacterSave save) { ImGui.PushFont(UiBuilder.IconFont); if (ImGui.Button(FontAwesomeIcon.Clipboard.ToIconString())) - Clipboard.SetText(save.ToBase64()); + ImGui.SetClipboardText(save.ToBase64()); ImGui.PopFont(); if (ImGui.IsItemHovered()) ImGui.SetTooltip("Copy customization code to clipboard."); @@ -54,7 +59,7 @@ namespace Glamourer.Gui if (!applyButton) return false; - var text = Clipboard.GetText(); + var text = ImGui.GetClipboardText(); if (!text.Any()) return false; @@ -88,41 +93,100 @@ namespace Glamourer.Gui private void DrawTargetPlayerButton() { if (ImGui.Button("Target Player")) - Glamourer.PluginInterface.ClientState.Targets.SetCurrentTarget(_player); + Dalamud.Targets.SetTarget(_player); } private void DrawApplyToPlayerButton(CharacterSave save) { - if (ImGui.Button("Apply to Self")) + if (!ImGui.Button("Apply to Self")) + return; + + var player = _inGPose + ? (Character?) Dalamud.Objects[GPoseObjectId] + : Dalamud.ClientState.LocalPlayer; + var fallback = _inGPose ? Dalamud.ClientState.LocalPlayer : null; + if (player == null) + return; + + save.Apply(player); + if (_inGPose) + save.Apply(fallback!); + Glamourer.Penumbra.UpdateCharacters(player, fallback); + } + + private const int ModelTypeOffset = 0x01B4; + + private static unsafe int ModelType(GameObject actor) + => *(int*) (actor.Address + ModelTypeOffset); + + private static unsafe void SetModelType(GameObject actor, int value) + => *(int*) (actor.Address + ModelTypeOffset) = value; + + private Character Character(IntPtr address) + => (Character) _characterConstructor.Invoke(new object[] { - var player = _inGPose - ? Glamourer.PluginInterface.ClientState.Actors[GPoseActorId] - : Glamourer.PluginInterface.ClientState.LocalPlayer; - var fallback = _inGPose ? Glamourer.PluginInterface.ClientState.LocalPlayer : null; - if (player != null) + address, + }); + + private Character? CreateCharacter(GameObject? actor) + { + if (actor == null) + return null; + + return actor switch + { + PlayerCharacter p => p, + BattleChara b => b, + _ => actor.ObjectKind switch { - save.Apply(player); - if (_inGPose) - save.Apply(fallback!); - _plugin.UpdateActors(player, fallback); - } - } + ObjectKind.BattleNpc => Character(actor.Address), + ObjectKind.Companion => Character(actor.Address), + ObjectKind.EventNpc => Character(actor.Address), + _ => null, + }, + }; + } + + + private static Character? TransformToCustomizable(Character? actor) + { + if (actor == null) + return null; + + if (ModelType(actor) == 0) + return actor; + + SetModelType(actor, 0); + CharacterCustomization.Default.Write(actor.Address); + return actor; } private void DrawApplyToTargetButton(CharacterSave save) { - if (ImGui.Button("Apply to Target")) - { - var player = Glamourer.PluginInterface.ClientState.Targets.CurrentTarget; - if (player != null) - { - var fallBackActor = _playerNames[player.Name]; - save.Apply(player); - if (fallBackActor != null) - save.Apply(fallBackActor); - _plugin.UpdateActors(player, fallBackActor); - } - } + if (!ImGui.Button("Apply to Target")) + return; + + var player = TransformToCustomizable(CreateCharacter(Dalamud.Targets.Target)); + if (player == null) + return; + + var fallBackCharacter = _gPoseActors.TryGetValue(player.Name.ToString(), out var f) ? f : null; + save.Apply(player); + if (fallBackCharacter != null) + save.Apply(fallBackCharacter); + Glamourer.Penumbra.UpdateCharacters(player, fallBackCharacter); + } + + private void DrawRevertButton() + { + if (!DrawDisableButton("Revert", _player == null)) + return; + + Glamourer.RevertableDesigns.Revert(_player!); + var fallBackCharacter = _gPoseActors.TryGetValue(_player!.Name.ToString(), out var f) ? f : null; + if (fallBackCharacter != null) + Glamourer.RevertableDesigns.Revert(fallBackCharacter); + Glamourer.Penumbra.UpdateCharacters(_player, fallBackCharacter); } private void SaveNewDesign(CharacterSave save) @@ -130,13 +194,13 @@ namespace Glamourer.Gui try { var (folder, name) = _designs.FileSystem.CreateAllFolders(_newDesignName); - if (name.Any()) - { - var newDesign = new Design(folder, name) { Data = save }; - folder.AddChild(newDesign); - _designs.Designs[newDesign.FullName()] = save; - _designs.SaveToFile(); - } + if (!name.Any()) + return; + + var newDesign = new Design(folder, name) { Data = save }; + folder.AddChild(newDesign); + _designs.Designs[newDesign.FullName()] = save; + _designs.SaveToFile(); } catch (Exception e) { @@ -144,13 +208,42 @@ namespace Glamourer.Gui } } - private void DrawActorPanel() + private void DrawMonsterPanel() { - ImGui.BeginGroup(); - DrawActorHeader(); - if (!ImGui.BeginChild("##actorData", -Vector2.One, true)) + if (DrawApplyClipboardButton()) + Glamourer.Penumbra.UpdateCharacters(_player!); + + ImGui.SameLine(); + if (ImGui.Button("Convert to Character")) + { + TransformToCustomizable(_player); + _currentLabel = _currentLabel.Replace("(Monster)", "(NPC)"); + Glamourer.Penumbra.UpdateCharacters(_player!); + } + + if (!_inGPose) + { + ImGui.SameLine(); + DrawTargetPlayerButton(); + } + + var currentModel = ModelType(_player!); + using var raii = new ImGuiRaii(); + if (!raii.Begin(() => ImGui.BeginCombo("Model Id", currentModel.ToString()), ImGui.EndCombo)) return; + foreach (var (id, _) in _models.Skip(1)) + { + if (!ImGui.Selectable($"{id:D6}##models", id == currentModel) || id == currentModel) + continue; + + SetModelType(_player!, (int) id); + Glamourer.Penumbra.UpdateCharacters(_player!); + } + } + + private void DrawPlayerPanel() + { DrawCopyClipboardButton(_currentSave); ImGui.SameLine(); var changes = DrawApplyClipboardButton(); @@ -169,9 +262,12 @@ namespace Glamourer.Gui } } + ImGui.SameLine(); + DrawRevertButton(); if (DrawCustomization(ref _currentSave.Customizations) && _player != null) { + Glamourer.RevertableDesigns.Add(_player); _currentSave.Customizations.Write(_player.Address); changes = true; } @@ -180,9 +276,25 @@ namespace Glamourer.Gui changes |= DrawMiscellaneous(_currentSave, _player); if (_player != null && changes) - _plugin.UpdateActors(_player); + Glamourer.Penumbra.UpdateCharacters(_player); + } + + private void DrawActorPanel() + { + using var raii = ImGuiRaii.NewGroup(); + DrawPlayerHeader(); + if (!ImGui.BeginChild("##playerData", -Vector2.One, true)) + { + ImGui.EndChild(); + return; + } + + if (_player == null || ModelType(_player) == 0) + DrawPlayerPanel(); + else + DrawMonsterPanel(); + ImGui.EndChild(); - ImGui.EndGroup(); } } } diff --git a/Glamourer/Gui/InterfaceActorSelector.cs b/Glamourer/Gui/InterfaceActorSelector.cs index 889b01a..10f5fea 100644 --- a/Glamourer/Gui/InterfaceActorSelector.cs +++ b/Glamourer/Gui/InterfaceActorSelector.cs @@ -1,8 +1,8 @@ using System.Collections.Generic; using System.Linq; using System.Numerics; -using Dalamud.Game.ClientState.Actors; -using Dalamud.Game.ClientState.Actors.Types; +using Dalamud.Game.ClientState.Objects.Enums; +using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Interface; using ImGuiNET; @@ -10,51 +10,83 @@ namespace Glamourer.Gui { internal partial class Interface { - private Actor? _player; - private string _currentActorName = string.Empty; - private string _actorFilter = string.Empty; - private string _actorFilterLower = string.Empty; - private readonly Dictionary _playerNames = new(400); + private Character? _player; + private string _currentLabel = string.Empty; + private string _playerFilter = string.Empty; + private string _playerFilterLower = string.Empty; + private readonly Dictionary _playerNames = new(100); + private readonly Dictionary _gPoseActors = new(48); - private void DrawActorFilter() + private void DrawPlayerFilter() { using var raii = new ImGuiRaii() .PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero) .PushStyle(ImGuiStyleVar.FrameRounding, 0); ImGui.SetNextItemWidth(SelectorWidth * ImGui.GetIO().FontGlobalScale); - if (ImGui.InputTextWithHint("##actorFilter", "Filter Players...", ref _actorFilter, 32)) - _actorFilterLower = _actorFilter.ToLowerInvariant(); + if (ImGui.InputTextWithHint("##playerFilter", "Filter Players...", ref _playerFilter, 32)) + _playerFilterLower = _playerFilter.ToLowerInvariant(); } - private void DrawActorSelectable(Actor actor, bool gPose) + private void DrawGPoseSelectable(Character player) { - var actorName = actor.Name; - if (!actorName.Any()) + var playerName = player.Name.ToString(); + if (!playerName.Any()) return; - if (_playerNames.ContainsKey(actorName)) + _gPoseActors[playerName] = null; + + DrawSelectable(player, $"{playerName} (GPose)"); + } + + private static string GetLabel(Character player, string playerName, int num) + { + if (player.ObjectKind == ObjectKind.Player) + return num == 1 ? playerName : $"{playerName} #{num}"; + + if (ModelType(player) == 0) + return num == 1 ? $"{playerName} (NPC)" : $"{playerName} #{num} (NPC)"; + + return num == 1 ? $"{playerName} (Monster)" : $"{playerName} #{num} (Monster)"; + } + + private void DrawPlayerSelectable(Character player) + { + var playerName = player.Name.ToString(); + if (!playerName.Any()) + return; + + if (_playerNames.TryGetValue(playerName, out var num)) + _playerNames[playerName] = ++num; + else + _playerNames[playerName] = num = 1; + + if (_gPoseActors.ContainsKey(playerName)) { - _playerNames[actorName] = actor; + _gPoseActors[playerName] = player; return; } - _playerNames.Add(actorName, null); + var label = GetLabel(player, playerName, num); + DrawSelectable(player, label); + } - var label = gPose ? $"{actorName} (GPose)" : actorName; - if (!_actorFilterLower.Any() || actorName.ToLowerInvariant().Contains(_actorFilterLower)) - if (ImGui.Selectable(label, _currentActorName == actorName)) + + private void DrawSelectable(Character player, string label) + { + if (!_playerFilterLower.Any() || label.ToLowerInvariant().Contains(_playerFilterLower)) + if (ImGui.Selectable(label, _currentLabel == label)) { - _currentActorName = actorName; - _currentSave.LoadActor(actor); - _player = actor; + _currentLabel = label; + _currentSave.LoadCharacter(player); + _player = player; return; } - if (_currentActorName == actor.Name) - { - _currentSave.LoadActor(actor); - _player = actor; - } + if (_currentLabel != label) + return; + + _currentSave.LoadCharacter(player); + _player = player; } private void DrawSelectionButtons() @@ -63,10 +95,10 @@ namespace Glamourer.Gui .PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero) .PushStyle(ImGuiStyleVar.FrameRounding, 0) .PushFont(UiBuilder.IconFont); - Actor? select = null; - var buttonWidth = Vector2.UnitX * SelectorWidth / 2; + Character? select = null; + var buttonWidth = Vector2.UnitX * SelectorWidth / 2; if (ImGui.Button(FontAwesomeIcon.UserCircle.ToIconString(), buttonWidth)) - select = Glamourer.PluginInterface.ClientState.LocalPlayer; + select = Dalamud.ClientState.LocalPlayer; raii.PopFonts(); if (ImGui.IsItemHovered()) ImGui.SetTooltip("Select the local player character."); @@ -81,49 +113,60 @@ namespace Glamourer.Gui else { if (ImGui.Button(FontAwesomeIcon.HandPointer.ToIconString(), buttonWidth)) - select = Glamourer.PluginInterface.ClientState.Targets.CurrentTarget; + select = CreateCharacter(Dalamud.Targets.Target); } raii.PopFonts(); if (ImGui.IsItemHovered()) - ImGui.SetTooltip("Select the current target, if it is a player actor."); + ImGui.SetTooltip("Select the current target, if it is in the list."); - if (select == null || select.ObjectKind != ObjectKind.Player) + if (select == null) return; - _player = select; - _currentActorName = _player.Name; - _currentSave.LoadActor(_player); + _player = select; + _currentLabel = _player.Name.ToString(); + _currentSave.LoadCharacter(_player); } - private void DrawActorSelector() + private void DrawPlayerSelector() { ImGui.BeginGroup(); - DrawActorFilter(); - if (!ImGui.BeginChild("##actorSelector", + DrawPlayerFilter(); + if (!ImGui.BeginChild("##playerSelector", new Vector2(SelectorWidth * ImGui.GetIO().FontGlobalScale, -ImGui.GetFrameHeight() - 1), true)) + { + ImGui.EndChild(); + ImGui.EndGroup(); return; + } _playerNames.Clear(); - for (var i = GPoseActorId; i < GPoseActorId + 48; ++i) + _gPoseActors.Clear(); + for (var i = GPoseObjectId; i < GPoseObjectId + 48; ++i) { - var actor = _actors[i]; - if (actor == null) + var player = CreateCharacter(Dalamud.Objects[i]); + if (player == null) break; - if (actor.ObjectKind == ObjectKind.Player) - DrawActorSelectable(actor, true); + DrawGPoseSelectable(player); } - for (var i = 0; i < GPoseActorId; i += 2) + for (var i = 0; i < GPoseObjectId; ++i) { - var actor = _actors[i]; - if (actor != null && actor.ObjectKind == ObjectKind.Player) - DrawActorSelectable(actor, false); + var player = CreateCharacter(Dalamud.Objects[i])!; + if (player != null) + DrawPlayerSelectable(player); + } + + for (var i = GPoseObjectId + 48; i < Dalamud.Objects.Length; ++i) + { + var player = CreateCharacter(Dalamud.Objects[i])!; + if (player != null) + DrawPlayerSelectable(player); } - using (var raii = new ImGuiRaii().PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero)) + using (var _ = new ImGuiRaii().PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero)) { ImGui.EndChild(); } @@ -132,16 +175,16 @@ namespace Glamourer.Gui ImGui.EndGroup(); } - private void DrawActorTab() + private void DrawPlayerTab() { using var raii = new ImGuiRaii(); + _player = null; if (!raii.Begin(() => ImGui.BeginTabItem("Current Players"), ImGui.EndTabItem)) return; - _player = null; - DrawActorSelector(); + DrawPlayerSelector(); - if (!_currentActorName.Any()) + if (!_currentLabel.Any()) return; ImGui.SameLine(); diff --git a/Glamourer/Gui/InterfaceConfig.cs b/Glamourer/Gui/InterfaceConfig.cs index 9e294a7..54c020d 100644 --- a/Glamourer/Gui/InterfaceConfig.cs +++ b/Glamourer/Gui/InterfaceConfig.cs @@ -53,11 +53,8 @@ namespace Glamourer.Gui return; } - if (ImGui.Button(buttonLabel) && _plugin.GetPenumbra()) - { - _plugin.UnregisterFunctions(); - _plugin.RegisterFunctions(); - } + if (ImGui.Button(buttonLabel)) + Glamourer.Penumbra.Reattach(true); if (ImGui.IsItemHovered()) ImGui.SetTooltip( @@ -87,18 +84,25 @@ namespace Glamourer.Gui { cfg.AttachToPenumbra = v; if (v) - { - if (_plugin.GetPenumbra()) - _plugin.RegisterFunctions(); - } + Glamourer.Penumbra.Reattach(true); else - { - _plugin.UnregisterFunctions(); - } + Glamourer.Penumbra.Unattach(); }); ImGui.SameLine(); DrawRestorePenumbraButton(); + DrawConfigCheckMark("Apply Fixed Designs", + "Automatically apply fixed designs to characters and redraw them when anything changes.", + cfg.ApplyFixedDesigns, + v => + { + cfg.ApplyFixedDesigns = v; + if (v) + Glamourer.PlayerWatcher.Enable(); + else + Glamourer.PlayerWatcher.Disable(); + }); + ImGui.Dummy(Vector2.UnitY * ImGui.GetTextLineHeightWithSpacing() / 2); DrawColorPicker("Customization Color", "The color for designs that only apply their character customization.", diff --git a/Glamourer/Gui/InterfaceCustomization.cs b/Glamourer/Gui/InterfaceCustomization.cs index 0e56c22..88ecb6f 100644 --- a/Glamourer/Gui/InterfaceCustomization.cs +++ b/Glamourer/Gui/InterfaceCustomization.cs @@ -2,7 +2,7 @@ using System.Linq; using System.Numerics; using Dalamud.Interface; -using Dalamud.Plugin; +using Dalamud.Logging; using Glamourer.Customization; using ImGuiNET; using Penumbra.GameData.Enums; @@ -39,20 +39,20 @@ namespace Glamourer.Gui return ret; } - private Vector2 _iconSize = Vector2.Zero; - private Vector2 _actualIconSize = Vector2.Zero; - private float _raceSelectorWidth = 0; - private float _inputIntSize = 0; - private float _comboSelectorSize = 0; - private float _percentageSize = 0; - private float _itemComboWidth = 0; + private Vector2 _iconSize = Vector2.Zero; + private Vector2 _actualIconSize = Vector2.Zero; + private float _raceSelectorWidth; + private float _inputIntSize; + private float _comboSelectorSize; + private float _percentageSize; + private float _itemComboWidth; private bool InputInt(string label, ref int value, int minValue, int maxValue) { var ret = false; var tmp = value + 1; ImGui.SetNextItemWidth(_inputIntSize); - if (ImGui.InputInt(label, ref tmp, 1) && tmp != value + 1 && tmp >= minValue && tmp <= maxValue) + if (ImGui.InputInt(label, ref tmp, 1, 1, ImGuiInputTextFlags.EnterReturnsTrue) && tmp != value + 1 && tmp >= minValue && tmp <= maxValue) { value = tmp - 1; ret = true; @@ -64,7 +64,7 @@ namespace Glamourer.Gui return ret; } - private static (int, Customization.Customization) GetCurrentCustomization(ref ActorCustomization customization, CustomizationId id, + private static (int, Customization.Customization) GetCurrentCustomization(ref CharacterCustomization customization, CustomizationId id, CustomizationSet set) { var current = set.DataByValue(id, customization[id], out var custom); @@ -78,7 +78,7 @@ namespace Glamourer.Gui return (current, custom!.Value); } - private bool DrawColorPicker(string label, string tooltip, ref ActorCustomization customization, CustomizationId id, + private bool DrawColorPicker(string label, string tooltip, ref CharacterCustomization customization, CustomizationId id, CustomizationSet set) { var ret = false; @@ -93,11 +93,11 @@ namespace Glamourer.Gui ImGui.SameLine(); - using (var group = ImGuiRaii.NewGroup()) + using (var _ = ImGuiRaii.NewGroup()) { if (InputInt($"##text_{id}", ref current, 1, count)) { - customization[id] = set.Data(id, current - 1).Value; + customization[id] = set.Data(id, current).Value; ret = true; } @@ -116,7 +116,7 @@ namespace Glamourer.Gui return ret; } - private bool DrawListSelector(string label, string tooltip, ref ActorCustomization customization, CustomizationId id, + private bool DrawListSelector(string label, string tooltip, ref CharacterCustomization customization, CustomizationId id, CustomizationSet set) { using var bigGroup = ImGuiRaii.NewGroup(); @@ -158,7 +158,7 @@ namespace Glamourer.Gui private static readonly Vector4 NoColor = new(1f, 1f, 1f, 1f); private static readonly Vector4 RedColor = new(0.6f, 0.3f, 0.3f, 1f); - private bool DrawMultiSelector(ref ActorCustomization customization, CustomizationSet set) + private bool DrawMultiSelector(ref CharacterCustomization customization, CustomizationSet set) { using var bigGroup = ImGuiRaii.NewGroup(); var ret = false; @@ -242,7 +242,7 @@ namespace Glamourer.Gui return ret; } - private bool DrawIconSelector(string label, string tooltip, ref ActorCustomization customization, CustomizationId id, + private bool DrawIconSelector(string label, string tooltip, ref CharacterCustomization customization, CustomizationId id, CustomizationSet set) { using var bigGroup = ImGuiRaii.NewGroup(); @@ -282,6 +282,9 @@ namespace Glamourer.Gui ret = true; } + if (id == CustomizationId.Hairstyle && customization.Race == Race.Hrothgar) + customization[CustomizationId.Face] = (byte) ((customization[CustomizationId.Hairstyle] + 1) / 2); + ImGui.Text(label); if (tooltip.Any() && ImGui.IsItemHovered()) ImGui.SetTooltip(tooltip); @@ -290,7 +293,7 @@ namespace Glamourer.Gui } - private bool DrawPercentageSelector(string label, string tooltip, ref ActorCustomization customization, CustomizationId id, + private bool DrawPercentageSelector(string label, string tooltip, ref CharacterCustomization customization, CustomizationId id, CustomizationSet set) { using var bigGroup = ImGuiRaii.NewGroup(); @@ -320,7 +323,7 @@ namespace Glamourer.Gui return ret; } - private bool DrawRaceSelector(ref ActorCustomization customization) + private bool DrawRaceSelector(ref CharacterCustomization customization) { using var group = ImGuiRaii.NewGroup(); var ret = false; @@ -345,7 +348,7 @@ namespace Glamourer.Gui return ret; } - private bool DrawGenderSelector(ref ActorCustomization customization) + private bool DrawGenderSelector(ref CharacterCustomization customization) { var ret = false; ImGui.PushFont(UiBuilder.IconFont); @@ -376,7 +379,7 @@ namespace Glamourer.Gui return ret; } - private bool DrawPicker(CustomizationSet set, CustomizationId id, ref ActorCustomization customization) + private bool DrawPicker(CustomizationSet set, CustomizationId id, ref CharacterCustomization customization) { if (!set.IsAvailable(id)) return false; @@ -394,9 +397,18 @@ namespace Glamourer.Gui return false; } - private static readonly CustomizationId[] AllCustomizations = (CustomizationId[]) Enum.GetValues(typeof(CustomizationId)); + private static CustomizationId[] GetCustomizationOrder() + { + var ret = (CustomizationId[])Enum.GetValues(typeof(CustomizationId)); + ret[(int) CustomizationId.TattooColor] = CustomizationId.EyeColorL; + ret[(int) CustomizationId.EyeColorL] = CustomizationId.EyeColorR; + ret[(int) CustomizationId.EyeColorR] = CustomizationId.TattooColor; + return ret; + } - private bool DrawCustomization(ref ActorCustomization custom) + private static readonly CustomizationId[] AllCustomizations = GetCustomizationOrder(); + + private bool DrawCustomization(ref CharacterCustomization custom) { if (!ImGui.CollapsingHeader("Character Customization")) return false; @@ -457,7 +469,7 @@ namespace Glamourer.Gui } tmp = custom.SmallIris; - if (ImGui.Checkbox($"{Glamourer.Customization.GetName(CustomName.IrisSmall)} {set.Option(CustomizationId.EyeColorL)}", + if (ImGui.Checkbox($"{Glamourer.Customization.GetName(CustomName.IrisSmall)} {Glamourer.Customization.GetName(CustomName.IrisSize)}", ref tmp) && tmp != custom.SmallIris) { diff --git a/Glamourer/Gui/InterfaceDesigns.cs b/Glamourer/Gui/InterfaceDesigns.cs index 6116e87..557feb3 100644 --- a/Glamourer/Gui/InterfaceDesigns.cs +++ b/Glamourer/Gui/InterfaceDesigns.cs @@ -1,9 +1,8 @@ using System; using System.Linq; using System.Numerics; -using System.Windows.Forms; using Dalamud.Interface; -using Dalamud.Plugin; +using Dalamud.Logging; using Glamourer.Designs; using Glamourer.FileSystem; using ImGuiNET; @@ -12,9 +11,10 @@ namespace Glamourer.Gui { internal partial class Interface { - private int _totalObject = 0; + private int _totalObject; - private Design? _selection = null; + private bool _inDesignMode; + private Design? _selection; private string _newChildName = string.Empty; private void DrawDesignSelector() @@ -50,7 +50,7 @@ namespace Glamourer.Gui if (_selection!.Data.WriteProtected || !applyButton) return; - var text = Clipboard.GetText(); + var text = ImGui.GetClipboardText(); if (!text.Any()) return; @@ -208,7 +208,8 @@ namespace Glamourer.Gui { using var raii = new ImGuiRaii(); raii.PushStyle(ImGuiStyleVar.IndentSpacing, 12.5f * ImGui.GetIO().FontGlobalScale); - if (!raii.Begin(() => ImGui.BeginTabItem("Saves"), ImGui.EndTabItem)) + _inDesignMode = raii.Begin(() => ImGui.BeginTabItem("Designs"), ImGui.EndTabItem); + if (!_inDesignMode) return; DrawDesignSelector(); @@ -279,8 +280,7 @@ namespace Glamourer.Gui private void ContextMenu(IFileSystemBase child) { - var label = $"##fsPopup{child.FullName()}"; - var renameLabel = $"{label}_rename"; + var label = $"##fsPopup{child.FullName()}"; if (ImGui.BeginPopup(label)) { if (ImGui.MenuItem("Delete")) @@ -289,7 +289,7 @@ namespace Glamourer.Gui RenameChildInput(child); if (child is Design d && ImGui.MenuItem("Copy to Clipboard")) - Clipboard.SetText(d.Data.ToBase64()); + ImGui.SetClipboardText(d.Data.ToBase64()); ImGui.EndPopup(); } @@ -310,12 +310,12 @@ namespace Glamourer.Gui var changesStates = save.SetHatState || save.SetVisorState || save.SetWeaponState || save.IsWet || save.Alpha != 1.0f; if (save.WriteCustomizations) - if (save.WriteEquipment != ActorEquipMask.None) + if (save.WriteEquipment != CharacterEquipMask.None) return white; else return changesStates ? white : Glamourer.Config.CustomizationColor; - if (save.WriteEquipment != ActorEquipMask.None) + if (save.WriteEquipment != CharacterEquipMask.None) return changesStates ? white : Glamourer.Config.EquipmentColor; return changesStates ? Glamourer.Config.StateColor : grey; diff --git a/Glamourer/Gui/InterfaceEquipment.cs b/Glamourer/Gui/InterfaceEquipment.cs index 56233a9..d9cc282 100644 --- a/Glamourer/Gui/InterfaceEquipment.cs +++ b/Glamourer/Gui/InterfaceEquipment.cs @@ -15,10 +15,17 @@ namespace Glamourer.Gui stainCombo.PostPreview = () => ImGui.PopStyleColor(previewPush); } - if (stainCombo.Draw(string.Empty, out var newStain) && _player != null && !newStain.RowIndex.Equals(stainIdx)) + if (stainCombo.Draw(string.Empty, out var newStain) && !newStain.RowIndex.Equals(stainIdx)) { - newStain.Write(_player.Address, slot); - return true; + if (_player != null) + { + Glamourer.RevertableDesigns.Add(_player); + newStain.Write(_player.Address, slot); + return true; + } + + if (_inDesignMode && (_selection?.Data.WriteStain(slot, newStain.RowIndex) ?? false)) + return true; } return false; @@ -27,22 +34,29 @@ namespace Glamourer.Gui private bool DrawItemSelector(ComboWithFilter equipCombo, Lumina.Excel.GeneratedSheets.Item? item) { var currentName = item?.Name.ToString() ?? "Nothing"; - if (equipCombo.Draw(currentName, out var newItem, _itemComboWidth) && _player != null && newItem.Base.RowId != item?.RowId) + if (equipCombo.Draw(currentName, out var newItem, _itemComboWidth) && newItem.Base.RowId != item?.RowId) { - newItem.Write(_player.Address); - return true; + if (_player != null) + { + Glamourer.RevertableDesigns.Add(_player); + newItem.Write(_player.Address); + return true; + } + + if (_inDesignMode && (_selection?.Data.WriteItem(newItem) ?? false)) + return true; } return false; } - private static bool DrawCheckbox(ActorEquipMask flag, ref ActorEquipMask mask) + private static bool DrawCheckbox(CharacterEquipMask flag, ref CharacterEquipMask mask) { var tmp = (uint) mask; var ret = false; if (ImGui.CheckboxFlags($"##flag_{(uint) flag}", ref tmp, (uint) flag) && tmp != (uint) mask) { - mask = (ActorEquipMask) tmp; + mask = (CharacterEquipMask) tmp; ret = true; } @@ -51,7 +65,7 @@ namespace Glamourer.Gui return ret; } - private bool DrawEquipSlot(EquipSlot slot, ActorArmor equip) + private bool DrawEquipSlot(EquipSlot slot, CharacterArmor equip) { var (equipCombo, stainCombo) = _combos[slot]; @@ -63,7 +77,7 @@ namespace Glamourer.Gui return ret; } - private bool DrawEquipSlotWithCheck(EquipSlot slot, ActorArmor equip, ActorEquipMask flag, ref ActorEquipMask mask) + private bool DrawEquipSlotWithCheck(EquipSlot slot, CharacterArmor equip, CharacterEquipMask flag, ref CharacterEquipMask mask) { var ret = DrawCheckbox(flag, ref mask); ImGui.SameLine(); @@ -71,7 +85,7 @@ namespace Glamourer.Gui return ret; } - private bool DrawWeapon(EquipSlot slot, ActorWeapon weapon) + private bool DrawWeapon(EquipSlot slot, CharacterWeapon weapon) { var (equipCombo, stainCombo) = _combos[slot]; @@ -83,7 +97,7 @@ namespace Glamourer.Gui return ret; } - private bool DrawWeaponWithCheck(EquipSlot slot, ActorWeapon weapon, ActorEquipMask flag, ref ActorEquipMask mask) + private bool DrawWeaponWithCheck(EquipSlot slot, CharacterWeapon weapon, CharacterEquipMask flag, ref CharacterEquipMask mask) { var ret = DrawCheckbox(flag, ref mask); ImGui.SameLine(); @@ -91,7 +105,7 @@ namespace Glamourer.Gui return ret; } - private bool DrawEquip(ActorEquipment equip) + private bool DrawEquip(CharacterEquipment equip) { var ret = false; if (ImGui.CollapsingHeader("Character Equipment")) @@ -113,23 +127,23 @@ namespace Glamourer.Gui return ret; } - private bool DrawEquip(ActorEquipment equip, ref ActorEquipMask mask) + private bool DrawEquip(CharacterEquipment equip, ref CharacterEquipMask mask) { var ret = false; if (ImGui.CollapsingHeader("Character Equipment")) { - ret |= DrawWeaponWithCheck(EquipSlot.MainHand, equip.MainHand, ActorEquipMask.MainHand, ref mask); - ret |= DrawWeaponWithCheck(EquipSlot.OffHand, equip.OffHand, ActorEquipMask.OffHand, ref mask); - ret |= DrawEquipSlotWithCheck(EquipSlot.Head, equip.Head, ActorEquipMask.Head, ref mask); - ret |= DrawEquipSlotWithCheck(EquipSlot.Body, equip.Body, ActorEquipMask.Body, ref mask); - ret |= DrawEquipSlotWithCheck(EquipSlot.Hands, equip.Hands, ActorEquipMask.Hands, ref mask); - ret |= DrawEquipSlotWithCheck(EquipSlot.Legs, equip.Legs, ActorEquipMask.Legs, ref mask); - ret |= DrawEquipSlotWithCheck(EquipSlot.Feet, equip.Feet, ActorEquipMask.Feet, ref mask); - ret |= DrawEquipSlotWithCheck(EquipSlot.Ears, equip.Ears, ActorEquipMask.Ears, ref mask); - ret |= DrawEquipSlotWithCheck(EquipSlot.Neck, equip.Neck, ActorEquipMask.Neck, ref mask); - ret |= DrawEquipSlotWithCheck(EquipSlot.Wrists, equip.Wrists, ActorEquipMask.Wrists, ref mask); - ret |= DrawEquipSlotWithCheck(EquipSlot.RFinger, equip.RFinger, ActorEquipMask.RFinger, ref mask); - ret |= DrawEquipSlotWithCheck(EquipSlot.LFinger, equip.LFinger, ActorEquipMask.LFinger, ref mask); + ret |= DrawWeaponWithCheck(EquipSlot.MainHand, equip.MainHand, CharacterEquipMask.MainHand, ref mask); + ret |= DrawWeaponWithCheck(EquipSlot.OffHand, equip.OffHand, CharacterEquipMask.OffHand, ref mask); + ret |= DrawEquipSlotWithCheck(EquipSlot.Head, equip.Head, CharacterEquipMask.Head, ref mask); + ret |= DrawEquipSlotWithCheck(EquipSlot.Body, equip.Body, CharacterEquipMask.Body, ref mask); + ret |= DrawEquipSlotWithCheck(EquipSlot.Hands, equip.Hands, CharacterEquipMask.Hands, ref mask); + ret |= DrawEquipSlotWithCheck(EquipSlot.Legs, equip.Legs, CharacterEquipMask.Legs, ref mask); + ret |= DrawEquipSlotWithCheck(EquipSlot.Feet, equip.Feet, CharacterEquipMask.Feet, ref mask); + ret |= DrawEquipSlotWithCheck(EquipSlot.Ears, equip.Ears, CharacterEquipMask.Ears, ref mask); + ret |= DrawEquipSlotWithCheck(EquipSlot.Neck, equip.Neck, CharacterEquipMask.Neck, ref mask); + ret |= DrawEquipSlotWithCheck(EquipSlot.Wrists, equip.Wrists, CharacterEquipMask.Wrists, ref mask); + ret |= DrawEquipSlotWithCheck(EquipSlot.RFinger, equip.RFinger, CharacterEquipMask.RFinger, ref mask); + ret |= DrawEquipSlotWithCheck(EquipSlot.LFinger, equip.LFinger, CharacterEquipMask.LFinger, ref mask); } return ret; diff --git a/Glamourer/Gui/InterfaceFixedDesigns.cs b/Glamourer/Gui/InterfaceFixedDesigns.cs new file mode 100644 index 0000000..24e520a --- /dev/null +++ b/Glamourer/Gui/InterfaceFixedDesigns.cs @@ -0,0 +1,140 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using Dalamud.Interface; +using Glamourer.Designs; +using Glamourer.FileSystem; +using ImGuiNET; + +namespace Glamourer.Gui +{ + internal partial class Interface + { + private const string FixDragDropLabel = "##FixDragDrop"; + + private List? _fullPathCache; + private string _newFixCharacterName = string.Empty; + private string _newFixDesignPath = string.Empty; + private Design? _newFixDesign; + private int _fixDragDropIdx = -1; + + private static unsafe bool IsDropping() + => ImGui.AcceptDragDropPayload(FixDragDropLabel).NativePtr != null; + + private void DrawFixedDesignsTab() + { + using var raii = new ImGuiRaii(); + if (!raii.Begin(() => ImGui.BeginTabItem("Fixed Designs"), ImGui.EndTabItem)) + { + _fullPathCache = null; + _newFixDesign = null; + _newFixDesignPath = string.Empty; + return; + } + + _fullPathCache ??= _plugin.FixedDesigns.Data.Select(d => d.Design.FullName()).ToList(); + + raii.Begin(() => ImGui.BeginTable("##FixedTable", 3), ImGui.EndTable); + + var buttonWidth = 23.5f * ImGuiHelpers.GlobalScale; + + + ImGui.TableSetupColumn("##DeleteColumn", ImGuiTableColumnFlags.WidthFixed, 2 * buttonWidth); + ImGui.TableSetupColumn("Character", ImGuiTableColumnFlags.WidthFixed, 200 * ImGuiHelpers.GlobalScale); + ImGui.TableSetupColumn("Design", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableHeadersRow(); + var xPos = 0f; + for (var i = 0; i < _fullPathCache.Count; ++i) + { + var path = _fullPathCache[i]; + var name = _plugin.FixedDesigns.Data[i]; + + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + raii.PushStyle(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing / 2); + raii.PushFont(UiBuilder.IconFont); + if (ImGui.Button($"{FontAwesomeIcon.Trash.ToIconChar()}##{i}")) + { + _fullPathCache.RemoveAt(i); + _plugin.FixedDesigns.Remove(name); + } + + var tmp = name.Enabled; + ImGui.SameLine(); + xPos = ImGui.GetCursorPosX(); + if (ImGui.Checkbox($"##Enabled{i}", ref tmp)) + if (tmp && _plugin.FixedDesigns.EnableDesign(name) + || !tmp && _plugin.FixedDesigns.DisableDesign(name)) + Glamourer.Config.Save(); + raii.PopStyles(); + raii.PopFonts(); + ImGui.TableNextColumn(); + ImGui.Selectable($"{name.Name}##Fix{i}"); + if (ImGui.BeginDragDropSource()) + { + _fixDragDropIdx = i; + ImGui.SetDragDropPayload("##FixDragDrop", IntPtr.Zero, 0); + ImGui.Text($"Dragging {name.Name} ({path})..."); + ImGui.EndDragDropSource(); + } + if (ImGui.BeginDragDropTarget()) + { + if (IsDropping() && _fixDragDropIdx >= 0) + { + var d = _plugin.FixedDesigns.Data[_fixDragDropIdx]; + _plugin.FixedDesigns.Move(d, i); + var p = _fullPathCache[_fixDragDropIdx]; + _fullPathCache.RemoveAt(_fixDragDropIdx); + _fullPathCache.Insert(i, p); + _fixDragDropIdx = -1; + } + ImGui.EndDragDropTarget(); + } + + ImGui.TableNextColumn(); + ImGui.Text(path); + } + + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + raii.PushFont(UiBuilder.IconFont); + + ImGui.SetCursorPosX(xPos); + if (_newFixDesign == null || _newFixCharacterName == string.Empty) + { + raii.PushStyle(ImGuiStyleVar.Alpha, 0.5f); + ImGui.Button($"{FontAwesomeIcon.Plus.ToIconChar()}##NewFix"); + raii.PopStyles(); + } + else if (ImGui.Button($"{FontAwesomeIcon.Plus.ToIconChar()}##NewFix")) + { + _fullPathCache.Add(_newFixDesignPath); + _plugin.FixedDesigns.Add(_newFixCharacterName, _newFixDesign, false); + _newFixCharacterName = string.Empty; + _newFixDesignPath = string.Empty; + _newFixDesign = null; + } + + raii.PopFonts(); + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale); + ImGui.InputTextWithHint("##NewFix", "Enter new Character", ref _newFixCharacterName, 32); + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(-1); + if (!raii.Begin(() => ImGui.BeginCombo("##NewFixPath", _newFixDesignPath), ImGui.EndCombo)) + return; + + foreach (var design in _plugin.Designs.FileSystem.Root.AllLeaves(SortMode.Lexicographical).Cast()) + { + var fullName = design.FullName(); + ImGui.SetNextItemWidth(-1); + if (!ImGui.Selectable($"{fullName}##NewFixDesign", fullName == _newFixDesignPath)) + continue; + + _newFixDesignPath = fullName; + _newFixDesign = design; + } + } + } +} diff --git a/Glamourer/Gui/InterfaceHelpers.cs b/Glamourer/Gui/InterfaceHelpers.cs index 84c0c5f..21251fa 100644 --- a/Glamourer/Gui/InterfaceHelpers.cs +++ b/Glamourer/Gui/InterfaceHelpers.cs @@ -1,11 +1,8 @@ using System; using System.Linq; -using System.Windows.Forms; -using Dalamud.Game.ClientState.Actors.Types; -using Dalamud.Plugin; +using Dalamud.Logging; using Glamourer.Customization; using ImGuiNET; -using Penumbra.Api; using Penumbra.GameData.Enums; namespace Glamourer.Gui @@ -27,7 +24,7 @@ namespace Glamourer.Gui } // Go through a whole customization struct and fix up all settings that need fixing. - private static void FixUpAttributes(ref ActorCustomization customization) + private static void FixUpAttributes(ref CharacterCustomization customization) { var set = Glamourer.Customization.GetList(customization.Clan, customization.Gender); foreach (CustomizationId id in Enum.GetValues(typeof(CustomizationId))) @@ -46,7 +43,7 @@ namespace Glamourer.Gui break; default: var count = set.Count(id); - if (set.DataByValue(id, customization[id], out var value) < 0) + if (set.DataByValue(id, customization[id], out _) < 0) if (count == 0) customization[id] = 0; else @@ -57,7 +54,7 @@ namespace Glamourer.Gui } // Change a race and fix up all required customizations afterwards. - private static bool ChangeRace(ref ActorCustomization customization, SubRace clan) + private static bool ChangeRace(ref CharacterCustomization customization, SubRace clan) { if (clan == customization.Clan) return false; @@ -79,7 +76,7 @@ namespace Glamourer.Gui } // Change a gender and fix up all required customizations afterwards. - private static bool ChangeGender(ref ActorCustomization customization, Gender gender) + private static bool ChangeGender(ref CharacterCustomization customization, Gender gender) { if (gender == customization.Gender) return false; @@ -159,7 +156,7 @@ namespace Glamourer.Gui break; case DesignNameUse.NewDesign: var empty = new CharacterSave(); - empty.Load(ActorCustomization.Default); + empty.Load(CharacterCustomization.Default); empty.WriteCustomizations = false; SaveNewDesign(empty); break; @@ -173,7 +170,7 @@ namespace Glamourer.Gui case DesignNameUse.FromClipboard: try { - var text = Clipboard.GetText(); + var text = ImGui.GetClipboardText(); var save = CharacterSave.FromString(text); SaveNewDesign(save); } diff --git a/Glamourer/Gui/InterfaceInitialization.cs b/Glamourer/Gui/InterfaceInitialization.cs index 4c279a6..1e4e1d5 100644 --- a/Glamourer/Gui/InterfaceInitialization.cs +++ b/Glamourer/Gui/InterfaceInitialization.cs @@ -53,7 +53,7 @@ namespace Glamourer.Gui { var rawImage = new byte[resource.Length]; resource.Read(rawImage, 0, (int) resource.Length); - return Glamourer.PluginInterface.UiBuilder.LoadImageRaw(rawImage, 192, 192, 4); + return Dalamud.PluginInterface.UiBuilder.LoadImageRaw(rawImage, 192, 192, 4); } return null; @@ -61,7 +61,7 @@ namespace Glamourer.Gui private static Dictionary GetEquipSlotNames() { - var sheet = Glamourer.PluginInterface.Data.GetExcelSheet(); + var sheet = Dalamud.GameData.GetExcelSheet()!; var ret = new Dictionary(12) { [EquipSlot.MainHand] = sheet.GetRow(738)?.Text.ToString() ?? "Main Hand", diff --git a/Glamourer/Gui/InterfaceMiscellaneous.cs b/Glamourer/Gui/InterfaceMiscellaneous.cs index f2ba9f6..c589139 100644 --- a/Glamourer/Gui/InterfaceMiscellaneous.cs +++ b/Glamourer/Gui/InterfaceMiscellaneous.cs @@ -1,5 +1,5 @@ using System; -using Dalamud.Game.ClientState.Actors.Types; +using Dalamud.Game.ClientState.Objects.Types; using ImGuiNET; namespace Glamourer.Gui @@ -18,7 +18,18 @@ namespace Glamourer.Gui return false; } - private static bool DrawMiscellaneous(CharacterSave save, Actor? player) + private static bool DrawDisableButton(string label, bool disabled) + { + if (!disabled) + return ImGui.Button(label); + + using var raii = new ImGuiRaii(); + raii.PushStyle(ImGuiStyleVar.Alpha, 0.5f); + ImGui.Button(label); + return false; + } + + private static bool DrawMiscellaneous(CharacterSave save, Character? player) { var ret = false; if (!ImGui.CollapsingHeader("Miscellaneous")) diff --git a/Glamourer/Gui/InterfaceRevertables.cs b/Glamourer/Gui/InterfaceRevertables.cs new file mode 100644 index 0000000..d0b2686 --- /dev/null +++ b/Glamourer/Gui/InterfaceRevertables.cs @@ -0,0 +1,88 @@ +using System.Diagnostics; +using System.Linq; +using System.Numerics; +using ImGuiNET; + +namespace Glamourer.Gui +{ + internal partial class Interface + { + private string? _currentRevertableName; + private CharacterSave? _currentRevertable; + + private void DrawRevertablesSelector() + { + ImGui.BeginGroup(); + DrawPlayerFilter(); + if (!ImGui.BeginChild("##playerSelector", + new Vector2(SelectorWidth * ImGui.GetIO().FontGlobalScale, -ImGui.GetFrameHeight() - 1), true)) + { + ImGui.EndChild(); + ImGui.EndGroup(); + return; + } + + foreach (var (name, save) in Glamourer.RevertableDesigns.Saves) + { + if (name.ToLowerInvariant().Contains(_playerFilterLower) && ImGui.Selectable(name, name == _currentRevertableName)) + { + _currentRevertableName = name; + _currentRevertable = save; + } + } + + using (var _ = new ImGuiRaii().PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero)) + { + ImGui.EndChild(); + } + + DrawSelectionButtons(); + ImGui.EndGroup(); + } + + private void DrawRevertablePanel() + { + using var group = ImGuiRaii.NewGroup(); + { + var buttonColor = ImGui.GetColorU32(ImGuiCol.FrameBg); + using var raii = new ImGuiRaii() + .PushColor(ImGuiCol.Text, GreenHeaderColor) + .PushColor(ImGuiCol.Button, buttonColor) + .PushColor(ImGuiCol.ButtonHovered, buttonColor) + .PushColor(ImGuiCol.ButtonActive, buttonColor) + .PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero) + .PushStyle(ImGuiStyleVar.FrameRounding, 0); + ImGui.Button($"{_currentRevertableName}##playerHeader", -Vector2.UnitX * 0.0001f); + } + + if (!ImGui.BeginChild("##revertableData", -Vector2.One, true)) + { + ImGui.EndChild(); + return; + } + + var save = _currentRevertable!.Copy(); + DrawCustomization(ref save.Customizations); + DrawEquip(save.Equipment); + DrawMiscellaneous(save, null); + + ImGui.EndChild(); + } + + [Conditional("DEBUG")] + private void DrawRevertablesTab() + { + using var raii = new ImGuiRaii(); + if (!raii.Begin(() => ImGui.BeginTabItem("Revertables"), ImGui.EndTabItem)) + return; + + DrawRevertablesSelector(); + + if (_currentRevertableName == null) + return; + + ImGui.SameLine(); + DrawRevertablePanel(); + } + } +} diff --git a/Glamourer/Main.cs b/Glamourer/Main.cs deleted file mode 100644 index 3823fc8..0000000 --- a/Glamourer/Main.cs +++ /dev/null @@ -1,320 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Windows.Forms; -using Dalamud.Game.ClientState.Actors.Types; -using Dalamud.Game.Command; -using Dalamud.Plugin; -using Glamourer.Customization; -using Glamourer.Designs; -using Glamourer.FileSystem; -using Glamourer.Gui; -using ImGuiNET; -using Penumbra.Api; -using Penumbra.PlayerWatch; - -namespace Glamourer -{ - public class Glamourer : IDalamudPlugin - { - public const int RequiredPenumbraShareVersion = 1; - - private const string HelpString = "[Copy|Apply|Save],[Name or PlaceHolder],"; - - public string Name - => "Glamourer"; - - public static DalamudPluginInterface PluginInterface = null!; - public static GlamourerConfig Config = null!; - private Interface _interface = null!; - public static ICustomizationManager Customization = null!; - public DesignManager Designs = null!; - public IPlayerWatcher PlayerWatcher = null!; - - public static string Version = string.Empty; - - public static IPenumbraApi? Penumbra; - - private Dalamud.Dalamud _dalamud = null!; - private List<(IDalamudPlugin Plugin, PluginDefinition Definition, DalamudPluginInterface PluginInterface, bool IsRaw)> _plugins = null!; - - private void SetDalamud(DalamudPluginInterface pi) - { - var dalamud = (Dalamud.Dalamud?) pi.GetType() - ?.GetField("dalamud", BindingFlags.NonPublic | BindingFlags.Instance) - ?.GetValue(pi); - - _dalamud = dalamud ?? throw new Exception("Could not obtain Dalamud."); - } - - private static void PenumbraTooltip(object? it) - { - if (it is Lumina.Excel.GeneratedSheets.Item) - ImGui.Text("Right click to apply to current Glamourer Set. [Glamourer]"); - } - - private void PenumbraRightClick(MouseButton button, object? it) - { - if (button == MouseButton.Right && it is Lumina.Excel.GeneratedSheets.Item item) - { - var actors = PluginInterface.ClientState.Actors; - var gPose = actors[Interface.GPoseActorId]; - var player = actors[0]; - var writeItem = new Item(item, string.Empty); - if (gPose != null) - { - writeItem.Write(gPose.Address); - UpdateActors(gPose, player); - } - else if (player != null) - { - writeItem.Write(player.Address); - UpdateActors(player); - } - } - } - - public void RegisterFunctions() - { - if (Penumbra == null || !Penumbra.Valid) - return; - - Penumbra!.ChangedItemTooltip += PenumbraTooltip; - Penumbra!.ChangedItemClicked += PenumbraRightClick; - } - - public void UnregisterFunctions() - { - if (Penumbra == null || !Penumbra.Valid) - return; - - Penumbra!.ChangedItemTooltip -= PenumbraTooltip; - Penumbra!.ChangedItemClicked -= PenumbraRightClick; - } - - private void SetPlugins(DalamudPluginInterface pi) - { - var pluginManager = _dalamud?.GetType() - ?.GetProperty("PluginManager", BindingFlags.Instance | BindingFlags.NonPublic) - ?.GetValue(_dalamud); - - if (pluginManager == null) - throw new Exception("Could not obtain plugin manager."); - - var pluginsList = - (List<(IDalamudPlugin Plugin, PluginDefinition Definition, DalamudPluginInterface PluginInterface, bool IsRaw)>?) pluginManager - ?.GetType() - ?.GetProperty("Plugins", BindingFlags.Instance | BindingFlags.Public) - ?.GetValue(pluginManager); - - _plugins = pluginsList ?? throw new Exception("Could not obtain Dalamud."); - } - - public bool GetPenumbra() - { - if (Penumbra?.Valid ?? false) - return true; - - var plugin = _plugins.Find(p - => p.Definition.InternalName == "Penumbra" - && string.Compare(p.Definition.AssemblyVersion, "0.4.0.3", StringComparison.Ordinal) >= 0).Plugin; - - var penumbra = (IPenumbraApiBase?) plugin?.GetType().GetProperty("Api", BindingFlags.Instance | BindingFlags.Public) - ?.GetValue(plugin); - if (penumbra != null && penumbra.Valid && penumbra.ApiVersion >= RequiredPenumbraShareVersion) - Penumbra = (IPenumbraApi) penumbra!; - else - Penumbra = null; - - return Penumbra != null; - } - - public void Initialize(DalamudPluginInterface pluginInterface) - { - Version = Assembly.GetExecutingAssembly()?.GetName().Version.ToString() ?? ""; - PluginInterface = pluginInterface; - Config = GlamourerConfig.Create(); - Customization = CustomizationManager.Create(PluginInterface); - SetDalamud(PluginInterface); - SetPlugins(PluginInterface); - Designs = new DesignManager(PluginInterface); - if (GetPenumbra() && Config.AttachToPenumbra) - RegisterFunctions(); - PlayerWatcher = PlayerWatchFactory.Create(PluginInterface); - - PluginInterface.CommandManager.AddHandler("/glamourer", new CommandInfo(OnGlamourer) - { - HelpMessage = "Open or close the Glamourer window.", - }); - PluginInterface.CommandManager.AddHandler("/glamour", new CommandInfo(OnGlamour) - { - HelpMessage = $"Use Glamourer Functions: {HelpString}", - }); - - _interface = new Interface(this); - } - - public void OnGlamourer(string command, string arguments) - => _interface?.ToggleVisibility(null!, null!); - - private Actor? GetActor(string name) - { - var lowerName = name.ToLowerInvariant(); - return lowerName switch - { - "" => null, - "" => PluginInterface.ClientState.Actors[Interface.GPoseActorId] ?? PluginInterface.ClientState.LocalPlayer, - "self" => PluginInterface.ClientState.Actors[Interface.GPoseActorId] ?? PluginInterface.ClientState.LocalPlayer, - "" => PluginInterface.ClientState.Targets.CurrentTarget, - "target" => PluginInterface.ClientState.Targets.CurrentTarget, - "" => PluginInterface.ClientState.Targets.FocusTarget, - "focus" => PluginInterface.ClientState.Targets.FocusTarget, - "" => PluginInterface.ClientState.Targets.MouseOverTarget, - "mouseover" => PluginInterface.ClientState.Targets.MouseOverTarget, - _ => PluginInterface.ClientState.Actors.LastOrDefault( - a => string.Equals(a.Name, lowerName, StringComparison.InvariantCultureIgnoreCase)), - }; - } - - public void CopyToClipboard(Actor actor) - { - var save = new CharacterSave(); - save.LoadActor(actor); - Clipboard.SetText(save.ToBase64()); - } - - public void ApplyCommand(Actor actor, string target) - { - CharacterSave? save = null; - if (target.ToLowerInvariant() == "clipboard") - try - { - save = CharacterSave.FromString(Clipboard.GetText()); - } - catch (Exception) - { - PluginInterface.Framework.Gui.Chat.PrintError("Clipboard does not contain a valid customization string."); - } - else if (!Designs.FileSystem.Find(target, out var child) || child is not Design d) - PluginInterface.Framework.Gui.Chat.PrintError("The given path to a saved design does not exist or does not point to a design."); - else - save = d.Data; - - save?.Apply(actor); - UpdateActors(actor); - } - - public void SaveCommand(Actor actor, string path) - { - var save = new CharacterSave(); - save.LoadActor(actor); - try - { - var (folder, name) = Designs.FileSystem.CreateAllFolders(path); - var design = new Design(folder, name) { Data = save }; - folder.FindOrAddChild(design); - Designs.Designs.Add(design.FullName(), design.Data); - Designs.SaveToFile(); - } - catch (Exception e) - { - PluginInterface.Framework.Gui.Chat.PrintError("Could not save file:"); - PluginInterface.Framework.Gui.Chat.PrintError($" {e.Message}"); - } - } - - public void OnGlamour(string command, string arguments) - { - static void PrintHelp() - { - PluginInterface.Framework.Gui.Chat.Print("Usage:"); - PluginInterface.Framework.Gui.Chat.Print($" {HelpString}"); - } - - arguments = arguments.Trim(); - if (!arguments.Any()) - { - PrintHelp(); - return; - } - - var split = arguments.Split(new[] - { - ',', - }, 3, StringSplitOptions.RemoveEmptyEntries); - - if (split.Length < 2) - { - PrintHelp(); - return; - } - - var actor = GetActor(split[1]); - if (actor == null) - { - PluginInterface.Framework.Gui.Chat.Print($"Could not find actor for {split[1]}."); - return; - } - - switch (split[0].ToLowerInvariant()) - { - case "copy": - CopyToClipboard(actor); - return; - case "apply": - { - if (split.Length < 3) - { - PluginInterface.Framework.Gui.Chat.Print("Applying requires a name for the save to be applied or 'clipboard'."); - return; - } - - ApplyCommand(actor, split[2]); - - return; - } - case "save": - { - if (split.Length < 3) - { - PluginInterface.Framework.Gui.Chat.Print("Saving requires a name for the save."); - return; - } - - SaveCommand(actor, split[2]); - return; - } - default: - PrintHelp(); - return; - } - } - - public void Dispose() - { - PlayerWatcher?.Dispose(); - UnregisterFunctions(); - _interface?.Dispose(); - PluginInterface.CommandManager.RemoveHandler("/glamour"); - PluginInterface.CommandManager.RemoveHandler("/glamourer"); - PluginInterface.Dispose(); - } - - // Update actors without triggering PlayerWatcher Events, - // then manually redraw using Penumbra. - public void UpdateActors(Actor actor, Actor? gPoseOriginalActor = null) - { - var newEquip = PlayerWatcher.UpdateActorWithoutEvent(actor); - Penumbra?.RedrawActor(actor, RedrawType.WithSettings); - - // Special case for carrying over changes to the gPose actor to the regular player actor, too. - if (gPoseOriginalActor != null) - { - newEquip.Write(gPoseOriginalActor.Address); - PlayerWatcher.UpdateActorWithoutEvent(gPoseOriginalActor); - Penumbra?.RedrawActor(gPoseOriginalActor, RedrawType.AfterGPoseWithSettings); - } - } - } -} diff --git a/Glamourer/PenumbraAttach.cs b/Glamourer/PenumbraAttach.cs new file mode 100644 index 0000000..3b75c86 --- /dev/null +++ b/Glamourer/PenumbraAttach.cs @@ -0,0 +1,140 @@ +using System; +using Dalamud.Game.ClientState.Objects.Types; +using Dalamud.Logging; +using Dalamud.Plugin.Ipc; +using Glamourer.Gui; +using ImGuiNET; +using Penumbra.GameData.Enums; + +namespace Glamourer +{ + public class PenumbraAttach : IDisposable + { + public const int RequiredPenumbraShareVersion = 3; + + private ICallGateSubscriber? _tooltipSubscriber; + private ICallGateSubscriber? _clickSubscriber; + private ICallGateSubscriber? _redrawSubscriberName; + private ICallGateSubscriber? _redrawSubscriberObject; + + public PenumbraAttach(bool attach) + => Reattach(attach); + + public void Reattach(bool attach) + { + try + { + Unattach(); + + var versionSubscriber = Dalamud.PluginInterface.GetIpcSubscriber("Penumbra.ApiVersion"); + var version = versionSubscriber.InvokeFunc(); + if (version != RequiredPenumbraShareVersion) + throw new Exception($"Invalid Version {version}, required Version {RequiredPenumbraShareVersion}."); + + _redrawSubscriberName = Dalamud.PluginInterface.GetIpcSubscriber("Penumbra.RedrawObjectByName"); + _redrawSubscriberObject = Dalamud.PluginInterface.GetIpcSubscriber("Penumbra.RedrawObject"); + + if (!attach) + return; + + _tooltipSubscriber = Dalamud.PluginInterface.GetIpcSubscriber("Penumbra.ChangedItemTooltip"); + _clickSubscriber = + Dalamud.PluginInterface.GetIpcSubscriber("Penumbra.ChangedItemClick"); + _tooltipSubscriber.Subscribe(PenumbraTooltip); + _clickSubscriber.Subscribe(PenumbraRightClick); + } + catch (Exception e) + { + PluginLog.Debug($"Could not attach to Penumbra:\n{e}"); + } + } + + public void Unattach() + { + _tooltipSubscriber?.Unsubscribe(PenumbraTooltip); + _clickSubscriber?.Unsubscribe(PenumbraRightClick); + _tooltipSubscriber = null; + _clickSubscriber = null; + _redrawSubscriberName = null; + _redrawSubscriberObject = null; + } + + public void Dispose() + => Unattach(); + + private static void PenumbraTooltip(ChangedItemType type, uint _) + { + if (type == ChangedItemType.Item) + ImGui.Text("Right click to apply to current Glamourer Set. [Glamourer]"); + } + + private void PenumbraRightClick(MouseButton button, ChangedItemType type, uint id) + { + if (button != MouseButton.Right || type != ChangedItemType.Item) + return; + + var gPose = Dalamud.Objects[Interface.GPoseObjectId] as Character; + var player = Dalamud.Objects[0] as Character; + var item = (Lumina.Excel.GeneratedSheets.Item) type.GetObject(id)!; + var writeItem = new Item(item, string.Empty); + if (gPose != null) + { + writeItem.Write(gPose.Address); + UpdateCharacters(gPose, player); + } + else if (player != null) + { + writeItem.Write(player.Address); + UpdateCharacters(player); + } + } + + public void RedrawObject(GameObject actor, RedrawType settings, bool repeat) + { + if (_redrawSubscriberObject != null) + { + try + { + _redrawSubscriberObject.InvokeAction(actor, (int) settings); + } + catch (Exception e) + { + if (repeat) + { + Reattach(Glamourer.Config.AttachToPenumbra); + RedrawObject(actor, settings, false); + } + else + { + PluginLog.Debug($"Failure redrawing object:\n{e}"); + } + } + } + else if (repeat) + { + Reattach(Glamourer.Config.AttachToPenumbra); + RedrawObject(actor, settings, false); + } + else + { + PluginLog.Debug("Trying to redraw object, but not attached to Penumbra."); + } + } + + // Update objects without triggering PlayerWatcher Events, + // then manually redraw using Penumbra. + public void UpdateCharacters(Character character, Character? gPoseOriginalCharacter = null) + { + var newEquip = Glamourer.PlayerWatcher.UpdatePlayerWithoutEvent(character); + RedrawObject(character, RedrawType.WithSettings, true); + + // Special case for carrying over changes to the gPose player to the regular player, too. + if (gPoseOriginalCharacter == null) + return; + + newEquip.Write(gPoseOriginalCharacter.Address); + Glamourer.PlayerWatcher.UpdatePlayerWithoutEvent(gPoseOriginalCharacter); + RedrawObject(gPoseOriginalCharacter, RedrawType.AfterGPoseWithSettings, false); + } + } +} diff --git a/images/icon.png b/images/icon.png new file mode 100644 index 0000000..517164e Binary files /dev/null and b/images/icon.png differ diff --git a/repo.json b/repo.json index 0b33e73..edbd6b6 100644 --- a/repo.json +++ b/repo.json @@ -1,19 +1,21 @@ [ { - "Author": "Ottermandias", - "Name": "Glamourer", - "Description": "Adds functionality to change appearance of actors. Requires Penumbra to be installed and activated to work.", - "InternalName": "Glamourer", - "AssemblyVersion": "0.0.3.0", - "TestingAssemblyVersion": "0.0.3.0", - "RepoUrl": "https://github.com/Ottermandias/Glamourer", - "ApplicableVersion": "any", - "DalamudApiLevel": 3, - "IsHide": "False", - "IsTestingExclusive": "false", - "DownloadCount": 1, - "LastUpdate": 1618608322, - "DownloadLinkInstall": "https://github.com/Ottermandias/Glamourer/raw/main/Glamourer.zip", - "DownloadLinkUpdate": "https://github.com/Ottermandias/Glamourer/raw/main/Glamourer.zip", + "Author": "Ottermandias", + "Name": "Glamourer", + "Punchline": "Change and save appearance of players.", + "Description": "Adds functionality to change and store appearance of players, customization and equip. Requires Penumbra to be installed and activated to work. Can also add preview options to the Changed Items tab for Penumbra.", + "Tags": [ "Appearance", "Glamour", "Race", "Outfit", "Armor", "Clothes", "Skins", "Customization", "Design", "Character" ], + "InternalName": "Glamourer", + "AssemblyVersion": "0.0.5.4", + "TestingAssemblyVersion": "0.0.5.4", + "RepoUrl": "https://github.com/Ottermandias/Glamourer", + "ApplicableVersion": "any", + "DalamudApiLevel": 4, + "IsHide": "False", + "IsTestingExclusive": "false", + "DownloadCount": 1, + "LastUpdate": 1618608322, + "DownloadLinkInstall": "https://github.com/Ottermandias/Glamourer/raw/api4/Glamourer.zip", + "DownloadLinkUpdate": "https://github.com/Ottermandias/Glamourer/raw/api4/Glamourer.zip" } ]